The package registers four gateways with the engine. Each is a separate checkout choice, can be switched on separately, and has its own storefront driver.
In this section
- PayPal Buttons - PayPal, Pay Later, and Venmo, the decline restart flow, and button style
- Card Fields - PayPal-hosted card fields, 3-D Secure, and the liability-shift policy
- Apple Pay - Domain registration, the domain association file, and going live
- Google Pay - Readiness, 3-D Secure, and going live
- Pay Later Messaging - PayPal's Pay Later messages on product, cart, and checkout pages
The gateway family
| Gateway key | Checkout label | Config key | Driver | PayPal payment_source |
Saved methods |
|---|---|---|---|---|---|
paypal |
PayPal | methods.paypal |
paypal-buttons |
paypal (and Venmo, Pay Later) |
With vault.enabled |
paypal-card |
Credit or debit card | methods.card |
paypal-card-fields |
card |
With vault.enabled |
paypal-apple-pay |
Apple Pay | methods.apple_pay |
paypal-apple-pay |
apple_pay |
No |
paypal-google-pay |
Google Pay | methods.google_pay |
paypal-google-pay |
google_pay |
No |
All four support full and partial refunds. Only the paypal gateway receives webhooks: PayPal posts every event to ecommerce/webhooks/paypal, and that gateway reconciles checkouts placed with any of the four. See Webhooks.
How a payment runs
- Create. When the shopper reaches the payment step, the gateway creates a PayPal Order on the server (
POST /v2/checkout/orders) with the cart's items, totals, shipping address, and brand. The Order id is the engine's payment session reference. - Approve. The driver mounts PayPal's UI with the gateway's client config. The buyer approves in PayPal (the buttons, the card fields, or the wallet sheet). The browser never creates the Order:
createOrderhands back the server's id. - Confirm. The driver checks the approved Order belongs to the session and reports it to the checkout. With
require_liability_shifton, a card or wallet payment without a liability shift is refused here. - Finalize. The engine retrieves the session, runs its fraud check, and calls
capturePayment(). The gateway captures (or authorizes and captures; see Intent) with an idempotentPayPal-Request-Id, and stores the capture id, authorization, and payer details onorders.meta.paypal.
Before capturing, the gateway checks the PayPal Order against the checkout. An Order whose custom_id names another cart fails with paypal_order_mismatch, and one paid with a source the gateway doesn't take (a card on the paypal gateway while card fields are on, a wallet on paypal-card) fails with payment_source_mismatch, so a switched-off method can't be paid through a sibling. If the shopper changed the shipping address after the Order was created, PayPal and Venmo Orders get the new address (PATCH /v2/checkout/orders/{id}) before capture, so the transaction keeps PayPal Seller Protection; a PATCH PayPal refuses is logged with a paypal.shipping_patch_failed timeline entry, and the capture still goes ahead.
A PENDING capture (an eCheck or a review hold) places the order as paid and is settled later by webhook. A decline returns a failed result with a translated message; nothing throws.
Session statuses
| PayPal Order status | Engine session status |
|---|---|
CREATED, SAVED |
requires_payment_method |
PAYER_ACTION_REQUIRED |
requires_action |
APPROVED |
authorized (ready for finalize) |
COMPLETED with a COMPLETED capture |
succeeded |
COMPLETED with a PENDING capture |
processing |
COMPLETED with a CREATED authorization |
authorized |
VOIDED |
canceled |
Money
Amounts are sent as PayPal's decimal strings. HUF, JPY, and TWD are sent without decimals. When that rounding makes the line items and breakdown differ from the engine total, the difference goes into discount or handling, so PayPal's breakdown check passes.
The total itself is never rounded. The engine keeps HUF and TWD in hundredths, but PayPal only takes whole forints and dollars, so a cart whose HUF or TWD total has a fraction isn't offered any PayPal method, and createPaymentSession() throws for one ("PayPal only accepts whole :currency amounts."). For every cart PayPal is offered, amount.value equals the engine's total. JPY is stored in whole yen, so it is unaffected.
Refunds
refund() refunds the order's capture (POST /v2/payments/captures/{id}/refund) with the engine's idempotency key as PayPal-Request-Id, invoice_id set to refund-{refund id}, custom_id set to the engine refund id (so the webhook matches it back), and the reason as note_to_payer (up to 255 characters). PayPal shows note_to_payer to the buyer, so the engine's reason codes are mapped first: fraudulent and duplicate send no note, requested_by_customer sends "Refund requested by the customer.", and any other text is sent as is. A HUF or TWD amount with a fraction fails with amount_not_representable before PayPal is called. PayPal's declines (CAPTURE_FULLY_REFUNDED, REFUND_AMOUNT_EXCEEDED, REFUND_TIME_LIMIT_EXCEEDED, and others) return a failed result with a translated message. A PENDING refund succeeds and the PAYMENT.CAPTURE.REFUNDED webhook settles it. Refunds made in the PayPal dashboard are recorded on the order from the webhook.
Without storefront-livewire
The gateways, refunds, and webhooks work without any storefront. A React or Vue storefront renders the payment step with the JavaScript module, using the client config the engine's checkout API returns.
Replacing a driver
The storefront's payments.drivers config wins over the package's drivers, whichever provider boots first. To use your own Livewire component for a PayPal driver:
// config/artisanpack/ecommerce-storefront-livewire.php
'payments' => [
'drivers' => [
'paypal-buttons' => \App\Livewire\MyPayPalButtons::class,
],
],

