fix(ecommerce,tosspayments,templates): 에스크로 표시 누락·결제수단 전환 시 입력 잔류 수정
- 에스크로 표시가 영구히 렌더되지 않던 문제: is_escrow 가 DB·모델·부분취소 차단 로직에는 있으나 OrderPaymentResource 가 내보내지 않아, 이 값을 참조하는 주문 상세(관리자·구매자)의 에스크로 행이 항상 false 로 평가됐다. Resource 에 노출한다. - 결제수단을 바꿔도 이전 수단의 입력이 주문에 실리던 문제: 결제수단 버튼이 _local.paymentMethod 만 바꾸고 슬롯이 소유한 확장 페이로드·환불계좌는 비우지 않았다. 확장 슬롯과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남으므로, 무통장에서 현금영수증·환불계좌를 입력한 뒤 카드로 전환하면 그 값이 주문 생성 요청에 그대로 실렸다. 프론트는 결제수단 전환 시 슬롯 소유 상태를 초기화하고, 서버는 환불계좌가 쓰이지 않는 결제수단이면 저장하지 않도록 게이팅한다(현금영수증은 이미 dbank 게이트 보유). - Modal 이 받지 않는 size prop 을 쓰던 4곳을 width 로 교체(무시되어 의도한 폭이 나오지 않았다). 토스 에러 모달의 setState target 을 params 안으로 옮긴다. - 레이아웃 편집기 팔레트에서 체크박스를 넣으면 라벨 없이 네모 칸만 놓이던 문제: Checkbox 는 label prop 을 렌더하지 않으므로 Label 래핑 + Span 텍스트로 바꾸고, Accordion 이 Label 을 받도록 중첩 스펙을 맞춘다.
This commit is contained in:
@@ -31,14 +31,14 @@
|
||||
|
||||
#### 신청·발급 화면
|
||||
|
||||
- 주문서의 무통장입금 결제 영역에서 현금영수증을 신청할 수 있습니다. 용도(소득공제/지출증빙)에 따라 고를 수 있는 발급 수단이 바뀌며, 그 용도에 쓸 수 없는 수단이 선택되어 있으면 자동으로 되돌립니다. 발급 업체를 설정하지 않은 상점에서는 신청란이 나타나지 않습니다.
|
||||
- 주문서의 무통장입금 결제 영역에서 현금영수증을 신청할 수 있습니다. 용도(소득공제/지출증빙)에 따라 고를 수 있는 발급 수단이 바뀌며, 그 용도에 쓸 수 없는 수단이 선택되어 있으면 자동으로 되돌립니다. 발급 업체를 설정하지 않은 상점에서는 신청란이 나타나지 않습니다. 결제수단을 다른 것으로 바꾸면 입력했던 신청 내용은 지워집니다.
|
||||
- 구매자 주문 상세에 현금영수증 카드가 표시됩니다. 입금 전에는 안내만, 입금 후 미발급이면 발급 버튼, 발급 후에는 발급일시·용도·식별번호와 영수증 보기 링크가 나타납니다. 환불에 따른 재발급이 실패한 경우에는 안내 문구가 표시됩니다.
|
||||
- 관리자 주문 상세에 현금영수증 카드가 표시됩니다. 발급·발급취소·수동 재발급을 할 수 있고 발급·취소 이력을 함께 볼 수 있습니다. 재발급이 실패한 주문에는 경고와 함께 다시 발급하는 버튼이 나타납니다.
|
||||
- 이커머스 환경설정 주문설정 탭에서 현금영수증 발급 업체, 배송비 과세 방식, 자진발급 사용 여부를 설정할 수 있습니다.
|
||||
|
||||
#### 환불 계좌
|
||||
|
||||
- 주문서의 무통장입금·가상계좌 결제 영역에서 환불받을 계좌(은행·계좌번호·예금주)를 미리 입력할 수 있습니다. 입력하지 않아도 주문할 수 있지만, 세 칸 중 하나라도 입력하면 나머지도 입력해야 합니다.
|
||||
- 주문서의 무통장입금·가상계좌 결제 영역에서 환불받을 계좌(은행·계좌번호·예금주)를 미리 입력할 수 있습니다. 입력하지 않아도 주문할 수 있지만, 세 칸 중 하나라도 입력하면 나머지도 입력해야 합니다. 무통장입금·가상계좌가 아닌 결제수단으로 주문하면 환불 계좌는 저장되지 않습니다 — 계좌를 입력한 뒤 카드 등으로 결제수단을 바꿔도 필요 없는 계좌정보가 주문에 남지 않습니다.
|
||||
- 관리자 주문 취소 화면에서 환불 계좌를 입력하거나 수정할 수 있습니다. 주문할 때 입력한 계좌가 있으면 미리 채워집니다. 입금이 완료된 가상계좌 주문은 환불에 계좌가 반드시 필요하므로 입력하지 않으면 취소할 수 없습니다. 무통장입금은 관리자가 직접 이체할 때 참고하도록 선택 입력이며, 카드·간편결제는 원래 결제수단으로 환불되므로 계좌란이 나타나지 않습니다.
|
||||
|
||||
#### 금액·과세
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-ecommerce.order.cash_receipt.issue_title",
|
||||
"size": "small"
|
||||
"width": "480px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-ecommerce.admin.order.detail.cash_receipt.issue_title",
|
||||
"size": "small"
|
||||
"width": "480px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
|
||||
@@ -90,6 +90,23 @@ enum PaymentMethodEnum: string
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환불 계좌를 받아야 하는 결제수단인지 확인합니다.
|
||||
*
|
||||
* 무통장입금은 관리자가 수동으로 이체할 대상 계좌가 필요하고, 가상계좌는 PG 환불 API 의
|
||||
* refundReceiveAccount 로 계좌가 필요하다. 카드·계좌이체·휴대폰은 원거래 취소로 환불되고
|
||||
* 마일리지·예치금·무료는 내부 처리이므로 계좌가 필요 없다.
|
||||
*
|
||||
* 체크아웃 화면은 결제수단을 바꿔도 입력값을 비우지 않으므로, 계좌가 필요 없는 주문에
|
||||
* 계좌정보가 저장되지 않도록 서버가 이 축으로 게이팅한다.
|
||||
*
|
||||
* @return bool 환불 계좌 필요 여부
|
||||
*/
|
||||
public function needsRefundBankAccount(): bool
|
||||
{
|
||||
return in_array($this, [self::DBANK, self::VBANK], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제수단의 현금성(현금영수증 발급 대상) 금액을 산정합니다.
|
||||
*
|
||||
|
||||
@@ -364,6 +364,13 @@ class CreateOrderRequest extends FormRequest
|
||||
*/
|
||||
public function getRefundBankInfo(): ?array
|
||||
{
|
||||
// 환불 계좌가 쓰이지 않는 결제수단(카드·계좌이체·휴대폰·마일리지·예치금·무료)이면 저장하지 않는다.
|
||||
// 체크아웃 화면은 결제수단을 바꿔도 입력값을 비우지 않으므로, 무통장에서 계좌를 넣고
|
||||
// 카드로 전환하면 그 값이 그대로 전송된다. getCashReceiptInfo() 와 같은 축으로 게이팅한다.
|
||||
if (! PaymentMethodEnum::tryFrom((string) $this->input('payment_method'))?->needsRefundBankAccount()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bankCode = $this->input('refund_bank.bank_code');
|
||||
|
||||
if (blank($bankCode)) {
|
||||
|
||||
@@ -72,6 +72,10 @@ class OrderPaymentResource extends BaseApiResource
|
||||
'vbank_due_at' => $this->vbank_due_at?->toIso8601String(), // audit:allow datetime-display-user-timezone reason: machine ISO8601 parsed by PG plugin JS injectors, display uses *_formatted sibling
|
||||
'vbank_due_at_formatted' => $this->formatDateTimeStringForUser($this->vbank_due_at),
|
||||
|
||||
// 에스크로 여부 — 결제 시점 스냅샷. 부분취소 차단(PaymentRefundListener)의 기준이자
|
||||
// 주문 상세(관리자/회원)에서 에스크로 거래임을 표시하는 값.
|
||||
'is_escrow' => (bool) $this->is_escrow,
|
||||
|
||||
// 무통장입금 정보
|
||||
'dbank_name' => $this->dbank_name,
|
||||
'dbank_account' => $this->dbank_account,
|
||||
|
||||
+56
@@ -348,6 +348,62 @@ class CheckoutCashReceiptAndRefundBankTest extends ModuleTestCase
|
||||
$this->assertNotNull($payment->refund_bank_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환불계좌가 의미를 갖지 않는 결제수단이면 저장하지 않는다.
|
||||
*
|
||||
* 환불계좌는 무통장(관리자 수동 이체 대상)과 가상계좌(PG 환불 API 의 refundReceiveAccount)
|
||||
* 에서만 쓰인다. 카드/간편결제는 원거래 취소로 환불되므로 계좌가 필요 없다.
|
||||
*
|
||||
* 체크아웃 화면은 결제수단을 바꿔도 _local 의 환불계좌 입력값을 비우지 않으므로,
|
||||
* 무통장에서 계좌를 넣고 카드로 전환하면 그 값이 그대로 전송된다. 발급될 수 없는
|
||||
* 주문에 계좌정보를 남기지 않도록 현금영수증(getCashReceiptInfo)과 같은 축으로 게이팅한다.
|
||||
*
|
||||
* @param string $paymentMethod 환불계좌를 쓰지 않는 결제수단
|
||||
*/
|
||||
#[DataProvider('nonRefundBankPaymentMethodProvider')]
|
||||
public function test_환불계좌를_쓰지_않는_결제수단이면_저장하지_않는다(string $paymentMethod): void
|
||||
{
|
||||
$this->postOrder([
|
||||
'payment_method' => $paymentMethod,
|
||||
'dbank' => null,
|
||||
'refund_bank' => [
|
||||
'bank_code' => '004',
|
||||
'account_number' => '110-123-456789',
|
||||
'holder' => '홍길동',
|
||||
],
|
||||
])->assertStatus(201);
|
||||
|
||||
$payment = $this->latestPayment();
|
||||
|
||||
$this->assertNull(
|
||||
$payment->refund_bank_code,
|
||||
"{$paymentMethod} 주문에 환불계좌가 저장되어서는 안 된다"
|
||||
);
|
||||
$this->assertNull($payment->refund_bank_account);
|
||||
$this->assertNull($payment->refund_bank_holder);
|
||||
$this->assertNull($payment->refund_bank_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환불계좌를 쓰지 않는 결제수단 (무통장·가상계좌 제외).
|
||||
*
|
||||
* @return array<string, array{string}>
|
||||
*/
|
||||
public static function nonRefundBankPaymentMethodProvider(): array
|
||||
{
|
||||
$cases = [];
|
||||
|
||||
foreach (PaymentMethodEnum::cases() as $method) {
|
||||
if (in_array($method, [PaymentMethodEnum::DBANK, PaymentMethodEnum::VBANK], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cases[$method->value] = [$method->value];
|
||||
}
|
||||
|
||||
return $cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{array<string, mixed>, array<int, string>}>
|
||||
*/
|
||||
|
||||
+33
@@ -20,6 +20,7 @@
|
||||
* checkout_purpose_switch_resets_invalid_identifier,
|
||||
* checkout_identifier_type_options_by_purpose,
|
||||
* checkout_identifier_cleared_on_type_change,
|
||||
* checkout_state_reset_on_payment_method_change,
|
||||
* new_ui_uses_portable_only,
|
||||
* new_ui_has_no_tailwind_breakpoint
|
||||
*/
|
||||
@@ -313,6 +314,38 @@ test.describe('체크아웃 현금영수증 신청 폼 (무통장 슬롯 주입)
|
||||
await expect(page.locator(IDENTIFIER)).toHaveValue('');
|
||||
});
|
||||
|
||||
test('결제수단을 바꾸면 현금영수증 입력이 초기화된다 (이전 수단 값 잔류 금지)', async ({ page }) => {
|
||||
// 회귀: 결제수단 버튼이 _local.paymentMethod 만 바꾸고 슬롯이 소유한 _local
|
||||
// (checkoutExtraPayload / refundBank*) 은 비우지 않아, 무통장에서 현금영수증을 입력한 뒤
|
||||
// 카드로 전환하면 그 값이 주문 생성 POST 에 그대로 실렸다.
|
||||
await gotoCheckout(page);
|
||||
await selectDbank(page);
|
||||
|
||||
const others = page.locator('[data-testid^="checkout-payment-method-"]:not([data-testid$="-dbank"])');
|
||||
if ((await others.count()) === 0) {
|
||||
test.info().annotations.push({
|
||||
type: 'coverage-gap',
|
||||
description: '무통장 외 활성 결제수단이 없어 전환 시 초기화는 미검증 (환경설정 의존)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 무통장에서 현금영수증 신청 + 번호 입력
|
||||
await page.locator(TOGGLE).check();
|
||||
await page.locator(IDENTIFIER).fill('01012345678');
|
||||
await expect(page.locator(IDENTIFIER)).toHaveValue('01012345678');
|
||||
|
||||
// 다른 결제수단으로 전환 → 슬롯 언마운트
|
||||
await others.first().click();
|
||||
await expect(page.locator(SLOT)).toHaveCount(0);
|
||||
|
||||
// 무통장으로 돌아오면 이전 입력이 남아 있지 않다 (신청 토글이 꺼진 초기 상태)
|
||||
await selectDbank(page);
|
||||
await expect(page.locator(SLOT)).toHaveCount(1);
|
||||
await expect(page.locator(TOGGLE)).not.toBeChecked();
|
||||
await expect(page.locator(FIELDS)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('발급수단을 바꾸면 이전에 입력한 번호가 비워진다', async ({ page }) => {
|
||||
await gotoCheckout(page);
|
||||
await selectDbank(page);
|
||||
|
||||
+25
@@ -50,4 +50,29 @@ class OrderPaymentResourceTest extends ModuleTestCase
|
||||
$this->assertStringContainsString('신한카드', $array['account_info']);
|
||||
$this->assertStringContainsString('1234-****-****-5678', $array['account_info']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 에스크로 여부가 API 로 노출된다.
|
||||
*
|
||||
* 회귀 배경(#454): is_escrow 는 DB·모델·부분취소 차단 로직(PaymentRefundListener)에는
|
||||
* 있었지만 Resource 가 내보내지 않아, 이 값을 참조하는 주문 상세 화면(관리자/회원)의
|
||||
* 에스크로 표시가 항상 false 로 평가되어 렌더되지 않았다.
|
||||
*/
|
||||
public function test_is_escrow_is_exposed(): void
|
||||
{
|
||||
$escrow = OrderPaymentFactory::new()->make([
|
||||
'payment_method' => PaymentMethodEnum::VBANK,
|
||||
'is_escrow' => true,
|
||||
]);
|
||||
|
||||
$normal = OrderPaymentFactory::new()->make([
|
||||
'payment_method' => PaymentMethodEnum::VBANK,
|
||||
'is_escrow' => false,
|
||||
]);
|
||||
|
||||
$request = Request::create('/');
|
||||
|
||||
$this->assertTrue((new OrderPaymentResource($escrow))->toArray($request)['is_escrow']);
|
||||
$this->assertFalse((new OrderPaymentResource($normal))->toArray($request)['is_escrow']);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -139,6 +139,8 @@ effects:
|
||||
- checkout_purpose_switch_resets_invalid_identifier # 소득공제 전환 시 business → phone 리셋
|
||||
- checkout_identifier_type_options_by_purpose # 소득공제 2종 / 지출증빙 3종
|
||||
- checkout_identifier_cleared_on_type_change # 발급수단 변경 시 번호 초기화
|
||||
- checkout_state_reset_on_payment_method_change # 결제수단 전환 시 슬롯 소유 _local 초기화 (이전 수단 값 잔류 금지)
|
||||
- refund_bank_not_stored_for_non_bank_methods # 환불계좌를 쓰지 않는 결제수단이면 서버가 저장하지 않음
|
||||
|
||||
# 주문 생성 저장 (FormRequest 4단계 사슬)
|
||||
- checkout_cash_receipt_request_persisted # is_cash_receipt_requested + 용도 + 수단
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- 가상계좌·계좌이체 결제에 에스크로(구매안전서비스)를 적용할 수 있습니다. 사용 안 함 / 강제 사용 / 구매자 선택 중에서 고를 수 있습니다.
|
||||
- 에스크로 결제 시 결제창에 상품 정보(상품명·단가·수량)를 함께 전달합니다. 가상계좌와 계좌이체 모두에 적용됩니다.
|
||||
- 에스크로를 켜면 설정 화면에 운영 안내가 표시됩니다 — 에스크로 주문은 부분취소가 불가하다는 점과, 배송정보는 토스 상점관리자에서 등록해야 한다는 점을 안내하며 상점관리자로 바로 이동할 수 있습니다.
|
||||
- 에스크로로 결제된 주문은 관리자 주문 상세와 구매자 주문 상세에 에스크로 거래임이 표시됩니다.
|
||||
|
||||
#### 현금영수증
|
||||
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-tosspayments.payment_error_title",
|
||||
"size": "sm"
|
||||
"width": "420px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -54,8 +54,8 @@
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"target": "local",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"paymentErrorMessage": null
|
||||
}
|
||||
},
|
||||
@@ -79,7 +79,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-tosspayments.payment_cancel_title",
|
||||
"size": "sm"
|
||||
"width": "420px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
|
||||
+43
@@ -200,6 +200,49 @@ describe('sirsoft-admin_basic editor-spec defaultNode 공통 디자인', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Checkbox 라벨 렌더 회귀 가드.
|
||||
// Checkbox 컴포넌트는 label prop 을 받기만 하고 렌더하지 않는다(부모 Label 이 표시 담당).
|
||||
// 팔레트가 label prop 만 심으면 편집기 삽입 시 라벨 없는 빈 체크박스가 나온다.
|
||||
describe('Checkbox defaultNode — 라벨이 실제로 렌더된다', () => {
|
||||
it('Checkbox defaultNode 는 label prop 이 아니라 Label 래핑 + Span 텍스트를 쓴다', () => {
|
||||
const node = entries.Checkbox?.defaultNode;
|
||||
expect(node).toBeDefined();
|
||||
|
||||
// 최상위는 Label — Checkbox 를 감싸 클릭 영역과 표시를 함께 제공한다.
|
||||
expect(node?.name).toBe('Label');
|
||||
|
||||
const children = node?.children ?? [];
|
||||
const checkbox = children.find((c) => c.name === 'Checkbox');
|
||||
const span = children.find((c) => c.name === 'Span');
|
||||
|
||||
expect(checkbox, 'Checkbox 자식 누락').toBeDefined();
|
||||
expect(span, '라벨 텍스트 Span 누락').toBeDefined();
|
||||
expect(span?.text).toBe('$t:layout_editor.palette.checkbox.default_label');
|
||||
|
||||
// 렌더되지 않는 label prop 에 의존하지 않는다.
|
||||
expect(node?.props?.label, 'Label 에 label prop 사용 금지').toBeUndefined();
|
||||
expect(checkbox?.props?.label, 'Checkbox 의 label prop 은 렌더되지 않는다').toBeUndefined();
|
||||
});
|
||||
|
||||
it('Checkbox defaultNode 를 렌더하면 라벨 텍스트가 DOM 에 나타난다', async () => {
|
||||
const node = entries.Checkbox?.defaultNode as DefaultNode;
|
||||
const registry = createMockComponentRegistryWithBasics();
|
||||
const t = createLayoutTest(wrapAsLayout(node), {
|
||||
templateId: 'sirsoft-admin_basic',
|
||||
componentRegistry: registry,
|
||||
locale: 'ko',
|
||||
});
|
||||
const { container } = await t.render();
|
||||
|
||||
// 체크박스 input 과 라벨 텍스트가 함께 렌더된다(빈 체크박스 회귀 차단).
|
||||
const label = container.querySelector('label');
|
||||
expect(label, 'Label 래퍼가 렌더되어야 함').not.toBeNull();
|
||||
expect((label?.textContent ?? '').trim().length, '라벨 텍스트가 비어 있음').toBeGreaterThan(0);
|
||||
|
||||
t.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
// defaultNode prop shape 결함 회귀 가드.
|
||||
// 라이브 검증에서 발견한 defaultNode↔컴포넌트 shape 불일치 결함들이 되돌아가지 않게 한다.
|
||||
describe('defaultNode prop shape 정합', () => {
|
||||
|
||||
@@ -673,11 +673,24 @@
|
||||
"requiresDefaultNode": true,
|
||||
"defaultNode": {
|
||||
"type": "basic",
|
||||
"name": "Checkbox",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"label": "$t:layout_editor.palette.checkbox.default_label",
|
||||
"className": "w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500"
|
||||
}
|
||||
"className": "flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Checkbox",
|
||||
"props": {
|
||||
"className": "w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:layout_editor.palette.checkbox.default_label"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"RadioGroup": {
|
||||
|
||||
@@ -466,7 +466,8 @@
|
||||
"Input",
|
||||
"Textarea",
|
||||
"Select",
|
||||
"Checkbox"
|
||||
"Checkbox",
|
||||
"Label"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -18,6 +18,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DataBindingEngine } from '@core/template-engine/DataBindingEngine';
|
||||
import checkoutSummaryJson from '../../layouts/partials/shop/_checkout_summary.json';
|
||||
import checkoutPaymentJson from '../../layouts/partials/shop/_checkout_payment.json';
|
||||
import checkoutJson from '../../layouts/shop/checkout.json';
|
||||
|
||||
/** 객체 트리에서 조건을 만족하는 첫 노드를 깊이우선 탐색 */
|
||||
@@ -231,6 +232,57 @@ describe('주문 생성 body — 확장 병합 칸 계약', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 결제수단 전환 시 이전 수단이 소유하던 입력값이 남아 payload 에 실리던 결함의 회귀 가드.
|
||||
// 확장 슬롯(현금영수증)과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남는다.
|
||||
// 무통장에서 입력 → 카드로 전환 시 그 값이 그대로 주문 생성 POST 에 실렸다.
|
||||
describe('결제수단 전환 — 이전 수단 소유 상태 초기화', () => {
|
||||
it('결제수단 선택 액션이 확장 칸과 환불계좌를 함께 비운다', () => {
|
||||
const methodSelect = findNode(
|
||||
checkoutPaymentJson,
|
||||
(n: any) =>
|
||||
Array.isArray(n?.actions) &&
|
||||
n.actions.some(
|
||||
(a: any) =>
|
||||
a?.type === 'click' &&
|
||||
JSON.stringify(a).includes('_local.paymentMethod'),
|
||||
),
|
||||
);
|
||||
expect(methodSelect, '결제수단 선택 액션 노드를 찾지 못했다').toBeDefined();
|
||||
|
||||
const clickAction = methodSelect.actions.find((a: any) => a?.type === 'click');
|
||||
const inner = clickAction?.params?.actions ?? [];
|
||||
const targets = inner.map((a: any) => a?.params?.target);
|
||||
|
||||
// 결제수단 자체는 설정하고
|
||||
expect(targets).toContain('_local.paymentMethod');
|
||||
// 이전 수단이 소유하던 상태는 비운다
|
||||
expect(targets).toContain('_local.checkoutExtraPayload');
|
||||
expect(targets).toContain('_local.refundBankCode');
|
||||
expect(targets).toContain('_local.refundBankAccount');
|
||||
expect(targets).toContain('_local.refundBankHolder');
|
||||
});
|
||||
|
||||
it('초기화된 상태로 조립한 payload 에는 이전 수단의 값이 남지 않는다', () => {
|
||||
// 무통장에서 현금영수증·환불계좌를 입력한 뒤 카드로 전환된 직후의 _local 상태
|
||||
const ctx = baseCtx({
|
||||
_local: {
|
||||
...baseCtx()._local,
|
||||
paymentMethod: 'card',
|
||||
checkoutExtraPayload: {},
|
||||
refundBankCode: '',
|
||||
refundBankAccount: '',
|
||||
refundBankHolder: '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = evalBody(ctx);
|
||||
|
||||
expect(body.refund_bank).toBeNull();
|
||||
expect(body.cash_receipt_requested).toBeUndefined();
|
||||
expect(body.cash_receipt_identifier).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// #454 — 플러그인 결제수단(toss_*)의 코어 결제수단 전송
|
||||
//
|
||||
|
||||
@@ -146,9 +146,33 @@
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"comment": "결제수단 전환 시 이전 수단이 소유하던 입력값을 함께 비운다. 확장 슬롯(현금영수증 등)과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남으므로, 초기화하지 않으면 무통장에서 입력한 값이 카드 주문 payload 에 그대로 실린다.",
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.paymentMethod", "value": "{{method.id}}" }
|
||||
"handler": "sequence",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.paymentMethod", "value": "{{method.id}}" }
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.checkoutExtraPayload", "value": {} }
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.refundBankCode", "value": "" }
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.refundBankAccount", "value": "" }
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": { "target": "_local.refundBankHolder", "value": "" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user