관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다. 설정 화면(min/max)·저장 검증(FormRequest)·런타임 적용(Service) 세 계층이 각자 리터럴을 들고 있어 서로 조용히 갈라지던 것이 공통 원인이다. 경계값 SSoT 도입: - config/core.php `settings_limits` 를 단일 출처로 두고, FormRequest 는 이 값으로 규칙을 만들고 화면은 설정 응답의 `_meta.limits` 로 같은 값을 받아 바인딩한다. - 게시판·이커머스도 각 모듈 config 의 limits 를 동일 구조로 노출한다. 적용되지 않던 정책을 실제로 적용: - 비밀번호 최소 길이·특수문자 필수(신규 지정 7개 경로. 로그인은 제외 — 정책을 올렸을 때 기존 회원이 자기 계정에 접근조차 못 하는 데드락을 막기 위함) - 계정 잠금 시간 0 = 무한대(전용 컬럼 분리. timestamp 상한과 NULL 규약 때문에 기존 컬럼 재사용이 불가능하다) + 관리자 수동 해제 경로 - 업로드 최대 크기·허용 확장자, 게시판 첨부 개수·댓글 깊이, 페이지 첨부 정책, 장바구니 수량·구매 한도, 마일리지 사용 한도, 주문 부가세 파생 결함: - 큐 Job(sync 드라이버)이 진행 중인 요청의 Request 바인딩을 덮어써, 뒤이어 실행되는 훅 리스너가 헤더를 잃고 조용히 건너뛰었다. Job 이 원 바인딩을 복원하도록 고치고, 전송 헤더가 필요한 리스너는 동기 실행으로 선언한다. - 확장 업데이트 실패 시 상태만 되돌리고 상태 캐시를 비우지 않아, 그 확장의 관리자 화면만 캐시 TTL(기본 하루) 동안 404 로 남았다. 실패 경로에도 무효화를 대칭으로 두고, 빈 목록 자체를 캐시하지 않는다. - 글 작성 시 첨부 파일이 Service 로 전달되지 않아 검증·권한은 통과하고 첨부만 사라졌다. - 환경설정 저장 실패 안내가 내부 설정 키를 그대로 노출했다(105개 항목에 라벨 부여). 재발 방지: - audit 룰 3종(레이아웃 경계 리터럴 / FormRequest 경계 불일치 / Service 재클램프) - 테스트 실행 범위 강제 훅(check-test-scope) - 트러블슈팅 사례 2건(큐 Job 요청 오염 / 확장 상태 캐시 고착) 공개 이슈: gnuboard/g7
565 lines
22 KiB
PHP
565 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Cart;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Modules\Sirsoft\Ecommerce\Database\Factories\CartFactory;
|
|
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory;
|
|
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderOptionFactory;
|
|
use Modules\Sirsoft\Ecommerce\Database\Factories\ProductFactory;
|
|
use Modules\Sirsoft\Ecommerce\Database\Factories\ProductOptionFactory;
|
|
use Modules\Sirsoft\Ecommerce\Exceptions\CartQuantityLimitException;
|
|
use Modules\Sirsoft\Ecommerce\Exceptions\CartUnavailableException;
|
|
use Modules\Sirsoft\Ecommerce\Models\Cart;
|
|
use Modules\Sirsoft\Ecommerce\Models\Order;
|
|
use Modules\Sirsoft\Ecommerce\Models\ProductOption;
|
|
use Modules\Sirsoft\Ecommerce\Services\CartService;
|
|
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* 장바구니 수량 상한 · 구매수량 한도 테스트 (E7 · E8)
|
|
*
|
|
* - E7: `config('sirsoft-ecommerce.cart.max_quantity')`(99)가 죽어 있고 Request 3종이 9999 를
|
|
* 하드코딩했다. 누적 합산 경로에는 상한 자체가 없었다.
|
|
* - E8: 상품 구매수량 한도(`max_purchase_qty`)가 수량 변경 경로에서만 검사되고
|
|
* 담기·옵션변경·비회원 병합 경로에서는 검사되지 않았다.
|
|
*/
|
|
class CartQuantityAndPurchaseLimitTest extends ModuleTestCase
|
|
{
|
|
/**
|
|
* 판매중 상품 옵션을 만듭니다.
|
|
*
|
|
* @param int $maxPurchaseQty 구매수량 한도 (0 = 무제한)
|
|
* @param int $stock 재고
|
|
* @return ProductOption 생성된 옵션
|
|
*/
|
|
private function makeOption(int $maxPurchaseQty = 0, int $stock = 1000): ProductOption
|
|
{
|
|
$product = ProductFactory::new()->create([
|
|
'shipping_policy_id' => null,
|
|
'list_price' => 10000,
|
|
'selling_price' => 10000,
|
|
'max_purchase_qty' => $maxPurchaseQty,
|
|
'stock_quantity' => $stock,
|
|
]);
|
|
|
|
return ProductOptionFactory::new()->forProduct($product)->create([
|
|
'selling_price' => 10000,
|
|
'price_adjustment' => 0,
|
|
'stock_quantity' => $stock,
|
|
]);
|
|
}
|
|
|
|
// ========================================
|
|
// E7 — 장바구니 수량 상한
|
|
// ========================================
|
|
|
|
/**
|
|
* 설정 상한을 넘는 담기는 차단되는지 테스트합니다.
|
|
*/
|
|
public function test_add_over_cart_quantity_limit_is_blocked(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption();
|
|
|
|
$this->expectException(CartQuantityLimitException::class);
|
|
|
|
app(CartService::class)->bulkAddToCart([
|
|
'user_id' => $user->id,
|
|
'product_id' => $option->product_id,
|
|
'items' => [
|
|
['product_option_id' => $option->id, 'quantity' => 100],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 기존 수량과 합산해 상한을 넘으면 차단되는지 테스트합니다.
|
|
*
|
|
* 필드 규칙(`max:`)은 이번 요청분만 보므로 누적 경로는 서버가 따로 판정해야 한다.
|
|
*/
|
|
public function test_accumulated_quantity_over_limit_is_blocked(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption();
|
|
|
|
CartFactory::new()->forUser($user)->forOption($option)->create(['quantity' => 90]);
|
|
|
|
$this->expectException(CartQuantityLimitException::class);
|
|
|
|
app(CartService::class)->bulkAddToCart([
|
|
'user_id' => $user->id,
|
|
'product_id' => $option->product_id,
|
|
'items' => [
|
|
['product_option_id' => $option->id, 'quantity' => 20],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 상한과 정확히 같은 합산 수량은 통과하는지 테스트합니다 (경계).
|
|
*/
|
|
public function test_accumulated_quantity_at_limit_passes(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption();
|
|
|
|
CartFactory::new()->forUser($user)->forOption($option)->create(['quantity' => 90]);
|
|
|
|
app(CartService::class)->bulkAddToCart([
|
|
'user_id' => $user->id,
|
|
'product_id' => $option->product_id,
|
|
'items' => [
|
|
['product_option_id' => $option->id, 'quantity' => 9],
|
|
],
|
|
]);
|
|
|
|
$this->assertSame(99, (int) Cart::where('user_id', $user->id)->sum('quantity'));
|
|
}
|
|
|
|
// ========================================
|
|
// E8 — 구매수량 한도
|
|
// ========================================
|
|
|
|
/**
|
|
* 담기 경로에서 구매수량 한도가 강제되는지 테스트합니다.
|
|
*/
|
|
public function test_add_to_cart_enforces_purchase_quantity_limit(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
|
|
$this->expectException(CartUnavailableException::class);
|
|
|
|
app(CartService::class)->bulkAddToCart([
|
|
'user_id' => $user->id,
|
|
'product_id' => $option->product_id,
|
|
'items' => [
|
|
['product_option_id' => $option->id, 'quantity' => 8],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 옵션 변경 경로에서 구매수량 한도가 강제되는지 테스트합니다.
|
|
*
|
|
* @scenario entry=change_option
|
|
*
|
|
* @effects change_option_blocks_above_max_qty
|
|
*/
|
|
public function test_change_option_enforces_purchase_quantity_limit(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
$other = ProductOptionFactory::new()
|
|
->forProduct($option->product)
|
|
->create(['selling_price' => 10000, 'price_adjustment' => 0, 'stock_quantity' => 1000]);
|
|
|
|
$cart = CartFactory::new()->forUser($user)->forOption($option)->create(['quantity' => 1]);
|
|
|
|
$this->expectException(CartUnavailableException::class);
|
|
|
|
app(CartService::class)->changeOption($cart->id, $other->id, 8, $user->id, null);
|
|
}
|
|
|
|
/**
|
|
* 한도 이내 옵션 변경은 통과하는지 테스트합니다.
|
|
*/
|
|
public function test_change_option_within_limit_passes(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
$other = ProductOptionFactory::new()
|
|
->forProduct($option->product)
|
|
->create(['selling_price' => 10000, 'price_adjustment' => 0, 'stock_quantity' => 1000]);
|
|
|
|
$cart = CartFactory::new()->forUser($user)->forOption($option)->create(['quantity' => 1]);
|
|
|
|
$updated = app(CartService::class)->changeOption($cart->id, $other->id, 5, $user->id, null);
|
|
|
|
$this->assertSame(5, $updated->quantity);
|
|
}
|
|
|
|
/**
|
|
* 수량 상한 초과 안내 문구가 실제 상한값을 말하는지 테스트합니다.
|
|
*
|
|
* 규칙은 `config('sirsoft-ecommerce.cart.max_quantity')` 를 읽는데 안내 문구가 다른 숫자를
|
|
* 고정하고 있으면, 사용자는 "최대 N개까지 가능" 이라는 안내를 보면서 그보다 적은 수량에서
|
|
* 차단당한다 — 차단 사유와 안내가 서로 다른 말을 하는 상태다.
|
|
*/
|
|
public function test_quantity_limit_message_states_the_configured_limit(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption();
|
|
|
|
$cart = CartFactory::new()->forOption($option)->create([
|
|
'user_id' => $user->id,
|
|
'quantity' => 1,
|
|
]);
|
|
|
|
$response = $this->actingAs($user, 'sanctum')
|
|
->patchJson("/api/modules/sirsoft-ecommerce/cart/{$cart->id}/quantity", ['quantity' => 100])
|
|
->assertStatus(422);
|
|
|
|
$message = json_encode($response->json('errors'), JSON_UNESCAPED_UNICODE);
|
|
|
|
$this->assertStringContainsString('99', $message, '안내 문구가 설정 상한을 말하지 않습니다.');
|
|
$this->assertStringNotContainsString(
|
|
'9,999',
|
|
$message,
|
|
'안내 문구가 실제 상한(99)이 아닌 옛 하드코딩 값(9,999)을 말하고 있습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 비회원 병합은 예외 대신 상한까지 클램프하고 조정 내역을 남기는지 테스트합니다.
|
|
*
|
|
* 로그인 시 자동 병합이라 예외를 던지면 로그인 자체가 실패한다.
|
|
*/
|
|
public function test_guest_merge_clamps_instead_of_throwing(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 5]);
|
|
|
|
$user = User::factory()->create();
|
|
$option = $this->makeOption();
|
|
$cartKey = 'ck_'.str_repeat('m', 32);
|
|
|
|
CartFactory::new()->forOption($option)->create([
|
|
'user_id' => null,
|
|
'cart_key' => $cartKey,
|
|
'quantity' => 10,
|
|
]);
|
|
|
|
$service = app(CartService::class);
|
|
$merged = $service->mergeGuestCartToUser($cartKey, $user->id);
|
|
|
|
$this->assertSame(1, $merged);
|
|
|
|
// 조용한 클램프 금지 — 조정 내역이 반환되어야 한다
|
|
$adjustments = $service->getLastMergeAdjustments();
|
|
$this->assertCount(1, $adjustments);
|
|
$this->assertSame(10, $adjustments[0]['requested']);
|
|
$this->assertSame(5, $adjustments[0]['applied']);
|
|
}
|
|
|
|
/**
|
|
* 병합 응답이 조정 내역을 실제로 사용자에게 전달하는지 테스트합니다.
|
|
*
|
|
* 서비스가 내역을 들고 있어도 응답에 실리지 않으면 사용자 관점에서는 여전히 조용한
|
|
* 클램프다 — 계약의 양 끝을 모두 고정한다.
|
|
*
|
|
* @scenario entry=guest_merge
|
|
*
|
|
* @effects guest_merge_reports_clamp_adjustments
|
|
*/
|
|
public function test_merge_endpoint_exposes_clamp_adjustments(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 5]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption();
|
|
$cartKey = 'ck_'.str_repeat('n', 32);
|
|
|
|
CartFactory::new()->forOption($option)->create([
|
|
'user_id' => null,
|
|
'cart_key' => $cartKey,
|
|
'quantity' => 10,
|
|
]);
|
|
|
|
$response = $this->actingAs($user, 'sanctum')
|
|
->withHeaders(['X-Cart-Key' => $cartKey])
|
|
->postJson('/api/modules/sirsoft-ecommerce/cart/merge')
|
|
->assertStatus(200);
|
|
|
|
$adjustments = $response->json('data.adjustments');
|
|
|
|
$this->assertIsArray($adjustments, '병합 응답에 조정 내역이 없습니다 — 사용자는 수량이 줄어든 것을 알 수 없습니다.');
|
|
$this->assertCount(1, $adjustments);
|
|
$this->assertSame(10, $adjustments[0]['requested']);
|
|
$this->assertSame(5, $adjustments[0]['applied']);
|
|
}
|
|
|
|
/**
|
|
* 비회원 병합이 상품별 구매수량 한도까지 클램프하는지 테스트합니다.
|
|
*
|
|
* 병합은 장바구니 전역 상한(`cart.max_quantity`)만 보고 상품 정책(`max_purchase_qty`)을
|
|
* 보지 않으면, 한도 5짜리 상품을 비회원으로 10개 담고 로그인하는 것만으로 한도를
|
|
* 통과시킬 수 있다 — 담기·수량변경·옵션변경 경로를 모두 막아도 남는 우회로다.
|
|
*
|
|
* @scenario entry=guest_merge
|
|
*
|
|
* @effects guest_merge_clamps_to_product_limit
|
|
*/
|
|
public function test_guest_merge_clamps_to_product_purchase_limit(): void
|
|
{
|
|
// 전역 상한은 넉넉히 — 상품 한도만으로 클램프되어야 한다
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = User::factory()->create();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
$cartKey = 'ck_'.str_repeat('p', 32);
|
|
|
|
CartFactory::new()->forOption($option)->create([
|
|
'user_id' => null,
|
|
'cart_key' => $cartKey,
|
|
'quantity' => 10,
|
|
]);
|
|
|
|
$service = app(CartService::class);
|
|
$service->mergeGuestCartToUser($cartKey, $user->id);
|
|
|
|
$merged = Cart::where('user_id', $user->id)->first();
|
|
$this->assertNotNull($merged);
|
|
$this->assertSame(
|
|
5,
|
|
(int) $merged->quantity,
|
|
'병합이 상품 구매수량 한도를 넘겼습니다 — 비회원으로 담고 로그인하면 한도가 무력화됩니다.'
|
|
);
|
|
|
|
$adjustments = $service->getLastMergeAdjustments();
|
|
$this->assertCount(1, $adjustments);
|
|
$this->assertSame(10, $adjustments[0]['requested']);
|
|
$this->assertSame(5, $adjustments[0]['applied']);
|
|
}
|
|
|
|
/**
|
|
* 병합의 한도 판정이 같은 상품의 기존 회원 장바구니 수량까지 합산하는지 테스트합니다.
|
|
*
|
|
* 담기 경로는 옵션을 나눠 담는 우회를 막으려고 상품 총수량으로 판정한다. 병합만 라인
|
|
* 단위로 판정하면 "회원 장바구니에 4개 + 비회원 장바구니에 다른 옵션 4개" 로 한도 5를
|
|
* 넘길 수 있다.
|
|
*
|
|
* @scenario entry=guest_merge
|
|
*
|
|
* @effects guest_merge_limit_counts_existing_lines
|
|
*/
|
|
public function test_guest_merge_limit_counts_existing_member_cart_lines(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = User::factory()->create();
|
|
$optionA = $this->makeOption(maxPurchaseQty: 5);
|
|
$optionB = ProductOptionFactory::new()->forProduct($optionA->product)->create([
|
|
'selling_price' => 10000,
|
|
'price_adjustment' => 0,
|
|
'stock_quantity' => 1000,
|
|
]);
|
|
$cartKey = 'ck_'.str_repeat('q', 32);
|
|
|
|
CartFactory::new()->forOption($optionA)->create([
|
|
'user_id' => $user->id,
|
|
'cart_key' => 'ck_'.str_repeat('r', 32),
|
|
'quantity' => 4,
|
|
]);
|
|
CartFactory::new()->forOption($optionB)->create([
|
|
'user_id' => null,
|
|
'cart_key' => $cartKey,
|
|
'quantity' => 4,
|
|
]);
|
|
|
|
$service = app(CartService::class);
|
|
$service->mergeGuestCartToUser($cartKey, $user->id);
|
|
|
|
$total = (int) Cart::where('user_id', $user->id)
|
|
->where('product_id', $optionA->product_id)
|
|
->sum('quantity');
|
|
|
|
$this->assertSame(
|
|
5,
|
|
$total,
|
|
'옵션을 나눠 담으면 병합에서 상품 한도를 넘길 수 있습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 재주문에서 구매수량 한도를 넘는 항목이 사유와 함께 제외되는지 테스트합니다.
|
|
*
|
|
* 재주문은 여러 항목을 한 번에 담는 조작이라, 한 항목 때문에 전체를 실패시키면 나머지
|
|
* 정상 항목까지 담기지 않는다. 품절·단종과 동일하게 항목 단위로 제외하고 사유를
|
|
* 돌려주되, 한도를 넘긴 수량이 장바구니에 들어가서는 안 된다.
|
|
*
|
|
* @scenario entry=reorder
|
|
*
|
|
* @effects reorder_skips_items_over_limit_with_reason
|
|
*/
|
|
public function test_reorder_skips_items_over_purchase_limit_with_reason(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
|
|
$order = OrderFactory::new()->create(['user_id' => $user->id]);
|
|
OrderOptionFactory::new()->forOrder($order)->create([
|
|
'product_id' => $option->product_id,
|
|
'product_option_id' => $option->id,
|
|
'parent_option_id' => null,
|
|
'quantity' => 8,
|
|
]);
|
|
|
|
$result = app(CartService::class)->reorderFromOrder($order->id, $user->id);
|
|
|
|
$this->assertSame(0, $result['added_count']);
|
|
$this->assertCount(1, $result['skipped']);
|
|
$this->assertNotSame('', (string) $result['skipped'][0]['reason'], '제외 사유가 비어 있으면 사용자가 이유를 알 수 없습니다.');
|
|
$this->assertSame(0, Cart::where('user_id', $user->id)->count(), '한도를 넘는 수량이 장바구니에 들어갔습니다.');
|
|
}
|
|
|
|
/**
|
|
* 실제 로그인 경로에서 한도 초과 비회원 장바구니가 로그인을 깨뜨리지 않는지 테스트합니다.
|
|
*
|
|
* 병합이 클램프를 택한 유일한 근거가 "로그인 시 자동 병합이라 예외를 던지면 로그인 자체가
|
|
* 실패한다" 였다. 그 근거를 서비스 직접 호출로만 검증하면 실제 로그인 왕복은 무가드다.
|
|
*
|
|
* 동시에 병합이 **실제로 일어났는지**까지 단언한다. 로그인이 200 이어도 장바구니가
|
|
* 넘어오지 않으면 사용자는 담아둔 것을 잃은 채 로그인만 된다 — 조용한 소실이다.
|
|
*
|
|
* @scenario entry=guest_merge
|
|
*
|
|
* @effects guest_merge_clamps_to_product_limit
|
|
*/
|
|
public function test_login_with_over_limit_guest_cart_succeeds_and_merges_clamped(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = User::factory()->create([
|
|
'email' => 'merge-on-login@example.com',
|
|
'password' => Hash::make('correct-password'),
|
|
'status' => 'active',
|
|
]);
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
$cartKey = 'ck_'.str_repeat('s', 32);
|
|
|
|
CartFactory::new()->forOption($option)->create([
|
|
'user_id' => null,
|
|
'cart_key' => $cartKey,
|
|
'quantity' => 10,
|
|
]);
|
|
|
|
$this->withHeaders(['X-Cart-Key' => $cartKey])
|
|
->postJson('/api/auth/login', [
|
|
'email' => 'merge-on-login@example.com',
|
|
'password' => 'correct-password',
|
|
])
|
|
->assertStatus(200);
|
|
|
|
$merged = Cart::where('user_id', $user->id)->first();
|
|
|
|
$this->assertNotNull(
|
|
$merged,
|
|
'로그인은 성공했으나 비회원 장바구니가 회원 계정으로 넘어오지 않았습니다 — 담아둔 항목이 조용히 사라집니다.'
|
|
);
|
|
$this->assertSame(
|
|
5,
|
|
(int) $merged->quantity,
|
|
'병합이 상품 구매수량 한도를 넘겼습니다.'
|
|
);
|
|
$this->assertSame(0, Cart::whereNull('user_id')->where('cart_key', $cartKey)->count());
|
|
}
|
|
|
|
/**
|
|
* 한도 이내 항목은 재주문으로 정상 담기는지 테스트합니다 (제외 로직이 과잉 차단하지 않음).
|
|
*/
|
|
public function test_reorder_adds_items_within_purchase_limit(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$user = $this->createUser();
|
|
$option = $this->makeOption(maxPurchaseQty: 5);
|
|
|
|
$order = OrderFactory::new()->create(['user_id' => $user->id]);
|
|
OrderOptionFactory::new()->forOrder($order)->create([
|
|
'product_id' => $option->product_id,
|
|
'product_option_id' => $option->id,
|
|
'parent_option_id' => null,
|
|
'quantity' => 3,
|
|
]);
|
|
|
|
$result = app(CartService::class)->reorderFromOrder($order->id, $user->id);
|
|
|
|
$this->assertSame(1, $result['added_count']);
|
|
$this->assertSame([], $result['skipped']);
|
|
$this->assertSame(3, (int) Cart::where('user_id', $user->id)->sum('quantity'));
|
|
}
|
|
|
|
/**
|
|
* 무통장 입금기한이 클라이언트 입력이 아니라 서버 설정을 따르는지 테스트합니다 (E6).
|
|
*
|
|
* 수정 전: `dbank.due_days` 를 클라이언트가 보내면 그대로 반영되어 자동취소 스케줄러와
|
|
* 안내 기한이 어긋났다. VBANK 는 이미 서버 설정 단일 SSoT 였다.
|
|
*/
|
|
public function test_dbank_due_date_uses_server_setting_not_client_input(): void
|
|
{
|
|
config(['sirsoft-ecommerce.cart.max_quantity' => 99]);
|
|
|
|
$settingsDir = storage_path('framework/testing/modules/sirsoft-ecommerce/settings');
|
|
if (! is_dir($settingsDir)) {
|
|
mkdir($settingsDir, 0755, true);
|
|
}
|
|
file_put_contents($settingsDir.'/order_settings.json', json_encode(['auto_cancel_days' => 1]));
|
|
|
|
try {
|
|
$option = $this->makeOption();
|
|
$user = $this->createUser();
|
|
|
|
$cart = CartFactory::new()->forUser($user)->forOption($option)->create(['quantity' => 1]);
|
|
|
|
$this->actingAs($user, 'sanctum')
|
|
->postJson('/api/modules/sirsoft-ecommerce/checkout', ['item_ids' => [$cart->id]])
|
|
->assertStatus(201);
|
|
|
|
$this->actingAs($user, 'sanctum')->postJson(
|
|
'/api/modules/sirsoft-ecommerce/user/orders',
|
|
[
|
|
'orderer' => ['name' => '홍길동', 'phone' => '010-1234-5678', 'email' => 'dbank@test.com'],
|
|
'shipping' => [
|
|
'recipient_name' => '김철수',
|
|
'recipient_phone' => '010-9876-5432',
|
|
'country_code' => 'KR',
|
|
'zipcode' => '12345',
|
|
'address' => '서울시 강남구 테헤란로 123',
|
|
'address_detail' => '101동 1001호',
|
|
],
|
|
'payment_method' => 'dbank',
|
|
'expected_total_amount' => 10000,
|
|
'depositor_name' => '홍길동',
|
|
'dbank' => [
|
|
'bank_code' => 'KB',
|
|
'bank_name' => '국민은행',
|
|
'account_number' => '123-456-789012',
|
|
'account_holder' => '주식회사 테스트',
|
|
// 클라이언트가 30일을 보내도 서버 설정(1일)이 이겨야 한다
|
|
'due_days' => 30,
|
|
],
|
|
]
|
|
)->assertStatus(201);
|
|
|
|
$order = Order::where('user_id', $user->id)->latest('id')->first();
|
|
$dueAt = $order->payment->deposit_due_at;
|
|
|
|
$this->assertNotNull($dueAt);
|
|
$this->assertSame(1, (int) round(now()->diffInDays($dueAt, false)));
|
|
} finally {
|
|
$path = $settingsDir.'/order_settings.json';
|
|
if (file_exists($path)) {
|
|
unlink($path);
|
|
}
|
|
}
|
|
}
|
|
}
|