fix(nicepayments): 주문설정 테스트결제 경고 표시 개선

This commit is contained in:
thisgun
2026-07-07 16:38:47 +09:00
parent f744b816b6
commit c6cab1f668
10 changed files with 352 additions and 1 deletions
@@ -9,3 +9,4 @@
### Added
- 가상계좌 환불 검증 안내(`errors.vbank_refund_*` — 환불 대상 주문 없음·환불 불가 결제·환불 금액 불일치·환불 처리 진행 중) 일본어 번역 추가 — 관리자 가상계좌 환불 요청 검증 실패 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 주문 설정 화면의 나이스페이먼츠 테스트모드 경고(`nicepay_test_mode_status`, `test_mode_settings_warning_*`) 일본어 번역 추가 — 테스트결제 상태 안내와 실결제 설정 이동 버튼이 일본어 로케일에서 자연스럽게 표시됩니다.
@@ -4,6 +4,7 @@
"nicepay_easy_pay_config": "簡易決済設定",
"nicepay_escrow": "エスクロー",
"nicepay_status": "決済ステータス",
"nicepay_test_mode_status": "テストモード状態",
"nicepay_test_map": "テスト決済マップ",
"settings": "決済設定",
"vbank_info": "仮想口座情報"
@@ -109,6 +110,10 @@
"escrow_register_error": "エスクロー配送登録に失敗しました。",
"test_mode_badge": "テスト決済",
"test_mode_order_notice": "テスト決済として処理された注文です。実際のカード承認・出金通知が発生する可能性がありますが、NicePaymentsテストアカウント決済は当日23時30分頃一括自動キャンセルされます。配送前に運営決済の有無をご確認ください。",
"test_mode_settings_warning_plugin": "NicePayments",
"test_mode_settings_warning_title": "(注意)NicePayments決済設定が現在テスト決済になっています。",
"test_mode_settings_warning_body": "テスト決済では実際の決済は発生しません。ショップ運営中の場合は必ず実決済に設定して運営してください。",
"test_mode_settings_warning_action": "実決済に設定",
"test_mode_orders_in_list": "テスト決済注文を含む"
},
"easy_pay": {
@@ -27,6 +27,7 @@
- 환불 요청 시 현재 설정값이 아니라 결제 당시 MID와 테스트/운영 모드로 나이스페이먼츠 API 서명 컨텍스트를 복원하도록 수정했습니다.
- 관리자 가상계좌 환불이 PG 취소만 호출하고 주문/결제/환불 장부를 갱신하지 않던 문제를 수정했습니다.
- 결제완료 커밋 후 후속 처리 예외가 발생한 경우 이미 결제완료 상태인 거래에 망취소를 보내지 않도록 보상 처리 가드를 추가했습니다.
- 이커머스 주문설정 화면에서 나이스페이먼츠 플러그인이 테스트 모드일 때 운영 전환 경고와 설정 이동 버튼을 표시하도록 보강했습니다.
## [1.0.0-beta.4] - 2026-06-03
@@ -4,6 +4,7 @@
"nicepay_easy_pay_config": "Easy Pay Config",
"nicepay_escrow": "Escrow",
"nicepay_status": "Payment Status",
"nicepay_test_mode_status": "Test Mode Status",
"nicepay_test_map": "Test Payment Map",
"settings": "Payment Settings",
"vbank_info": "Virtual Account Info"
@@ -109,6 +110,10 @@
"escrow_register_error": "Failed to register escrow delivery.",
"test_mode_badge": "TEST",
"test_mode_order_notice": "This order was processed in test mode. Real card authorization/withdrawal notifications may occur, but NicePayments test account payments are batch-cancelled around 23:30 on the same day. Verify live payment status before shipping.",
"test_mode_settings_warning_plugin": "NicePayments",
"test_mode_settings_warning_title": "Warning: NicePayments is currently set to test mode.",
"test_mode_settings_warning_body": "Test payments do not create real payments. If the shop is live, switch this plugin to live payment mode before operating.",
"test_mode_settings_warning_action": "Switch to live mode",
"test_mode_orders_in_list": "Test payment orders included"
},
"easy_pay": {
@@ -4,6 +4,7 @@
"nicepay_easy_pay_config": "간편결제 설정",
"nicepay_escrow": "에스크로",
"nicepay_status": "결제 상태",
"nicepay_test_mode_status": "테스트 모드 상태",
"nicepay_test_map": "테스트 결제 맵",
"settings": "결제 설정",
"vbank_info": "가상계좌 정보"
@@ -109,6 +110,10 @@
"escrow_register_error": "에스크로 배송 등록에 실패했습니다.",
"test_mode_badge": "테스트 결제",
"test_mode_order_notice": "테스트 결제로 처리된 주문입니다. 실제 카드 승인/출금 알림이 발생할 수 있으나, 나이스페이먼츠 테스트 계정 결제는 당일 23:30경 일괄 자동 취소됩니다. 배송 전 운영 결제 여부를 확인하세요.",
"test_mode_settings_warning_plugin": "나이스페이먼츠",
"test_mode_settings_warning_title": "(주의!) 나이스페이먼츠 결제 설정이 현재 테스트결제로 되어 있습니다.",
"test_mode_settings_warning_body": "테스트결제 시 실제 결제가 되지 않으므로, 쇼핑몰 운영 중이면 반드시 실결제로 설정하여 운영하세요.",
"test_mode_settings_warning_action": "실결제로 설정",
"test_mode_orders_in_list": "테스트 결제 주문 포함"
},
"easy_pay": {
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNicepayments\Controllers;
use App\Helpers\ResponseHelper;
use App\Services\PluginSettingsService;
use Illuminate\Http\JsonResponse;
class AdminSettingsStatusController
{
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nicepayments';
public function __construct(private readonly PluginSettingsService $settingsService) {}
/**
* 테스트 모드 활성 여부만 반환한다.
*
* @return JsonResponse
*/
public function testMode(): JsonResponse
{
return ResponseHelper::success('messages.success', [
'is_test_mode' => (bool) $this->settingsService->get(self::PLUGIN_IDENTIFIER, 'is_test_mode', true),
]);
}
}
@@ -13,6 +13,14 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
{
private const TARGET_LAYOUT = 'admin_ecommerce_settings';
private const TEST_MODE_DATA_SOURCE_ID = 'nicepay_test_mode_status';
private const TEST_MODE_NOTICE_ID = 'payment_test_mode_order_settings_notice';
private const TEST_MODE_NOTICE_ROW_ID = 'nicepay_test_mode_order_settings_notice';
private const TEST_MODE_CONDITION = 'nicepay_test_mode_status.data?.is_test_mode === true';
private const CORE_NO_PG_METHODS = ['point', 'deposit', 'free', 'dbank'];
private const NICEPAY_NO_PG_METHODS = [
@@ -47,6 +55,7 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
/**
* @param array<string, mixed> $layout
* @param int $templateId
* @return array<string, mixed>
*/
public function markEasyPayMethodsAsPgNotRequired(array $layout, int $templateId): array
@@ -55,7 +64,9 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
return $layout;
}
return $this->replaceNoPgMethodExpressions($layout);
$layout = $this->replaceNoPgMethodExpressions($layout);
return $this->ensureTestModeWarning($layout);
}
/**
@@ -103,4 +114,222 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
$expression
) ?? $expression;
}
/**
* @param array<string, mixed> $layout
* @return array<string, mixed>
*/
private function ensureTestModeWarning(array $layout): array
{
$layout = $this->ensureTestModeDataSource($layout);
return $this->insertTestModeNotice($layout);
}
/**
* @param array<string, mixed> $layout
* @return array<string, mixed>
*/
private function ensureTestModeDataSource(array $layout): array
{
$dataSources = is_array($layout['data_sources'] ?? null) ? $layout['data_sources'] : [];
foreach ($dataSources as $source) {
if (is_array($source) && ($source['id'] ?? null) === self::TEST_MODE_DATA_SOURCE_ID) {
$layout['data_sources'] = $dataSources;
return $layout;
}
}
$dataSources[] = [
'id' => self::TEST_MODE_DATA_SOURCE_ID,
'label_key' => '$t:sirsoft-pay_nicepayments.editor.data_source.nicepay_test_mode_status',
'type' => 'api',
'endpoint' => '/api/plugins/sirsoft-pay_nicepayments/admin/settings/test-mode-status',
'method' => 'GET',
'auto_fetch' => true,
'auth_required' => true,
];
$layout['data_sources'] = $dataSources;
return $layout;
}
/**
* @param array<string, mixed> $node
* @return array<string, mixed>
*/
private function insertTestModeNotice(array $node): array
{
if (($node['id'] ?? null) === 'tab_content_order_settings' && is_array($node['children'] ?? null)) {
$noticeIndex = $this->findChildIndexById($node['children'], self::TEST_MODE_NOTICE_ID);
if ($noticeIndex === null) {
$insertAt = $this->findChildIndexById($node['children'], 'payment_methods_card') ?? count($node['children']);
array_splice($node['children'], $insertAt, 0, [$this->testModeNoticeContainerNode()]);
$noticeIndex = $insertAt;
}
if (is_array($node['children'][$noticeIndex] ?? null)) {
$notice = $node['children'][$noticeIndex];
$notice['if'] = $this->mergeTestModeCondition((string) ($notice['if'] ?? ''));
$node['children'][$noticeIndex] = $this->appendTestModeNoticeRow($notice);
}
return $node;
}
foreach ($node as $key => $value) {
if (is_array($value)) {
$node[$key] = $this->insertTestModeNotice($value);
}
}
return $node;
}
/**
* @param array<int, mixed> $children
*/
private function findChildIndexById(array $children, string $id): ?int
{
foreach ($children as $index => $child) {
if (is_array($child) && ($child['id'] ?? null) === $id) {
return $index;
}
}
return null;
}
/**
* @param array<int, mixed> $children
*/
private function hasChildWithId(array $children, string $id): bool
{
return $this->findChildIndexById($children, $id) !== null;
}
private function mergeTestModeCondition(string $condition): string
{
if (str_contains($condition, self::TEST_MODE_CONDITION)) {
return $condition !== '' ? $condition : '{{' . self::TEST_MODE_CONDITION . '}}';
}
$inner = trim($condition);
if (str_starts_with($inner, '{{') && str_ends_with($inner, '}}')) {
$inner = trim(substr($inner, 2, -2));
}
if ($inner === '') {
return '{{' . self::TEST_MODE_CONDITION . '}}';
}
return '{{' . $inner . ' || ' . self::TEST_MODE_CONDITION . '}}';
}
/**
* @param array<string, mixed> $notice
* @return array<string, mixed>
*/
private function appendTestModeNoticeRow(array $notice): array
{
$children = is_array($notice['children'] ?? null) ? $notice['children'] : [];
if (! $this->hasChildWithId($children, self::TEST_MODE_NOTICE_ROW_ID)) {
$children[] = $this->testModeNoticeRowNode();
}
$notice['children'] = $children;
return $notice;
}
/**
* @return array<string, mixed>
*/
private function testModeNoticeContainerNode(): array
{
return [
'id' => self::TEST_MODE_NOTICE_ID,
'type' => 'basic',
'name' => 'Div',
'if' => '{{' . self::TEST_MODE_CONDITION . '}}',
'props' => [
'className' => 'mb-4 space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-4 dark:border-orange-700 dark:bg-orange-900/20',
],
'children' => [
$this->testModeNoticeRowNode(),
],
];
}
/**
* @return array<string, mixed>
*/
private function testModeNoticeRowNode(): array
{
return [
'id' => self::TEST_MODE_NOTICE_ROW_ID,
'type' => 'basic',
'name' => 'Div',
'if' => '{{' . self::TEST_MODE_CONDITION . '}}',
'props' => [
'className' => 'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
],
'children' => [
[
'type' => 'basic',
'name' => 'Div',
'props' => [
'className' => 'min-w-0',
],
'children' => [
[
'type' => 'basic',
'name' => 'Div',
'props' => [
'className' => 'text-sm font-medium text-orange-950 dark:text-orange-50',
],
'text' => '$t:sirsoft-pay_nicepayments.admin.test_mode_settings_warning_plugin',
],
[
'type' => 'basic',
'name' => 'P',
'props' => [
'className' => 'mt-0.5 text-sm leading-6 text-orange-800 dark:text-orange-200',
],
'text' => '$t:sirsoft-pay_nicepayments.admin.test_mode_settings_warning_body',
],
],
],
[
'type' => 'basic',
'name' => 'Button',
'props' => [
'type' => 'button',
'className' => 'btn btn-outline btn-sm w-full flex-shrink-0 border-orange-300 text-orange-900 hover:bg-orange-100 sm:w-auto dark:border-orange-600 dark:text-orange-100 dark:hover:bg-orange-900/40',
],
'actions' => [
[
'type' => 'click',
'handler' => 'navigate',
'params' => [
'path' => '/admin/plugins/sirsoft-pay_nicepayments/settings',
],
],
],
'children' => [
[
'type' => 'basic',
'name' => 'Span',
'text' => '$t:sirsoft-pay_nicepayments.admin.test_mode_settings_warning_action',
],
],
],
],
];
}
}
@@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminEscrowController;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminOrderListController;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminSettingsStatusController;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminTransactionController;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminVbankNotificationController;
use Plugins\Sirsoft\PayNicepayments\Controllers\AdminVbankRefundController;
@@ -39,6 +40,10 @@ Route::prefix('admin')->name('admin.')->middleware(['auth:sanctum', 'admin'])->g
})->middleware('permission:admin,sirsoft-ecommerce.settings.read')
->name('vbank.notify.url');
Route::get('/settings/test-mode-status', [AdminSettingsStatusController::class, 'testMode'])
->middleware('permission:admin,sirsoft-ecommerce.settings.read')
->name('settings.test-mode-status');
// TID 단건 거래 조회
Route::post('/transaction/query', [AdminTransactionController::class, 'query'])
->middleware('permission:admin,sirsoft-ecommerce.orders.read')
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNicepayments\Tests\Feature\Controllers;
use App\Services\PluginSettingsService;
use Plugins\Sirsoft\PayNicepayments\Tests\PluginTestCase;
class AdminSettingsStatusControllerTest extends PluginTestCase
{
public function test_test_mode_status_returns_current_setting(): void
{
$settings = $this->createMock(PluginSettingsService::class);
$settings->expects($this->once())
->method('get')
->with('sirsoft-pay_nicepayments', 'is_test_mode', true)
->willReturn(true);
$this->app->instance(PluginSettingsService::class, $settings);
$admin = $this->createAdminUser(['sirsoft-ecommerce.settings.read']);
$response = $this->actingAs($admin)
->getJson('/api/plugins/sirsoft-pay_nicepayments/admin/settings/test-mode-status');
$response->assertOk()
->assertJsonPath('data.is_test_mode', true);
$data = $response->json('data');
$this->assertArrayNotHasKey('test_mid', $data);
$this->assertArrayNotHasKey('live_mid', $data);
}
}
@@ -62,6 +62,45 @@ class AdjustEcommercePaymentMethodsLayoutListenerTest extends TestCase
$this->assertStringNotContainsString("['point','deposit','free','dbank'].includes", $json);
}
public function test_adds_test_mode_warning_to_order_settings_tab(): void
{
$listener = new AdjustEcommercePaymentMethodsLayoutListener();
$layout = [
'layout_name' => 'admin_ecommerce_settings',
'children' => [
[
'id' => 'tab_content_order_settings',
'children' => [
['id' => 'default_pg_card'],
['id' => 'payment_methods_card'],
],
],
],
];
$result = $listener->markEasyPayMethodsAsPgNotRequired($layout, 1);
$result = $listener->markEasyPayMethodsAsPgNotRequired($result, 1);
$json = json_encode($result, JSON_UNESCAPED_SLASHES);
$this->assertIsString($json);
$this->assertStringContainsString('nicepay_test_mode_status', $json);
$this->assertStringContainsString('/api/plugins/sirsoft-pay_nicepayments/admin/settings/test-mode-status', $json);
$this->assertStringContainsString('payment_test_mode_order_settings_notice', $json);
$this->assertStringContainsString('nicepay_test_mode_order_settings_notice', $json);
$this->assertStringNotContainsString('sirsoft-pay_nicepayments.admin.test_mode_settings_summary_title', $json);
$this->assertStringNotContainsString('sirsoft-pay_nicepayments.admin.test_mode_settings_summary_body', $json);
$this->assertStringContainsString('sirsoft-pay_nicepayments.admin.test_mode_settings_warning_plugin', $json);
$this->assertStringContainsString('sirsoft-pay_nicepayments.admin.test_mode_settings_warning_body', $json);
$this->assertStringContainsString('/admin/plugins/sirsoft-pay_nicepayments/settings', $json);
$this->assertSame(1, substr_count($json, 'payment_test_mode_order_settings_notice'));
$this->assertSame(1, substr_count($json, 'nicepay_test_mode_order_settings_notice'));
$this->assertLessThan(
strpos($json, 'payment_methods_card'),
strpos($json, 'payment_test_mode_order_settings_notice')
);
}
public function test_leaves_other_layouts_unchanged(): void
{
$listener = new AdjustEcommercePaymentMethodsLayoutListener();