tomatophp/tomato-payments
tomatophp/tomato-payments is a Laravel package for adding payment management to TomatoPHP apps. It provides gateway integration and transaction tracking so you can accept, record, and manage payments through a consistent, configurable workflow.
Architecture fit. The package positions itself as an abstraction layer — a single contract over up to 11 gateway drivers, with transaction records, status tracking, and callback handling shipped as publishable migrations. For a TPM, the architectural question is not "does this work?" but "does the shape of this abstraction match the shape of our payment roadmap?" The answer depends almost entirely on two variables: whether we are already inside the TomatoPHP ecosystem (in which case the service provider, config surface, and admin-panel conventions are already in place and the marginal cost is near zero), and whether our roadmap genuinely anticipates provider churn. If we plan to run one processor for the next 18 months, a unified multi-gateway contract is architecture we pay for and do not use. If we anticipate a regional gateway for market expansion, a processor renegotiation, or a wallet rail alongside cards, the abstraction earns its keep — but only if the lifecycle coverage is real, not just the "11 methods" headline.
Integration feasibility. Structurally feasible in any Laravel application: Composer install, provider auto-discovery, two publish tags (config and migrations), and a driver selected from configuration. The work is not the install, it is the due diligence. Concretely, a TPM should budget the first half-day as verification rather than setup — read composer.json for the laravel/framework and php constraints before anyone types composer require, inspect the published migrations for indexes on lookup columns (order ID, provider reference, status) and for foreign keys that match our actual schema, and read the config file end to end because that file is the API surface. The acceptance gate should be a single sandbox charge walked end to end through one gateway, executed in a throwaway command rather than wired into a controller.
Technical risk. This is the dominant consideration and it is not a code-quality judgment — it is a maintenance-signal judgment. One star, zero dependents, one release in August 2023, no published changelog, and no evidence of how the package behaves across a Laravel major upgrade. In the money path, that means: no security patches for a pinned provider SDK, no one to escalate to when a gateway changes its API shape, and a compatibility cliff we discover rather than schedule. Secondary risks are the classic ones for this category — amount-unit handling (integer minor units vs. decimal strings vs. zero-decimal currencies like JPY) is the single most expensive silent bug available here, and lifecycle gaps (partial refunds, disputes, 3DS/SCA, idempotency on webhook retries) are precisely where thin abstractions leak. A one-release package in 2023 is unlikely to cover those edges to a production standard. The MIT license removes legal friction but does nothing for the operational one.
Key questions.
require block hard-pin a provider SDK version we would otherwise control? That becomes a security-review finding owned by someone.Stack fit. Clean fit for a Laravel application, and a conspicuously clean fit for a TomatoPHP admin panel where the credential-management and settings UI is already part of the product. The decisive architectural pattern — and the one a TPM should mandate as an acceptance criterion — is that no application code ever calls the package facade directly. Every call site depends on our own PaymentGateway interface (charge, verify, refund) implemented by a thin delegating adapter. That single decision converts the package from a dependency into a swappable detail, and it is what makes every other risk in this document survivable. Treat the driver as a strategy resolved at the call site, not a singleton read from config.
Migration path. Greenfield is the easy case: publish, read the migrations, add the indexes we know we will need before first run, then migrate. Brownfield requires more care — the package will create its own payment and transaction tables, and we should plan a nullable provider and provider_reference on our existing orders table (or a small payment_attempts pivot) as a stable join key that survives the package's removal. The realistic long-term path is a two-step: adopt as vendored source now, and migrate to provider SDKs behind our own interface when a Laravel major upgrade forces the question. Plan that replacement on day one, not on the day it breaks.
Compatibility. Verified against a specific Laravel and PHP pair, pinned — an exact tag or 1.0.*, never a floating constraint. If the constraint caps at Laravel 9/10 and we are on 11+, do not reach for --ignore-platform-reqs in anything touching money; that is a stop signal, not a workaround. Webhooks should be our own controller regardless of what the package ships: verify the signature ourselves, log the raw payload, apply an idempotent state update keyed on the provider's event ID, and only then call the package's verify. Providers retry, and idempotency is not optional.
Sequencing. Five steps, each with a gate. (1) Due diligence — read composer.json, provider FQN, config, driver contract, one concrete driver; gate on a written compatibility verdict. (2) Isolated spike — sandbox credentials in .env, one real charge of 1.00 through the driver in tinker; gate on confirming amount units and currency exponent against the provider's own dashboard, not against the package's internal record. (3) Adapter and interface — land our PaymentGateway contract with a fake driver bound in tests before any controller integration. (4) Single-gateway checkout — one gateway in production behind the adapter, package records persisting, our reconciliation columns populated, our webhook endpoint live. (5) Only then evaluate gateway #2 — and only if the roadmap actually calls for it.
Maintenance. The honest framing for stakeholders: at one star and zero dependents, we become the maintainer the moment we depend on it. There is no community pressure to fix anything and no upstream to escalate to. Budget this explicitly — a named owner, a dependency-review entry recording version, verification date, and Laravel/PHP pair, and a standing line item for patch work. Published migrations also become ours permanently; copy their definitions somewhere diffable before running them, because any future removal means hand-writing the drop migration.
Support. No SLA, no security disclosure process, no changelog. Enterprise questionnaires will ask and there is no answer to give — so the answer we give is "internal ownership plus compensating controls," which is a legitimate position only if someone actually holds it. Credentials must flow through env() only, never the committed config file, and any driver key the config fails to expose via env() should be patched in our copy immediately.
Scaling. Throughput is unlikely to be the constraint — payments are I/O-bound and low-volume relative to typical web traffic, and the package is a thin wrapper over provider HTTP calls. The real scaling question is organizational: how many engineers can safely touch the payment path? The adapter interface keeps that number high, because the package's mixed or untyped returns are asserted in one place rather than discovered in production. Reconciliation is the scaling pressure point — a unified transaction table is genuinely useful for admin history and refunds, but the package's models are not our domain model and we should own the reconciliation queries.
Failure modes. Ranked by cost. (1) Silent amount-unit error — a ×100 or zero-decimal mistake that ships real money in the wrong magnitude; mitigated only by a sandbox charge verified against the provider dashboard. (2) Stale driver against a changed provider API — the driver is 2023-era and gateways move; mitigated by the adapter seam and by preferring a custom registered driver over a composer patch when one breaks. (3) Duplicate charges from non-idempotent webhook retries. (4) Compatibility break at the next Laravel major, with no upstream fix — the scheduled outcome, not a surprise. (5) Opaque driver failures swallowed during response normalization — mitigate by logging the raw provider response inside the driver, not around the facade call. (6) A hard-pinned provider SDK lagging on a security advisory, requiring a fragile root-level constraint override.
Ramp-up. Low technical ramp-up, moderate judgment ramp-up. Any Laravel developer can install and read this in an afternoon; the skill that matters is knowing what to verify before trusting it. Plan for a half-day of due diligence by a senior engineer, then roughly a sprint to land the adapter interface, the fake driver, the webhook endpoint, and a single production gateway end to end. For a prototype or internal spike, expect days. Set one explicit re-evaluation trigger now: our next Laravel major upgrade. That is the moment the maintenance debt comes due, and the realistic resolution is replacement with provider SDKs behind the interface we already own — which is exactly why that interface, and not the package, should be the thing the application depends on.
How can I help you explore Laravel packages today?