Laravel Cashier (Stripe)
업데이트됨번역일: 2026년 10월 22일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 10월 15일
- 번역 갱신
- 2026년 10월 22일
Laravel Cashier (Stripe)
- 소개
- Cashier 업그레이드
- 설치
- 설정
- 빠른 시작
- 고객
- 결제 수단
- 구독
- 구독 체험 기간
- Stripe 웹훅 처리
- 단건 결제
- 인보이스
- 체크아웃
- 결제 실패 처리
- 강력한 고객 인증(SCA)
- Stripe SDK
- 테스트
소개
Laravel Cashier Stripe는 Stripe의 구독 결제 서비스를 간결하고 유창한(fluent) 인터페이스로 다룰 수 있게 해 주는 패키지입니다. 직접 작성하기엔 부담스러운 구독 결제용 반복 코드를 거의 대부분 대신 처리해 줍니다. 기본적인 구독 관리 외에도 쿠폰, 구독 플랜 교체(swap), 구독 "수량", 취소 유예 기간을 지원하며 인보이스 PDF까지 생성할 수 있습니다.
Cashier 업그레이드
Cashier를 새 버전으로 업그레이드할 때는 업그레이드 가이드를 꼼꼼히 확인해야 합니다.
WARNING
Cashier는 설치된 stripe/stripe-php SDK 버전에 고정된 Stripe API 버전을 사용합니다. 예를 들어 stripe/stripe-php 21.x는 Stripe API 버전 2026-08-26.dahlia를 사용합니다. Stripe SDK의 메이저 버전이 올라가면 보통 새로운 Stripe API 버전을 대상으로 하므로, stripe/stripe-php를 새 메이저 버전으로 올리기 전에 Stripe API 변경 내역을 반드시 확인하세요.
설치
먼저 Composer 패키지 관리자로 Stripe용 Cashier 패키지를 설치합니다.
composer require laravel/cashier패키지를 설치한 뒤에는 vendor:publish Artisan 명령어로 Cashier의 마이그레이션을 게시(publish)합니다.
php artisan vendor:publish --tag="cashier-migrations"그다음 데이터베이스를 마이그레이션합니다.
php artisan migrateCashier의 마이그레이션은 users 테이블에 여러 컬럼을 추가합니다. 또한 고객의 모든 구독을 저장하는 subscriptions 테이블과, 여러 가격이 포함된 구독을 위한 subscription_items 테이블도 새로 만듭니다.
필요하다면 vendor:publish Artisan 명령어로 Cashier의 설정 파일도 게시할 수 있습니다.
php artisan vendor:publish --tag="cashier-config"마지막으로, Cashier가 모든 Stripe 이벤트를 올바르게 처리하도록 Cashier의 웹훅 처리를 설정하는 것을 잊지 마세요.
WARNING
Stripe는 Stripe 식별자를 저장하는 모든 컬럼을 대소문자를 구분하도록 설정할 것을 권장합니다. 따라서 MySQL을 사용한다면 stripe_id 컬럼의 콜레이션(collation)을 utf8_bin으로 지정해야 합니다. 자세한 내용은 Stripe 문서를 참고하세요.
설정
결제 대상 모델(Billable Model)
Cashier를 사용하기 전에, 결제 대상이 되는 모델에 Billable 트레이트를 추가하세요. 일반적으로는 App\Models\User 모델입니다. 이 트레이트는 구독 생성, 쿠폰 적용, 결제 수단 정보 갱신 등 자주 쓰는 결제 작업을 수행하는 다양한 메서드를 제공합니다.
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}Cashier는 결제 대상 모델이 Laravel에 기본 포함된 App\Models\User 클래스라고 가정합니다. 다른 모델을 사용하려면 useCustomerModel 메서드로 지정할 수 있습니다. 이 메서드는 보통 AppServiceProvider 클래스의 boot 메서드에서 호출합니다.
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}WARNING
Laravel 기본 App\Models\User 모델이 아닌 다른 모델을 사용한다면, 제공되는 Cashier 마이그레이션을 게시한 뒤 해당 모델의 테이블 이름에 맞게 수정해야 합니다.
API 키
다음으로 애플리케이션의 .env 파일에 Stripe API 키를 설정합니다. Stripe API 키는 Stripe 관리 콘솔에서 확인할 수 있습니다.
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secretWARNING
애플리케이션의 .env 파일에 STRIPE_WEBHOOK_SECRET 환경 변수가 반드시 정의되어 있어야 합니다. 이 값은 들어오는 웹훅이 실제로 Stripe에서 온 것인지 검증하는 데 사용됩니다.
통화 설정
Cashier의 기본 통화는 미국 달러(USD)입니다. 애플리케이션의 .env 파일에서 CASHIER_CURRENCY 환경 변수를 설정하면 기본 통화를 바꿀 수 있습니다. 예를 들어 원화(KRW)를 사용하는 서비스라면 다음과 같이 지정합니다.
CASHIER_CURRENCY=eurCashier의 통화를 설정하는 것과 별도로, 인보이스에 표시되는 금액을 포맷할 때 사용할 로캘(locale)도 지정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용해 통화 로캘을 설정합니다.
CASHIER_CURRENCY_LOCALE=nl_BEWARNING
en 이외의 로캘을 사용하려면 서버에 ext-intl PHP 확장이 설치되어 있고 올바르게 설정되어 있는지 확인하세요.
세금 설정
Stripe Tax 덕분에 Stripe가 생성하는 모든 인보이스의 세금을 자동으로 계산할 수 있습니다. 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 calculateTaxes 메서드를 호출하면 자동 세금 계산이 활성화됩니다.
use Laravel\Cashier\Cashier;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}세금 계산을 활성화하면, 이후 생성되는 모든 새 구독과 일회성 인보이스에 자동 세금 계산이 적용됩니다.
이 기능이 제대로 동작하려면 고객의 이름, 주소, 세금 ID 같은 청구 정보가 Stripe에 동기화되어 있어야 합니다. Cashier가 제공하는 고객 데이터 동기화 기능과 세금 ID 관련 메서드를 활용하면 됩니다.
로깅
Cashier에서는 치명적인 Stripe 오류를 기록할 때 사용할 로그 채널을 지정할 수 있습니다. 애플리케이션의 .env 파일에 CASHIER_LOGGER 환경 변수를 정의하면 됩니다.
CASHIER_LOGGER=stackStripe API 호출 과정에서 발생한 예외는 애플리케이션의 기본 로그 채널을 통해 기록됩니다.
커스텀 모델 사용하기
Cashier가 내부적으로 사용하는 모델을 확장하려면, 직접 모델을 정의하고 해당하는 Cashier 모델을 상속하면 됩니다.
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}모델을 정의한 뒤에는 Laravel\Cashier\Cashier 클래스를 통해 Cashier가 커스텀 모델을 사용하도록 지정할 수 있습니다. 보통 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 커스텀 모델을 알려 줍니다.
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}빠른 시작
상품 판매하기
NOTE
Stripe Checkout을 사용하기 전에 Stripe 대시보드에서 고정 가격이 설정된 상품(Product)을 먼저 정의해야 합니다. 또한 Cashier의 웹훅 처리를 설정해 두어야 합니다.
애플리케이션에서 상품 판매와 구독 결제를 제공하는 일은 부담스럽게 느껴질 수 있습니다. 하지만 Cashier와 Stripe Checkout을 사용하면 현대적이고 견고한 결제 연동을 손쉽게 만들 수 있습니다.
반복 결제가 아닌 단건 상품을 판매하려면, Cashier를 이용해 고객을 Stripe Checkout으로 보냅니다. 고객은 그곳에서 결제 정보를 입력하고 구매를 확정합니다. Checkout에서 결제가 완료되면 고객은 애플리케이션 내에서 지정한 성공 URL로 리디렉션됩니다.
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
$stripePriceId = 'price_deluxe_album';
$quantity = 1;
return $request->user()->checkout([$stripePriceId => $quantity], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->name('checkout');
Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');위 예제에서 볼 수 있듯이, Cashier가 제공하는 checkout 메서드로 "가격 식별자(price identifier)"를 지정해 고객을 Stripe Checkout으로 리디렉션합니다. Stripe에서 "가격(price)"은 특정 상품에 대해 정의된 가격을 의미합니다.
필요한 경우 checkout 메서드는 Stripe에 고객을 자동으로 생성하고, 해당 Stripe 고객 레코드를 애플리케이션 데이터베이스의 사용자와 연결합니다. 체크아웃 세션이 끝나면 고객은 성공 또는 취소 전용 페이지로 이동하며, 이 페이지에서 안내 메시지를 보여 줄 수 있습니다.
Stripe Checkout에 메타데이터 전달하기
상품을 판매할 때는 보통 애플리케이션에서 직접 정의한 Cart, Order 모델로 완료된 주문과 구매한 상품을 관리합니다. 고객을 Stripe Checkout으로 보내 결제를 완료하게 할 때는, 고객이 애플리케이션으로 돌아왔을 때 완료된 결제를 해당 주문과 연결할 수 있도록 기존 주문 식별자를 함께 전달해야 할 수 있습니다.
이럴 때는 checkout 메서드에 metadata 배열을 전달하면 됩니다. 사용자가 결제 과정을 시작할 때 애플리케이션에서 대기 중(pending) 상태의 Order가 생성된다고 가정해 보겠습니다. 이 예제의 Cart와 Order 모델은 설명을 위한 것이며 Cashier가 제공하는 모델이 아닙니다. 이러한 개념은 애플리케이션의 요구에 맞게 자유롭게 구현하면 됩니다.
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
$order = Order::create([
'cart_id' => $cart->id,
'price_ids' => $cart->price_ids,
'status' => 'incomplete',
]);
return $request->user()->checkout($order->price_ids, [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
'metadata' => ['order_id' => $order->id],
]);
})->name('checkout');위 예제처럼 사용자가 결제 과정을 시작하면, 장바구니(주문)에 연결된 모든 Stripe 가격 식별자를 checkout 메서드에 전달합니다. 고객이 상품을 담을 때 이 항목들을 "장바구니"나 주문에 연결하는 일은 물론 애플리케이션이 직접 처리해야 합니다. 또한 metadata 배열을 통해 주문 ID를 Stripe Checkout 세션에 함께 전달합니다. 마지막으로 Checkout 성공 라우트에는 CHECKOUT_SESSION_ID 템플릿 변수를 추가했습니다. Stripe가 고객을 애플리케이션으로 다시 리디렉션할 때 이 템플릿 변수에는 Checkout 세션 ID가 자동으로 채워집니다.
이제 Checkout 성공 라우트를 만들어 보겠습니다. 이 라우트는 사용자가 Stripe Checkout에서 구매를 마친 뒤 리디렉션되는 곳입니다. 이 라우트에서 Stripe Checkout 세션 ID와 그에 해당하는 Stripe Checkout 인스턴스를 조회하면, 앞서 전달한 메타데이터에 접근해 고객의 주문을 알맞게 갱신할 수 있습니다.
use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
Route::get('/checkout/success', function (Request $request) {
$sessionId = $request->get('session_id');
if ($sessionId === null) {
return;
}
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
if ($session->payment_status !== 'paid') {
return;
}
$orderId = $session['metadata']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
return view('checkout-success', ['order' => $order]);
})->name('checkout-success');Checkout 세션 객체에 포함된 데이터에 대한 자세한 내용은 Stripe 문서의 Checkout 세션 객체를 참고하세요.
구독 판매하기
NOTE
Stripe Checkout을 사용하기 전에 Stripe 대시보드에서 고정 가격이 설정된 상품(Product)을 먼저 정의해야 합니다. 또한 Cashier의 웹훅 처리를 설정해 두어야 합니다.
애플리케이션에서 상품 판매와 구독 결제를 제공하는 일은 부담스럽게 느껴질 수 있습니다. 하지만 Cashier와 Stripe Checkout을 사용하면 현대적이고 견고한 결제 연동을 손쉽게 만들 수 있습니다.
Cashier와 Stripe Checkout으로 구독을 판매하는 방법을 알아보기 위해, 월간 요금제(price_basic_monthly)와 연간 요금제(price_basic_yearly)를 가진 간단한 구독 서비스를 가정해 보겠습니다. 이 두 가격은 Stripe 대시보드에서 "Basic" 상품(pro_basic)으로 묶을 수 있습니다. 또한 이 구독 서비스에 pro_expert라는 Expert 플랜이 있다고 가정하겠습니다.
먼저 고객이 서비스를 구독하는 방법을 살펴보겠습니다. 고객이 애플리케이션의 요금제 페이지에서 Basic 플랜의 "구독" 버튼을 클릭한다고 생각해 보세요. 이 버튼이나 링크는 고객이 선택한 플랜의 Stripe Checkout 세션을 생성하는 Laravel 라우트로 연결되어야 합니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_basic_monthly')
->trialDays(5)
->allowPromotionCodes()
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});위 예제처럼 고객을 Stripe Checkout 세션으로 리디렉션하면 Basic 플랜을 구독할 수 있습니다. 체크아웃이 성공하거나 취소되면 고객은 checkout 메서드에 전달한 URL로 되돌아옵니다. 일부 결제 수단은 처리에 몇 초가 걸리므로, 구독이 실제로 시작된 시점을 알려면 Cashier의 웹훅 처리를 설정해야 합니다.
이제 고객이 구독을 시작할 수 있으니, 구독한 사용자만 접근할 수 있도록 애플리케이션의 일부 영역을 제한해야 합니다. 사용자의 현재 구독 상태는 언제든 Cashier의 Billable 트레이트가 제공하는 subscribed 메서드로 확인할 수 있습니다.
@if ($user->subscribed())
<p>구독 중입니다.</p>
@endif사용자가 특정 상품이나 가격을 구독 중인지도 쉽게 확인할 수 있습니다.
@if ($user->subscribedToProduct('pro_basic'))
<p>Basic 상품을 구독 중입니다.</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>월간 Basic 플랜을 구독 중입니다.</p>
@endif구독 확인 미들웨어 만들기
편의를 위해 들어온 요청이 구독 중인 사용자의 것인지 판단하는 미들웨어를 만들어 둘 수 있습니다. 이 미들웨어를 정의해 두면 라우트에 쉽게 할당해, 구독하지 않은 사용자의 접근을 막을 수 있습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// Redirect user to billing page and ask them to subscribe...
return redirect('/billing');
}
return $next($request);
}
}미들웨어를 정의했다면 라우트에 할당할 수 있습니다.
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);고객이 직접 요금제를 관리하게 하기
고객은 구독 중인 요금제를 다른 상품이나 "등급(tier)"으로 바꾸고 싶어 할 수 있습니다. 가장 쉬운 방법은 Stripe의 고객 결제 관리 포털(Customer Billing Portal)로 안내하는 것입니다. 이 포털은 Stripe가 호스팅하는 사용자 인터페이스로, 고객이 인보이스를 내려받고, 결제 수단을 갱신하고, 구독 플랜을 변경할 수 있게 해 줍니다.
먼저 애플리케이션 안에 Billing Portal 세션을 시작하는 Laravel 라우트로 연결되는 링크나 버튼을 만듭니다.
<a href="{{ route('billing') }}">
결제 관리
</a>다음으로 Stripe 고객 결제 관리 포털 세션을 시작하고 사용자를 포털로 리디렉션하는 라우트를 정의합니다. redirectToBillingPortal 메서드는 사용자가 포털을 나갈 때 돌아올 URL을 인자로 받습니다.
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');NOTE
Cashier의 웹훅 처리를 설정해 두었다면, Cashier는 Stripe에서 들어오는 웹훅을 확인해 애플리케이션의 Cashier 관련 데이터베이스 테이블을 자동으로 동기화합니다. 예를 들어 사용자가 Stripe 고객 결제 관리 포털에서 구독을 취소하면, Cashier가 해당 웹훅을 받아 애플리케이션 데이터베이스에서 그 구독을 "취소됨" 상태로 표시합니다.
고객
고객 조회
Cashier::findBillable 메서드를 사용하면 Stripe ID로 고객을 조회할 수 있습니다. 이 메서드는 결제 대상 모델(billable model)의 인스턴스를 반환합니다.
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);고객 생성
구독을 시작하지 않고 Stripe 고객만 먼저 생성하고 싶을 때가 있습니다. 이럴 때는 createAsStripeCustomer 메서드를 사용합니다.
$stripeCustomer = $user->createAsStripeCustomer();Stripe에 고객이 생성되면 나중에 언제든 구독을 시작할 수 있습니다. 선택 사항인 $options 배열을 전달해 Stripe API가 지원하는 고객 생성 파라미터를 추가로 넘길 수도 있습니다.
$stripeCustomer = $user->createAsStripeCustomer($options);결제 대상 모델의 Stripe 고객 객체를 반환받고 싶다면 asStripeCustomer 메서드를 사용하면 됩니다.
$stripeCustomer = $user->asStripeCustomer();결제 대상 모델의 Stripe 고객 객체를 조회하고 싶지만 해당 모델이 이미 Stripe 고객인지 확실하지 않다면 createOrGetStripeCustomer 메서드를 사용할 수 있습니다. Stripe에 고객이 아직 없다면 새로 생성합니다.
$stripeCustomer = $user->createOrGetStripeCustomer();고객 정보 수정
Stripe 고객에 추가 정보를 직접 갱신하고 싶을 때가 있습니다. 이럴 때는 updateStripeCustomer 메서드를 사용합니다. 이 메서드는 Stripe API가 지원하는 고객 수정 옵션 배열을 인자로 받습니다.
$stripeCustomer = $user->updateStripeCustomer($options);잔액
Stripe에서는 고객의 "잔액(balance)"에 금액을 적립(credit)하거나 차감(debit)할 수 있으며, 이 잔액은 이후 새로 발행되는 인보이스에 반영됩니다. 고객의 총 잔액은 결제 대상 모델의 balance 메서드로 확인할 수 있습니다. balance 메서드는 고객의 통화 기준으로 포맷된 잔액 문자열을 반환합니다.
$balance = $user->balance();고객의 잔액을 적립하려면 creditBalance 메서드에 금액을 전달합니다. 원한다면 설명도 함께 지정할 수 있습니다.
$user->creditBalance(500, 'Premium customer top-up.');debitBalance 메서드에 금액을 전달하면 고객의 잔액이 차감됩니다.
$user->debitBalance(300, 'Bad usage penalty.');applyBalance 메서드는 고객에 대한 새로운 잔액 트랜잭션을 생성합니다. 이 트랜잭션 레코드는 balanceTransactions 메서드로 조회할 수 있으며, 고객이 적립 및 차감 내역을 확인할 수 있는 로그를 제공할 때 유용합니다.
// Retrieve all transactions...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// Transaction amount...
$amount = $transaction->amount(); // $2.31
// Retrieve the related invoice when available...
$invoice = $transaction->invoice();
}세금 ID
Cashier는 고객의 세금 ID를 쉽게 관리할 수 있는 방법을 제공합니다. 예를 들어 taxIds 메서드를 사용하면 고객에게 할당된 모든 세금 ID를 컬렉션으로 조회할 수 있습니다.
$taxIds = $user->taxIds();식별자로 고객의 특정 세금 ID를 조회할 수도 있습니다.
$taxId = $user->findTaxId('txi_belgium');유효한 타입과 값을 createTaxId 메서드에 전달하면 새 세금 ID를 생성할 수 있습니다.
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');createTaxId 메서드는 VAT ID를 고객 계정에 즉시 추가합니다. VAT ID 검증도 Stripe에서 수행하지만, 이는 비동기로 처리됩니다. customer.tax_id.updated 웹훅 이벤트를 구독하고 VAT ID의 verification 파라미터를 확인하면 검증 결과 변경을 알림으로 받을 수 있습니다. 웹훅 처리에 대한 자세한 내용은 웹훅 핸들러 정의 문서를 참고하세요.
deleteTaxId 메서드로 세금 ID를 삭제할 수 있습니다.
$user->deleteTaxId('txi_belgium');Stripe와 고객 데이터 동기화
일반적으로 애플리케이션 사용자가 이름, 이메일 주소 등 Stripe에도 저장되어 있는 정보를 수정하면 Stripe에도 그 변경 사항을 알려야 합니다. 그래야 Stripe가 가진 정보와 애플리케이션의 정보가 일치합니다.
이를 자동화하려면 결제 대상 모델의 updated 이벤트에 반응하는 이벤트 리스너를 정의하고, 리스너 안에서 모델의 syncStripeCustomerDetails 메서드를 호출하면 됩니다.
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}이제 고객 모델이 업데이트될 때마다 해당 정보가 Stripe와 동기화됩니다. 편의를 위해 Cashier는 고객을 처음 생성할 때 고객 정보를 Stripe와 자동으로 동기화합니다.
Cashier가 제공하는 여러 메서드를 오버라이드하면 Stripe로 고객 정보를 동기화할 때 사용하는 컬럼을 원하는 대로 바꿀 수 있습니다. 예를 들어 stripeName 메서드를 오버라이드하면, Cashier가 고객 정보를 Stripe에 동기화할 때 고객의 "이름"으로 간주할 속성을 지정할 수 있습니다.
/**
* Get the customer name that should be synced to Stripe.
*/
public function stripeName(): string|null
{
return $this->company_name;
}같은 방식으로 stripeEmail, stripePhone(최대 20자), stripeAddress, stripePreferredLocales 메서드도 오버라이드할 수 있습니다. 이 메서드들은 Stripe 고객 객체를 수정할 때 각각에 대응하는 고객 파라미터로 정보를 동기화합니다. 고객 정보 동기화 과정을 완전히 직접 제어하고 싶다면 syncStripeCustomerDetails 메서드를 오버라이드하면 됩니다.
결제 관리 포털(Billing Portal)
Stripe는 고객이 구독과 결제 수단을 관리하고 결제 내역을 확인할 수 있도록 결제 관리 포털을 간단히 구성하는 방법을 제공합니다. 컨트롤러나 라우트에서 결제 대상 모델의 redirectToBillingPortal 메서드를 호출하면 사용자를 결제 관리 포털로 리디렉션할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});기본적으로 사용자가 구독 관리를 마치면 Stripe 결제 관리 포털 안의 링크를 통해 애플리케이션의 home 라우트로 돌아올 수 있습니다. redirectToBillingPortal 메서드에 URL을 인자로 전달하면 사용자가 돌아올 URL을 직접 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});HTTP 리디렉션 응답을 만들지 않고 결제 관리 포털의 URL만 생성하고 싶다면 billingPortalUrl 메서드를 호출하면 됩니다.
$url = $request->user()->billingPortalUrl(route('billing'));결제 수단
결제 수단 저장
Stripe로 구독을 만들거나 "일회성(one-off)" 결제를 처리하려면, 애플리케이션이 고객의 결제 정보를 안전하게 수집해야 합니다. 이를 구현하는 방식은 향후 구독을 위해 결제 수단을 저장할지, 아니면 단건 결제를 즉시 처리할지에 따라 달라지므로 아래에서 두 경우를 모두 살펴보겠습니다.
Stripe의 Payment Element를 사용하면 카드, Apple Pay, Google Pay, iDEAL 등 다양한 결제 수단을 지원할 수 있습니다.
구독을 위한 Payment Element
먼저 Setup Intent를 생성하여 뷰에 전달합니다.
return view('subscribe', [
'intent' => $user->createSetupIntent()
]);Setup Intent의 client_secret을 사용해 Payment Element를 마운트합니다.
<div id="payment-element"></div>
<button id="submit">Subscribe</button>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements({
clientSecret: '{{ $intent->client_secret }}'
});
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
document.getElementById('submit').addEventListener('click', async () => {
const { error } = await stripe.confirmSetup({
elements,
confirmParams: {
return_url: '{{ route("subscription.complete") }}',
},
});
if (error) {
// Display "error.message" to the user...
}
});
</script>Stripe가 return_url로 리디렉션하면 setup_intent ID가 쿼리 스트링 파라미터로 전달됩니다. 이 값을 사용해 결제 수단을 조회하고 구독을 생성할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription/complete', function (Request $request) {
$setupIntent = $request->user()->findSetupIntent(
$request->setup_intent
);
$paymentMethod = $setupIntent->payment_method;
$request->user()
->newSubscription('default', 'price_xxx')
->create($paymentMethod);
return redirect('/dashboard');
})->name('subscription.complete');구독을 생성하는 대신 Payment Element로 고객의 기본 결제 수단을 갱신하려는 경우에는, 결제 수단 식별자를 updateDefaultPaymentMethod 메서드에 전달하면 됩니다.
단건 결제를 위한 Payment Element
일회성 결제는 Cashier의 pay 메서드로 Payment Intent를 생성합니다. 보통 Payment Intent ID를 애플리케이션의 해당 주문에 저장해 두어야, Stripe가 고객을 애플리케이션으로 다시 리디렉션한 뒤에 그 주문을 조회할 수 있습니다. 다음 예제는 애플리케이션에 user_id, amount, status, stripe_payment_intent_id 컬럼을 가진 Order 모델이 있다고 가정합니다.
use App\Models\Order;
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$amount = 1000;
$payment = $request->user()->pay($amount);
$order = Order::create([
'user_id' => $request->user()->id,
'amount' => $amount,
'status' => 'pending',
'stripe_payment_intent_id' => $payment->id,
]);
return view('checkout', [
'clientSecret' => $payment->client_secret,
'order' => $order,
]);
});그런 다음 Payment Element를 마운트하고 결제를 확정합니다.
<div id="payment-element"></div>
<button id="submit">Pay Now</button>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements({
clientSecret: '{{ $clientSecret }}'
});
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
document.getElementById('submit').addEventListener('click', async () => {
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: '{{ route("payment.complete") }}',
},
});
if (error) {
// Display "error.message" to the user...
}
});
</script>리디렉션된 뒤에는 payment_intent 쿼리 스트링 파라미터로 해당 주문과 Payment Intent를 조회할 수 있습니다. 주문을 이행하기 전에, 주문이 인증된 고객의 것인지, 그리고 Payment Intent가 인증된 고객의 것이며 결제에 성공했는지를 반드시 검증해야 합니다.
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/payment/complete', function (Request $request) {
$order = Order::where('user_id', $request->user()->id)
->where('stripe_payment_intent_id', $request->payment_intent)
->firstOrFail();
$paymentIntent = $request->user()
->stripe()
->paymentIntents
->retrieve($request->payment_intent);
if ($paymentIntent->customer === $request->user()->stripe_id &&
$paymentIntent->status === 'succeeded') {
$order->update(['status' => 'paid']);
// Fulfill the order...
}
return redirect('/dashboard');
})->name('payment.complete');결제 수단 조회
결제 대상 모델 인스턴스의 paymentMethods 메서드는 Laravel\Cashier\PaymentMethod 인스턴스의 컬렉션을 반환합니다.
$paymentMethods = $user->paymentMethods();기본적으로 이 메서드는 모든 유형의 결제 수단을 반환합니다. 특정 유형의 결제 수단만 조회하려면 type을 인자로 전달합니다.
$paymentMethods = $user->paymentMethods('sepa_debit');고객의 기본 결제 수단을 조회하려면 defaultPaymentMethod 메서드를 사용합니다.
$paymentMethod = $user->defaultPaymentMethod();findPaymentMethod 메서드를 사용하면 결제 대상 모델에 연결된 특정 결제 수단을 조회할 수 있습니다.
$paymentMethod = $user->findPaymentMethod($paymentMethodId);결제 수단 보유 여부 확인
결제 대상 모델의 계정에 기본 결제 수단이 연결되어 있는지 확인하려면 hasDefaultPaymentMethod 메서드를 호출합니다.
if ($user->hasDefaultPaymentMethod()) {
// ...
}hasPaymentMethod 메서드를 사용하면 결제 대상 모델의 계정에 결제 수단이 하나 이상 연결되어 있는지 확인할 수 있습니다.
if ($user->hasPaymentMethod()) {
// ...
}이 메서드는 결제 수단의 종류와 관계없이 하나라도 있는지를 확인합니다. 특정 유형의 결제 수단이 있는지 확인하려면 type을 인자로 전달하세요.
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}기본 결제 수단 변경
updateDefaultPaymentMethod 메서드는 고객의 기본 결제 수단 정보를 갱신합니다. 이 메서드는 Stripe 결제 수단 식별자를 받아 새 결제 수단을 기본 청구용 결제 수단으로 지정합니다.
$user->updateDefaultPaymentMethod($paymentMethod);기본 결제 수단 정보를 Stripe에 등록된 고객의 기본 결제 수단 정보와 동기화하려면 updateDefaultPaymentMethodFromStripe 메서드를 사용합니다.
$user->updateDefaultPaymentMethodFromStripe();WARNING
고객의 기본 결제 수단은 인보이스 청구와 새 구독 생성에만 사용할 수 있습니다. Stripe의 제약 때문에 단건 결제에는 사용할 수 없습니다.
결제 수단 추가
새 결제 수단을 추가하려면 결제 대상 모델의 addPaymentMethod 메서드에 결제 수단 식별자를 전달해 호출합니다.
$user->addPaymentMethod($paymentMethod);NOTE
결제 수단 식별자를 가져오는 방법은 결제 수단 저장 문서를 참고하세요.
결제 수단 삭제
결제 수단을 삭제하려면 삭제할 Laravel\Cashier\PaymentMethod 인스턴스의 delete 메서드를 호출합니다.
$paymentMethod->delete();deletePaymentMethod 메서드는 결제 대상 모델에서 특정 결제 수단을 삭제합니다.
$user->deletePaymentMethod('pm_visa');deletePaymentMethods 메서드는 결제 대상 모델의 모든 결제 수단 정보를 삭제합니다.
$user->deletePaymentMethods();기본적으로 이 메서드는 모든 유형의 결제 수단을 삭제합니다. 특정 유형의 결제 수단만 삭제하려면 type을 인자로 전달하세요.
$user->deletePaymentMethods('sepa_debit');WARNING
사용자에게 활성 구독이 있다면, 애플리케이션에서 기본 결제 수단을 삭제하지 못하도록 막아야 합니다.
구독
구독은 고객에게 반복 결제를 설정하는 방법입니다. Cashier가 관리하는 Stripe 구독은 여러 구독 가격, 구독 수량, 체험 기간 등 다양한 기능을 지원합니다.
구독 생성
구독을 생성하려면 먼저 결제 대상 모델의 인스턴스를 가져옵니다. 보통은 App\Models\User의 인스턴스입니다. 모델 인스턴스를 가져왔다면 newSubscription 메서드로 해당 모델의 구독을 생성할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});newSubscription 메서드의 첫 번째 인자는 구독의 내부 타입입니다. 애플리케이션이 구독을 하나만 제공한다면 default나 primary 같은 이름을 쓸 수 있습니다. 이 구독 타입은 애플리케이션 내부에서만 사용하는 값이므로 사용자에게 보여 주지 않아야 하고, 공백을 포함해서는 안 되며, 구독을 생성한 뒤에는 절대 변경해서는 안 됩니다. 두 번째 인자는 사용자가 구독할 구체적인 가격이며, Stripe에서의 가격 식별자와 일치해야 합니다.
Stripe 결제 수단 식별자 또는 Stripe PaymentMethod 객체를 받는 create 메서드는 구독을 시작하고, 결제 대상 모델의 Stripe 고객 ID와 기타 관련 청구 정보를 데이터베이스에 갱신합니다.
WARNING
결제 수단 식별자를 구독의 create 메서드에 직접 전달하면, 해당 결제 수단이 사용자의 저장된 결제 수단 목록에도 자동으로 추가됩니다.
인보이스 이메일로 반복 결제 받기
고객의 반복 결제를 자동으로 청구하는 대신, 결제 시점이 될 때마다 Stripe가 고객에게 인보이스를 이메일로 보내도록 설정할 수 있습니다. 그러면 고객은 인보이스를 받은 뒤 직접 결제하면 됩니다. 인보이스 방식으로 반복 결제를 받을 때는 고객이 결제 수단을 미리 등록하지 않아도 됩니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();구독이 취소되기 전까지 고객이 인보이스를 결제할 수 있는 기간은 days_until_due 옵션으로 결정됩니다. 기본값은 30일이며, 원한다면 이 옵션에 원하는 값을 지정할 수 있습니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);수량
구독을 생성할 때 가격에 특정 수량을 지정하려면, 구독을 생성하기 전에 구독 빌더의 quantity 메서드를 호출하면 됩니다.
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);추가 정보
Stripe가 지원하는 고객 옵션이나 구독 옵션을 추가로 지정하려면 create 메서드의 두 번째와 세 번째 인자로 전달하면 됩니다.
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Some extra information.'],
]);쿠폰
구독을 생성할 때 쿠폰을 적용하려면 withCoupon 메서드를 사용합니다.
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);또는 Stripe 프로모션 코드를 적용하려면 withPromotionCode 메서드를 사용합니다.
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);여기에 전달하는 프로모션 코드 ID는 고객에게 보여 주는 프로모션 코드가 아니라, 프로모션 코드에 할당된 Stripe API ID여야 합니다. 고객에게 노출되는 프로모션 코드로 프로모션 코드 ID를 찾아야 한다면 findPromotionCode 메서드를 사용할 수 있습니다.
// Find a promotion code ID by its customer facing code...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// Find an active promotion code ID by its customer facing code...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');위 예제에서 반환된 $promotionCode 객체는 Laravel\Cashier\PromotionCode의 인스턴스입니다. 이 클래스는 내부의 Stripe\PromotionCode 객체를 감싸는(decorate) 클래스입니다. coupon 메서드를 호출하면 프로모션 코드에 연결된 쿠폰을 조회할 수 있습니다.
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();쿠폰 인스턴스로는 할인 금액과, 쿠폰이 고정 금액 할인인지 비율 할인인지를 확인할 수 있습니다.
if ($coupon->isPercentage()) {
return $coupon->percentOff().'%'; // 21.5%
} else {
return $coupon->amountOff(); // $5.99
}고객이나 구독에 현재 적용된 할인도 조회할 수 있습니다.
$discount = $billable->discount();
$discount = $subscription->discount();반환되는 Laravel\Cashier\Discount 인스턴스는 내부의 Stripe\Discount 객체 인스턴스를 감쌉니다. 이 할인과 관련된 쿠폰은 coupon 메서드로 조회할 수 있습니다.
$coupon = $subscription->discount()->coupon();고객이나 구독에 새 쿠폰 또는 프로모션 코드를 적용하려면 applyCoupon 또는 applyPromotionCode 메서드를 사용합니다.
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');이 경우에도 고객에게 보여 주는 프로모션 코드가 아니라 프로모션 코드에 할당된 Stripe API ID를 사용해야 합니다. 고객이나 구독에는 한 번에 하나의 쿠폰 또는 프로모션 코드만 적용할 수 있습니다.
이 주제에 대한 자세한 내용은 Stripe 문서의 쿠폰과 프로모션 코드 항목을 참고하세요.
구독 추가하기
이미 기본 결제 수단이 있는 고객에게 구독을 추가하려면 구독 빌더의 add 메서드를 호출합니다.
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();Stripe 대시보드에서 구독 생성하기
Stripe 대시보드에서 직접 구독을 생성할 수도 있습니다. 이 경우 Cashier는 새로 추가된 구독을 동기화하고 default 타입을 할당합니다. 대시보드에서 생성한 구독에 할당되는 구독 타입을 바꾸려면 웹훅 이벤트 핸들러를 정의하세요.
또한 Stripe 대시보드에서는 한 가지 타입의 구독만 생성할 수 있습니다. 애플리케이션이 서로 다른 타입을 사용하는 여러 구독을 제공하더라도, 대시보드를 통해 추가할 수 있는 구독 타입은 하나뿐입니다.
마지막으로, 애플리케이션이 제공하는 구독 타입마다 활성 구독은 하나만 추가하도록 항상 주의해야 합니다. 고객에게 default 구독이 두 개 있다면 둘 다 애플리케이션 데이터베이스에 동기화되더라도, Cashier는 가장 최근에 추가된 구독만 사용합니다.
구독 상태 확인
고객이 애플리케이션을 구독하고 나면 다양한 편의 메서드로 구독 상태를 쉽게 확인할 수 있습니다. 먼저 subscribed 메서드는 고객에게 활성 구독이 있으면 현재 체험 기간 중이더라도 true를 반환합니다. subscribed 메서드는 첫 번째 인자로 구독 타입을 받습니다.
if ($user->subscribed('default')) {
// ...
}subscribed 메서드는 라우트 미들웨어로 만들기에도 적합해서, 사용자의 구독 상태에 따라 라우트와 컨트롤러에 대한 접근을 걸러낼 수 있습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// This user is not a paying customer...
return redirect('/billing');
}
return $next($request);
}
}사용자가 아직 체험 기간 중인지 확인하려면 onTrial 메서드를 사용합니다. 사용자에게 체험 기간 중이라는 안내를 표시할지 판단할 때 유용합니다.
if ($user->subscription('default')->onTrial()) {
// ...
}subscribedToProduct 메서드는 Stripe 상품 식별자를 기준으로 사용자가 특정 상품을 구독 중인지 확인합니다. Stripe에서 상품(product)은 가격(price)의 묶음입니다. 아래 예제에서는 사용자의 default 구독이 애플리케이션의 "premium" 상품을 활성 상태로 구독 중인지 확인합니다. 전달하는 Stripe 상품 식별자는 Stripe 대시보드에 있는 상품 식별자 중 하나와 일치해야 합니다.
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}subscribedToProduct 메서드에 배열을 전달하면, 사용자의 default 구독이 애플리케이션의 "basic" 또는 "premium" 상품 중 하나를 활성 상태로 구독 중인지 확인할 수 있습니다.
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}subscribedToPrice 메서드는 고객의 구독이 특정 가격 ID에 해당하는지 확인합니다.
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}recurring 메서드는 사용자가 현재 구독 중이며 더 이상 체험 기간이 아닌지 확인합니다.
if ($user->subscription('default')->recurring()) {
// ...
}WARNING
사용자에게 같은 타입의 구독이 두 개 있다면 subscription 메서드는 항상 가장 최근 구독을 반환합니다. 예를 들어 사용자에게 default 타입의 구독 레코드가 두 개 있을 수 있는데, 하나는 오래전에 만료된 구독이고 다른 하나는 현재 활성 상태인 구독일 수 있습니다. 이런 경우에도 항상 가장 최근 구독이 반환되며, 이전 구독은 이력 확인을 위해 데이터베이스에 남아 있습니다.
취소된 구독 상태
사용자가 한때 활성 구독자였으나 구독을 취소했는지 확인하려면 canceled 메서드를 사용합니다.
if ($user->subscription('default')->canceled()) {
// ...
}사용자가 구독을 취소했지만 구독이 완전히 만료될 때까지의 "유예 기간(grace period)"에 있는지도 확인할 수 있습니다. 예를 들어 원래 3월 10일에 만료될 예정이던 구독을 사용자가 3월 5일에 취소했다면, 사용자는 3월 10일까지 "유예 기간"에 있는 것입니다. 이 기간 동안에도 subscribed 메서드는 계속 true를 반환한다는 점에 유의하세요.
if ($user->subscription('default')->onGracePeriod()) {
// ...
}사용자가 구독을 취소했고 더 이상 "유예 기간"에도 속하지 않는지 확인하려면 ended 메서드를 사용합니다.
if ($user->subscription('default')->ended()) {
// ...
}미완료 및 연체 상태
구독을 생성한 뒤 추가 결제 작업이 필요하면 구독은 incomplete(미완료) 상태로 표시됩니다. 구독 상태는 Cashier의 subscriptions 데이터베이스 테이블의 stripe_status 컬럼에 저장됩니다.
마찬가지로 가격을 교체(swap)할 때 추가 결제 작업이 필요하면 구독은 past_due(연체) 상태로 표시됩니다. 구독이 이 두 상태 중 하나에 있으면 고객이 결제를 확인하기 전까지는 활성화되지 않습니다. 결제가 완료되지 않은 구독인지는 결제 대상 모델 또는 구독 인스턴스의 hasIncompletePayment 메서드로 확인할 수 있습니다.
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}구독에 미완료 결제가 있다면, latestPayment 식별자를 전달하여 사용자를 Cashier의 결제 확인 페이지로 안내해야 합니다. 이 식별자는 구독 인스턴스의 latestPayment 메서드로 가져올 수 있습니다.
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
Please confirm your payment.
</a>구독이 past_due 또는 incomplete 상태일 때도 활성 상태로 간주하고 싶다면 Cashier가 제공하는 keepPastDueSubscriptionsActive와 keepIncompleteSubscriptionsActive 메서드를 사용할 수 있습니다. 보통 이 메서드들은 App\Providers\AppServiceProvider의 register 메서드에서 호출합니다.
use Laravel\Cashier\Cashier;
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}WARNING
구독이 incomplete 상태일 때는 결제가 확인되기 전까지 구독을 변경할 수 없습니다. 따라서 구독이 incomplete 상태에서 swap과 updateQuantity 메서드를 호출하면 예외가 발생합니다.
구독 스코프
대부분의 구독 상태는 쿼리 스코프로도 제공되므로, 특정 상태의 구독을 데이터베이스에서 쉽게 조회할 수 있습니다.
// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();
// Get all of the canceled subscriptions for a user...
$subscriptions = $user->subscriptions()->canceled()->get();사용할 수 있는 전체 스코프 목록은 다음과 같습니다.
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();가격 변경
고객이 애플리케이션을 구독한 뒤에 새로운 구독 가격으로 변경하고 싶어 할 수 있습니다. 고객의 가격을 바꾸려면 Stripe 가격 식별자를 swap 메서드에 전달합니다. 가격을 교체할 때는 이전에 구독을 취소한 사용자가 구독을 다시 활성화하려는 것으로 간주합니다. 전달하는 가격 식별자는 Stripe 대시보드에 있는 Stripe 가격 식별자와 일치해야 합니다.
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');고객이 체험 기간 중이라면 체험 기간이 유지됩니다. 또한 구독에 "수량"이 있다면 그 수량도 그대로 유지됩니다.
가격을 교체하면서 고객이 현재 진행 중인 체험 기간을 취소하려면 skipTrial 메서드를 호출합니다.
$user->subscription('default')
->skipTrial()
->swap('price_yearly');다음 청구 주기를 기다리지 않고 가격을 교체하면서 즉시 고객에게 인보이스를 발행하려면 swapAndInvoice 메서드를 사용합니다.
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');일할 계산(Proration)
기본적으로 Stripe는 가격을 교체할 때 요금을 일할 계산(proration)합니다. noProrate 메서드를 사용하면 요금을 일할 계산하지 않고 구독 가격을 갱신할 수 있습니다.
$user->subscription('default')->noProrate()->swap('price_yearly');구독 일할 계산에 대한 자세한 내용은 Stripe 문서를 참고하세요.
WARNING
swapAndInvoice 메서드보다 먼저 noProrate 메서드를 실행해도 일할 계산에는 아무런 영향이 없습니다. 인보이스는 항상 발행됩니다.
구독 수량
구독은 때로 "수량"의 영향을 받습니다. 예를 들어 프로젝트 관리 애플리케이션이 프로젝트 하나당 월 10달러를 청구할 수 있습니다. incrementQuantity와 decrementQuantity 메서드를 사용하면 구독 수량을 쉽게 늘리거나 줄일 수 있습니다.
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// Subtract five from the subscription's current quantity...
$user->subscription('default')->decrementQuantity(5);또는 updateQuantity 메서드로 특정 수량을 직접 지정할 수도 있습니다.
$user->subscription('default')->updateQuantity(10);noProrate 메서드를 사용하면 요금을 일할 계산하지 않고 구독 수량을 갱신할 수 있습니다.
$user->subscription('default')->noProrate()->updateQuantity(10);구독 수량에 대한 자세한 내용은 Stripe 문서를 참고하세요.
여러 상품이 포함된 구독의 수량
여러 상품이 포함된 구독이라면, 수량을 늘리거나 줄일 가격의 ID를 증가/감소 메서드의 두 번째 인자로 전달해야 합니다.
$user->subscription('default')->incrementQuantity(1, 'price_chat');여러 상품이 포함된 구독
여러 상품이 포함된 구독을 사용하면 하나의 구독에 여러 청구 상품을 할당할 수 있습니다. 예를 들어 월 10달러의 기본 구독 가격에 월 15달러의 실시간 채팅 애드온 상품을 추가로 제공하는 고객 지원 "헬프데스크" 애플리케이션을 만든다고 가정해 보세요. 여러 상품이 포함된 구독의 정보는 Cashier의 subscription_items 데이터베이스 테이블에 저장됩니다.
newSubscription 메서드의 두 번째 인자로 가격 배열을 전달하면 하나의 구독에 여러 상품을 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});위 예제에서 고객의 default 구독에는 두 개의 가격이 연결됩니다. 두 가격은 각자의 청구 주기에 따라 청구됩니다. 필요하다면 quantity 메서드로 각 가격별 수량을 지정할 수 있습니다.
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);기존 구독에 다른 가격을 추가하려면 구독의 addPrice 메서드를 호출합니다.
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');위 예제는 새 가격을 추가하며, 고객에게는 다음 청구 주기에 해당 가격이 청구됩니다. 고객에게 즉시 청구하려면 addPriceAndInvoice 메서드를 사용합니다.
$user->subscription('default')->addPriceAndInvoice('price_chat');특정 수량으로 가격을 추가하려면 addPrice 또는 addPriceAndInvoice 메서드의 두 번째 인자로 수량을 전달할 수 있습니다.
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);removePrice 메서드로 구독에서 가격을 제거할 수 있습니다.
$user->subscription('default')->removePrice('price_chat');WARNING
구독의 마지막 가격은 제거할 수 없습니다. 대신 구독을 취소해야 합니다.
가격 교체
여러 상품이 포함된 구독에서도 구독에 연결된 가격을 변경할 수 있습니다. 예를 들어 고객이 price_basic 구독에 price_chat 애드온 상품을 함께 사용 중이고, 이 고객을 price_basic에서 price_pro 가격으로 업그레이드하려는 경우를 생각해 보세요.
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);위 예제를 실행하면 price_basic에 해당하는 기존 구독 항목(subscription item)은 삭제되고 price_chat 항목은 그대로 유지됩니다. 또한 price_pro에 대한 새 구독 항목이 생성됩니다.
swap 메서드에 키/값 쌍의 배열을 전달해 구독 항목 옵션을 지정할 수도 있습니다. 예를 들어 구독 가격별 수량을 지정해야 할 수 있습니다.
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);구독의 가격 하나만 교체하고 싶다면 구독 항목 자체의 swap 메서드를 사용할 수 있습니다. 이 방식은 구독의 다른 가격에 있는 기존 메타데이터를 모두 보존하고 싶을 때 특히 유용합니다.
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');일할 계산(Proration)
기본적으로 Stripe는 여러 상품이 포함된 구독에서 가격을 추가하거나 제거할 때 요금을 일할 계산합니다. 일할 계산 없이 가격을 조정하려면 가격 관련 작업에 noProrate 메서드를 체이닝하세요.
$user->subscription('default')->noProrate()->removePrice('price_chat');수량
개별 구독 가격의 수량을 갱신하려면 기존 수량 메서드에 가격 ID를 추가 인자로 전달하면 됩니다.
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'price_chat');
$user->subscription('default')->decrementQuantity(3, 'price_chat');
$user->subscription('default')->updateQuantity(10, 'price_chat');WARNING
구독에 여러 가격이 있으면 Subscription 모델의 stripe_price와 quantity 속성은 null이 됩니다. 개별 가격의 속성에 접근하려면 Subscription 모델의 items 관계를 사용해야 합니다.
구독 항목(Subscription Items)
구독에 여러 가격이 있으면 데이터베이스의 subscription_items 테이블에 여러 구독 "항목"이 저장됩니다. 구독의 items 관계를 통해 이 항목들에 접근할 수 있습니다.
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// Retrieve the Stripe price and quantity for a specific item...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;findItemOrFail 메서드로 특정 가격을 조회할 수도 있습니다.
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');복수 구독
Stripe에서는 고객이 여러 구독을 동시에 가질 수 있습니다. 예를 들어 수영 구독과 웨이트 트레이닝 구독을 제공하는 체육관을 운영하며 각 구독의 가격이 서로 다를 수 있습니다. 물론 고객은 둘 중 하나만 구독할 수도, 둘 다 구독할 수도 있어야 합니다.
애플리케이션에서 구독을 생성할 때 newSubscription 메서드에 구독 타입을 전달할 수 있습니다. 타입은 사용자가 시작하는 구독의 종류를 나타내는 임의의 문자열이면 됩니다.
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});이 예제에서는 고객의 월간 수영 구독을 시작했습니다. 하지만 고객이 나중에 연간 구독으로 바꾸고 싶어 할 수 있습니다. 고객의 구독을 조정할 때는 swimming 구독의 가격만 교체하면 됩니다.
$user->subscription('swimming')->swap('price_swimming_yearly');물론 구독을 완전히 취소할 수도 있습니다.
$user->subscription('swimming')->cancel();사용량 기반 과금
사용량 기반 과금은 청구 주기 동안의 상품 사용량에 따라 고객에게 요금을 부과하는 방식입니다. 예를 들어 고객이 한 달 동안 보낸 문자 메시지나 이메일 수에 따라 요금을 청구할 수 있습니다.
사용량 기반 과금을 시작하려면 먼저 Stripe 대시보드에서 사용량 기반 과금 모델과 미터(meter)를 가진 새 상품을 만들어야 합니다. 미터를 만든 뒤에는 사용량을 보고하고 조회할 때 필요한 이벤트 이름과 미터 ID를 저장해 두세요. 그런 다음 meteredPrice 메서드로 고객 구독에 종량제 가격 ID를 추가합니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});Stripe Checkout으로 종량제 구독을 시작할 수도 있습니다.
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);사용량 보고
고객이 애플리케이션을 사용하는 동안, 정확한 요금이 청구되도록 사용량을 Stripe에 보고해야 합니다. 종량제 이벤트의 사용량을 보고하려면 Billable 모델의 reportMeterEvent 메서드를 사용합니다.
$user = User::find(1);
$user->reportMeterEvent('emails-sent');기본적으로 청구 기간에는 "사용량" 1이 추가됩니다. 또는 청구 기간의 고객 사용량에 추가할 특정 "사용량"을 직접 전달할 수도 있습니다.
$user = User::find(1);
$user->reportMeterEvent('emails-sent', quantity: 15);미터에 대한 고객의 이벤트 요약을 조회하려면 Billable 인스턴스의 meterEventSummaries 메서드를 사용합니다.
$user = User::find(1);
$meterUsage = $user->meterEventSummaries($meterId);
$meterUsage->first()->aggregated_value; // 10미터 이벤트 요약에 대한 자세한 내용은 Stripe의 Meter Event Summary 객체 문서를 참고하세요.
모든 미터를 조회하려면 Billable 인스턴스의 meters 메서드를 사용합니다.
$user = User::find(1);
$user->meters();구독 세금
WARNING
세율을 수동으로 계산하는 대신 Stripe Tax를 사용해 세금을 자동으로 계산할 수 있습니다.
사용자가 구독에 대해 부담하는 세율을 지정하려면 결제 대상 모델에 taxRates 메서드를 구현하고 Stripe 세율 ID가 담긴 배열을 반환하세요. 세율은 Stripe 대시보드에서 정의할 수 있습니다.
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}taxRates 메서드를 사용하면 고객별로 세율을 적용할 수 있어, 여러 국가와 세율에 걸쳐 있는 사용자 기반에 유용할 수 있습니다.
여러 상품이 포함된 구독을 제공한다면, 결제 대상 모델에 priceTaxRates 메서드를 구현해 가격별로 서로 다른 세율을 정의할 수 있습니다.
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}WARNING
taxRates 메서드는 구독 요금에만 적용됩니다. Cashier로 "일회성" 결제를 한다면 그 시점에 세율을 직접 지정해야 합니다.
세율 동기화
taxRates 메서드가 반환하는 하드코딩된 세율 ID를 변경해도, 사용자의 기존 구독에 설정된 세금 설정은 그대로 유지됩니다. 기존 구독의 세금 값을 새 taxRates 값으로 갱신하려면 사용자의 구독 인스턴스에서 syncTaxRates 메서드를 호출해야 합니다.
$user->subscription('default')->syncTaxRates();이 메서드는 여러 상품이 포함된 구독의 항목별 세율도 함께 동기화합니다. 애플리케이션이 여러 상품이 포함된 구독을 제공한다면, 결제 대상 모델이 앞에서 설명한 priceTaxRates 메서드를 구현하고 있는지 확인하세요.
면세
Cashier는 고객이 면세 대상인지 확인할 수 있도록 isNotTaxExempt, isTaxExempt, reverseChargeApplies 메서드도 제공합니다. 이 메서드들은 Stripe API를 호출해 고객의 면세 상태를 판단합니다.
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();WARNING
이 메서드들은 Laravel\Cashier\Invoice 객체에서도 사용할 수 있습니다. 다만 Invoice 객체에서 호출하면 인보이스가 생성된 시점의 면세 상태를 판단합니다.
구독 기준일
기본적으로 청구 주기의 기준일(billing cycle anchor)은 구독이 생성된 날짜이며, 체험 기간을 사용한다면 체험 기간이 끝나는 날짜입니다. 청구 기준일을 변경하려면 anchorBillingCycleOn 메서드를 사용합니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});구독 청구 주기 관리에 대한 자세한 내용은 Stripe 청구 주기 문서를 참고하세요.
구독 취소
구독을 취소하려면 사용자의 구독에서 cancel 메서드를 호출합니다.
$user->subscription('default')->cancel();구독이 취소되면 Cashier는 subscriptions 데이터베이스 테이블의 ends_at 컬럼을 자동으로 설정합니다. 이 컬럼은 subscribed 메서드가 언제부터 false를 반환해야 하는지 판단하는 데 사용됩니다.
예를 들어 고객이 3월 1일에 구독을 취소했지만 구독이 3월 5일에 종료될 예정이었다면, subscribed 메서드는 3월 5일까지 계속 true를 반환합니다. 사용자는 일반적으로 청구 주기가 끝날 때까지 애플리케이션을 계속 사용할 수 있기 때문입니다.
사용자가 구독을 취소했지만 아직 "유예 기간"에 있는지는 onGracePeriod 메서드로 확인할 수 있습니다.
if ($user->subscription('default')->onGracePeriod()) {
// ...
}구독을 즉시 취소하려면 사용자의 구독에서 cancelNow 메서드를 호출합니다.
$user->subscription('default')->cancelNow();구독을 즉시 취소하면서 아직 인보이스로 청구되지 않은 종량제 사용량이나 새로 생긴/대기 중인 일할 계산 인보이스 항목까지 청구하려면 사용자의 구독에서 cancelNowAndInvoice 메서드를 호출합니다.
$user->subscription('default')->cancelNowAndInvoice();특정 시점에 구독이 취소되도록 지정할 수도 있습니다.
$user->subscription('default')->cancelAt(
now()->plus(days: 10)
);마지막으로, 사용자 모델을 삭제하기 전에는 항상 해당 사용자의 구독을 먼저 취소해야 합니다.
$user->subscription('default')->cancelNow();
$user->delete();구독 재개
고객이 구독을 취소한 뒤 다시 재개하려면 구독의 resume 메서드를 호출합니다. 구독을 재개하려면 고객이 아직 "유예 기간" 안에 있어야 합니다.
$user->subscription('default')->resume();고객이 구독을 취소했다가 구독이 완전히 만료되기 전에 다시 재개하면, 고객에게 즉시 요금이 청구되지 않습니다. 대신 구독이 다시 활성화되고 원래의 청구 주기에 따라 요금이 청구됩니다.
구독 체험 기간
결제 수단을 먼저 받는 경우
결제 수단 정보를 미리 받으면서 고객에게 체험 기간을 제공하려면, 구독을 생성할 때 trialDays 메서드를 사용합니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});이 메서드는 데이터베이스의 구독 레코드에 체험 기간 종료일을 설정하고, Stripe에는 이 날짜 이후에 고객에게 청구를 시작하도록 지시합니다. trialDays 메서드를 사용하면 Cashier는 Stripe의 해당 가격에 설정된 기본 체험 기간을 덮어씁니다.
WARNING
체험 기간 종료일 전에 고객이 구독을 취소하지 않으면 체험이 끝나는 즉시 요금이 청구됩니다. 따라서 사용자에게 체험 기간 종료일을 반드시 안내해야 합니다.
trialUntil 메서드에는 체험 기간이 끝나는 시점을 나타내는 DateTime 인스턴스를 전달할 수 있습니다.
use Illuminate\Support\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->plus(days: 10))
->create($paymentMethod);사용자가 체험 기간 중인지는 사용자 인스턴스의 onTrial 메서드 또는 구독 인스턴스의 onTrial 메서드로 확인할 수 있습니다. 아래 두 예제는 동일하게 동작합니다.
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}endTrial 메서드를 사용하면 구독 체험 기간을 즉시 종료할 수 있습니다.
$user->subscription('default')->endTrial();기존 체험 기간이 만료되었는지는 hasExpiredTrial 메서드로 확인할 수 있습니다.
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}Stripe / Cashier에서 체험 일수 정의하기
가격별 체험 일수는 Stripe 대시보드에서 정의할 수도 있고, 항상 Cashier에서 명시적으로 전달할 수도 있습니다. 체험 일수를 Stripe에서 정의한다면, 과거에 구독한 적이 있는 고객의 새 구독을 포함해 모든 새 구독에 항상 체험 기간이 적용된다는 점에 유의해야 합니다. 체험 기간을 적용하지 않으려면 skipTrial() 메서드를 명시적으로 호출해야 합니다.
결제 수단 없이 시작하는 경우
사용자의 결제 수단 정보를 미리 받지 않고 체험 기간을 제공하려면, 사용자 레코드의 trial_ends_at 컬럼에 원하는 체험 기간 종료일을 설정하면 됩니다. 보통 사용자 가입 시점에 설정합니다.
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->plus(days: 10),
]);WARNING
결제 대상 모델의 클래스 정의에서 trial_ends_at 속성에 날짜 캐스트를 반드시 추가하세요.
Cashier는 이런 체험 기간을 어떤 기존 구독에도 연결되어 있지 않다는 의미에서 "일반 체험(generic trial)"이라고 부릅니다. 현재 날짜가 trial_ends_at 값을 지나지 않았다면 결제 대상 모델 인스턴스의 onTrial 메서드가 true를 반환합니다.
if ($user->onTrial()) {
// User is within their trial period...
}사용자의 실제 구독을 생성할 준비가 되면 평소처럼 newSubscription 메서드를 사용하면 됩니다.
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);사용자의 체험 기간 종료일을 조회하려면 trialEndsAt 메서드를 사용합니다. 이 메서드는 사용자가 체험 기간 중이면 Carbon 날짜 인스턴스를, 그렇지 않으면 null을 반환합니다. 기본 구독이 아닌 특정 구독의 체험 종료일을 얻고 싶다면 선택 사항인 구독 타입 파라미터를 전달할 수도 있습니다.
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}사용자가 아직 실제 구독을 만들지 않은 "일반" 체험 기간 중인지를 구체적으로 알고 싶다면 onGenericTrial 메서드를 사용할 수 있습니다.
if ($user->onGenericTrial()) {
// User is within their "generic" trial period...
}체험 기간 연장
extendTrial 메서드를 사용하면 구독을 생성한 뒤에도 구독의 체험 기간을 연장할 수 있습니다. 체험 기간이 이미 끝나서 고객에게 구독 요금이 청구되고 있더라도 연장된 체험 기간을 제공할 수 있으며, 체험 기간에 해당하는 기간은 고객의 다음 인보이스에서 차감됩니다.
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// End the trial 7 days from now...
$subscription->extendTrial(
now()->plus(days: 7)
);
// Add an additional 5 days to the trial...
$subscription->extendTrial(
$subscription->trial_ends_at->plus(days: 5)
);Stripe 웹훅 처리
NOTE
로컬 개발 중 웹훅을 테스트할 때는 Stripe CLI를 활용할 수 있습니다.
Stripe는 웹훅을 통해 애플리케이션에 다양한 이벤트를 알릴 수 있습니다. 기본적으로 Cashier 서비스 프로바이더는 Cashier의 웹훅 컨트롤러를 가리키는 라우트를 자동으로 등록합니다. 이 컨트롤러가 들어오는 모든 웹훅 요청을 처리합니다.
기본적으로 Cashier 웹훅 컨트롤러는 결제 실패 횟수가 너무 많은 구독의 취소(Stripe 설정에 따라 정의됨), 고객 정보 갱신, 고객 삭제, 구독 갱신, 결제 수단 변경을 자동으로 처리합니다. 다만 곧 살펴보겠지만, 이 컨트롤러를 확장하여 원하는 어떤 Stripe 웹훅 이벤트든 처리할 수 있습니다.
애플리케이션이 Stripe 웹훅을 처리할 수 있도록 Stripe 관리 콘솔에서 웹훅 URL을 반드시 설정하세요. 기본적으로 Cashier의 웹훅 컨트롤러는 /stripe/webhook URL 경로에 응답합니다. Stripe 관리 콘솔에서 활성화해야 하는 웹훅의 전체 목록은 다음과 같습니다.
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
편의를 위해 Cashier에는 cashier:webhook Artisan 명령어가 포함되어 있습니다. 이 명령어는 Cashier에 필요한 모든 이벤트를 수신하는 웹훅을 Stripe에 생성합니다.
php artisan cashier:webhook기본적으로 생성된 웹훅은 APP_URL 환경 변수에 정의된 URL과 Cashier에 포함된 cashier.webhook 라우트를 가리킵니다. 다른 URL을 사용하려면 명령어를 실행할 때 --url 옵션을 지정하면 됩니다.
php artisan cashier:webhook --url "https://example.com/stripe/webhook"생성되는 웹훅은 Cashier와 동일한 Stripe API 버전을 사용하며, 이 버전은 설치된 stripe/stripe-php 버전에 따라 결정됩니다. 다른 Stripe 버전을 사용하려면 --api-version 옵션을 지정할 수 있습니다.
php artisan cashier:webhook --api-version="2019-12-03"웹훅은 생성 직후 바로 활성화됩니다. 웹훅을 만들되 준비가 될 때까지 비활성 상태로 두고 싶다면 명령어를 실행할 때 --disabled 옵션을 지정하세요.
php artisan cashier:webhook --disabledWARNING
들어오는 Stripe 웹훅 요청은 Cashier에 포함된 웹훅 서명 검증 미들웨어로 반드시 보호하세요.
웹훅과 CSRF 보호
Stripe 웹훅은 Laravel의 CSRF 보호를 우회해야 하므로, Laravel이 들어오는 Stripe 웹훅의 CSRF 토큰을 검증하지 않도록 해야 합니다. 이를 위해 애플리케이션의 bootstrap/app.php 파일에서 stripe/*를 CSRF 보호 대상에서 제외하세요.
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(except: [
'stripe/*',
]);
})웹훅 이벤트 핸들러 정의
Cashier는 결제 실패로 인한 구독 취소와 그 밖의 일반적인 Stripe 웹훅 이벤트를 자동으로 처리합니다. 하지만 처리하고 싶은 웹훅 이벤트가 더 있다면, Cashier가 디스패치하는 다음 이벤트를 리슨(listen)하면 됩니다.
Laravel\Cashier\Events\WebhookReceivedLaravel\Cashier\Events\WebhookHandled
두 이벤트 모두 Stripe 웹훅의 전체 페이로드를 담고 있습니다. 예를 들어 invoice.payment_succeeded 웹훅을 처리하려면, 해당 이벤트를 처리할 리스너를 등록하면 됩니다.
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* Handle received Stripe webhooks.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// Handle the incoming event...
}
}
}웹훅 서명 검증
웹훅을 안전하게 보호하려면 Stripe의 웹훅 서명을 사용할 수 있습니다. 편의를 위해 Cashier는 들어오는 Stripe 웹훅 요청이 유효한지 검증하는 미들웨어를 자동으로 포함합니다.
웹훅 검증을 활성화하려면 애플리케이션의 .env 파일에 STRIPE_WEBHOOK_SECRET 환경 변수가 설정되어 있는지 확인하세요. 웹훅 secret은 Stripe 계정 대시보드에서 확인할 수 있습니다.
단건 결제
간단한 결제
결제 수단 식별자를 사용해 고객에게 일회성 결제를 하려면 결제 대상 모델 인스턴스의 charge 메서드를 사용합니다. 일회성 결제를 처리하기 전에 고객의 결제 정보를 수집해야 한다면 단건 결제를 위한 Payment Element 문서를 참고하세요.
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$payment = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});charge 메서드는 세 번째 인자로 배열을 받아, 내부적으로 Stripe Payment Intent를 생성할 때 원하는 옵션을 전달할 수 있습니다. Payment Intent 생성 시 사용할 수 있는 옵션은 Stripe 문서에서 확인할 수 있습니다.
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);고객이나 사용자 없이 charge 메서드를 사용할 수도 있습니다. 이 경우 애플리케이션의 결제 대상 모델의 새 인스턴스에서 charge 메서드를 호출하세요.
use App\Models\User;
$payment = (new User)->charge(100, $paymentMethod);charge 메서드는 결제에 실패하면 예외를 던집니다. 결제에 성공하면 Laravel\Cashier\Payment 인스턴스가 반환됩니다.
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}WARNING
charge 메서드는 애플리케이션에서 사용하는 통화의 최소 단위로 결제 금액을 받습니다. 예를 들어 고객이 미국 달러로 결제한다면 금액은 센트(cent) 단위로 지정해야 합니다. 원화(KRW)처럼 소수 단위가 없는 통화는 원 단위 금액을 그대로 전달하면 됩니다.
인보이스를 포함한 결제
일회성 결제를 하면서 고객에게 PDF 인보이스를 제공해야 할 때가 있습니다. invoicePrice 메서드가 바로 그 용도입니다. 예를 들어 고객에게 새 셔츠 5벌에 대한 인보이스를 발행해 보겠습니다.
$user->invoicePrice('price_tshirt', 5);인보이스는 사용자의 기본 결제 수단으로 즉시 청구됩니다. invoicePrice 메서드는 세 번째 인자로 배열도 받으며, 이 배열에는 인보이스 항목의 청구 옵션을 담습니다. 네 번째 인자 역시 배열이며, 인보이스 자체의 청구 옵션을 담아야 합니다.
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);invoicePrice와 비슷하게 tabPrice 메서드를 사용하면 고객의 "탭(tab)"에 여러 항목(인보이스당 최대 250개)을 담은 뒤 고객에게 인보이스를 발행하는 방식으로 일회성 결제를 만들 수 있습니다. 예를 들어 셔츠 5벌과 머그컵 2개에 대한 인보이스를 발행할 수 있습니다.
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();또는 invoiceFor 메서드로 고객의 기본 결제 수단에 "일회성" 결제를 할 수도 있습니다.
$user->invoiceFor('One Time Fee', 500);invoiceFor 메서드를 사용할 수는 있지만, 미리 정의된 가격과 함께 invoicePrice, tabPrice 메서드를 사용하는 것을 권장합니다. 그러면 Stripe 대시보드에서 상품별 매출에 대한 더 나은 분석과 데이터를 확인할 수 있습니다.
WARNING
invoice, invoicePrice, invoiceFor 메서드는 결제 실패 시 재시도하는 Stripe 인보이스를 생성합니다. 인보이스가 실패한 결제를 재시도하지 않기를 원한다면, 첫 번째 결제 실패 후 Stripe API로 해당 인보이스를 닫아야 합니다.
Payment Intent 생성
결제 대상 모델 인스턴스의 pay 메서드를 호출하면 새 Stripe Payment Intent를 생성할 수 있습니다. 이 메서드를 호출하면 Laravel\Cashier\Payment 인스턴스로 감싼 Payment Intent가 생성됩니다.
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->pay(
$request->get('amount')
);
return $payment->client_secret;
});Payment Intent를 생성한 뒤에는 클라이언트 시크릿을 애플리케이션의 프런트엔드로 반환해 사용자가 브라우저에서 결제를 완료하게 할 수 있습니다. Stripe Payment Intent로 전체 결제 흐름을 구성하는 방법은 Stripe 문서를 참고하세요.
pay 메서드를 사용하면 Stripe 대시보드에서 활성화된 기본 결제 수단이 고객에게 제공됩니다. 특정 결제 수단만 허용하고 싶다면 payWith 메서드를 사용할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->payWith(
$request->get('amount'), ['card', 'bancontact']
);
return $payment->client_secret;
});WARNING
pay와 payWith 메서드는 애플리케이션에서 사용하는 통화의 최소 단위로 결제 금액을 받습니다. 예를 들어 고객이 미국 달러로 결제한다면 금액은 센트(cent) 단위로 지정해야 합니다.
결제 환불
Stripe 결제를 환불해야 한다면 refund 메서드를 사용합니다. 이 메서드는 첫 번째 인자로 Stripe Payment Intent ID를 받습니다.
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);인보이스
인보이스 조회
invoices 메서드를 사용하면 결제 대상 모델의 인보이스 배열을 쉽게 조회할 수 있습니다. invoices 메서드는 Laravel\Cashier\Invoice 인스턴스의 컬렉션을 반환합니다.
$invoices = $user->invoices();결과에 대기 중(pending) 인보이스도 포함하려면 invoicesIncludingPending 메서드를 사용합니다.
$invoices = $user->invoicesIncludingPending();findInvoice 메서드로 ID를 지정해 특정 인보이스를 조회할 수 있습니다.
$invoice = $user->findInvoice($invoiceId);인보이스 정보 표시하기
고객의 인보이스 목록을 보여 줄 때는 인보이스의 메서드를 사용해 필요한 인보이스 정보를 표시할 수 있습니다. 예를 들어 모든 인보이스를 표로 나열하고, 사용자가 원하는 인보이스를 쉽게 내려받을 수 있게 할 수 있습니다.
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
</tr>
@endforeach
</table>예정된 인보이스
고객의 예정된 인보이스를 조회하려면 upcomingInvoice 메서드를 사용합니다.
$invoice = $user->upcomingInvoice();마찬가지로 고객에게 여러 구독이 있다면 특정 구독의 예정된 인보이스를 조회할 수도 있습니다.
$invoice = $user->subscription('default')->upcomingInvoice();구독 인보이스 미리보기
previewInvoice 메서드를 사용하면 가격을 변경하기 전에 인보이스를 미리 확인할 수 있습니다. 특정 가격 변경이 적용되었을 때 고객의 인보이스가 어떻게 보일지 알 수 있습니다.
$invoice = $user->subscription('default')->previewInvoice('price_yearly');previewInvoice 메서드에 가격 배열을 전달하면 여러 개의 새 가격이 적용된 인보이스를 미리 볼 수 있습니다.
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);인보이스 PDF 생성
인보이스 PDF를 생성하기 전에, Composer로 Cashier의 기본 인보이스 렌더러인 Dompdf 라이브러리를 설치해야 합니다.
composer require dompdf/dompdf라우트나 컨트롤러에서 downloadInvoice 메서드를 사용하면 특정 인보이스의 PDF 다운로드를 생성할 수 있습니다. 이 메서드는 인보이스 다운로드에 필요한 적절한 HTTP 응답을 자동으로 만들어 줍니다.
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
return $request->user()->downloadInvoice($invoiceId);
});기본적으로 인보이스의 모든 데이터는 Stripe에 저장된 고객 및 인보이스 데이터에서 가져오며, 파일 이름은 app.name 설정값을 기준으로 정해집니다. 다만 downloadInvoice 메서드의 두 번째 인자로 배열을 전달하면 이 데이터 중 일부를 직접 지정할 수 있습니다. 이 배열로 회사 정보와 상품 정보 등을 지정할 수 있습니다.
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
'street' => 'Main Str. 1',
'location' => '2000 Antwerp, Belgium',
'phone' => '+32 499 00 00 00',
'email' => 'info@example.com',
'url' => 'https://example.com',
'vendorVat' => 'BE123456789',
]);downloadInvoice 메서드는 세 번째 인자로 사용자 지정 파일 이름도 받을 수 있습니다. 이 파일 이름에는 자동으로 .pdf가 붙습니다.
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');커스텀 인보이스 렌더러
Cashier에서는 커스텀 인보이스 렌더러를 사용할 수도 있습니다. 기본적으로 Cashier는 dompdf PHP 라이브러리로 인보이스를 생성하는 DompdfInvoiceRenderer 구현체를 사용합니다. 하지만 Laravel\Cashier\Contracts\InvoiceRenderer 인터페이스를 구현하면 원하는 렌더러를 무엇이든 사용할 수 있습니다. 예를 들어 서드파티 PDF 렌더링 서비스에 API 호출을 보내 인보이스 PDF를 렌더링할 수 있습니다.
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* Render the given invoice and return the raw PDF bytes.
*/
public function render(Invoice $invoice, array $data = [], array $options = []): string
{
$html = $invoice->view($data)->render();
return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
}
}인보이스 렌더러 컨트랙트를 구현했다면, 애플리케이션의 config/cashier.php 설정 파일에서 cashier.invoices.renderer 설정값을 수정해야 합니다. 이 설정값에는 커스텀 렌더러 구현 클래스의 이름을 지정하세요.
체크아웃
Cashier Stripe는 Stripe Checkout도 지원합니다. Stripe Checkout은 미리 만들어진 호스팅 결제 페이지를 제공하므로, 결제를 받기 위한 커스텀 페이지를 직접 구현하는 수고를 덜어 줍니다.
다음 문서에서는 Cashier와 함께 Stripe Checkout을 시작하는 방법을 설명합니다. Stripe Checkout에 대해 더 알고 싶다면 Stripe의 Checkout 공식 문서도 함께 살펴보세요.
상품 체크아웃
결제 대상 모델의 checkout 메서드로 Stripe 대시보드에 생성해 둔 기존 상품에 대한 체크아웃을 진행할 수 있습니다. checkout 메서드는 새 Stripe Checkout 세션을 시작하며, 기본적으로 Stripe 가격 ID를 전달해야 합니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});필요하다면 상품 수량도 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});고객이 이 라우트에 접속하면 Stripe의 Checkout 페이지로 리디렉션됩니다. 기본적으로 사용자가 구매를 성공적으로 완료하거나 취소하면 home 라우트 위치로 리디렉션되지만, success_url과 cancel_url 옵션으로 원하는 콜백 URL을 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});success_url 체크아웃 옵션을 정의할 때, Stripe가 URL을 호출하면서 체크아웃 세션 ID를 쿼리 스트링 파라미터로 추가하도록 지시할 수 있습니다. 이를 위해 success_url 쿼리 스트링에 {CHECKOUT_SESSION_ID}라는 문자열을 그대로 추가하세요. Stripe가 이 플레이스홀더를 실제 체크아웃 세션 ID로 바꿔 줍니다.
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');프로모션 코드
기본적으로 Stripe Checkout은 사용자가 직접 사용하는 프로모션 코드를 허용하지 않습니다. 다행히 Checkout 페이지에서 이를 활성화하는 간단한 방법이 있습니다. allowPromotionCodes 메서드를 호출하면 됩니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});단건 결제 체크아웃
Stripe 대시보드에 생성하지 않은 임시(ad-hoc) 상품에 대해서도 간단한 결제를 진행할 수 있습니다. 결제 대상 모델의 checkoutCharge 메서드에 결제 금액, 상품 이름, 그리고 선택 사항인 수량을 전달하면 됩니다. 고객이 이 라우트에 접속하면 Stripe의 Checkout 페이지로 리디렉션됩니다.
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});WARNING
checkoutCharge 메서드를 사용하면 Stripe는 항상 Stripe 대시보드에 새 상품과 가격을 생성합니다. 따라서 상품을 Stripe 대시보드에 미리 만들어 두고 checkout 메서드를 대신 사용하는 것을 권장합니다.
구독 체크아웃
WARNING
구독에 Stripe Checkout을 사용하려면 Stripe 대시보드에서 customer.subscription.created 웹훅을 활성화해야 합니다. 이 웹훅이 데이터베이스에 구독 레코드를 생성하고 관련된 모든 구독 항목을 저장합니다.
Stripe Checkout으로 구독을 시작할 수도 있습니다. Cashier의 구독 빌더 메서드로 구독을 정의한 뒤 checkout 메서드를 호출하면 됩니다. 고객이 이 라우트에 접속하면 Stripe의 Checkout 페이지로 리디렉션됩니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});상품 체크아웃과 마찬가지로 성공 및 취소 URL을 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});물론 구독 체크아웃에도 프로모션 코드를 활성화할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});WARNING
안타깝게도 Stripe Checkout은 구독을 시작할 때 모든 구독 청구 옵션을 지원하지 않습니다. 구독 빌더의 anchorBillingCycleOn 메서드를 사용하거나, 일할 계산 동작 또는 결제 동작을 설정해도 Stripe Checkout 세션에서는 아무런 효과가 없습니다. 사용할 수 있는 파라미터는 Stripe Checkout Session API 문서에서 확인하세요.
Stripe Checkout과 체험 기간
Stripe Checkout으로 완료할 구독을 만들 때도 체험 기간을 정의할 수 있습니다.
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();다만 체험 기간은 Stripe Checkout이 지원하는 최소 체험 시간인 48시간 이상이어야 합니다.
구독과 웹훅
Stripe와 Cashier는 웹훅을 통해 구독 상태를 갱신한다는 점을 기억하세요. 따라서 고객이 결제 정보를 입력하고 애플리케이션으로 돌아왔을 때 구독이 아직 활성화되지 않았을 수 있습니다. 이런 경우를 처리하려면 사용자에게 결제 또는 구독이 처리 중이라는 메시지를 표시하는 것이 좋습니다.
세금 ID 수집
Checkout은 고객의 세금 ID 수집도 지원합니다. 체크아웃 세션에서 이를 활성화하려면 세션을 생성할 때 collectTaxIds 메서드를 호출합니다.
$checkout = $user->collectTaxIds()->checkout('price_tshirt');이 메서드를 호출하면 고객이 회사 명의로 구매하는지 표시할 수 있는 체크박스가 새로 나타납니다. 회사 명의로 구매한다고 표시하면 세금 ID 번호를 입력할 수 있습니다.
WARNING
애플리케이션의 서비스 프로바이더에서 이미 자동 세금 계산을 설정했다면 이 기능은 자동으로 활성화되므로 collectTaxIds 메서드를 호출할 필요가 없습니다.
비회원 체크아웃
Checkout::guest 메서드를 사용하면 "계정"이 없는 애플리케이션 방문자(guest)를 위한 체크아웃 세션을 시작할 수 있습니다.
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});기존 사용자를 위한 체크아웃 세션을 만들 때와 마찬가지로, Laravel\Cashier\CheckoutBuilder 인스턴스에서 제공하는 추가 메서드를 사용해 비회원 체크아웃 세션을 사용자 지정할 수 있습니다.
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()
->withPromotionCode('promo-code')
->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});비회원 체크아웃이 완료되면 Stripe는 checkout.session.completed 웹훅 이벤트를 보낼 수 있으므로, 이 이벤트가 실제로 애플리케이션에 전송되도록 Stripe 웹훅을 설정해야 합니다. Stripe 대시보드에서 웹훅을 활성화하면 Cashier로 웹훅을 처리할 수 있습니다. 웹훅 페이로드에 담긴 객체는 checkout 객체이며, 이를 확인해 고객의 주문을 이행할 수 있습니다.
결제 실패 처리
구독이나 단건 결제가 실패하는 경우가 있습니다. 이때 Cashier는 결제 실패를 알리는 Laravel\Cashier\Exceptions\IncompletePayment 예외를 던집니다. 이 예외를 잡은 뒤에는 두 가지 방법으로 처리할 수 있습니다.
첫 번째는 Cashier에 포함된 전용 결제 확인 페이지로 고객을 리디렉션하는 것입니다. 이 페이지에는 Cashier의 서비스 프로바이더가 등록한 이름이 지정된 라우트가 이미 연결되어 있습니다. 따라서 IncompletePayment 예외를 잡아 사용자를 결제 확인 페이지로 리디렉션하면 됩니다.
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}결제 확인 페이지에서 고객은 신용카드 정보를 다시 입력하고 "3D Secure" 인증과 같이 Stripe가 요구하는 추가 절차를 수행하게 됩니다. 결제를 확인하고 나면 위에서 지정한 redirect 파라미터의 URL로 리디렉션됩니다. 리디렉션될 때 URL에는 message(문자열)와 success(정수) 쿼리 스트링 변수가 추가됩니다. 이 결제 페이지는 현재 다음 결제 수단 유형을 지원합니다.
- Credit Cards
- Alipay
- Bancontact
- BECS Direct Debit
- EPS
- Giropay
- iDEAL
- SEPA Direct Debit
다른 방법으로는 Stripe가 결제 확인을 대신 처리하도록 할 수도 있습니다. 이 경우 결제 확인 페이지로 리디렉션하는 대신 Stripe 대시보드에서 Stripe의 자동 청구 이메일을 설정하면 됩니다. 다만 IncompletePayment 예외를 잡았다면, 추가 결제 확인 안내가 담긴 이메일을 받게 된다는 점을 사용자에게 안내해야 합니다.
Billable 트레이트를 사용하는 모델의 charge, invoiceFor, invoice 메서드에서 결제 예외가 발생할 수 있습니다. 구독을 다룰 때는 SubscriptionBuilder의 create 메서드와, Subscription 및 SubscriptionItem 모델의 incrementAndInvoice, swapAndInvoice 메서드가 미완료 결제 예외를 던질 수 있습니다.
기존 구독에 미완료 결제가 있는지는 결제 대상 모델 또는 구독 인스턴스의 hasIncompletePayment 메서드로 확인할 수 있습니다.
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}예외 인스턴스의 payment 속성을 살펴보면 미완료 결제의 구체적인 상태를 알 수 있습니다.
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// Get the payment intent status...
$exception->payment->status;
// Check specific conditions...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}결제 확인
일부 결제 수단은 결제를 확인하기 위해 추가 데이터가 필요합니다. 예를 들어 SEPA 결제 수단은 결제 과정에서 추가로 "위임(mandate)" 데이터가 필요합니다. 이 데이터는 withPaymentConfirmationOptions 메서드로 Cashier에 전달할 수 있습니다.
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');결제를 확인할 때 사용할 수 있는 모든 옵션은 Stripe API 문서에서 확인하세요.
강력한 고객 인증(SCA)
비즈니스나 고객 중 한쪽이라도 유럽에 기반을 두고 있다면 EU의 강력한 고객 인증(Strong Customer Authentication, SCA) 규정을 준수해야 합니다. 이 규정은 결제 사기를 방지하기 위해 2019년 9월 유럽 연합이 도입했습니다. 다행히 Stripe와 Cashier는 SCA를 준수하는 애플리케이션을 만들 수 있도록 준비되어 있습니다.
WARNING
시작하기 전에 PSD2와 SCA에 대한 Stripe 가이드와 새로운 SCA API 문서를 검토하세요.
추가 확인이 필요한 결제
SCA 규정에 따라 결제를 확인하고 처리하려면 추가 인증이 필요한 경우가 많습니다. 이때 Cashier는 추가 인증이 필요함을 알리는 Laravel\Cashier\Exceptions\IncompletePayment 예외를 던집니다. 이 예외를 처리하는 방법은 결제 실패 처리 문서에서 더 자세히 볼 수 있습니다.
Stripe나 Cashier가 보여 주는 결제 확인 화면은 특정 은행이나 카드 발급사의 결제 흐름에 맞춰질 수 있으며, 추가 카드 확인, 임시 소액 결제, 별도 기기 인증 등 다양한 인증 절차가 포함될 수 있습니다.
미완료 및 연체 상태
결제에 추가 확인이 필요하면 구독은 stripe_status 데이터베이스 컬럼에 표시된 대로 incomplete 또는 past_due 상태로 유지됩니다. 결제 확인이 완료되고 Stripe가 웹훅으로 그 완료를 애플리케이션에 알리면, Cashier가 고객의 구독을 자동으로 활성화합니다.
incomplete와 past_due 상태에 대한 자세한 내용은 이 상태들에 대한 추가 문서를 참고하세요.
오프세션 결제 알림
SCA 규정에 따라 구독이 활성 상태인 동안에도 고객이 결제 정보를 가끔 다시 확인해야 하므로, Cashier는 오프세션(off-session) 결제 확인이 필요할 때 고객에게 알림을 보낼 수 있습니다. 예를 들어 구독이 갱신될 때 이런 상황이 생길 수 있습니다. CASHIER_PAYMENT_NOTIFICATION 환경 변수에 알림 클래스를 지정하면 Cashier의 결제 알림을 활성화할 수 있으며, 기본적으로는 비활성화되어 있습니다. Cashier에는 이 용도로 쓸 수 있는 알림 클래스가 포함되어 있지만, 원한다면 직접 만든 알림 클래스를 지정해도 됩니다.
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment오프세션 결제 확인 알림이 제대로 전달되려면, 애플리케이션에 Stripe 웹훅이 설정되어 있고 Stripe 대시보드에서 invoice.payment_action_required 웹훅이 활성화되어 있는지 확인하세요. 또한 Billable 모델은 Laravel의 Illuminate\Notifications\Notifiable 트레이트도 사용해야 합니다.
WARNING
고객이 추가 확인이 필요한 결제를 직접 수동으로 진행하는 경우에도 알림이 전송됩니다. 안타깝게도 Stripe는 해당 결제가 수동으로 이루어졌는지, 아니면 "오프세션"인지 알 방법이 없습니다. 다만 고객이 이미 결제를 확인한 뒤에 결제 페이지에 접속하면 "결제 성공" 메시지만 보게 됩니다. 고객이 실수로 같은 결제를 두 번 확인해서 의도치 않게 두 번 청구되는 일은 허용되지 않습니다.
Stripe SDK
Cashier의 많은 객체는 Stripe SDK 객체를 감싸는 래퍼입니다. Stripe 객체와 직접 상호작용하고 싶다면 asStripe 계열 메서드로 편리하게 가져올 수 있습니다.
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();updateStripeSubscription 메서드로 Stripe 구독을 직접 갱신할 수도 있습니다.
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);Stripe\StripeClient 클라이언트를 직접 사용하고 싶다면 Cashier 클래스의 stripe 메서드를 호출하면 됩니다. 예를 들어 이 메서드로 StripeClient 인스턴스에 접근해 Stripe 계정의 가격 목록을 조회할 수 있습니다.
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();테스트
Cashier를 사용하는 애플리케이션을 테스트할 때 Stripe API로 나가는 실제 HTTP 요청을 모킹(mock)할 수도 있지만, 그러려면 Cashier 자체의 동작을 일부 다시 구현해야 합니다. 따라서 테스트가 실제 Stripe API를 호출하도록 두는 것을 권장합니다. 속도는 느리지만 애플리케이션이 기대대로 동작한다는 더 높은 확신을 얻을 수 있으며, 느린 테스트는 별도의 Pest / PHPUnit 테스트 그룹으로 분리해 둘 수 있습니다.
테스트할 때는 Cashier 자체에 이미 훌륭한 테스트 스위트가 있다는 점을 기억하세요. 따라서 Cashier 내부 동작을 모두 테스트하려 하지 말고, 직접 만든 애플리케이션의 구독 및 결제 흐름만 테스트하는 데 집중하면 됩니다.
시작하려면 phpunit.xml 파일에 Stripe 시크릿의 테스트용 버전을 추가합니다.
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>이제 테스트 중에 Cashier를 사용하면 실제 API 요청이 Stripe 테스트 환경으로 전송됩니다. 편의를 위해 Stripe 테스트 계정에 테스트 중에 사용할 구독 / 가격을 미리 채워 두는 것이 좋습니다.
NOTE
신용카드 거절이나 실패 같은 다양한 결제 시나리오를 테스트하려면 Stripe가 제공하는 폭넓은 테스트용 카드 번호와 토큰을 사용할 수 있습니다.