stetodd / payment-gateway
Provider-neutral payment gateway contracts, models and test simulator
Requires
- php: >=8.4
Requires (Dev)
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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 theirmetadata,createdAtanddescription. 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 theirmetadata.listBalanceTransactions()— the balance rows (BalanceTransaction:type, the vendor'srawTypeandreportingCategory, signedamount,fee,net,sourceId,paymentId,feeDetails). Onetypeper request, as the vendor filters. A kind this package does not name isOther, with the vendor's word kept inrawType.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.