Payments
결제 action 재사용, reconcile과 서버 전용 custom bridge.
Payments
명시된 호출자 환경에서만 사용하세요. 먼저 자격 증명을 설정하세요. 패키지 surface는 SDK README가 소유합니다.
PortOne Standard V2 취소와 재시도
portone_sdk next action은 cancelable: false와
retryPolicy: 'same_action_only'를 명시합니다. PaymentSession 취소 메서드를
호출하기 전에 이 capability를 확인하세요. 브라우저 실패, 이탈, local expiry,
성공하지 않은 provider 관측은 Checkout 결제 slot을 해제하거나 새 payment ID를
허용하지 않습니다.
현재 SDK는 Standard create와 profile을 미리 알 수 없는 browser/server PortOne
retrieve에서만 이 capability projection을 협상합니다. Hosted create와
browser/server cancel은 capability Accept header를 보내지 않습니다. 또한 released
Console의 released exact Standard action을 받아 동일한 고정 의미로 정규화하므로,
SDK와 Console을 lockstep으로 배포할 필요가 없습니다. 직접 HTTP 호출은 기본적으로
released exact shape을 받고 create/retrieve에서만
Accept: application/vnd.01software.portone-action-capabilities.v1+json으로 opt-in할
수 있습니다.
이 값은 앞뒤 공백을 제외하고 단독 exact media type이어야 합니다. Accept 목록,
wildcard, parameter, q-value는 의도적으로 default projection을 유지합니다. 응답은
private no-store이며, 실제 Standard capability action을 반환하는 성공한 exact
opt-in에만 협상 media type을 Content-Type으로 사용합니다. Hosted, Toss, cancel,
confirm, error 응답은 application/json을 유지합니다.
import { createClient } from '@01.software/sdk'
type Client = ReturnType<typeof createClient>
type Session = Awaited<
ReturnType<Client['commerce']['paymentSessions']['retrievePortOne']>
>
export async function retryStandardAction(
session: Session,
requestPayment: (request: unknown) => Promise<unknown>,
) {
if (
session.presentationStatus === 'actionable' &&
session.nextAction.type === 'portone_sdk'
) {
const { cancelable, retryPolicy, ...paymentRequest } = session.nextAction
if (!cancelable && retryPolicy === 'same_action_only') {
// Adapter calls PortOne.requestPayment with this exact request.
return requestPayment(paymentRequest)
}
}
return null
}브라우저 결과 뒤에는 같은 PaymentSession을 retrieve하세요. 결제 finality는 Console의 인증된 PortOne lookup 또는 webhook convergence만 결정합니다.
서버 전용 PortOne identity reconcile
createServerClient().commerce.paymentSessions.findPortOne/reconcilePortOne은
provider + providerAccountKey + environment + providerPaymentId 전체 canonical
identity로 managed PortOne 결제를 찾습니다. tenant 범위는 server credential에서만
파생하며 strict request body는 caller가 보낸 tenant ID를 거부합니다.
import { createServerClient } from '@01.software/sdk/server'
type Server = ReturnType<typeof createServerClient>
export async function reconcilePayment(
server: Server,
selector: Parameters<Server['commerce']['paymentSessions']['findPortOne']>[0],
idempotencyKey: string,
) {
const local = await server.commerce.paymentSessions.findPortOne(selector)
const reconciled = await server.commerce.paymentSessions.reconcilePortOne({
...selector,
idempotencyKey,
})
return { local, reconciled }
}findPortOne은 local read-only입니다. reconcilePortOne은 write scope와 durable
idempotency key를 요구하고 기존 managed convergence engine을 실행합니다. 응답이
중단되면 같은 key를 재사용하세요. Console은 provider I/O를 반복하지 않고 domain
convergence와 함께 원자적으로 완료된 최초 결과를 replay합니다. 결과
disposition은 applied, observed, recovered_after_ambiguous_result,
requires_attention 중 하나입니다. HTTP와 SDK 모두 local not-found를
{ found: false }로 표현하며, reconcile은 알 수 없는 provider payment의 owner를
주장하거나 Checkout/Order에 heuristic으로 연결하지 않습니다. 두 메서드 모두
browser client에는 없습니다.
서버 전용 커스텀 결제 bridge
createServerClient().commerce.paymentSessions.custom.create/retrieve/confirm은
1st-party adapter가 없는 provider를 지원합니다. 이 namespace와 type은
@01.software/sdk/server 뒤에 두어야 하며 browser client와 SDK root에는
노출되지 않습니다.
import { createServerClient } from '@01.software/sdk/server'
type Server = ReturnType<typeof createServerClient>
export async function recoverCustomPayment(
server: Server,
providerSessionId: string,
) {
const recovered = await server.commerce.paymentSessions.custom.retrieve({
provider: 'portone',
by: { providerSessionId },
})
if (recovered.finalizationStatus === 'committed') {
return { kind: 'committed' as const, orderId: recovered.orderId }
}
return { kind: 'pending' as const, recovered }
}retrieve는 providerSessionId 또는 providerPaymentId 중 정확히 하나를
받습니다. Exact lookup을 수행하고 제한된 status, Money, resolved orderId만
반환하며, session 목록이나 raw PaymentSession relation, provider evidence,
fingerprint, credential은 노출하지 않습니다. 이 결과는 복구를 위한 현재 상태
관찰이지 provider capture가 유효하다는 증거가 아닙니다. 신뢰된 서버 코드는
confirm 전에 provider API 응답 또는 서명된 webhook을 계속 독립적으로 검증하고,
정확한 capture identity와 Money를 전달해야 합니다. 이 bridge는 generic “mark
paid” API나 수동·오프라인 pending 주문 생성 흐름이 아닙니다. 전체 경계는 SDK
README와 커머스를 확인하세요.
커스텀 bridge 세션은 의도적으로
checkout.paymentState.activePaymentSession에 null로 나타납니다.
custom.create가 반환한 providerSessionId를 보관하고 위 exact lookup을 커스텀
레인의 지원되는 정산 복구 경로로 사용하세요.
멱등 mutation 응답은 exactly-once 게이트가 아닙니다
운영 알람, 외부 webhook, 원장 기록 같은 비멱등 부수효과를 exactly-once로
게이팅하는 데 안전한 commerce mutation 응답 필드는 없습니다. 부수효과의 안정된
논리 식별자를 키로 사용하는 소비자 소유 durable claim으로 게이팅하세요.
processOnce webhook 패턴에 따라 process memory나
응답 필드가 아니라 database unique key, queue idempotency store, provider-safe
send ledger를 사용합니다.
paymentSessions.custom.confirm의 alreadyConfirmed: true는 관측 시점에
session이 이미 확정되어 있었다는 뜻이지, 현재 호출이 전이를 일으켰거나 일으키지
않았다는 뜻이 아닙니다. 최초 성공 confirm 응답에는 이 필드가 없습니다. 응답
모양만 해석해야 한다면 result.alreadyConfirmed === false가 아니라
'alreadyConfirmed' in result를 검사하세요. 어느 분기도 비멱등 부수효과를
발화하는 데 사용하면 안 됩니다. 최초 confirm이 커밋된 뒤 응답이 유실되면
재시도는 alreadyConfirmed: true를 반환하므로, 이 필드를 게이트로 쓰면 전이를
조용히 놓치게 됩니다.
orders.cancelOrder의 cancelCommitted와 alreadyCanceled는 transport replay가
아니라 도메인 효과 처분을 설명합니다. 같은 idempotency key로 재시도하면 캐시된
최초 2xx body가 byte-for-byte로 재생되므로 원래의 cancelCommitted: true가
다시 반환될 수 있습니다. 어느 필드도 이 응답이 replay인지, 부수효과가 이미
실행됐는지 알려주지 않습니다.