구간별 배송비의 종료값이 구간에 포함되지 않아 경계값이 무료배송으로 새고, 상품 옵션의 g/cm³ 저장값을 정책의 kg/L 로 환산하지 않아 금액이 1000배로 청구되던 문제를 수정한다. - 구간 매칭을 종료값 오름차순 사다리로 교체 — 시작값은 표시 전용이 되어 기존 저장 데이터의 두 형태 모두에서 정확히 계산된다 - 단위 환산을 배송비 계산 한 지점으로 모으고, 부피무게와 실무게를 같은 kg 단위로 비교하도록 교정 - 계산이 조용히 0원이 되는 설정(구간 미등록·단위값/기준금액 누락· 중간 구간 무제한·계산 API 폴백 0원)을 저장 시점에 차단하고, 런타임 잔여 경로에는 경고 로그를 남긴다 - 연속성 규칙을 정책 타입별로 분리해 소수 구간(2.5kg) 저장을 허용 - 상품 옵션 무게/부피 응답 왕복, 주문 총 무게/부피 실값 기록
424 lines
16 KiB
PHP
424 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Database\Seeders;
|
|
|
|
use Illuminate\Routing\Redirector;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Modules\Sirsoft\Ecommerce\Database\Seeders\Sample\ShippingPolicySeeder;
|
|
use Modules\Sirsoft\Ecommerce\Database\Seeders\ShippingTypeSeeder;
|
|
use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum;
|
|
use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreShippingPolicyRequest;
|
|
use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy;
|
|
use Modules\Sirsoft\Ecommerce\Models\ShippingPolicyCountrySetting;
|
|
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* 배송정책 시더 테스트
|
|
*
|
|
* 배송정책의 국가별 설정(charge_policy, base_fee 등)은
|
|
* ShippingPolicyCountrySetting 테이블에서 관리됩니다.
|
|
*/
|
|
class ShippingPolicySeederTest extends ModuleTestCase
|
|
{
|
|
/**
|
|
* 시더 실행
|
|
*/
|
|
protected function runSeeder(): void
|
|
{
|
|
$this->seed(ShippingPolicySeeder::class);
|
|
}
|
|
|
|
// ========================================
|
|
// 기본 생성 테스트
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_15_policies(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$this->assertEquals(15, ShippingPolicy::count());
|
|
}
|
|
|
|
public function test_seeder_is_idempotent(): void
|
|
{
|
|
$this->runSeeder();
|
|
$this->runSeeder();
|
|
|
|
$this->assertEquals(15, ShippingPolicy::count());
|
|
}
|
|
|
|
// ========================================
|
|
// charge_policy 커버리지 테스트 (countrySettings 기반)
|
|
// ========================================
|
|
|
|
public function test_seeder_covers_all_charge_policies(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
foreach (ChargePolicyEnum::values() as $value) {
|
|
$this->assertTrue(
|
|
ShippingPolicyCountrySetting::where('charge_policy', $value)->exists(),
|
|
"charge_policy '{$value}' 정책이 시더에 포함되어야 합니다."
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_seeder_creates_free_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', 'free')->first();
|
|
|
|
$this->assertNotNull($countrySetting);
|
|
$this->assertEquals(0, (float) $countrySetting->base_fee);
|
|
|
|
$policy = $countrySetting->shippingPolicy;
|
|
$this->assertTrue($policy->is_active);
|
|
}
|
|
|
|
public function test_seeder_creates_fixed_policies(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$fixedSettings = ShippingPolicyCountrySetting::where('charge_policy', 'fixed')->get();
|
|
|
|
// fixed 정책은 2건 (국내 택배, 퀵서비스)
|
|
$this->assertEquals(2, $fixedSettings->count());
|
|
}
|
|
|
|
public function test_seeder_creates_conditional_free_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', 'conditional_free')->first();
|
|
|
|
$this->assertNotNull($countrySetting);
|
|
$this->assertGreaterThan(0, (float) $countrySetting->base_fee);
|
|
$this->assertGreaterThan(0, (float) $countrySetting->free_threshold);
|
|
}
|
|
|
|
public function test_seeder_creates_api_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', 'api')->first();
|
|
|
|
$this->assertNotNull($countrySetting);
|
|
$this->assertNotNull($countrySetting->api_endpoint);
|
|
}
|
|
|
|
// ========================================
|
|
// 구간별(range_*) 정책 테스트 (countrySettings 기반)
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_range_policies_with_tiers(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$rangePolicies = [
|
|
'range_amount' => 5,
|
|
'range_quantity' => 2,
|
|
'range_weight' => 4,
|
|
'range_volume' => 3,
|
|
'range_volume_weight' => 4,
|
|
];
|
|
|
|
foreach ($rangePolicies as $chargePolicy => $expectedTiers) {
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', $chargePolicy)->first();
|
|
|
|
$this->assertNotNull($countrySetting, "{$chargePolicy} 정책이 존재해야 합니다.");
|
|
$this->assertIsArray($countrySetting->ranges, "{$chargePolicy} 정책의 ranges가 배열이어야 합니다.");
|
|
$this->assertArrayHasKey('tiers', $countrySetting->ranges, "{$chargePolicy} 정책에 tiers가 있어야 합니다.");
|
|
$this->assertCount(
|
|
$expectedTiers,
|
|
$countrySetting->ranges['tiers'],
|
|
"{$chargePolicy} 정책의 구간 수가 {$expectedTiers}이어야 합니다."
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_seeder_range_tiers_have_correct_structure(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', 'range_amount')->first();
|
|
$tier = $countrySetting->ranges['tiers'][0];
|
|
|
|
$this->assertArrayHasKey('min', $tier);
|
|
$this->assertArrayHasKey('max', $tier);
|
|
$this->assertArrayHasKey('fee', $tier);
|
|
}
|
|
|
|
/**
|
|
* 시더가 만든 모든 정책이 관리자 저장 검증(StoreShippingPolicyRequest)을 통과해야 한다.
|
|
*
|
|
* 시더는 Model::create 로 검증을 우회하므로, 키 존재만 확인하면 화면에서는 저장조차
|
|
* 불가한 설정이 샘플 데이터로 배포된다 (구간 연속성·필수값 규칙 위반).
|
|
*/
|
|
public function test_seeded_policies_pass_admin_store_validation(): void
|
|
{
|
|
$this->seed(ShippingTypeSeeder::class);
|
|
$this->runSeeder();
|
|
|
|
$policies = ShippingPolicy::with('countrySettings')->get();
|
|
$this->assertTrue($policies->isNotEmpty());
|
|
|
|
foreach ($policies as $policy) {
|
|
$payload = [
|
|
'name' => $policy->name,
|
|
'is_active' => $policy->is_active,
|
|
'is_default' => $policy->is_default,
|
|
'sort_order' => $policy->sort_order,
|
|
'country_settings' => $policy->countrySettings
|
|
->map(fn (ShippingPolicyCountrySetting $cs) => [
|
|
'country_code' => $cs->country_code,
|
|
'shipping_method' => $cs->shipping_method,
|
|
'custom_shipping_name' => $cs->custom_shipping_name,
|
|
'currency_code' => $cs->currency_code,
|
|
'charge_policy' => $cs->charge_policy->value,
|
|
'base_fee' => (float) $cs->base_fee,
|
|
'free_threshold' => $cs->free_threshold !== null ? (float) $cs->free_threshold : null,
|
|
'ranges' => $cs->ranges,
|
|
'api_endpoint' => $cs->api_endpoint,
|
|
'api_request_fields' => $cs->api_request_fields,
|
|
'api_response_fee_field' => $cs->api_response_fee_field,
|
|
'api_config' => $cs->api_config,
|
|
'extra_fee_enabled' => (bool) $cs->extra_fee_enabled,
|
|
'extra_fee_settings' => $cs->extra_fee_settings,
|
|
'extra_fee_multiply' => (bool) $cs->extra_fee_multiply,
|
|
'is_active' => (bool) $cs->is_active,
|
|
])
|
|
->values()
|
|
->all(),
|
|
];
|
|
|
|
$errors = $this->collectStoreValidationErrors($payload);
|
|
|
|
$this->assertSame(
|
|
[],
|
|
$errors,
|
|
"정책 '{$policy->getLocalizedName()}' 이 관리자 저장 검증을 통과하지 못했습니다: "
|
|
.json_encode($errors, JSON_UNESCAPED_UNICODE)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배송정책 저장 요청 검증을 수행하고 에러 메시지 배열을 반환합니다.
|
|
*
|
|
* @param array $payload 저장 요청 페이로드
|
|
* @return array<string, array<int, string>> 필드별 에러 메시지 (통과 시 빈 배열)
|
|
*/
|
|
private function collectStoreValidationErrors(array $payload): array
|
|
{
|
|
$request = StoreShippingPolicyRequest::create('/', 'POST', $payload);
|
|
$request->setContainer(app())->setRedirector(app(Redirector::class));
|
|
|
|
try {
|
|
$request->validateResolved();
|
|
} catch (ValidationException $e) {
|
|
return $e->errors();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// ========================================
|
|
// 단위당(per_*) 정책 테스트 (countrySettings 기반)
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_five_per_unit_policies(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$perUnitSettings = ShippingPolicyCountrySetting::whereIn('charge_policy', [
|
|
'per_quantity',
|
|
'per_weight',
|
|
'per_volume',
|
|
'per_volume_weight',
|
|
'per_amount',
|
|
])->get();
|
|
|
|
$this->assertCount(5, $perUnitSettings);
|
|
}
|
|
|
|
public function test_seeder_per_unit_policies_have_unit_value(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$expectedValues = [
|
|
'per_quantity' => 3,
|
|
'per_weight' => 1,
|
|
'per_volume' => 10,
|
|
'per_volume_weight' => 5,
|
|
'per_amount' => 10000,
|
|
];
|
|
|
|
foreach ($expectedValues as $chargePolicy => $expectedUnit) {
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', $chargePolicy)->first();
|
|
|
|
$this->assertNotNull($countrySetting, "{$chargePolicy} 정책이 존재해야 합니다.");
|
|
$this->assertIsArray($countrySetting->ranges, "{$chargePolicy} 정책의 ranges가 배열이어야 합니다.");
|
|
$this->assertArrayHasKey('unit_value', $countrySetting->ranges, "{$chargePolicy} 정책에 unit_value가 있어야 합니다.");
|
|
$this->assertEquals(
|
|
$expectedUnit,
|
|
$countrySetting->ranges['unit_value'],
|
|
"{$chargePolicy} 정책의 unit_value가 {$expectedUnit}이어야 합니다."
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_seeder_per_unit_policies_have_base_fee(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
foreach (ChargePolicyEnum::perUnitPolicies() as $policy) {
|
|
$countrySetting = ShippingPolicyCountrySetting::where('charge_policy', $policy->value)->first();
|
|
|
|
$this->assertNotNull($countrySetting, "{$policy->value} 정책이 존재해야 합니다.");
|
|
$this->assertGreaterThan(
|
|
0,
|
|
(float) $countrySetting->base_fee,
|
|
"{$policy->value} 정책의 base_fee가 0보다 커야 합니다."
|
|
);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// 다국어 name 필드 테스트
|
|
// ========================================
|
|
|
|
public function test_seeder_policies_have_multilingual_name(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$policies = ShippingPolicy::all();
|
|
|
|
foreach ($policies as $policy) {
|
|
$this->assertIsArray($policy->name, "정책 #{$policy->id}의 name이 배열이어야 합니다.");
|
|
$this->assertArrayHasKey('ko', $policy->name, "정책 #{$policy->id}에 ko 이름이 있어야 합니다.");
|
|
$this->assertArrayHasKey('en', $policy->name, "정책 #{$policy->id}에 en 이름이 있어야 합니다.");
|
|
$this->assertNotEmpty($policy->name['ko'], "정책 #{$policy->id}의 ko 이름이 비어있으면 안됩니다.");
|
|
$this->assertNotEmpty($policy->name['en'], "정책 #{$policy->id}의 en 이름이 비어있으면 안됩니다.");
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// extra_fee 설정 테스트 (countrySettings 기반)
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_policies_with_extra_fee(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$withExtraFee = ShippingPolicyCountrySetting::where('extra_fee_enabled', true)->get();
|
|
|
|
$this->assertEquals(2, $withExtraFee->count());
|
|
|
|
foreach ($withExtraFee as $countrySetting) {
|
|
$this->assertNotNull($countrySetting->extra_fee_settings);
|
|
$this->assertIsArray($countrySetting->extra_fee_settings);
|
|
$this->assertNotEmpty($countrySetting->extra_fee_settings);
|
|
|
|
// 각 추가배송비 설정에 zipcode와 fee가 있는지 확인
|
|
foreach ($countrySetting->extra_fee_settings as $setting) {
|
|
$this->assertArrayHasKey('zipcode', $setting);
|
|
$this->assertArrayHasKey('fee', $setting);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// is_active 테스트
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_one_inactive_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$inactivePolicies = ShippingPolicy::where('is_active', false)->get();
|
|
|
|
$this->assertCount(1, $inactivePolicies);
|
|
|
|
// 비활성 정책의 국가별 설정에서 quick 배송방법 확인
|
|
$countrySetting = $inactivePolicies->first()->countrySettings()->first();
|
|
$this->assertNotNull($countrySetting);
|
|
$this->assertEquals('quick', $countrySetting->shipping_method);
|
|
}
|
|
|
|
// ========================================
|
|
// 해외배송 테스트 (countrySettings 기반)
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_international_shipping_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$usSetting = ShippingPolicyCountrySetting::where('country_code', 'US')
|
|
->first();
|
|
|
|
$this->assertNotNull($usSetting);
|
|
$this->assertEquals('api', $usSetting->charge_policy->value);
|
|
// 통화는 상점 기본 통화(KRW)로 고정 — 해외 국가 설정도 동일
|
|
$this->assertEquals('KRW', $usSetting->currency_code);
|
|
}
|
|
|
|
// ========================================
|
|
// sort_order 테스트
|
|
// ========================================
|
|
|
|
public function test_seeder_policies_have_sequential_sort_order(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$sortOrders = ShippingPolicy::orderBy('sort_order')->pluck('sort_order')->toArray();
|
|
|
|
$this->assertEquals(range(1, 15), $sortOrders);
|
|
}
|
|
|
|
// ========================================
|
|
// 국가별 설정 (countrySettings) 테스트
|
|
// ========================================
|
|
|
|
public function test_seeder_creates_country_settings_for_each_policy(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
$policies = ShippingPolicy::with('countrySettings')->get();
|
|
|
|
foreach ($policies as $policy) {
|
|
$this->assertTrue(
|
|
$policy->countrySettings->isNotEmpty(),
|
|
"정책 #{$policy->id} ({$policy->getLocalizedName()})에 국가별 설정이 있어야 합니다."
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_seeder_international_policy_has_multiple_countries(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
// 해외배송 (DHL) 정책은 US, CN, JP 3개 국가 설정
|
|
$dhlPolicy = ShippingPolicy::whereJsonContains('name->en', 'International Shipping (DHL)')->first();
|
|
$this->assertNotNull($dhlPolicy);
|
|
|
|
$countryCodes = $dhlPolicy->countrySettings->pluck('country_code')->toArray();
|
|
$this->assertContains('US', $countryCodes);
|
|
$this->assertContains('CN', $countryCodes);
|
|
$this->assertContains('JP', $countryCodes);
|
|
}
|
|
|
|
public function test_seeder_domestic_intl_combined_policy_has_two_countries(): void
|
|
{
|
|
$this->runSeeder();
|
|
|
|
// 국내외 복합 배송 정책은 KR, US 2개 국가 설정
|
|
$combinedPolicy = ShippingPolicy::whereJsonContains('name->en', 'Domestic & Intl (Per Volume Weight)')->first();
|
|
$this->assertNotNull($combinedPolicy);
|
|
|
|
$countryCodes = $combinedPolicy->countrySettings->pluck('country_code')->toArray();
|
|
$this->assertContains('KR', $countryCodes);
|
|
$this->assertContains('US', $countryCodes);
|
|
}
|
|
}
|