Search by

stetodd / payment-gateway

stetodd

Provider-neutral payment gateway contracts, models and test simulator

Package info

github.com/stetodd/payment-gateway

pkg:composer/stetodd/payment-gateway

Statistics

Installs: 262

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.8.0 2026-09-28 07:04 UTC

This package is auto-updated.

Last update: 2026-09-28 07:07:17 UTC


README

Provider-neutral payment gateway contracts: PaymentGatewayInterface, request/response models, and a SimulatorPaymentGateway test double.

Implementations live in separate packages (e.g. stetodd/stripe-gateway-bundle).

Install

composer require stetodd/payment-gateway

Usage

Type-hint Stetodd\PaymentGateway\PaymentGatewayInterface in your application code. Bind it to a concrete gateway implementation in your container.

In tests, use Stetodd\PaymentGateway\Testing\SimulatorPaymentGateway and queue responses with willReturnResponse(string $key, object $response).

Refunds and subscription payments

Seed a paid subscription invoice with recordSubscriptionPayment(SubscriptionPayment). That also registers its captured payment, so refundPayment() works against it. failNextRefund($reason) makes the next refund throw RefundFailedException. A RefundPaymentRequest given an idempotencyKey pays at most once per payment: a repeat under the same key returns the first refund, with the first refund's amount. refunds, refundedAmount($paymentId) and checkoutSessionRequests let you assert on what was sent.

Tests: composer install && vendor/bin/phpunit.

Listing what the vendor holds (v0.8)

For reconciling a ledger against the vendor, every list reads newest first, one page at a time, over the objects created in a window (ListSinceRequest: since, optional before, cursor, limit 1–100). Pass a page's nextCursor back to read the next, older page; it is null on the last page.

  • listPaidInvoices() — paid invoices, each with the payment that paid it (paymentId), its subscription and billing reason.
  • listSucceededPayments() — captured payments with their metadata, createdAt and description. A hold charged days later is listed by when it was authorised, so read far enough back.
  • listRefunds() — refunds in every status, whoever made them (the app or the dashboard), with their metadata.
  • listBalanceTransactions() — the balance rows (BalanceTransaction: type, the vendor's rawType and reportingCategory, signed amount, fee, net, sourceId, paymentId, feeDetails). One type per request, as the vendor filters. A kind this package does not name is Other, with the vendor's word kept in rawType.
  • findPaymentBalanceTransaction() — the charge row of one payment, carrying the fee taken; null until the payment is captured and booked.
  • listPayouts() — payouts from the balance to our own bank.

The simulator books a charge row when a payment is captured or recorded as succeeded (fee from chargeFee($fixed, $basisPoints), 0 by default, or exactly with bookPaymentFee()), a refund row on every refund, and a payout row on recordPayout(). recordPayment(), recordPaidInvoice(), recordRefund() and recordBalanceTransaction() register what happened outside the app. It does not model which rows an automatic payout paid out: a payoutId filter lists nothing.

Also in v0.8: Payment carries cardBrand and cardLast4 (nullable) for a receipt to print, and createdAt, metadata and description; Refund carries createdAt and metadata. CreatePaymentHoldRequest takes an optional statementDescriptorSuffix (1–22 characters, none of < > \ ' " *) for the customer's card statement. The simulator's holds pay with the test card (visa, 4242) unless holdPayment() names another.