Saleor Core: Payment & Checkout 모듈 분석

한국 PG 연동(토스페이먼츠, 나이스페이 등)을 위한 포크 기반 통합 가이드


1. 아키텍처 개요

Saleor두 가지 결제 흐름을 동시에 지원한다:

흐름모델용도한국 PG 권장
Payment Flow (레거시)Payment + Transaction빌트인 게이트웨이 플러그인 (Stripe, Dummy 등)X
Transaction Flow (신규)TransactionItem + TransactionEvent외부 앱 기반 결제 처리, Saleor App으로 연동O

채널 설정의 order_mark_as_paid_strategyTRANSACTION_FLOW이면 Transaction Flow, 아니면 Payment Flow를 사용한다.

권장: 한국 PG 연동은 Transaction Flow를 사용하는 것이 적합하다. Payment Flow는 deprecated 경향이 있고, Transaction Flow가 비동기 webhook 기반 처리에 더 적합하다.


2. Payment 모듈 상세 분석

2.1 핵심 상수/열거형 (saleor/payment/__init__.py)

# TransactionKind (Payment Flow에서 사용)
class TransactionKind:
    EXTERNAL = "external"   # 외부 참조
    AUTH = "auth"           # 인가 (금액 홀드)
    CAPTURE = "capture"     # 캡처 (실제 청구)
    VOID = "void"           # 인가 취소
    REFUND = "refund"       # 환불
    CONFIRM = "confirm"     # 확인 (3DS 등)
    PENDING = "pending"     # 대기
    CANCEL = "cancel"       # 취소
 
# TransactionAction (Transaction Flow에서 사용)
class TransactionAction:
    CHARGE = "charge"       # 청구
    REFUND = "refund"       # 환불
    CANCEL = "cancel"       # 취소
 
# ChargeStatus
class ChargeStatus:
    NOT_CHARGED = "not-charged"
    PENDING = "pending"
    PARTIALLY_CHARGED = "partially-charged"
    FULLY_CHARGED = "fully-charged"
    PARTIALLY_REFUNDED = "partially-refunded"
    FULLY_REFUNDED = "fully-refunded"
    REFUSED = "refused"
    CANCELLED = "cancelled"
 
# TransactionEventType (Transaction Flow 이벤트)
class TransactionEventType:
    AUTHORIZATION_SUCCESS / FAILURE / ADJUSTMENT / REQUEST
    CHARGE_SUCCESS / FAILURE / BACK / REQUEST
    REFUND_SUCCESS / FAILURE / REVERSE / REQUEST
    CANCEL_SUCCESS / FAILURE / REQUEST
    INFO

2.2 모델: Payment (레거시) — saleor/payment/models.py:273-459

Payment Flow에서 사용하는 결제 정보 모델.

필드타입설명
gatewayCharField(255)게이트웨이 식별자 (예: mirumee.payments.stripe)
is_activeBooleanField활성 여부
to_confirmBooleanField3DS 등 추가 인증 필요 여부
charge_statusCharField(20)ChargeStatus
tokenCharField(512)PG사 토큰
totalDecimalField결제 총액
captured_amountDecimalField캡처된 금액
currencyCharField통화 코드
checkoutFK → Checkoutnull 가능, SET_NULL
orderFK → Ordernull 가능, PROTECT
store_payment_methodCharField(11)ON_SESSION / OFF_SESSION / NONE
billing_*다수청구 주소 비정규화 필드들
cc_*다수카드 정보 (first_digits, last_digits, brand, exp)
psp_referenceCharField(512)PG사 참조 ID
return_urlURLField결제 완료 후 리다이렉트 URL
extra_dataTextField추가 데이터 (JSON 문자열)

주요 메서드:

  • can_authorize() — 인가 가능 여부 (is_active and not_charged)
  • can_capture() — 캡처 가능 여부
  • can_void() — 인가 취소 가능 여부
  • can_refund() — 환불 가능 여부 (PARTIALLY/FULLY_CHARGED, PARTIALLY_REFUNDED)
  • get_authorized_amount() — 인가 금액 계산 (트랜잭션 기반)

2.3 모델: Transaction (레거시) — saleor/payment/models.py:461-509

