Files
Gnuboard7/plugins/_bundled/sirsoft-tosspayments/plugin.php
T
HeuJung a9f85ff92b feat(tosspayments,ecommerce,core): 토스 주문서형 결제수단·가상계좌·에스크로 + API 문서 web 라우트 확장
토스페이먼츠 플러그인을 프로덕션 수준으로 완성한다 (계획서 S4).

주문서형 결제(9종 동적등록)
- 플러그인이 filter_available_payment_methods 로 결제수단을 주입하면 체크아웃에
 독립 항목으로 노출된다. 서버 전송 시에는 core_payment_method 로 번역해 보낸다 —
 코어 PaymentMethodEnum 은 toss_* 를 거부하므로 번역 없이는 주문 자체가 422 로 실패했다.
 번역 근거인 core_payment_method 가 설정 병합/스냅샷의 화이트리스트에서 탈락하던 것을
 provider-agnostic 하게 보존하도록 고쳤다 (토스 전용 분기 없음).

가상계좌·웹훅·에스크로
- 입금통보/결제상태 웹훅 2종 신설. secret 대조(hash_equals)·리플레이 멱등·CSRF 면제.
- 에스크로 3상태(off/on/buyer_choice). escrowProducts 를 가상계좌·계좌이체 양쪽
 SDK 페이로드에 싣는다 — 서버가 조립해도 SDK 로 전달되지 않으면 토스가 결제를 거부한다.
 E2E 가 SDK 경계를 직접 캡처해 이 계약의 나머지 절반을 잠근다.
- 관리자 설정 저장 시 입금기한·에스크로 값 범위를 서버에서 검증한다 (UI max 는 클라 힌트라
 API 직접 호출을 막지 못한다).

레이아웃 리스너 실행 순서
- KG 는 no-PG 리스트를 통짜 리터럴로 str_replace 하므로 토스가 먼저 append 하면 KG 의
 매치가 깨진다. HookManager 는 ksort 오름차순이므로 토스 priority 를 30 으로 두어
 "KG(20) 먼저" 를 플러그인 로드 순서와 무관한 불변식으로 고정했다.

코어 — API 문서 생성기가 확장 web 라우트도 수집
- PG 콜백·웹훅은 CSRF/세션 특성상 web.php 에 등록되지만 외부 시스템이 호출하는
 machine-facing 엔드포인트라 API 레퍼런스 대상이다. api/ prefix 만 수집하던 탓에
 영구 무문서였다. 확장 소유 web 라우트를 수집하되 관리자 화면(admin 컨텍스트)은 제외한다.
 코어 라우트 수집 결과는 291건으로 불변.

하네스 — 확장 테스트 베이스클래스 룰 신설
- coverage.json 의 test-extension-base-class 가 status:todo(룰 미구현)라 규정이 있어도
 검출되지 않았고, 실제로 이번 신설 테스트가 그 사각에 빠졌다. 룰을 구현해 봉인한다.
2026-08-06 12:46:48 +09:00

277 lines
11 KiB
PHP

