Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php
T
2026-04-01 10:30:52 +09:00

1633 lines
63 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
use App\Extension\HookManager;
use App\Models\User;
use Modules\Sirsoft\Ecommerce\Database\Factories\TempOrderFactory;
use Modules\Sirsoft\Ecommerce\DTO\AppliedPromotions;
use Modules\Sirsoft\Ecommerce\DTO\AppliedShippingPolicy;
use Modules\Sirsoft\Ecommerce\DTO\CouponApplication;
use Modules\Sirsoft\Ecommerce\DTO\ItemCalculation;
use Modules\Sirsoft\Ecommerce\DTO\OrderCalculationResult;
use Modules\Sirsoft\Ecommerce\DTO\PromotionsSummary;
use Modules\Sirsoft\Ecommerce\DTO\Summary;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Modules\Sirsoft\Ecommerce\Exceptions\OrderAmountChangedException;
use Modules\Sirsoft\Ecommerce\Exceptions\PaymentAmountMismatchException;
use Modules\Sirsoft\Ecommerce\Models\Order;
use Modules\Sirsoft\Ecommerce\Models\OrderPayment;
use Modules\Sirsoft\Ecommerce\Models\UserAddress;
use Modules\Sirsoft\Ecommerce\Models\Cart;
use Modules\Sirsoft\Ecommerce\Models\Product;
use Modules\Sirsoft\Ecommerce\Models\ProductOption;
use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService;
use Modules\Sirsoft\Ecommerce\Services\OrderCalculationService;
use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService;
use Modules\Sirsoft\Ecommerce\Services\StockService;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* 주문 생성 서비스 테스트
*/
class OrderProcessingServiceTest extends ModuleTestCase
{
protected OrderProcessingService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = app(OrderProcessingService::class);
}
// ===== 기존 테스트 (determineInitialStatus, isFirstOrder, generateOrderNumber) =====
public function test_determine_initial_status_returns_pending_payment_for_vbank(): void
{
// Protected 메서드 테스트를 위한 리플렉션
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('determineInitialStatus');
$method->setAccessible(true);
$status = $method->invoke($this->service, 'vbank');
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $status);
}
public function test_determine_initial_status_returns_pending_order_for_card(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('determineInitialStatus');
$method->setAccessible(true);
$status = $method->invoke($this->service, 'card');
$this->assertEquals(OrderStatusEnum::PENDING_ORDER, $status);
}
public function test_determine_initial_status_returns_pending_order_for_pg(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('determineInitialStatus');
$method->setAccessible(true);
$status = $method->invoke($this->service, 'pg');
$this->assertEquals(OrderStatusEnum::PENDING_ORDER, $status);
}
public function test_is_first_order_returns_true_for_new_user(): void
{
$user = User::factory()->create();
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('isFirstOrder');
$method->setAccessible(true);
$isFirstOrder = $method->invoke($this->service, $user->id);
$this->assertTrue($isFirstOrder);
}
public function test_is_first_order_returns_false_for_user_with_orders(): void
{
$user = User::factory()->create();
// 기존 주문 생성
Order::factory()->create(['user_id' => $user->id]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('isFirstOrder');
$method->setAccessible(true);
$isFirstOrder = $method->invoke($this->service, $user->id);
$this->assertFalse($isFirstOrder);
}
public function test_is_first_order_returns_true_for_guest(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('isFirstOrder');
$method->setAccessible(true);
$isFirstOrder = $method->invoke($this->service, null);
$this->assertTrue($isFirstOrder);
}
public function test_generate_order_number_returns_unique_string(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('generateOrderNumber');
$method->setAccessible(true);
$orderNumber1 = $method->invoke($this->service);
$orderNumber2 = $method->invoke($this->service);
$this->assertIsString($orderNumber1);
$this->assertIsString($orderNumber2);
$this->assertNotEquals($orderNumber1, $orderNumber2);
}
public function test_generate_order_number_has_expected_format(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('generateOrderNumber');
$method->setAccessible(true);
$orderNumber = $method->invoke($this->service);
// SequenceService TIMESTAMP 알고리즘: Ymd-His + 밀리초3자리 + 랜덤1자리 (예: 20260208-1435226549)
$this->assertMatchesRegularExpression('/^\d{8}-\d{10}$/', $orderNumber);
}
public function test_generate_order_number_is_recorded_in_sequence_codes(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('generateOrderNumber');
$method->setAccessible(true);
$orderNumber = $method->invoke($this->service);
// 채번된 주문번호가 ecommerce_sequence_codes 테이블에 기록되는지 확인
$this->assertDatabaseHas('ecommerce_sequence_codes', [
'type' => 'order',
'code' => $orderNumber,
]);
}
public function test_generate_order_number_produces_unique_values(): void
{
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('generateOrderNumber');
$method->setAccessible(true);
$orderNumbers = [];
for ($i = 0; $i < 10; $i++) {
$orderNumbers[] = $method->invoke($this->service);
}
// 모든 주문번호가 유일해야 함
$uniqueNumbers = array_unique($orderNumbers);
$this->assertCount(10, $uniqueNumbers);
// 모든 주문번호가 타임스탬프 형식이어야 함
foreach ($orderNumbers as $orderNumber) {
$this->assertMatchesRegularExpression('/^\d{8}-\d{10}$/', $orderNumber);
}
}
// ===== createFromTempOrder 재계산 통합 테스트 =====
/**
* OrderCalculationService를 mock하여 재계산 결과를 주입하고, 서비스를 재생성합니다.
*
* @param OrderCalculationResult $result 재계산 결과
* @return void
*/
protected function mockCalculationService(OrderCalculationResult $result): void
{
$mock = $this->createMock(OrderCalculationService::class);
$mock->method('calculate')->willReturn($result);
$this->app->instance(OrderCalculationService::class, $mock);
$this->service = app(OrderProcessingService::class);
}
/**
* 기본 계산 결과 DTO를 생성합니다.
*
* @param int $finalAmount 최종 금액
* @param array $overrides 오버라이드 옵션
* @return OrderCalculationResult
*/
protected function makeCalculationResult(int $finalAmount = 103000, array $overrides = []): OrderCalculationResult
{
$summary = new Summary(
subtotal: $overrides['subtotal'] ?? 100000,
totalDiscount: $overrides['total_discount'] ?? 0,
productCouponDiscount: $overrides['product_coupon_discount'] ?? 0,
codeDiscount: $overrides['code_discount'] ?? 0,
totalShipping: $overrides['total_shipping'] ?? 3000,
taxableAmount: $overrides['taxable_amount'] ?? 93636,
taxFreeAmount: $overrides['tax_free_amount'] ?? 0,
pointsUsed: $overrides['points_used'] ?? 0,
pointsEarning: $overrides['points_earning'] ?? 0,
paymentAmount: $overrides['payment_amount'] ?? $finalAmount,
finalAmount: $finalAmount,
);
$items = $overrides['items'] ?? [];
$promotions = $overrides['promotions'] ?? new PromotionsSummary();
return new OrderCalculationResult(
items: $items,
summary: $summary,
promotions: $promotions,
validationErrors: $overrides['validation_errors'] ?? [],
);
}
/**
* 테스트용 기본 TempOrder를 생성합니다.
*
* @param User $user 사용자
* @param int $finalAmount 최종 금액 (calculation_result.summary.final_amount)
* @return \Modules\Sirsoft\Ecommerce\Models\TempOrder
*/
protected function createTestTempOrder(User $user, int $finalAmount = 103000)
{
return TempOrderFactory::new()
->forUser($user)
->withCalculationResult([
'summary' => [
'subtotal' => 100000,
'total_discount' => 0,
'product_coupon_discount' => 0,
'code_discount' => 0,
'total_shipping' => 3000,
'final_amount' => $finalAmount,
'taxable_amount' => 93636,
'tax_free_amount' => 0,
'points_used' => 0,
],
'items' => [],
'shippings' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
}
public function test_create_from_temp_order_creates_order(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// mock: 재계산 결과가 저장된 금액과 동일
$this->mockCalculationService($this->makeCalculationResult(103000));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => '홍길동', 'phone' => '010-1234-5678', 'email' => 'test@example.com'],
['recipient_name' => '홍길동', 'recipient_phone' => '010-1234-5678', 'zipcode' => '12345', 'address' => '서울시 강남구 테헤란로', 'address_detail' => '123동 456호'],
'card',
103000,
'문앞에 놓아주세요'
);
$this->assertInstanceOf(Order::class, $order);
$this->assertEquals($user->id, $order->user_id);
$this->assertEquals(OrderStatusEnum::PENDING_ORDER, $order->order_status);
$this->assertEquals(103000, $order->total_amount);
$this->assertMatchesRegularExpression('/^\d{8}-\d{10}$/', $order->order_number);
}
public function test_create_from_temp_order_with_vbank_sets_pending_payment(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user, 50000);
$this->mockCalculationService($this->makeCalculationResult(50000, [
'subtotal' => 50000,
'total_shipping' => 0,
'taxable_amount' => 45454,
]));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => '김철수', 'phone' => '010-9876-5432', 'email' => 'kim@example.com'],
['recipient_name' => '김철수', 'recipient_phone' => '010-9876-5432', 'zipcode' => '54321', 'address' => '부산시 해운대구', 'address_detail' => '마린시티 1동'],
'vbank',
50000,
null,
'김철수'
);
$this->assertInstanceOf(Order::class, $order);
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $order->order_status);
}
public function test_create_from_temp_order_preserves_temp_order(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user, 30000);
$this->mockCalculationService($this->makeCalculationResult(30000, [
'subtotal' => 30000,
'total_shipping' => 0,
'taxable_amount' => 27273,
]));
$tempOrderId = $tempOrder->id;
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test Address', 'address_detail' => 'Test Detail'],
'card',
30000
);
// PG 결제 취소 → 재결제를 위해 임시주문 유지
$this->assertDatabaseHas('ecommerce_temp_orders', ['id' => $tempOrderId]);
}
// ===== 신규 테스트: 프로모션/배송비/마일리지 데이터 누락 수정 검증 =====
public function test_create_from_temp_order_populates_promotions_applied_snapshot(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// 쿠폰이 적용된 프로모션 결과 구성
$coupon = new CouponApplication(
couponId: 1,
couponIssueId: 101,
name: '10% 할인 쿠폰',
targetType: 'product_amount',
discountType: 'rate',
discountValue: 10,
totalDiscount: 10000,
);
$productPromotions = new AppliedPromotions(coupons: [$coupon]);
$promotions = new PromotionsSummary(productPromotions: $productPromotions);
$this->mockCalculationService($this->makeCalculationResult(103000, [
'promotions' => $promotions,
]));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
// promotions_applied_snapshot이 비어있지 않아야 함
$snapshot = $order->promotions_applied_snapshot;
$this->assertNotEmpty($snapshot);
$this->assertArrayHasKey('product_promotions', $snapshot);
$this->assertNotEmpty($snapshot['product_promotions']['coupons']);
$this->assertEquals(101, $snapshot['product_promotions']['coupons'][0]['coupon_issue_id']);
}
public function test_create_from_temp_order_populates_shipping_policy_snapshot(): void
{
$user = User::factory()->create();
// 실제 상품/옵션 생성 (FK 제약조건 충족)
$product = \Modules\Sirsoft\Ecommerce\Models\Product::factory()->create();
$productOption = \Modules\Sirsoft\Ecommerce\Models\ProductOption::factory()->create([
'product_id' => $product->id,
]);
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
[
'cart_id' => 1,
'product_id' => $product->id,
'product_option_id' => $productOption->id,
'quantity' => 1,
],
])
->withCalculationResult([
'summary' => ['final_amount' => 103000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
// 배송정책이 적용된 아이템 구성
$shippingPolicy = new AppliedShippingPolicy(
policyId: 1,
policyName: '기본 배송',
chargePolicy: 'paid',
shippingAmount: 3000,
extraShippingAmount: 0,
totalShippingAmount: 3000,
policySnapshot: ['id' => 1, 'name' => '기본 배송', 'base_fee' => 3000],
);
$item = new ItemCalculation(
productId: $product->id,
productOptionId: $productOption->id,
quantity: 1,
unitPrice: 100000,
subtotal: 100000,
finalAmount: 100000,
appliedShippingPolicy: $shippingPolicy,
);
$this->mockCalculationService($this->makeCalculationResult(103000, [
'items' => [$item],
]));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
// shipping_policy_applied_snapshot이 비어있지 않아야 함
$snapshot = $order->shipping_policy_applied_snapshot;
$this->assertNotEmpty($snapshot);
$this->assertEquals($productOption->id, $snapshot[0]['product_option_id']);
$this->assertEquals(1, $snapshot[0]['policy']['policy_id']);
}
public function test_create_from_temp_order_saves_order_meta_with_calculation_input(): void
{
$user = User::factory()->create();
$calculationInput = [
'promotions' => [
'item_coupons' => [1 => [101]],
'order_coupon_issue_id' => null,
'shipping_coupon_issue_id' => null,
],
'use_points' => 500,
'shipping_address' => null,
];
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withCalculationInput($calculationInput)
->withCalculationResult([
'summary' => ['final_amount' => 103000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$this->mockCalculationService($this->makeCalculationResult(103000));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
// order_meta에 calculation_input이 포함되어야 함
$meta = $order->order_meta;
$this->assertArrayHasKey('temp_order_id', $meta);
$this->assertArrayHasKey('calculation_input', $meta);
$this->assertEquals(500, $meta['calculation_input']['use_points']);
}
public function test_create_order_shippings_uses_correct_dto_properties(): void
{
$user = User::factory()->create();
// 실제 상품/옵션 생성
$product = \Modules\Sirsoft\Ecommerce\Models\Product::factory()->create();
$productOption = \Modules\Sirsoft\Ecommerce\Models\ProductOption::factory()->create([
'product_id' => $product->id,
]);
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
[
'cart_id' => 1,
'product_id' => $product->id,
'product_option_id' => $productOption->id,
'quantity' => 2,
],
])
->withCalculationResult([
'summary' => ['final_amount' => 55000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
// 배송정책이 설정된 아이템
$shippingPolicy = new AppliedShippingPolicy(
policyId: 5,
policyName: '도서산간 배송',
chargePolicy: 'paid',
shippingAmount: 3000,
extraShippingAmount: 2000,
totalShippingAmount: 5000,
shippingDiscountAmount: 1000,
policySnapshot: ['id' => 5, 'name' => '도서산간 배송'],
);
$item = new ItemCalculation(
productId: $product->id,
productOptionId: $productOption->id,
quantity: 2,
unitPrice: 25000,
subtotal: 50000,
finalAmount: 50000,
appliedShippingPolicy: $shippingPolicy,
);
$this->mockCalculationService($this->makeCalculationResult(55000, [
'subtotal' => 50000,
'total_shipping' => 5000,
'items' => [$item],
]));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
55000
);
// 배송 정보가 올바른 DTO 속성값으로 저장되었는지 확인
$shipping = $order->shippings()->first();
$this->assertNotNull($shipping);
$this->assertEquals(3000, $shipping->base_shipping_amount); // shippingAmount (기본 배송비)
$this->assertEquals(2000, $shipping->extra_shipping_amount); // extraShippingAmount (추가 배송비)
$this->assertEquals(5000, $shipping->total_shipping_amount); // totalShippingAmount (총 배송비)
$this->assertEquals(1000, $shipping->shipping_discount_amount); // shippingDiscountAmount (배송비 할인)
$this->assertTrue((bool) $shipping->is_remote_area); // extraShippingAmount > 0
}
public function test_create_order_shippings_saves_delivery_policy_snapshot(): void
{
$user = User::factory()->create();
$product = \Modules\Sirsoft\Ecommerce\Models\Product::factory()->create();
$productOption = \Modules\Sirsoft\Ecommerce\Models\ProductOption::factory()->create([
'product_id' => $product->id,
]);
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
[
'cart_id' => 1,
'product_id' => $product->id,
'product_option_id' => $productOption->id,
'quantity' => 1,
],
])
->withCalculationResult([
'summary' => ['final_amount' => 33000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$policySnapshotData = [
'id' => 3,
'name' => '기본 배송정책',
'base_fee' => 3000,
'free_shipping_threshold' => 50000,
];
$shippingPolicy = new AppliedShippingPolicy(
policyId: 3,
policyName: '기본 배송정책',
chargePolicy: 'paid',
shippingAmount: 3000,
totalShippingAmount: 3000,
policySnapshot: $policySnapshotData,
);
$item = new ItemCalculation(
productId: $product->id,
productOptionId: $productOption->id,
quantity: 1,
unitPrice: 30000,
subtotal: 30000,
finalAmount: 30000,
appliedShippingPolicy: $shippingPolicy,
);
$this->mockCalculationService($this->makeCalculationResult(33000, [
'subtotal' => 30000,
'total_shipping' => 3000,
'items' => [$item],
]));
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
33000
);
// delivery_policy_snapshot이 저장되었는지 확인
$shipping = $order->shippings()->first();
$this->assertNotNull($shipping);
$this->assertNotNull($shipping->delivery_policy_snapshot);
$this->assertEquals(3, $shipping->delivery_policy_snapshot['id']);
$this->assertEquals('기본 배송정책', $shipping->delivery_policy_snapshot['name']);
}
public function test_create_from_temp_order_calls_coupon_use_hook(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// 쿠폰이 적용된 계산 결과
$coupon = new CouponApplication(
couponId: 1,
couponIssueId: 201,
name: '테스트 쿠폰',
targetType: 'product_amount',
discountType: 'fixed',
discountValue: 5000,
totalDiscount: 5000,
);
$productPromotions = new AppliedPromotions(coupons: [$coupon]);
$promotions = new PromotionsSummary(productPromotions: $productPromotions);
$this->mockCalculationService($this->makeCalculationResult(103000, [
'promotions' => $promotions,
]));
// 훅 호출 감지
$hookCalled = false;
$capturedCouponIds = [];
HookManager::addAction('sirsoft-ecommerce.coupon.use', function ($couponIds, $order) use (&$hookCalled, &$capturedCouponIds) {
$hookCalled = true;
$capturedCouponIds = $couponIds;
});
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
$this->assertTrue($hookCalled, '쿠폰 사용 훅이 호출되어야 합니다');
$this->assertContains(201, $capturedCouponIds);
}
public function test_create_from_temp_order_calls_mileage_use_hook(): void
{
$user = User::factory()->create();
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withUsePoints(1000)
->withCalculationResult([
'summary' => ['final_amount' => 102000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$this->mockCalculationService($this->makeCalculationResult(102000, [
'points_used' => 1000,
]));
// 훅 호출 감지
$hookCalled = false;
$capturedPoints = 0;
HookManager::addAction('sirsoft-ecommerce.mileage.use', function ($points, $order) use (&$hookCalled, &$capturedPoints) {
$hookCalled = true;
$capturedPoints = $points;
});
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
102000
);
$this->assertTrue($hookCalled, '마일리지 사용 훅이 호출되어야 합니다');
$this->assertEquals(1000, $capturedPoints);
}
public function test_create_from_temp_order_recalculates_from_stored_input(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// OrderCalculationService::calculate()가 호출되는지 확인
$mock = $this->createMock(OrderCalculationService::class);
$mock->expects($this->once()) // 정확히 1회 호출
->method('calculate')
->willReturn($this->makeCalculationResult(103000));
$this->app->instance(OrderCalculationService::class, $mock);
$this->service = app(OrderProcessingService::class);
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
}
public function test_create_from_temp_order_blocks_order_on_amount_drift(): void
{
$user = User::factory()->create();
// TempOrder에는 103000으로 저장
$tempOrder = $this->createTestTempOrder($user, 103000);
// 재계산 결과는 105000 (금액 변동)
$this->mockCalculationService($this->makeCalculationResult(105000));
$this->expectException(OrderAmountChangedException::class);
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
}
// ===== validatePaymentAmount 테스트 =====
/**
* 테스트용 주문 + 결제 레코드를 생성합니다.
*
* @param int $subtotal 소계
* @param int $shipping 배송비
* @param int $discount 할인액
* @return Order
*/
protected function createOrderWithPayment(
int $subtotal = 50000,
int $shipping = 3000,
int $discount = 0,
): Order {
$user = User::factory()->create();
$totalAmount = $subtotal - $discount + $shipping;
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_ORDER,
'subtotal_amount' => $subtotal,
'total_discount_amount' => $discount,
'total_product_coupon_discount_amount' => 0,
'total_order_coupon_discount_amount' => 0,
'total_code_discount_amount' => $discount,
'base_shipping_amount' => $shipping,
'extra_shipping_amount' => 0,
'shipping_discount_amount' => 0,
'total_shipping_amount' => $shipping,
'total_amount' => $totalAmount,
'total_due_amount' => $totalAmount,
'total_points_used_amount' => 0,
'total_deposit_used_amount' => 0,
'total_paid_amount' => 0,
]);
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_status' => PaymentStatusEnum::READY,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'tosspayments',
'paid_amount_local' => 0,
'paid_at' => null,
'transaction_id' => null,
'card_approval_number' => null,
]);
return $order;
}
public function test_validate_payment_amount_passes_when_amounts_match(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// 리플렉션으로 protected 메서드 테스트
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('validatePaymentAmount');
$method->setAccessible(true);
// 53000 = 50000 + 3000 - 0 → total_amount와 일치해야 함
$method->invoke($this->service, $order, 53000);
// 예외가 발생하지 않으면 성공
$this->assertTrue(true);
}
public function test_validate_payment_amount_throws_on_component_mismatch(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// DB에서 total_amount를 직접 변조 (컴포넌트 합산과 불일치)
Order::withoutEvents(function () use ($order) {
$order->update(['total_amount' => 99999]);
});
$order->refresh();
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('validatePaymentAmount');
$method->setAccessible(true);
$this->expectException(PaymentAmountMismatchException::class);
$method->invoke($this->service, $order, 99999);
}
public function test_validate_payment_amount_throws_on_pg_amount_mismatch(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('validatePaymentAmount');
$method->setAccessible(true);
// PG에서 다른 금액이 전달된 경우
$this->expectException(PaymentAmountMismatchException::class);
$method->invoke($this->service, $order, 99999);
}
// ===== completePayment 확장 테스트 =====
public function test_complete_payment_with_pg_amount_validates_and_updates(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$result = $this->service->completePayment($order, [
'transaction_id' => 'pk_test_123',
'card_approval_number' => '87654321',
'card_number_masked' => '5432-****-****-1234',
'card_name' => '삼성카드',
'card_installment_months' => 3,
'is_interest_free' => true,
'receipt_url' => 'https://receipt.example.com',
'payment_device' => 'pc',
], 53000);
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $result->order_status);
// 결제 정보 확인
$payment = $result->payment;
$payment->refresh();
$this->assertEquals('pk_test_123', $payment->transaction_id);
$this->assertEquals('87654321', $payment->card_approval_number);
$this->assertEquals('5432-****-****-1234', $payment->card_number_masked);
$this->assertEquals('삼성카드', $payment->card_name);
$this->assertEquals(3, $payment->card_installment_months);
$this->assertTrue((bool) $payment->is_interest_free);
$this->assertEquals('https://receipt.example.com', $payment->receipt_url);
$this->assertEquals('pc', $payment->payment_device);
}
public function test_complete_payment_without_pg_amount_skips_validation(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// $pgAmount = null → 금액 검증 생략 (무통장입금 등)
$result = $this->service->completePayment($order, [
'transaction_id' => 'manual_confirm_123',
]);
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $result->order_status);
}
public function test_complete_payment_calls_hooks(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$beforeCalled = false;
$afterCalled = false;
HookManager::addAction('sirsoft-ecommerce.order.before_payment_complete', function () use (&$beforeCalled) {
$beforeCalled = true;
});
HookManager::addAction('sirsoft-ecommerce.order.after_payment_complete', function () use (&$afterCalled) {
$afterCalled = true;
});
$this->service->completePayment($order, [], 53000);
$this->assertTrue($beforeCalled, 'before_payment_complete 훅이 호출되어야 합니다');
$this->assertTrue($afterCalled, 'after_payment_complete 훅이 호출되어야 합니다');
}
// ===== failPayment 테스트 =====
public function test_fail_payment_cancels_pending_order(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$this->assertEquals(OrderStatusEnum::PENDING_ORDER, $order->order_status);
$result = $this->service->failPayment($order, 'PAY_PROCESS_CANCELED', '결제가 취소되었습니다.');
$this->assertEquals(OrderStatusEnum::CANCELLED, $result->order_status);
$meta = $result->order_meta;
$this->assertEquals('PAY_PROCESS_CANCELED', $meta['payment_failure_code']);
$this->assertEquals('결제가 취소되었습니다.', $meta['payment_failure_message']);
$this->assertArrayHasKey('payment_failed_at', $meta);
}
public function test_fail_payment_ignores_non_pending_order(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// 이미 결제 완료된 상태로 변경
$order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]);
$order->refresh();
$result = $this->service->failPayment($order, 'SOME_ERROR', '에러');
// 상태 변경 없이 원래 주문이 반환되어야 함
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $result->order_status);
}
public function test_fail_payment_calls_payment_failed_hook(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$hookCalled = false;
$capturedCode = null;
HookManager::addAction('sirsoft-ecommerce.order.payment_failed', function ($o, $code, $message) use (&$hookCalled, &$capturedCode) {
$hookCalled = true;
$capturedCode = $code;
});
$this->service->failPayment($order, 'USER_CANCEL', '사용자 취소');
$this->assertTrue($hookCalled, 'payment_failed 훅이 호출되어야 합니다');
$this->assertEquals('USER_CANCEL', $capturedCode);
}
// ===== findByOrderNumber 테스트 =====
// ===== recordPaymentCancellation 테스트 =====
public function test_record_payment_cancellation_updates_payment_status(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$result = $this->service->recordPaymentCancellation($order);
$payment = $result->payment;
$payment->refresh();
$this->assertEquals(PaymentStatusEnum::CANCELLED->value, $payment->payment_status->value);
$this->assertNotNull($payment->cancelled_at);
}
public function test_record_payment_cancellation_appends_cancel_history(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$result = $this->service->recordPaymentCancellation($order, 'USER_CANCEL', '사용자가 결제를 취소했습니다.');
$payment = $result->payment;
$payment->refresh();
$cancelHistory = $payment->cancel_history;
$this->assertIsArray($cancelHistory);
$this->assertCount(1, $cancelHistory);
$this->assertEquals('USER_CANCEL', $cancelHistory[0]['cancel_code']);
$this->assertEquals('사용자가 결제를 취소했습니다.', $cancelHistory[0]['cancel_message']);
$this->assertArrayHasKey('cancelled_at', $cancelHistory[0]);
}
public function test_record_payment_cancellation_appends_to_existing_history(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// 1차 취소
$this->service->recordPaymentCancellation($order);
// 2차 취소
$result = $this->service->recordPaymentCancellation($order->fresh());
$payment = $result->payment;
$payment->refresh();
$cancelHistory = $payment->cancel_history;
$this->assertCount(2, $cancelHistory);
}
public function test_record_payment_cancellation_returns_order_when_no_payment(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_ORDER,
]);
// payment 없이 호출 → 오류 없이 반환
$result = $this->service->recordPaymentCancellation($order);
$this->assertEquals($order->id, $result->id);
}
// ===== completePayment 임시주문 정리 테스트 =====
public function test_complete_payment_deletes_temp_order(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// 임시주문 생성
$user = $order->user;
$tempOrder = \Modules\Sirsoft\Ecommerce\Models\TempOrder::create([
'user_id' => $user->id,
'cart_key' => 'test-cart-key',
'items' => [],
'calculation_result' => ['summary' => ['final_amount' => 53000]],
'expires_at' => now()->addMinutes(30),
]);
$this->assertDatabaseHas('ecommerce_temp_orders', ['id' => $tempOrder->id]);
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_123',
], 53000);
// completePayment 후 임시주문 삭제 확인
$this->assertDatabaseMissing('ecommerce_temp_orders', ['id' => $tempOrder->id]);
}
// ===== findByOrderNumber 테스트 =====
public function test_find_by_order_number_returns_order(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
$found = $this->service->findByOrderNumber($order->order_number);
$this->assertNotNull($found);
$this->assertEquals($order->id, $found->id);
}
public function test_find_by_order_number_returns_null_for_non_existent(): void
{
$found = $this->service->findByOrderNumber('NON_EXISTENT_ORDER_NUMBER');
$this->assertNull($found);
}
// ===== 재고 차감 타이밍 (stock_deduction_timing) 테스트 =====
/**
* StockService와 EcommerceSettingsService를 mock하여 재고 차감 타이밍을 제어합니다.
*
* @param string $timing 재고 차감 타이밍 ('order_placed', 'payment_complete', 'none')
* @return \PHPUnit\Framework\MockObject\MockObject StockService mock
*/
protected function mockStockAndSettingsForTiming(string $timing): \PHPUnit\Framework\MockObject\MockObject
{
$settingsMock = $this->createMock(EcommerceSettingsService::class);
$settingsMock->method('getStockDeductionTiming')->willReturn($timing);
$this->app->instance(EcommerceSettingsService::class, $settingsMock);
$stockMock = $this->createMock(StockService::class);
$this->app->instance(StockService::class, $stockMock);
return $stockMock;
}
public function test_create_from_temp_order_deducts_stock_for_order_placed_timing(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// StockService mock: deductStock이 정확히 1회 호출되어야 함
$stockMock = $this->mockStockAndSettingsForTiming('order_placed');
$stockMock->expects($this->once())->method('deductStock');
$this->mockCalculationService($this->makeCalculationResult(103000));
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'dbank',
103000
);
}
public function test_create_from_temp_order_does_not_deduct_stock_for_payment_complete_timing(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// StockService mock: deductStock이 호출되지 않아야 함
$stockMock = $this->mockStockAndSettingsForTiming('payment_complete');
$stockMock->expects($this->never())->method('deductStock');
$this->mockCalculationService($this->makeCalculationResult(103000));
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'card',
103000
);
}
public function test_create_from_temp_order_does_not_deduct_stock_for_none_timing(): void
{
$user = User::factory()->create();
$tempOrder = $this->createTestTempOrder($user);
// StockService mock: deductStock이 호출되지 않아야 함
$stockMock = $this->mockStockAndSettingsForTiming('none');
$stockMock->expects($this->never())->method('deductStock');
$this->mockCalculationService($this->makeCalculationResult(103000));
$this->service->createFromTempOrder(
$tempOrder,
['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'],
['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'],
'dbank',
103000
);
}
public function test_complete_payment_deducts_stock_for_payment_complete_timing(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// StockService mock: deductStock이 정확히 1회 호출되어야 함
$stockMock = $this->mockStockAndSettingsForTiming('payment_complete');
$stockMock->expects($this->once())->method('deductStock');
$this->service = app(OrderProcessingService::class);
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_stock',
], 53000);
}
public function test_complete_payment_does_not_deduct_stock_for_order_placed_timing(): void
{
$order = $this->createOrderWithPayment(50000, 3000, 0);
// StockService mock: deductStock이 호출되지 않아야 함 (이미 주문 시 차감됨)
$stockMock = $this->mockStockAndSettingsForTiming('order_placed');
$stockMock->expects($this->never())->method('deductStock');
$this->service = app(OrderProcessingService::class);
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_no_stock',
], 53000);
}
// ===== completePayment 배송지 자동 저장 테스트 =====
public function test_complete_payment_saves_shipping_address_when_meta_flag_set(): void
{
$order = $this->createOrderWithPayment();
$order->update([
'order_meta' => [
'save_shipping_address' => true,
'shipping_info_for_save' => [
'recipient_name' => '김철수',
'recipient_phone' => '010-9876-5432',
'country_code' => 'KR',
'zipcode' => '12345',
'address' => '서울시 강남구 테헤란로 123',
'address_detail' => '101동 1001호',
],
],
]);
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_addr_save',
], 53000);
$this->assertDatabaseHas('ecommerce_user_addresses', [
'user_id' => $order->user_id,
'recipient_name' => '김철수',
'zipcode' => '12345',
]);
// order_meta에서 플래그 제거 확인
$order->refresh();
$this->assertFalse($order->order_meta['save_shipping_address'] ?? false);
$this->assertArrayNotHasKey('shipping_info_for_save', $order->order_meta ?? []);
}
public function test_complete_payment_does_not_save_address_when_no_meta_flag(): void
{
$order = $this->createOrderWithPayment();
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_no_flag',
], 53000);
$this->assertDatabaseMissing('ecommerce_user_addresses', [
'user_id' => $order->user_id,
]);
}
public function test_complete_payment_does_not_save_address_for_guest_order(): void
{
$order = $this->createOrderWithPayment();
// user_id를 null로 변경 (비회원 주문 시뮬레이션)
Order::withoutEvents(function () use ($order) {
$order->update([
'user_id' => null,
'order_meta' => [
'save_shipping_address' => true,
'shipping_info_for_save' => [
'recipient_name' => '비회원',
'recipient_phone' => '010-0000-0000',
'zipcode' => '99999',
'address' => '서울시',
],
],
]);
});
$order->refresh();
$this->service->completePayment($order, [
'transaction_id' => 'test_tx_guest',
], 53000);
$this->assertEquals(0, UserAddress::count());
}
public function test_complete_payment_succeeds_even_if_address_save_fails(): void
{
$order = $this->createOrderWithPayment();
$order->update([
'order_meta' => [
'save_shipping_address' => true,
'shipping_info_for_save' => [], // 빈 데이터 (저장 시 예외 가능)
],
]);
// 결제 완료는 정상 처리되어야 함 (예외 미전파)
$result = $this->service->completePayment($order, [
'transaction_id' => 'test_tx_addr_fail',
], 53000);
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $result->order_status);
}
// ===== 장바구니 처리 테스트 =====
/**
* 테스트용 Cart 레코드를 생성합니다.
*
* @param int $userId 사용자 ID
* @param int $quantity 수량
* @param Product|null $product 상품 (null이면 새로 생성)
* @param ProductOption|null $option 옵션 (null이면 새로 생성)
* @return Cart
*/
protected function createTestCart(
int $userId,
int $quantity = 1,
?Product $product = null,
?ProductOption $option = null
): Cart {
$product = $product ?? Product::factory()->create();
$option = $option ?? ProductOption::factory()->create(['product_id' => $product->id]);
return Cart::create([
'user_id' => $userId,
'product_id' => $product->id,
'product_option_id' => $option->id,
'quantity' => $quantity,
]);
}
public function test_build_order_meta에_cart_items_포함(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$option1 = ProductOption::factory()->create(['product_id' => $product->id]);
$option2 = ProductOption::factory()->create(['product_id' => $product->id]);
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
['cart_id' => 10, 'product_id' => $product->id, 'product_option_id' => $option1->id, 'quantity' => 3],
['cart_id' => 20, 'product_id' => $product->id, 'product_option_id' => $option2->id, 'quantity' => 1],
])
->withCalculationResult([
'summary' => ['final_amount' => 50000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('buildOrderMeta');
$method->setAccessible(true);
$meta = $method->invoke($this->service, $tempOrder);
$this->assertArrayHasKey('cart_items', $meta);
$this->assertCount(2, $meta['cart_items']);
$this->assertEquals(10, $meta['cart_items'][0]['cart_id']);
$this->assertEquals(3, $meta['cart_items'][0]['quantity']);
$this->assertEquals(20, $meta['cart_items'][1]['cart_id']);
$this->assertEquals(1, $meta['cart_items'][1]['quantity']);
}
public function test_build_order_meta_cart_id_없는_아이템은_제외(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$option = ProductOption::factory()->create(['product_id' => $product->id]);
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
['product_id' => $product->id, 'product_option_id' => $option->id, 'quantity' => 1],
])
->withCalculationResult([
'summary' => ['final_amount' => 50000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('buildOrderMeta');
$method->setAccessible(true);
$meta = $method->invoke($this->service, $tempOrder);
$this->assertArrayHasKey('cart_items', $meta);
$this->assertEmpty($meta['cart_items']);
}
public function test_clear_ordered_cart_items_장바구니_수량과_주문_수량_같으면_삭제(): void
{
$user = User::factory()->create();
$cart = $this->createTestCart($user->id, 3);
$order = Order::factory()->create([
'user_id' => $user->id,
'order_meta' => ['cart_items' => [['cart_id' => $cart->id, 'quantity' => 3]]],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart->id]);
}
public function test_clear_ordered_cart_items_장바구니_수량이_주문보다_크면_차감(): void
{
$user = User::factory()->create();
$cart = $this->createTestCart($user->id, 5);
$order = Order::factory()->create([
'user_id' => $user->id,
'order_meta' => ['cart_items' => [['cart_id' => $cart->id, 'quantity' => 3]]],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
$this->assertDatabaseHas('ecommerce_carts', ['id' => $cart->id, 'quantity' => 2]);
}
public function test_clear_ordered_cart_items_장바구니_수량이_주문보다_작으면_삭제(): void
{
$user = User::factory()->create();
$cart = $this->createTestCart($user->id, 2);
$order = Order::factory()->create([
'user_id' => $user->id,
'order_meta' => ['cart_items' => [['cart_id' => $cart->id, 'quantity' => 3]]],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart->id]);
}
public function test_clear_ordered_cart_items_cart_items_비어있으면_스킵(): void
{
$order = Order::factory()->create([
'order_meta' => ['cart_items' => []],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
// 예외 없이 정상 완료
$this->assertTrue(true);
}
public function test_clear_ordered_cart_items_이미_삭제된_장바구니는_무시(): void
{
$order = Order::factory()->create([
'order_meta' => ['cart_items' => [['cart_id' => 999999, 'quantity' => 1]]],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
// 존재하지 않는 cart_id여도 예외 없이 정상 완료 (멱등성)
$this->assertTrue(true);
}
public function test_clear_ordered_cart_items_복수_아이템_혼합_처리(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$option1 = ProductOption::factory()->create(['product_id' => $product->id]);
$option2 = ProductOption::factory()->create(['product_id' => $product->id]);
$option3 = ProductOption::factory()->create(['product_id' => $product->id]);
$cart1 = $this->createTestCart($user->id, 5, $product, $option1);
$cart2 = $this->createTestCart($user->id, 3, $product, $option2);
$cart3 = $this->createTestCart($user->id, 1, $product, $option3);
$order = Order::factory()->create([
'user_id' => $user->id,
'order_meta' => ['cart_items' => [
['cart_id' => $cart1->id, 'quantity' => 2],
['cart_id' => $cart2->id, 'quantity' => 3],
['cart_id' => $cart3->id, 'quantity' => 5],
]],
]);
$reflection = new \ReflectionClass($this->service);
$method = $reflection->getMethod('clearOrderedCartItems');
$method->setAccessible(true);
$method->invoke($this->service, $order);
// cart1: 수량 5, 주문 2 → 잔여 3
$this->assertDatabaseHas('ecommerce_carts', ['id' => $cart1->id, 'quantity' => 3]);
// cart2: 수량 3, 주문 3 → 삭제
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart2->id]);
// cart3: 수량 1, 주문 5 → 삭제
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart3->id]);
}
public function test_order_placed_타이밍에서_장바구니_아이템_삭제(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$option = ProductOption::factory()->create(['product_id' => $product->id]);
$cart = $this->createTestCart($user->id, 2, $product, $option);
// order_placed 타이밍 설정
$settingsMock = $this->createMock(EcommerceSettingsService::class);
$settingsMock->method('getStockDeductionTiming')->willReturn('order_placed');
$this->app->instance(EcommerceSettingsService::class, $settingsMock);
$stockMock = $this->createMock(StockService::class);
$stockMock->expects($this->once())->method('deductStock');
$this->app->instance(StockService::class, $stockMock);
$this->service = app(OrderProcessingService::class);
$this->mockCalculationService($this->makeCalculationResult(103000));
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
['cart_id' => $cart->id, 'product_id' => $product->id, 'product_option_id' => $option->id, 'quantity' => 2],
])
->withCalculationResult([
'summary' => ['final_amount' => 103000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$this->service->createFromTempOrder(
$tempOrder,
['name' => '홍길동', 'phone' => '010-1234-5678', 'email' => 'test@example.com'],
['recipient_name' => '홍길동', 'recipient_phone' => '010-1234-5678', 'zipcode' => '12345', 'address' => '서울시 강남구', 'address_detail' => '123동'],
'dbank',
103000,
null,
'홍길동'
);
// order_placed 타이밍 → 재고 차감 + 장바구니 삭제
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart->id]);
}
public function test_payment_complete_타이밍에서_createFromTempOrder_장바구니_미삭제(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$option = ProductOption::factory()->create(['product_id' => $product->id]);
$cart = $this->createTestCart($user->id, 2, $product, $option);
// payment_complete 타이밍 설정
$settingsMock = $this->createMock(EcommerceSettingsService::class);
$settingsMock->method('getStockDeductionTiming')->willReturn('payment_complete');
$this->app->instance(EcommerceSettingsService::class, $settingsMock);
$this->service = app(OrderProcessingService::class);
$this->mockCalculationService($this->makeCalculationResult(103000));
$tempOrder = TempOrderFactory::new()
->forUser($user)
->withItems([
['cart_id' => $cart->id, 'product_id' => $product->id, 'product_option_id' => $option->id, 'quantity' => 2],
])
->withCalculationResult([
'summary' => ['final_amount' => 103000],
'items' => [],
'promotions' => [
'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []],
],
'validation_errors' => [],
])
->create();
$order = $this->service->createFromTempOrder(
$tempOrder,
['name' => '홍길동', 'phone' => '010-1234-5678', 'email' => 'test@example.com'],
['recipient_name' => '홍길동', 'recipient_phone' => '010-1234-5678', 'zipcode' => '12345', 'address' => '서울시 강남구', 'address_detail' => '123동'],
'card',
103000
);
// payment_complete 타이밍 → createFromTempOrder에서는 장바구니 미삭제
$this->assertDatabaseHas('ecommerce_carts', ['id' => $cart->id]);
// order_meta에 cart_items 저장 확인 (나중에 completePayment에서 사용)
$this->assertNotEmpty($order->order_meta['cart_items']);
}
public function test_complete_payment_시_장바구니_아이템_삭제(): void
{
$user = User::factory()->create();
$cart = $this->createTestCart($user->id, 2);
$order = $this->createOrderWithPayment();
$order->update([
'order_meta' => array_merge($order->order_meta ?? [], [
'cart_items' => [['cart_id' => $cart->id, 'quantity' => 2]],
]),
]);
$result = $this->service->completePayment($order, [
'transaction_id' => 'test_tx_cart_clear',
], $order->total_amount);
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $result->order_status);
$this->assertDatabaseMissing('ecommerce_carts', ['id' => $cart->id]);
}
}