Payment의 개별 결제 작업을 나타내는 모델.

필드타입설명
paymentFK → PaymentPROTECT
tokenCharField(512)작업 토큰
kindCharField(25)TransactionKind
is_successBooleanField성공 여부
action_requiredBooleanField추가 액션 필요 (3DS 등)
action_required_dataJSONField추가 액션에 필요한 데이터
amountDecimalField금액
errorTextField에러 메시지
gateway_responseJSONFieldPG사 원시 응답 (deprecated)

2.4 모델: TransactionItem (신규) — saleor/payment/models.py:31-203

Transaction Flow의 핵심 모델. 결제 상태를 이벤트 기반으로 추적한다.

필드타입설명
tokenUUIDField(unique)고유 토큰
psp_referenceCharField(512)PG사 참조 ID — 한국 PG 연동 시 핵심
nameCharField(512)결제 수단 이름
messageCharField(512)상태 메시지
available_actionsArrayField가능한 액션 목록 (charge, refund, cancel)
currencyCharField통화
authorized_valueDecimal인가 금액
charged_valueDecimal청구 금액
refunded_valueDecimal환불 금액
canceled_valueDecimal취소 금액
authorize_pending_valueDecimal인가 보류 금액
charge_pending_valueDecimal청구 보류 금액
refund_pending_valueDecimal환불 보류 금액
cancel_pending_valueDecimal취소 보류 금액
checkoutFK → CheckoutSET_NULL
orderFK → OrderPROTECT
appFK → App결제 앱
app_identifierCharField(256)앱 식별자 (재설치 시 매칭용)
last_refund_successBooleanField마지막 환불 성공 여부
cc_* / payment_method_*다수카드/결제수단 정보
external_urlURLFieldPG사 관리 페이지 URL

제약 조건:

  • (app_identifier, idempotency_key) UNIQUE — 멱등성 보장

2.5 모델: TransactionEventsaleor/payment/models.py:206-270

TransactionItem의 이벤트 로그. 모든 결제 상태 변경이 이벤트로 기록된다.

필드타입설명
transactionFK → TransactionItemCASCADE
typeCharField(128)TransactionEventType
amount_valueDecimal이벤트 금액
psp_referenceCharField(512)PG사 참조 ID (이벤트별)
include_in_calculationsBooleanField금액 계산 포함 여부
related_granted_refundFK → OrderGrantedRefund환불 승인 연관
app / app_identifier이벤트 생성 앱
userFK → User이벤트 트리거 사용자

제약 조건:

  • (transaction_id, idempotency_key) UNIQUE

2.6 모델 관계도

erDiagram
    Checkout ||--o{ Payment : "payments (레거시)"
    Checkout ||--o{ TransactionItem : "payment_transactions (신규)"
    Payment ||--o{ Transaction : "transactions"
    TransactionItem ||--o{ TransactionEvent : "events"
    Order ||--o{ Payment : "payments"
    Order ||--o{ TransactionItem : "payment_transactions"
    TransactionItem }o--|| App : "app"
    TransactionEvent }o--|| App : "app"
    TransactionEvent }o--o| OrderGrantedRefund : "related_granted_refund"
    Payment }o--o| Checkout : "checkout"
    Payment }o--o| Order : "order"
    TransactionItem }o--o| Checkout : "checkout"
    TransactionItem }o--o| Order : "order"
    Checkout }o--|| Channel : "channel"

    Checkout {
        UUID token PK
        FK user
        FK channel
        FK billing_address
        FK shipping_address
        Decimal total_gross_amount
        string authorize_status
        string charge_status
    }

    Payment {
        string gateway
        boolean is_active
        string charge_status
        Decimal total
        Decimal captured_amount
        string token
    }

    Transaction {
        FK payment
        string kind
        boolean is_success
        Decimal amount
    }

    TransactionItem {
        UUID token
        string psp_reference
        Decimal authorized_value
        Decimal charged_value
        Decimal refunded_value
        Decimal canceled_value
        FK app
        string app_identifier
    }

    TransactionEvent {
        FK transaction
        string type
        Decimal amount_value
        string psp_reference
        boolean include_in_calculations
    }

3. Gateway Interface — saleor/payment/interface.py