<?php
namespace Plugins\Sirsoft\Tosspayments;
use App\Extension\AbstractPlugin;
/**
* 토스페이먼츠 PG 플러그인
*
* 토스페이먼츠 통합결제창(카드/간편결제) 연동을 제공합니다.
* sirsoft-ecommerce 모듈 전용 플러그인입니다.
*/
class Plugin extends AbstractPlugin
{
/**
* 플러그인 메타데이터 반환
*
* @return array 메타데이터
*/
public function getMetadata(): array
{
return [
'author' => 'Sirsoft',
'license' => 'MIT',
'homepage' => 'https://sir.kr',
'keywords' => ['payment', 'tosspayments', 'pg', 'card', 'ecommerce'],
];
}
/**
* 환경설정 스키마 반환
*
* @return array 설정 스키마
*/
public function getSettingsSchema(): array
{
return [
'is_test_mode' => [
'type' => 'boolean',
'default' => true,
'label' => ['ko' => '테스트 모드', 'en' => 'Test Mode'],
'hint' => [
'ko' => '테스트 모드에서는 실제 결제가 발생하지 않습니다.',
'en' => 'No real payments occur in test mode.',
],
],
'test_client_key' => [
'type' => 'string',
'default' => '',
'label' => ['ko' => '테스트 클라이언트 키', 'en' => 'Test Client Key'],
'hint' => [
'ko' => '개발자센터 > API 개별 연동 키에서 확인',
'en' => 'Found in Developer Center > API Keys',
],
],
'test_secret_key' => [
'type' => 'string',
'default' => '',
'sensitive' => true,
'label' => ['ko' => '테스트 시크릿 키', 'en' => 'Test Secret Key'],
'hint' => [
'ko' => '외부에 노출되지 않도록 주의하세요.',
'en' => 'Keep this key secret.',
],
],
'live_client_key' => [
'type' => 'string',
'default' => '',
'label' => ['ko' => '라이브 클라이언트 키', 'en' => 'Live Client Key'],
],
'live_secret_key' => [
'type' => 'string',
'default' => '',
'sensitive' => true,
'label' => ['ko' => '라이브 시크릿 키', 'en' => 'Live Secret Key'],
'hint' => [
'ko' => '외부에 노출되지 않도록 주의하세요.',
'en' => 'Keep this key secret.',
],
],
'redirect_success_url' => [
'type' => 'string',
'default' => '/shop/orders/{orderId}/complete',
'label' => ['ko' => '결제 성공 리다이렉트 URL', 'en' => 'Payment Success Redirect URL'],
'hint' => [
'ko' => '상대 경로(/shop/...) 또는 전체 URL(https://...) 모두 가능합니다. {orderId}는 주문번호로 자동 치환됩니다.',
'en' => 'Supports relative paths (/shop/...) or full URLs (https://...). {orderId} will be replaced with the actual order number.',
],
],
'redirect_fail_url' => [
'type' => 'string',
'default' => '/shop/checkout',
'label' => ['ko' => '결제 실패 리다이렉트 URL', 'en' => 'Payment Failure Redirect URL'],
'hint' => [
'ko' => '상대 경로 또는 전체 URL 모두 가능합니다. 오류 정보는 쿼리 파라미터로 자동 추가됩니다.',
'en' => 'Supports relative paths or full URLs. Error details are appended as query parameters.',
],
],
// 결제 방식 — 주문서형(결제수단을 우리 체크아웃에서 선택) vs 결제창형(토스 통합결제창)
'order_sheet_mode' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '주문서형 결제', 'en' => 'Order-sheet Mode'],
'hint' => [
'ko' => '켜면 체크아웃에서 결제수단을 직접 선택합니다. 끄면 토스 통합결제창(카드) 하나로 처리됩니다.',
'en' => 'When ON, payment methods are chosen at checkout. When OFF, a single TossPayments integrated window (card) is used.',
],
],
// 주문서형에서 노출할 결제수단 토글 (order_sheet_mode 가 true 일 때만 유효)
'method_card' => [
'type' => 'boolean',
'default' => true,
'label' => ['ko' => '카드', 'en' => 'Card'],
],
'method_virtual_account' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '가상계좌', 'en' => 'Virtual Account'],
],
'method_transfer' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '계좌이체', 'en' => 'Bank Transfer'],
],
'method_mobile_phone' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '휴대폰', 'en' => 'Mobile Phone'],
],
'method_tosspay' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '토스페이', 'en' => 'TossPay'],
],
'method_kakaopay' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '카카오페이', 'en' => 'KakaoPay'],
],
'method_naverpay' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '네이버페이', 'en' => 'NaverPay'],
],
'method_payco' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '페이코', 'en' => 'PAYCO'],
],
'method_samsungpay' => [
'type' => 'boolean',
'default' => false,
'label' => ['ko' => '삼성페이', 'en' => 'Samsung Pay'],
],
// 가상계좌 옵션
'vbank_valid_hours' => [
'type' => 'integer',
'default' => 24,
'label' => ['ko' => '가상계좌 입금기한(시간)', 'en' => 'Virtual Account Valid Hours'],
'hint' => [
'ko' => '가상계좌 발급 후 입금 가능한 시간입니다. 최대 2160시간(90일).',
'en' => 'Hours available for deposit after issuing a virtual account. Max 2160 (90 days).',
],
],
'vbank_cash_receipt_type' => [
'type' => 'string',
'default' => '',
'label' => ['ko' => '가상계좌 현금영수증 유형', 'en' => 'Virtual Account Cash Receipt Type'],
'hint' => [
'ko' => '가상계좌 발급 시 토스가 자동 발급할 현금영수증 유형입니다. 비워두면 발급하지 않습니다.',
'en' => 'Cash receipt type TossPayments auto-issues when a virtual account is created. Leave blank to skip.',
],
],
// 에스크로 — 3-상태 (가상계좌·계좌이체에만 적용)
'use_escrow' => [
'type' => 'string',
'default' => 'off',
'label' => ['ko' => '에스크로 사용', 'en' => 'Use Escrow'],
'hint' => [
'ko' => '가상계좌·계좌이체 결제에만 적용됩니다. 구매자 선택은 결제창에서 구매자가 직접 결정합니다.',
'en' => 'Applies to virtual account and bank transfer only. "Buyer choice" lets the buyer decide in the payment window.',
],
],
// 가상계좌 입금통보(DEPOSIT_CALLBACK) 웹훅 secret 대조 강제
'webhook_secret_verify' => [
'type' => 'boolean',
'default' => true,
'label' => ['ko' => '웹훅 secret 검증', 'en' => 'Webhook Secret Verification'],
'hint' => [
'ko' => '가상계좌 입금통보 웹훅의 secret 을 결제 승인 응답과 대조해 위조 요청을 차단합니다.',
'en' => 'Verifies the deposit webhook secret against the payment confirmation response to block forged requests.',
],
],
];
}
/**
* 기본 설정값 반환
*
* @return array 기본 설정값
*/
public function getConfigValues(): array
{
return [
'is_test_mode' => true,
'test_client_key' => '',
'test_secret_key' => '',
'live_client_key' => '',
'live_secret_key' => '',
'redirect_success_url' => '/shop/orders/{orderId}/complete',
'redirect_fail_url' => '/shop/checkout',
'order_sheet_mode' => false,
'method_card' => true,
'method_virtual_account' => false,
'method_transfer' => false,
'method_mobile_phone' => false,
'method_tosspay' => false,
'method_kakaopay' => false,
'method_naverpay' => false,
'method_payco' => false,
'method_samsungpay' => false,
'vbank_valid_hours' => 24,
'vbank_cash_receipt_type' => '',
'use_escrow' => 'off',
'webhook_secret_verify' => true,
];
}
/**
* 훅 리스너 클래스 반환
*
* @return array 리스너 클래스 목록
*/
public function getHookListeners(): array
{
return [
Listeners\RegisterPgProviderListener::class,
Listeners\RegisterTossPaymentMethodsListener::class,
Listeners\AdjustEcommercePaymentMethodsLayoutListener::class,
Listeners\PaymentRefundListener::class,
Listeners\ValidateTossSettingsListener::class,
];
}
/**
* 훅 정의 반환
*
* @return array 훅 정의
*/
public function getHooks(): array
{
return [
[
'name' => 'sirsoft-tosspayments.payment.before_confirm',
'type' => 'action',
'description' => [
'ko' => '토스페이먼츠 결제 승인 API 호출 전',
'en' => 'Before TossPayments confirm API call',
],
],
[
'name' => 'sirsoft-tosspayments.payment.after_confirm',
'type' => 'action',
'description' => [
'ko' => '토스페이먼츠 결제 승인 완료 후',
'en' => 'After TossPayments confirm API completed',
],
],
];
}
}