Fakturácia cez Stripe
Purchases::stripe() vytvára a mení veci v Stripe cez jeho REST API a HTTP klienta Laravelu — bez Stripe SDK. Prichádzajúce webhooky udržiavajú odchádzajúci stav lokálne synchronizovaný. Fakturovať vie len Stripe; Apple a Google predávajú vo vlastných obchodoch. Fakturácia vyžaduje PURCHASES_STRIPE_SECRET a každé volanie používa pripnutú verziu API:
use RoundlyConsulting\Purchases\Facades\Purchases;
Purchases::canBill('stripe'); // true — the provider implements Contracts\SupportsBilling
Purchases::canBill('apple'); // false
Purchases::stripe(); // StripeBilling; UnsupportedCapabilityException without a registered Stripe provider
// Dependency injection — the same object behind the facade
public function __construct(private PurchasesManager $purchases) {}
$this->purchases->stripe()->subscriptions()->updateQuantity($seatItem, 11);| Sub-accessor | Metóda | Akcia |
|---|---|---|
customers() | sync($owner, CustomerData $data) / id($owner) / owner($customerId) | SyncStripeCustomerAction |
subscriptions() | create($owner, CreateSubscriptionData $data) | CreateStripeSubscriptionAction |
subscriptions() | checkout($owner, CheckoutSubscriptionData $data) | CreateStripeCheckoutSessionAction |
subscriptions() | updateQuantity() / swap() / addItem() / removeItem() / change() | ChangeStripeSubscriptionAction |
subscriptions() | cancel() / cancelAtPeriodEnd() | CancelStripeSubscriptionAction |
subscriptions() | resume() | ResumeStripeSubscriptionAction |
subscriptions() | preview($subscription, ?SubscriptionChange $change) | PreviewStripeInvoiceAction |
meters() | create() / find() / findOrCreate() | CreateStripeMeterAction / FindStripeMeterAction |
meters() | report() / queue() | ReportStripeMeterEventAction / ReportStripeMeterEvent job |
| — | portal($owner, ?$returnUrl, ?$configuration) | CreateStripePortalSessionAction |
Predplatné je uložené predplatné Stripe alebo jeho id (sub_…), položka uložený SubscriptionItem alebo jeho id (si_…); uložené predplatné iného poskytovateľa sa odmietne cez BillingException skôr, než sa čokoľvek odošle. Každý zápis posiela Idempotency-Key — váš alebo nové UUID —, ktorý nesie každá StripeException, takže neúspešné volanie zopakujete s tým istým kľúčom.
Zákazníci — jeden na vlastníka
use RoundlyConsulting\Purchases\DataTransferObjects\Stripe\{AddressData, CustomerData, TaxIdData};
$customer = Purchases::stripe()->customers()->sync($company, new CustomerData(
name: 'Acme s.r.o.',
email: '[email protected]',
address: new AddressData(line1: 'Hlavná 1', city: 'Bratislava', postalCode: '81101', country: 'SK'),
taxIds: [new TaxIdData('eu_vat', 'SK2020123456')],
metadata: ['plan' => 'team'],
preferredLocale: 'sk',
));
Purchases::stripe()->customers()->id($company); // 'cus_…' or null
Purchases::stripe()->customers()->owner('cus_…'); // the owner model or nullVlastníkom je ľubovoľný uložený Eloquent model. Bez zákazníka ho sync() vytvorí a prepojí; s existujúcim aktualizuje len polia, ktoré nastavíte, a zoznam taxIds sa stane presne týmto zoznamom. Prepojenie žije v billing_customers cez kontrakt BillingCustomers — ak id už ukladáte inde, naviažte vlastnú implementáciu. Keď sa uloží predplatné alebo platba, ktorá ešte nemá vlastníka, prepojí sa s vlastníkom svojho zákazníka.
Predplatné
use RoundlyConsulting\Purchases\DataTransferObjects\Stripe\{CreateSubscriptionData, SubscriptionPrice};
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\{CollectionMethod, PaymentBehavior};
$subscription = Purchases::stripe()->subscriptions()->create($company, new CreateSubscriptionData(
prices: [
SubscriptionPrice::licensed('price_seats', 10), // per seat, quantity 10
SubscriptionPrice::metered('price_storage_gb'), // usage-based: no quantity
],
name: 'team', // the local row's name, what subscribedTo() checks
trialDays: 7,
automaticTax: true,
collectionMethod: CollectionMethod::ChargeAutomatically, // or SendInvoice + daysUntilDue
paymentBehavior: PaymentBehavior::DefaultIncomplete,
metadata: ['company' => (string) $company->getKey()],
), idempotencyKey: "start-{$company->getKey()}");Odpoveď sa hneď uloží lokálne cez pipeline webhookov, s vlastníkom a názvom name; pri aktívnom predplatnom alebo predplatnom v skúšobnom období sa spustí SubscriptionStarted. Voľby, ktoré necháte null, sa neposielajú, takže platia predvolené hodnoty Stripe.
Flexibilný billing mode a zmiešané intervaly
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\BillingMode;
Purchases::stripe()->subscriptions()->create($company, new CreateSubscriptionData(
prices: [
SubscriptionPrice::licensed('price_licence_yearly'), // billed every year
SubscriptionPrice::metered('price_api_calls_monthly'), // usage, billed every month
],
name: 'platform',
billingMode: BillingMode::Flexible, // billing_mode[type]=flexible
));- billingMode posiela billing_mode[type]; ak nie je nastavený, použije sa predvolená hodnota purchases.settings.stripe.billing_mode, inak sa nepošle nič — Stripe má od verzie API 2025-09-30.clover predvolene flexible, teda už na pripnutej 2026-05-27.dahlia. Billing mode vyžaduje 2025-06-30.basil alebo novšiu: pri staršej pripnutej api_version volanie vyhodí BillingException skôr, než sa opýta Stripe. Predplatné prejde z classic na flexible, nikdy späť.
- Zmiešané intervaly vyžadujú flexibilný režim a interval každej položky musí byť násobkom najkratšieho (1 mesiac + 1 rok; nie týždeň + mesiac). Stripe účtuje každú položku za jej vlastné obdobie.
- Lokálne je ends_at najneskorší koniec obdobia položky — dátum, do ktorého je zaplatená ročná licencia; vlastné obdobie každej položky je v meta.
- Checkout predplatné so zmiešanými intervalmi vytvoriť nevie (použite create()) a cancelAtPeriodEnd() ho zruší k najskoršiemu koncu obdobia položky.
Skúšobné obdobie bez platobnej metódy
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\MissingPaymentMethod;
Purchases::stripe()->subscriptions()->create($company, new CreateSubscriptionData(
prices: [SubscriptionPrice::licensed('price_team')],
trialDays: 14,
trialEndBehavior: MissingPaymentMethod::Pause, // trial_settings[end_behavior][missing_payment_method]
));Skúšobné obdobie platobnú metódu nepotrebuje. Čo sa stane, keď skončí bez nej, určuje trialEndBehavior — inak purchases.settings.stripe.missing_payment_method, inak create_invoice zo Stripe. trialEndBehavior bez skúšobného obdobia je BillingException:
| MissingPaymentMethod | V Stripe | Lokálne |
|---|---|---|
Cancel | hneď ho zruší (customer.subscription.deleted) | Canceled, SubscriptionCanceled |
Pause | pozastaví ho; až do obnovenia žiadne faktúry (customer.subscription.paused) | OnHold, SubscriptionOnHold; po obnovení späť do Completed so SubscriptionResumed + SubscriptionRenewed |
CreateInvoice | aj tak vystaví faktúru (predvolené v Stripe); nezaplatené prejde do past_due | InGracePeriod (alebo OnHold pri settings.stripe.past_due = on_hold) |
Tri dni pred koncom skúšobného obdobia pošle Stripe customer.subscription.trial_will_end, ktorý spustí SubscriptionTrialWillEnd — vhodný moment poslať zákazníka do Customer Portalu, aby pridal platobnú metódu. Pozastavené predplatné sa obnoví z Portalu alebo z hostovanej stránky faktúry; balík volanie na endpoint /resume v Stripe nemá.
Jedno predplatné na vlastníka
// config/purchases.php — or PURCHASES_ONE_SUBSCRIPTION_PER_OWNER=true
'subscriptions' => [
'one_per_owner' => true,
'lock' => ['store' => 'redis', 'wait' => 10], // a store with atomic locks, shared by every server
],
Purchases::for($company)->hasOngoingSubscription(); // what the limit checks
Subscription::query()->ongoing()->get(); // every status but canceled, failed and refunded- Pri zapnutom purchases.subscriptions.one_per_owner vyhodia create() aj checkout() BillingException a nič neodošlú, kým má vlastník predplatné, ktoré neskončilo — v akomkoľvek stave okrem canceled, failed (expirované) a refunded, od ľubovoľného poskytovateľa. Predplatné zrušené ku koncu obdobia sa ráta, kým neskončí.
- Kontrola, volanie Stripe aj lokálne uloženie bežia pod jedným atomickým cache zámkom na vlastníka, takže z dvoch súbežných spustení druhé počká najviac lock.wait sekúnd, uvidí prvé a odmietne sa — alebo, ak zámok zostane obsadený, vyhodí BillingException („Another subscription is being started“).
- Checkout vytvorí predplatné až po zaplatení, takže dve relácie otvorené pred zaplatením ho môžu spustiť obe — zapnite preto aj nastavenie Checkout v Stripe „Limit customers to one subscription“. Predplatné spustené mimo balíka sa nikdy neblokuje a webhooky vždy uložia, čo Stripe nahlási.
Checkout v režime predplatného
use RoundlyConsulting\Purchases\DataTransferObjects\Stripe\CheckoutSubscriptionData;
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\PaymentMethodCollection;
$session = Purchases::stripe()->subscriptions()->checkout($company, new CheckoutSubscriptionData(
prices: [SubscriptionPrice::licensed('price_seats', 10), SubscriptionPrice::metered('price_storage_gb')],
successUrl: route('billing.done').'?session={CHECKOUT_SESSION_ID}',
cancelUrl: route('billing'),
trialDays: 7,
paymentMethodCollection: PaymentMethodCollection::IfRequired, // a free trial without a card
trialEndBehavior: MissingPaymentMethod::Cancel,
automaticTax: true, // also sends customer_update[address]=auto, which Stripe Tax needs
));
return redirect($session->url);Predplatné, ktoré Checkout vytvorí, príde cez webhook a s vlastníkom sa prepojí cez svojho zákazníka. PaymentMethodCollection::IfRequired si platobnú metódu vypýta, len keď je niečo splatné hneď, takže bezplatné skúšobné obdobie začne bez nej; trialEndBehavior a billingMode idú do subscription_data a padajú na tie isté predvolené hodnoty z konfigurácie ako create().
Zmeny, zrušenie, merače a Portal
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\ProrationBehavior;
$seats = $subscription->itemFor('price_seats'); // the stored item for a price
Purchases::stripe()->subscriptions()->updateQuantity($seats, $company->members()->count(), ProrationBehavior::CreateProrations);
Purchases::stripe()->subscriptions()->swap($seats, 'price_seats_annual', quantity: 11, proration: ProrationBehavior::AlwaysInvoice);
Purchases::stripe()->subscriptions()->addItem($subscription, 'price_api_calls'); // metered: no quantity
Purchases::stripe()->subscriptions()->removeItem('si_…', ProrationBehavior::None, clearUsage: true);
Purchases::stripe()->subscriptions()->cancelAtPeriodEnd($subscription); // POST cancel_at_period_end=true
Purchases::stripe()->subscriptions()->resume($subscription); // POST cancel_at_period_end=false
Purchases::stripe()->subscriptions()->cancel($subscription, invoiceNow: true, prorate: true); // DELETE, nowKaždá zmena je jedno POST /v1/subscriptions/{id}, uloží sa lokálne a pri novom počte či cene sa spustí SubscriptionUpdated. ProrationBehavior je CreateProrations (predvolené), AlwaysInvoice alebo None; viac zmien naraz pošlete jedným change() so SubscriptionChange a preview() ukáže faktúru ešte pred potvrdením. resume() vezme späť zrušenie naplánované ku koncu obdobia — nie je to endpoint /resume v Stripe.
use RoundlyConsulting\Purchases\DataTransferObjects\Stripe\MeterData;
use RoundlyConsulting\Purchases\Providers\Stripe\Enums\MeterAggregation;
$meter = Purchases::stripe()->meters()->findOrCreate(new MeterData(
eventName: 'storage_gb_days',
displayName: 'Storage (GB-days)',
aggregation: MeterAggregation::Sum,
));
Purchases::stripe()->meters()->report(
$company, // an owner with a Stripe customer, or 'cus_…'
'storage_gb_days',
12.5, // int, float or decimal string — sent as an exact decimal string
timestamp: now(),
identifier: "storage-{$company->id}-".today()->toDateString(),
);
$session = Purchases::stripe()->portal($company, returnUrl: route('settings.billing'));
return redirect($session->url);Spotreba sa hlási do Stripe Billing Meters ako presný desatinný reťazec; identifier (ak ho nezadáte, vygeneruje sa) slúži zároveň ako kľúč idempotencie a queue() zafixuje zákazníka, identifier aj čas už pri zaradení do fronty. URL Portalu platí krátko, preto ju vytvorte pri každej návšteve.
Chyby
Každé zlyhanie Stripe je typovaná StripeException — karta, neplatná požiadavka, autentifikácia, limit požiadaviek, idempotencia, API alebo spojenie — s httpStatus, stripeType, stripeCode, declineCode, param, requestId a idempotencyKey. Správa je od Stripe, pričom všetko, čo vyzerá ako tajný kľúč, sa nahradí [redacted], a žiadna výnimka si neponechá HTTP odpoveď. Problémy zistené pred odoslaním vyhodia BillingException; pozrite Enumy a výnimky.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.