3.1 핵심 데이터 클래스

PaymentData (L377-417) — Payment Flow 게이트웨이에 전달되는 결제 정보

@dataclass
class PaymentData:
    gateway: str              # 게이트웨이 식별자
    amount: Decimal           # 결제 금액
    currency: str             # 통화 코드
    billing: AddressData | None
    shipping: AddressData | None
    payment_id: int           # Payment 모델 ID
    order_id: str | None
    customer_ip_address: str | None
    customer_email: str
    token: str | None         # PG사 토큰
    checkout_token: str | None
    psp_reference: str | None
    store_payment_method: StorePaymentMethodEnum
    refund_data: RefundData | None
    transactions: list[TransactionData]

GatewayResponse (L303-326) — 게이트웨이 응답 통합 포맷

@dataclass
class GatewayResponse:
    is_success: bool          # 성공 여부
    action_required: bool     # 추가 인증 필요 (3DS, 리다이렉트 등)
    kind: str                 # TransactionKind 값
    amount: Decimal
    currency: str
    transaction_id: str       # PG사 트랜잭션 ID
    error: str | None
    payment_method_info: PaymentMethodInfo | None
    action_required_data: JSONType | None  # 리다이렉트 URL 등
    psp_reference: str | None

GatewayConfig (L428-443) — 게이트웨이 설정

@dataclass
class GatewayConfig:
    gateway_name: str
    auto_capture: bool        # 자동 캡처 여부
    supported_currencies: str # 지원 통화 (쉼표 구분)
    connection_params: dict   # API 키 등 연결 파라미터
    store_customer: bool
    require_3d_secure: bool

TransactionSessionData (L183-189) — Transaction Flow 세션 데이터

@dataclass
class TransactionSessionData:
    transaction: TransactionItem
    source_object: Checkout | Order  # 체크아웃 또는 주문
    action: TransactionProcessActionData  # action_type, amount, currency
    payment_gateway_data: PaymentGatewayData
    customer_ip_address: str | None
    idempotency_key: str | None

TransactionActionData (L112-118) — Transaction 액션 요청 데이터

@dataclass
class TransactionActionData:
    action_type: str              # "charge", "refund", "cancel"
    transaction: TransactionItem
    event: TransactionEvent
    transaction_app_owner: App | None
    action_value: Decimal
    granted_refund: OrderGrantedRefund | None

4. 게이트웨이 구현 패턴

4.1 빌트인 게이트웨이 목록 (saleor/payment/gateways/)

디렉토리설명
dummy/테스트용 더미 게이트웨이
dummy_credit_card/테스트용 신용카드 더미
stripe/Stripe 연동 (레거시 Payment Flow)
braintree/Braintree 연동
razorpay/Razorpay 연동
authorize_net/Authorize.net 연동

4.2 Dummy 게이트웨이 구현 패턴 (saleor/payment/gateways/dummy/__init__.py)

각 게이트웨이는 표준 함수를 구현해야 한다:

