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

611 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
use Illuminate\Support\Facades\Config;
use InvalidArgumentException;
use Modules\Sirsoft\Ecommerce\Models\Order;
use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* 통화 변환 서비스 Unit 테스트
*/
class CurrencyConversionServiceTest extends ModuleTestCase
{
protected CurrencyConversionService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new CurrencyConversionService;
// 테스트용 통화 설정 주입
$this->setupTestCurrencySettings();
}
/**
* 테스트용 통화 설정을 저장합니다.
*/
protected function setupTestCurrencySettings(): void
{
$settingsPath = storage_path('framework/testing/modules/sirsoft-ecommerce/settings');
if (! is_dir($settingsPath)) {
mkdir($settingsPath, 0755, true);
}
$settings = [
'default_language' => 'ko',
'default_currency' => 'KRW',
'currencies' => [
[
'code' => 'KRW',
'name' => ['ko' => 'KRW (원)', 'en' => 'KRW (Won)'],
'exchange_rate' => null,
'rounding_unit' => '1',
'rounding_method' => 'floor',
'decimal_places' => 0,
'is_default' => true,
],
[
'code' => 'USD',
'name' => ['ko' => 'USD (달러)', 'en' => 'USD (Dollar)'],
'exchange_rate' => 0.85,
'rounding_unit' => '0.01',
'rounding_method' => 'round',
'decimal_places' => 2,
'is_default' => false,
],
[
'code' => 'JPY',
'name' => ['ko' => 'JPY (엔)', 'en' => 'JPY (Yen)'],
'exchange_rate' => 115,
'rounding_unit' => '1',
'rounding_method' => 'floor',
'decimal_places' => 0,
'is_default' => false,
],
[
'code' => 'EUR',
'name' => ['ko' => 'EUR (유로)', 'en' => 'EUR (Euro)'],
'exchange_rate' => 0.78,
'rounding_unit' => '0.01',
'rounding_method' => 'ceil',
'decimal_places' => 2,
'is_default' => false,
],
],
];
file_put_contents(
$settingsPath.'/language_currency.json',
json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
// g7_module_settings() 는 Config::get('g7_settings.modules.{id}') 를 조회함.
// CoreServiceProvider::loadModuleSettingsToConfig 는 활성 모듈에만 적용되므로
// (테스트 환경에서는 모듈이 활성화 상태로 시드되지 않음) Config 를 수동으로 주입한다.
Config::set(
'g7_settings.modules.sirsoft-ecommerce.language_currency',
$settings
);
// 캐시 초기화
$this->service->clearCache();
}
protected function tearDown(): void
{
// 테스트 설정 파일 정리
$settingsFile = storage_path('framework/testing/modules/sirsoft-ecommerce/settings/language_currency.json');
if (file_exists($settingsFile)) {
unlink($settingsFile);
}
parent::tearDown();
}
// ========================================
// 1. 기본 통화 조회 테스트
// ========================================
public function test_it_returns_default_currency(): void
{
// When
$defaultCurrency = $this->service->getDefaultCurrency();
// Then
$this->assertEquals('KRW', $defaultCurrency);
}
// ========================================
// 2. 지원 통화 목록 조회 테스트
// ========================================
public function test_it_returns_supported_currencies(): void
{
// When
$currencies = $this->service->getSupportedCurrencies();
// Then
$this->assertContains('KRW', $currencies);
$this->assertContains('USD', $currencies);
$this->assertContains('JPY', $currencies);
$this->assertContains('EUR', $currencies);
}
// ========================================
// 3. KRW → USD 기본 변환 테스트
// ========================================
public function test_it_converts_krw_to_usd(): void
{
// Given
$basePrice = 100000; // 100,000 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'USD');
// Then
// 100,000 / 1000 * 0.85 = 85.00 (round)
$this->assertEquals(85.00, $result['price']);
$this->assertEquals('USD', $result['currency']);
$this->assertEquals(0.85, $result['exchange_rate']);
}
// ========================================
// 4. KRW → JPY 변환 테스트
// ========================================
public function test_it_converts_krw_to_jpy(): void
{
// Given
$basePrice = 100000; // 100,000 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'JPY');
// Then
// 100,000 / 1000 * 115 = 11,500 (floor)
$this->assertEquals(11500, $result['price']);
$this->assertEquals('JPY', $result['currency']);
$this->assertEquals(115, $result['exchange_rate']);
}
// ========================================
// 5. round 반올림 적용 테스트
// ========================================
public function test_it_applies_round_rounding(): void
{
// Given
$basePrice = 33333; // 33,333 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'USD');
// Then
// 33,333 / 1000 * 0.85 = 28.33305
// round to 0.01 → 28.33
$this->assertEqualsWithDelta(28.33, $result['price'], 0.001);
}
// ========================================
// 6. floor 버림 적용 테스트
// ========================================
public function test_it_applies_floor_rounding(): void
{
// Given
$basePrice = 33333; // 33,333 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'JPY');
// Then
// 33,333 / 1000 * 115 = 3833.295
// floor to 1 → 3833
$this->assertEquals(3833, $result['price']);
}
// ========================================
// 7. ceil 올림 적용 테스트
// ========================================
public function test_it_applies_ceil_rounding(): void
{
// Given
$basePrice = 33333; // 33,333 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'EUR');
// Then
// 33,333 / 1000 * 0.78 = 25.99974
// ceil to 0.01 → 26.00
$this->assertEquals(26.00, $result['price']);
}
// ========================================
// 8. 기본통화는 변환 없이 반환 테스트
// ========================================
public function test_it_returns_base_price_for_default_currency(): void
{
// Given
$basePrice = 100000; // 100,000 KRW
// When
$result = $this->service->convertToCurrency($basePrice, 'KRW');
// Then
$this->assertEquals(100000, $result['price']);
$this->assertEquals('KRW', $result['currency']);
$this->assertArrayNotHasKey('exchange_rate', $result);
}
// ========================================
// 9. 여러 필드 그룹 변환 테스트
// ========================================
public function test_it_converts_multiple_amounts_grouped_by_currency(): void
{
// Given
$amounts = [
'subtotal' => 100000,
'discount' => 10000,
'final_amount' => 90000,
];
// When
$result = $this->service->convertMultipleAmounts($amounts);
// Then
// KRW
$this->assertArrayHasKey('KRW', $result);
$this->assertEquals(100000, $result['KRW']['subtotal']);
$this->assertEquals(10000, $result['KRW']['discount']);
$this->assertEquals(90000, $result['KRW']['final_amount']);
$this->assertTrue($result['KRW']['_meta']['is_default']);
// USD
$this->assertArrayHasKey('USD', $result);
$this->assertEquals(85.00, $result['USD']['subtotal']);
$this->assertEquals(8.50, $result['USD']['discount']);
$this->assertEquals(76.50, $result['USD']['final_amount']);
$this->assertFalse($result['USD']['_meta']['is_default']);
$this->assertEquals(0.85, $result['USD']['_meta']['exchange_rate']);
// JPY
$this->assertArrayHasKey('JPY', $result);
$this->assertEquals(11500, $result['JPY']['subtotal']);
$this->assertEquals(1150, $result['JPY']['discount']);
$this->assertEquals(10350, $result['JPY']['final_amount']);
// EUR
$this->assertArrayHasKey('EUR', $result);
$this->assertEquals(78.00, $result['EUR']['subtotal']);
$this->assertEquals(7.80, $result['EUR']['discount']);
$this->assertEquals(70.20, $result['EUR']['final_amount']);
}
// ========================================
// 10. 미지원 통화 예외 테스트
// ========================================
public function test_it_throws_exception_for_unknown_currency(): void
{
// Given
$basePrice = 100000;
// Then — 번역된 예외 메시지 (sirsoft-ecommerce::exceptions.unknown_currency)
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('지원하지 않는 통화입니다: GBP');
// When
$this->service->convertToCurrency($basePrice, 'GBP');
}
// ========================================
// 추가 테스트: 가격 포맷팅
// ========================================
public function test_it_formats_price_for_each_currency(): void
{
// KRW
$this->assertEquals('100,000원', $this->service->formatPrice(100000, 'KRW'));
// USD
$this->assertEquals('$85.00', $this->service->formatPrice(85.00, 'USD'));
// JPY
$this->assertEquals('¥11,500', $this->service->formatPrice(11500, 'JPY'));
// EUR
$this->assertEquals('€78.00', $this->service->formatPrice(78.00, 'EUR'));
}
// ========================================
// 추가 테스트: 통화 지원 여부 확인
// ========================================
public function test_it_checks_if_currency_is_supported(): void
{
$this->assertTrue($this->service->isSupportedCurrency('KRW'));
$this->assertTrue($this->service->isSupportedCurrency('USD'));
$this->assertTrue($this->service->isSupportedCurrency('JPY'));
$this->assertTrue($this->service->isSupportedCurrency('EUR'));
// GBP is not in our test settings
$this->assertFalse($this->service->isSupportedCurrency('GBP'));
$this->assertFalse($this->service->isSupportedCurrency('XYZ'));
}
// ========================================
// 추가 테스트: 다통화 변환 (convertToMultiCurrency)
// ========================================
public function test_it_converts_single_price_to_all_currencies(): void
{
// Given
$basePrice = 50000;
// When
$result = $this->service->convertToMultiCurrency($basePrice);
// Then
// KRW
$this->assertArrayHasKey('KRW', $result);
$this->assertEquals(50000, $result['KRW']['price']);
$this->assertTrue($result['KRW']['is_default']);
// USD: 50000 / 1000 * 0.85 = 42.50
$this->assertArrayHasKey('USD', $result);
$this->assertEquals(42.50, $result['USD']['price']);
// JPY: 50000 / 1000 * 115 = 5750
$this->assertArrayHasKey('JPY', $result);
$this->assertEquals(5750, $result['JPY']['price']);
// EUR: 50000 / 1000 * 0.78 = 39.00
$this->assertArrayHasKey('EUR', $result);
$this->assertEquals(39.00, $result['EUR']['price']);
}
// ========================================
// 추가 테스트: 소수 자릿수 조회 (getDecimalPlaces)
// ========================================
public function test_it_returns_decimal_places_for_each_currency(): void
{
// KRW: 소수 자릿수 0
$this->assertEquals(0, $this->service->getDecimalPlaces('KRW'));
// USD: 소수 자릿수 2
$this->assertEquals(2, $this->service->getDecimalPlaces('USD'));
// JPY: 소수 자릿수 0
$this->assertEquals(0, $this->service->getDecimalPlaces('JPY'));
// EUR: 소수 자릿수 2
$this->assertEquals(2, $this->service->getDecimalPlaces('EUR'));
}
public function test_it_returns_default_decimal_places_for_unknown_currency(): void
{
// 설정에 없는 통화는 기본값 2 반환
$this->assertEquals(2, $this->service->getDecimalPlaces('GBP'));
$this->assertEquals(2, $this->service->getDecimalPlaces('XYZ'));
}
public function test_format_price_uses_decimal_places_from_settings(): void
{
// KRW: 소수 자릿수 0이므로 정수만 표시
$formatted = $this->service->formatPrice(10000.50, 'KRW');
$this->assertStringNotContainsString('.', $formatted);
// USD: 소수 자릿수 2이므로 소수점 표시
$formatted = $this->service->formatPrice(100.5, 'USD');
$this->assertStringContainsString('.50', $formatted);
}
// ──────────────────────────────────────────────
// resolveSnapshotPaymentCharge (PG/결제 청구 SSoT)
// ──────────────────────────────────────────────
/**
* base=USD 주문 스냅샷 (실제 버그 재현 형태) — KRW 결제통화는 환산 + minor-unit 정수.
*/
private function usdBaseSnapshot(string $orderCurrency): array
{
return [
'base_currency' => 'USD',
'order_currency' => $orderCurrency,
'exchange_rates' => [
// KRW: USD base 기준 환율(공식: base/1000 × rate). $6 → 7058원
'KRW' => ['rate' => 1176470, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
// USD: base 자기자신
'USD' => ['rate' => 1, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2],
// JPY: 현실적 환율 (≈157). $6 → (6/1000)×157000 = 942
'JPY' => ['rate' => 157000, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
// CNY: 환율 0 → 미지원(차단 대상)
'CNY' => ['rate' => 0, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2],
],
];
}
public function test_snapshot_charge_converts_base_usd_to_krw_minor_unit(): void
{
$charge = $this->service->resolveSnapshotPaymentCharge(6.0, $this->usdBaseSnapshot('KRW'));
$this->assertSame('KRW', $charge['currency']);
$this->assertEqualsWithDelta(7058, $charge['amount'], 0.001);
// KRW decimal_places=0 → minor unit = 환산금액 그대로
$this->assertSame(7058, $charge['minor_unit_amount']);
$this->assertSame(0, $charge['decimal_places']);
}
public function test_snapshot_charge_uses_base_amount_when_order_currency_is_base(): void
{
// 결제통화 = base(USD) → 환산 없이 $6, minor unit = $6 × 10^2 = 600 (KG "1달러=100" 규칙)
$charge = $this->service->resolveSnapshotPaymentCharge(6.0, $this->usdBaseSnapshot('USD'));
$this->assertSame('USD', $charge['currency']);
$this->assertEqualsWithDelta(6, $charge['amount'], 0.001);
$this->assertSame(600, $charge['minor_unit_amount']);
$this->assertSame(2, $charge['decimal_places']);
}
public function test_snapshot_charge_converts_base_usd_to_jpy_integer(): void
{
$charge = $this->service->resolveSnapshotPaymentCharge(6.0, $this->usdBaseSnapshot('JPY'));
$this->assertSame('JPY', $charge['currency']);
// (6/1000)×157000 = 942, floor, dp 0
$this->assertSame(942, $charge['minor_unit_amount']);
$this->assertSame(0, $charge['decimal_places']);
}
public function test_snapshot_charge_throws_for_zero_rate_currency(): void
{
$this->expectException(InvalidArgumentException::class);
$this->service->resolveSnapshotPaymentCharge(6.0, $this->usdBaseSnapshot('CNY'));
}
public function test_snapshot_charge_throws_for_currency_absent_from_snapshot(): void
{
// 스냅샷 exchange_rates 에 없는 통화 → 환율 0 폴백 → 차단
$this->expectException(InvalidArgumentException::class);
$this->service->resolveSnapshotPaymentCharge(6.0, $this->usdBaseSnapshot('XXX'));
}
public function test_order_payment_charge_amount_converts_base_to_order_currency(): void
{
// 주문 래퍼: total_due_amount(base USD 6) + 스냅샷(order KRW) → 7058원(KRW minor unit)
$order = new Order;
$order->total_due_amount = 6.0;
$order->currency_snapshot = $this->usdBaseSnapshot('KRW');
$this->assertSame(7058, $this->service->resolveOrderPaymentChargeAmount($order));
}
public function test_order_payment_charge_amount_uses_base_when_order_is_base(): void
{
// base==order(USD) → 환산 없이 base 금액 그대로 minor unit (6 → 600)
$order = new Order;
$order->total_due_amount = 6.0;
$order->currency_snapshot = $this->usdBaseSnapshot('USD');
$this->assertSame(600, $this->service->resolveOrderPaymentChargeAmount($order));
}
// ──────────────────────────────────────────────
// base_unit (통화별 기준 단위 — MP08-3 방향 B)
// ──────────────────────────────────────────────
/**
* USD base + base_unit 명시 설정 (¥0 버그 재현/정상화 검증용).
*/
private function usdBaseUnitSettings(): array
{
return [
['code' => 'USD', 'is_default' => true, 'base_unit' => 1, 'exchange_rate' => null, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2],
['code' => 'JPY', 'is_default' => false, 'base_unit' => 100, 'exchange_rate' => 157, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
['code' => 'KRW', 'is_default' => false, 'base_unit' => 1000, 'exchange_rate' => 1300, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
['code' => 'EUR', 'is_default' => false, 'base_unit' => 1, 'exchange_rate' => 0.92, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2],
];
}
private function injectSettings(array $currencies, string $default): void
{
$payload = ['default_currency' => $default, 'currencies' => $currencies];
Config::set('g7_settings.modules.sirsoft-ecommerce.language_currency', $payload);
$this->service->clearCache();
}
public function test_get_base_unit_reads_setting_then_falls_back(): void
{
// 기본 픽스처(KRW base, base_unit 미설정) → 폴백: KRW=1000, JPY=100, USD/EUR=1
$this->assertSame(1000, $this->service->getBaseUnit('KRW'));
$this->assertSame(100, $this->service->getBaseUnit('JPY'));
$this->assertSame(1, $this->service->getBaseUnit('USD'));
$this->assertSame(1, $this->service->getBaseUnit('EUR'));
// 설정에 없는 통화 → 폴백 테이블 없으면 1
$this->assertSame(1, $this->service->getBaseUnit('GBP'));
}
public function test_get_base_unit_prefers_explicit_setting_over_fallback(): void
{
$this->injectSettings($this->usdBaseUnitSettings(), 'USD');
$this->assertSame(1, $this->service->getBaseUnit('USD'));
$this->assertSame(100, $this->service->getBaseUnit('JPY'));
$this->assertSame(1000, $this->service->getBaseUnit('KRW'));
$this->assertSame(1, $this->service->getDefaultBaseUnit()); // 기본=USD
}
public function test_usd_base_converts_without_thousand_divisor_regression_jpy_zero(): void
{
// 회귀: base=USD, base_unit=1 일 때 $3.20 → JPY 가 0 이 아니어야 함
$this->injectSettings($this->usdBaseUnitSettings(), 'USD');
// $3 → JPY: (3 / 1) × 157 = 471 (floor)
$jpy = $this->service->convertToCurrency(3, 'JPY');
$this->assertSame(471.0, (float) $jpy['price']);
$this->assertNotSame(0.0, (float) $jpy['price'], 'USD base 환산이 ÷1000 잔재로 0 이 되면 안 됨');
// $3 → KRW: (3 / 1) × 1300 = 3900
$this->assertSame(3900.0, (float) $this->service->convertToCurrency(3, 'KRW')['price']);
// $3 → EUR: (3 / 1) × 0.92 = 2.76
$this->assertEqualsWithDelta(2.76, (float) $this->service->convertToCurrency(3, 'EUR')['price'], 0.001);
}
public function test_krw_base_unit_1000_is_equivalent_to_legacy_formula(): void
{
// 등가성: KRW base + base_unit=1000 → 옛 ÷1000 공식과 동일 결과
$currencies = [
['code' => 'KRW', 'is_default' => true, 'base_unit' => 1000, 'exchange_rate' => null, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
['code' => 'USD', 'is_default' => false, 'base_unit' => 1, 'exchange_rate' => 0.85, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2],
['code' => 'JPY', 'is_default' => false, 'base_unit' => 100, 'exchange_rate' => 115, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
];
$this->injectSettings($currencies, 'KRW');
// 53000 / 1000 × 0.85 = 45.05
$this->assertEqualsWithDelta(45.05, (float) $this->service->convertToCurrency(53000, 'USD')['price'], 0.001);
// 53000 / 1000 × 115 = 6095
$this->assertSame(6095.0, (float) $this->service->convertToCurrency(53000, 'JPY')['price']);
}
public function test_snapshot_base_unit_falls_back_to_1000_for_legacy_snapshot(): void
{
// base_unit 미박제 옛 스냅샷 → 폴백 1000 (÷1000 공식 유지, 환차손 0)
$this->assertSame(1000, $this->service->getSnapshotBaseUnit($this->usdBaseSnapshot('JPY')));
}
public function test_snapshot_base_unit_reads_embedded_value(): void
{
$snap = $this->usdBaseSnapshot('JPY');
$snap['base_unit'] = 1; // 새 주문: base=USD, base_unit=1 박제
$this->assertSame(1, $this->service->getSnapshotBaseUnit($snap));
}
public function test_snapshot_charge_uses_embedded_base_unit_new_formula(): void
{
// 새 스냅샷: base=USD, base_unit=1, JPY rate=157 (1달러=157엔) → $6 → ¥942
$snap = [
'base_currency' => 'USD',
'order_currency' => 'JPY',
'base_unit' => 1,
'exchange_rates' => [
'USD' => ['rate' => 1, 'rounding_unit' => '0.01', 'rounding_method' => 'round', 'decimal_places' => 2, 'base_unit' => 1],
'JPY' => ['rate' => 157, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0],
],
];
$charge = $this->service->resolveSnapshotPaymentCharge(6.0, $snap);
// (6 / 1) × 157 = 942
$this->assertSame(942, $charge['minor_unit_amount']);
$this->assertSame('JPY', $charge['currency']);
}
}