# 필수 구현 함수 시그니처
def authorize(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """인가 — 금액을 홀드한다."""
    return GatewayResponse(
        is_success=True,
        action_required=False,
        kind=TransactionKind.AUTH,
        amount=payment_information.amount,
        currency=payment_information.currency,
        transaction_id=payment_information.token,
        error=None,
        payment_method_info=PaymentMethodInfo(...),
    )
 
def capture(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """캡처 — 홀드된 금액을 실제 청구한다."""
 
def void(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """취소 — 인가를 취소한다."""
 
def refund(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """환불 — 청구된 금액을 환불한다."""
 
def confirm(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """확인 — 3DS 등 추가 인증 후 결제를 확정한다."""
 
def process_payment(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
    """단일 호출로 결제 처리 (authorize → capture 일괄)."""
 
def get_client_token(**_) -> str:
    """클라이언트 사이드 토큰 생성."""

4.3 플러그인 등록 (saleor/payment/gateways/dummy/plugin.py)

class DeprecatedDummyGatewayPlugin(BasePlugin):
    PLUGIN_ID = "mirumee.payments.dummy"
    PLUGIN_NAME = "Dummy"
    DEFAULT_CONFIGURATION = [
        {"name": "Store customers card", "value": False},
        {"name": "Automatic payment capture", "value": True},
        {"name": "Supported currencies", "value": "USD, PLN"},
    ]
 
    def authorize_payment(self, payment_information, previous_value) -> GatewayResponse:
        return authorize(payment_information, self._get_gateway_config())
 
    def capture_payment(self, payment_information, previous_value) -> GatewayResponse:
        return capture(payment_information, self._get_gateway_config())
 
    # process_payment, refund_payment, void_payment, confirm_payment 등

4.4 Stripe 웹훅 처리 (saleor/payment/gateways/stripe/webhooks.py)

Stripe 웹훅 핸들러는 다음 이벤트를 처리한다:

  • WEBHOOK_SUCCESS_EVENT → charge 성공
  • WEBHOOK_AUTHORIZED_EVENT → 인가 성공
  • WEBHOOK_CANCELED_EVENT → 취소
  • WEBHOOK_FAILED_EVENT → 실패
  • WEBHOOK_REFUND_EVENT → 환불
  • WEBHOOK_PROCESSING_EVENT → 처리 중

5. Transaction 금액 재계산 (saleor/payment/transaction_item_calculations.py)

Transaction Flow에서 TransactionItem의 금액은 TransactionEvent 이벤트들을 기반으로 재계산된다.

5.1 계산 로직

def recalculate_transaction_amounts(transaction: TransactionItem, save=True):
    """이벤트 기반 금액 재계산.
 
    1. 모든 이벤트를 created_at 순으로 조회
    2. psp_reference 기준으로 그룹핑
    3. 각 액션 타입별 (auth/charge/refund/cancel) 금액 계산
    4. pending 금액: request만 있고 success/failure 없을 때
    5. 확정 금액: success 이벤트 기준 (failure보다 최신일 때만)
    """

5.2 이벤트-금액 매핑

AUTHORIZATION_REQUEST → authorize_pending_value ↑
AUTHORIZATION_SUCCESS → authorized_value ↑
CHARGE_REQUEST → charge_pending_value ↑, authorized_value ↓
CHARGE_SUCCESS → charged_value ↑, authorized_value ↓
CHARGE_BACK → charged_value ↓
REFUND_REQUEST → refund_pending_value ↑
REFUND_SUCCESS → refunded_value ↑, charged_value ↓
REFUND_REVERSE → refunded_value ↓, charged_value ↑
CANCEL_REQUEST → cancel_pending_value ↑
CANCEL_SUCCESS → canceled_value ↑, authorized_value ↓

6. Checkout 모듈 상세 분석

6.1 모델: Checkoutsaleor/checkout/models.py:110-432

필드타입설명
tokenUUIDField (PK)체크아웃 고유 식별자
userFK → User로그인 사용자
channelFK → Channel판매 채널
emailEmailField비회원 이메일
currencyCharField통화
countryCountryField국가 코드
billing_addressFK → Address청구 주소
shipping_addressFK → Address배송 주소
shipping_methodFK → ShippingMethod배송 방법
total_gross_amountDecimal총액 (세금 포함)
total_net_amountDecimal총액 (세금 미포함)
subtotal_*Decimal소계
shipping_price_*Decimal배송비
discount_amountDecimal할인 금액
authorize_statusCharField(32)NONE / PARTIAL / FULL
charge_statusCharField(32)NONE / PARTIAL / FULL / OVERCHARGED
completing_started_atDateTime완료 프로세스 시작 시간 (락 용도)
voucher_codeCharField적용된 바우처 코드
gift_cardsM2M → GiftCard적용된 기프트카드
redirect_urlURLField완료 후 리다이렉트 URL
language_codeCharField언어 코드
tax_exemptionBooleanField면세 여부
price_expirationDateTime가격 캐시 만료 시간
last_transaction_modified_atDateTime최신 TransactionItem 수정 시간
automatically_refundableBooleanField자동 환불 가능 여부

핵심 메서드:

  • is_checkout_locked() — 완료 프로세스 진행 중인지 확인 (completing_started_at 기반)
  • get_last_active_payment() — 마지막 활성 Payment 반환
  • get_total_gift_cards_balance() — 기프트카드 잔액 합산
  • safe_update()SELECT FOR UPDATE 기반 안전한 업데이트

6.2 모델: CheckoutLinesaleor/checkout/models.py:434-531

필드타입설명
idUUIDField (PK)라인 ID
checkoutFK → CheckoutCASCADE
variantFK → ProductVariant상품 변형
quantityPositiveIntegerField수량
price_overrideDecimalField가격 오버라이드
undiscounted_unit_price_amountDecimal할인 전 단가
total_price_gross_amountDecimal총 가격 (세금 포함)
tax_rateDecimal(5,4)세율

6.3 모델: CheckoutDeliverysaleor/checkout/models.py:38-108

배송 방법 캐시 모델. 외부 배송 앱의 응답을 저장한다.

6.4 모델: CheckoutMetadatasaleor/checkout/models.py:535-538

체크아웃 메타데이터를 분리 저장. SELECT FOR UPDATE 중에도 메타데이터 접근 가능.


7. Checkout → Order 전환 흐름

7.1 진입점: complete_checkout()saleor/checkout/complete_checkout.py:1695

def complete_checkout(manager, checkout_info, lines, payment_data, ...):
    # 1. 체크아웃 데이터 fetch (세금 계산 포함)
    fetch_checkout_data(checkout_info, manager, lines)
 
    # 2. 흐름 결정
    active_payment = checkout.get_last_active_payment()
    is_fully_authorized = checkout.authorize_status == "full"
 
    if is_fully_authorized or (transactions and not active_payment):
        # Transaction Flow
        return complete_checkout_with_transaction(...)
    else:
        # Payment Flow (레거시)
        return complete_checkout_with_payment(...)

7.2 시퀀스 다이어그램: Transaction Flow

sequenceDiagram
    participant Client as 프론트엔드
    participant GQL as GraphQL API
    participant CC as complete_checkout
    participant TF as Transaction Flow
    participant PG as 결제 앱 (한국 PG App)
    participant DB as Database

    Client->>GQL: checkoutPaymentCreate / transactionInitialize (GraphQL)
    GQL->>PG: 결제 세션 생성 요청
    PG-->>GQL: 결제 URL / 세션 토큰

    Note over Client,PG: 고객이 PG 결제 페이지에서 결제 완료

    PG->>GQL: Webhook: TRANSACTION_CHARGE_REQUESTED 또는 직접 보고
    GQL->>DB: TransactionEvent(AUTHORIZATION_SUCCESS) 생성
    GQL->>DB: TransactionItem 금액 재계산
    GQL->>DB: Checkout authorize_status = FULL 업데이트

    Client->>GQL: checkoutComplete
    GQL->>CC: complete_checkout()
    CC->>CC: authorize_status == FULL 확인
    CC->>TF: complete_checkout_with_transaction()

    TF->>TF: _prepare_checkout_with_transactions()
    Note over TF: billing_address 검증<br/>authorize_status == FULL 확인<br/>voucher 유효성 확인

    TF->>TF: create_order_from_checkout()
    TF->>DB: Order 생성
    TF->>DB: OrderLine 생성 (bulk_create)
    TF->>DB: 재고 할당 (allocate_stocks)
    TF->>DB: Payment → Order 연결 (checkout.payments.update)
    TF->>DB: Checkout 삭제

    TF-->>Client: Order 반환

7.3 시퀀스 다이어그램: Payment Flow (레거시)

sequenceDiagram
    participant Client as 프론트엔드
    participant GQL as GraphQL API
    participant CC as complete_checkout
    participant PF as Payment Flow
    participant GW as Gateway Plugin
    participant PG as PG사 API
    participant DB as Database

    Client->>GQL: checkoutPaymentCreate
    GQL->>DB: Payment 생성

    Client->>GQL: checkoutComplete
    GQL->>CC: complete_checkout()
    CC->>PF: complete_checkout_with_payment()

    PF->>DB: Checkout SELECT FOR UPDATE (동시성 제어)
    PF->>PF: complete_checkout_pre_payment_part()
    Note over PF: 배송/주소 검증<br/>주문 데이터 준비<br/>재고 확인

    PF->>PF: _process_payment()
    PF->>GW: gateway.process_payment() 또는 gateway.confirm()
    GW->>PG: PG사 API 호출
    PG-->>GW: 응답
    GW-->>PF: GatewayResponse

    alt 추가 인증 필요 (3DS)
        PF-->>Client: action_required=True, action_data (리다이렉트 URL)
        Client->>PG: 3DS 인증 수행
        PG-->>GQL: Webhook 콜백
    else 결제 성공
        PF->>PF: complete_checkout_post_payment_part()
        PF->>DB: _create_order() — Order 생성
        PF->>DB: Checkout 삭제
        PF-->>Client: Order 반환
    end

7.4 주문 생성 상세 (_create_order / _create_order_from_checkout)

  1. 중복 방지: Order.objects.filter(checkout_token=checkout.token) — 이미 주문이 있으면 반환
  2. 주문 상태: channel.automatically_confirm_all_new_orders에 따라 UNFULFILLED 또는 UNCONFIRMED
  3. 주문 라인 생성: checkout 라인 → order 라인 변환 (가격, 할인, 세금 계산 반영)
  4. 재고 할당: allocate_stocks(), allocate_preorders()
  5. 기프트카드 적용: add_gift_cards_to_order()
  6. 결제 연결: checkout.payments.update(order=order)
  7. 메타데이터 복사: checkout → order
  8. 이벤트 발행: order_created() (on_commit)
  9. 확인 이메일: send_order_confirmation() (on_commit)
  10. Checkout 삭제: delete_checkouts()

8. 세금 및 배송비 계산

8.1 세금 계산 (saleor/checkout/calculations.py)

def fetch_checkout_data(checkout_info, manager, lines, ...):
    """체크아웃 가격 데이터를 가져오고 캐싱한다.
 
    1. 할인 적용 (create_or_update_discount_objects_from_promotion_for_checkout)
    2. 세금 전략 결정 (TaxCalculationStrategy)
       - FLAT_RATES: 내장 세율 테이블 사용
       - TAX_APP: 외부 세금 앱 (webhook) 사용
    3. 결제 상태 업데이트 (update_checkout_payment_statuses)
    """

세금 계산 진입점:

  • checkout_shipping_price() — 배송비 (세금 포함)
  • checkout_line_unit_price() — 라인 단가
  • checkout_line_total() — 라인 합계
  • checkout_line_tax_rate() — 라인 세율
  • calculate_checkout_total_with_gift_cards() — 기프트카드 차감 후 총액 (가격 계산 참조)

8.2 배송비 계산

base_checkout_delivery_price()checkout_shipping_price() (세금 적용)

배송비는 CheckoutDelivery 모델에 캐시되며, 내장/외부 배송 앱 양쪽 지원.


9. Checkout 결제 상태 관리 (saleor/checkout/payment_utils.py)

9.1 authorize_status 계산

def _update_authorize_status(checkout, checkout_total_gross,
                              total_authorized, total_charged, checkout_has_lines):
    total_covered = total_authorized + total_charged
    if checkout_with_only_zero_price_lines:
        authorize_status = FULL
    elif total_covered >= checkout_total_gross:
        authorize_status = FULL
    elif total_covered > 0:
        authorize_status = PARTIAL
    else:
        authorize_status = NONE

9.2 charge_status 계산

def _update_charge_status(checkout, checkout_total_gross, total_charged, ...):
    if total_charged >= checkout_total_gross:
        charge_status = FULL
    elif total_charged > 0:
        charge_status = PARTIAL
    else:
        charge_status = NONE

9.3 금액 집계

def _get_payment_amount_for_checkout(checkout_transactions, currency):
    for transaction in checkout_transactions:
        total_authorized += transaction.amount_authorized + transaction.amount_authorize_pending
        total_charged += transaction.amount_charged + transaction.amount_charge_pending
    return total_authorized, total_charged

10. Gateway 모듈 (saleor/payment/gateway.py)

10.1 Transaction Flow 액션 요청

def request_charge_action(transaction, manager, charge_value, request_event, ...):
    """TransactionItem에 대한 charge 요청.
    → WebhookEventSyncType.TRANSACTION_CHARGE_REQUESTED 발행
    → manager.transaction_charge_requested() 호출"""
 
def request_refund_action(transaction, manager, refund_value, request_event, ...):
    """TransactionItem에 대한 refund 요청.
    → WebhookEventSyncType.TRANSACTION_REFUND_REQUESTED 발행"""
 
def request_cancelation_action(transaction, manager, cancel_value, ...):
    """TransactionItem에 대한 cancel 요청.
    → WebhookEventSyncType.TRANSACTION_CANCELATION_REQUESTED 발행"""

10.2 Payment Flow 게이트웨이 호출

# gateway.py의 데코레이터 패턴
@raise_payment_error    # 실패 시 PaymentError 발생
@payment_postprocess    # 결제 후처리 (charge_status 업데이트 등)
@require_active_payment # 비활성 결제 차단
@with_locked_payment    # SELECT FOR UPDATE 락
def process_payment(payment, token, manager, ...):
    """결제 처리 — 플러그인 매니저를 통해 게이트웨이 호출"""

11. 한국 PG 연동 포인트

11.1 연동 전략 비교

방식장점단점
A. Saleor App (Transaction Flow)코어 코드 수정 불필요, 업그레이드 용이Saleor App 인프라 필요
B. Payment Plugin (Payment Flow)빌트인, 코드 내 직접 제어deprecated 경향, 코어 포크 필요
C. Hybrid유연성복잡도 증가

권장: A. Saleor App 방식 (Transaction Flow)

11.2 Transaction Flow 기반 한국 PG 앱 구현 포인트

1) 결제 세션 초기화 — PAYMENT_GATEWAY_INITIALIZE_SESSION sync 웹훅

프론트엔드 → paymentGatewayInitialize GraphQL mutation
    → Saleor → PAYMENT_GATEWAY_INITIALIZE_SESSION 웹훅 → PG 앱
    → PG 앱: 토스페이먼츠 /payments 생성, clientKey 반환
    → 프론트엔드: TossPayments SDK로 결제 UI 렌더링

2) 결제 처리 — TRANSACTION_INITIALIZE_SESSION / TRANSACTION_PROCESS_SESSION 웹훅

프론트엔드 → transactionInitialize mutation (amount, paymentGateway, data)
    → Saleor → TransactionItem 생성
    → Saleor → TRANSACTION_INITIALIZE_SESSION 웹훅 → PG 앱
    → PG 앱:
        토스: /confirm API 호출 (paymentKey, orderId, amount)
        나이스: /approve API 호출
    → PG 앱 → 응답:
        {
            "pspReference": "pg_transaction_id",
            "result": "CHARGE_SUCCESS",
            "amount": 50000,
            "data": { ... }  // PG 원시 응답
        }
    → Saleor: TransactionEvent(CHARGE_SUCCESS) 생성
    → Saleor: TransactionItem 금액 재계산
    → Saleor: Checkout authorize/charge 상태 업데이트

3) 웹훅 수신 — TRANSACTION_CHARGE_REQUESTED / TRANSACTION_REFUND_REQUESTED

관리자 → Order에서 환불 요청
    → Saleor → TRANSACTION_REFUND_REQUESTED 웹훅 → PG 앱
    → PG 앱: 토스 /cancel API 또는 나이스 /cancel API 호출
    → PG 앱 → transactionEventReport mutation 호출
        (type: REFUND_SUCCESS, pspReference, amount)
    → Saleor: TransactionEvent 기록, 금액 재계산

4) 결과 보고 — transactionEventReport mutation

PG 앱은 비동기 결과를 transactionEventReport로 Saleor에 보고한다:

mutation {
  transactionEventReport(
    id: "transaction-item-id"
    type: CHARGE_SUCCESS
    amount: 50000
    pspReference: "toss_xxxx_yyyy"
    message: "결제 완료"
  ) {
    alreadyProcessed
    transactionEvent { ... }
  }
}

11.3 한국 PG 특화 고려사항

가상계좌 (Virtual Account)

토스페이먼츠: method === "가상계좌"
→ AUTHORIZATION_SUCCESS (입금 대기)
→ 웹훅으로 입금 확인 시 CHARGE_SUCCESS
→ TransactionItem.available_actions에서 "charge" 제거

한국 PG에서 가상계좌는 2단계 플로우:

  1. 결제 요청 → 계좌 발급 (AUTHORIZATION)
  2. 고객 입금 → 입금 확인 webhook → CHARGE_SUCCESS

이 패턴은 Saleor의 authorize → charge 패턴과 자연스럽게 매핑된다.

간편결제 (카카오페이, 네이버페이 등)

→ CHARGE_SUCCESS 직접 (authorize 없이 즉시 결제)
→ available_actions = ["refund"]
→ payment_method_type = "other"  (카드가 아닌 경우)

부분 취소

토스페이먼츠, 나이스페이 모두 부분 취소를 지원한다. Saleor의 REFUND_REQUESTREFUND_SUCCESS 패턴으로 처리.

통화

TransactionItem.currencyCheckout.currencyKRW여야 한다. KRW는 소수점이 없으므로 DECIMAL_PLACES=0 설정 확인 필요. Saleor 설정에서:

# settings.py
DEFAULT_CURRENCY = "KRW"
DEFAULT_DECIMAL_PLACES = 0  # 한국 원화는 소수점 없음

11.4 포크 시 수정이 필요한 파일들

Transaction Flow (Saleor App) 방식을 사용하면 코어 수정은 최소화된다. 필요한 경우:

파일수정 목적
saleor/payment/__init__.pyPaymentMethodType에 한국 PG 결제수단 추가 (선택)
saleor/payment/models.pyTransactionItem에 한국 PG 전용 필드 추가 (선택)
saleor/checkout/complete_checkout.py가상계좌 등 특수 플로우 지원 (선택)
settings.pyKRW 통화 설정, 소수점 설정

11.5 PG 앱 (별도 서비스) 구현 체크리스트

□ PAYMENT_GATEWAY_INITIALIZE_SESSION 웹훅 핸들러
□ TRANSACTION_INITIALIZE_SESSION 웹훅 핸들러
□ TRANSACTION_PROCESS_SESSION 웹훅 핸들러
□ TRANSACTION_CHARGE_REQUESTED 웹훅 핸들러
□ TRANSACTION_REFUND_REQUESTED 웹훅 핸들러
□ TRANSACTION_CANCELATION_REQUESTED 웹훅 핸들러
□ PG사 웹훅 수신 엔드포인트 (입금 확인, 결제 상태 변경 등)
□ transactionEventReport mutation 호출 로직
□ 멱등성 처리 (idempotency_key 활용)
□ 에러 핸들링 및 재시도 로직

12. 주요 파일 인덱스

파일설명
saleor/payment/__init__.py결제 상수, 열거형 정의
saleor/payment/models.pyPayment, Transaction, TransactionItem, TransactionEvent 모델
saleor/payment/interface.py게이트웨이 인터페이스 데이터 클래스
saleor/payment/gateway.py결제 요청 엔트리포인트, 액션 요청 함수
saleor/payment/utils.py결제 유틸리티 (PaymentData 생성, 트랜잭션 관리 등)
saleor/payment/transaction_item_calculations.pyTransactionItem 이벤트 기반 금액 재계산
saleor/payment/gateways/dummy/__init__.py더미 게이트웨이 참조 구현
saleor/payment/gateways/dummy/plugin.py게이트웨이 플러그인 등록 패턴
saleor/payment/gateways/stripe/plugin.pyStripe 플러그인 (실제 PG 연동 참조)
saleor/payment/gateways/stripe/webhooks.pyStripe 웹훅 핸들러
saleor/checkout/__init__.pyCheckout 상태 열거형 (ChargeStatus, AuthorizeStatus)
saleor/checkout/models.pyCheckout, CheckoutLine, CheckoutDelivery 모델
saleor/checkout/complete_checkout.py체크아웃 완료 전체 로직 (핵심)
saleor/checkout/calculations.py가격/세금 계산
saleor/checkout/fetch.pyCheckoutInfo, CheckoutLineInfo 데이터 클래스
saleor/checkout/payment_utils.py체크아웃 결제 상태 업데이트
saleor/checkout/actions.py체크아웃 이벤트 트리거 (webhook 발행)

관련 문서

핵심 개념

앱 및 확장

도메인

아키텍처 및 참조