Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Statamic Payments Laravel Package

goldnead/statamic-payments

Goldnead Statamic Payments is a Statamic addon for handling payments in your site, letting you integrate checkout flows and payment providers with your content. It aims to simplify common payment tasks for Statamic developers.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • The package sits on the critical money path: checkout creation (Checkout::start()), provider communication (MollieGateway::fetch()/createPayment()), and webhook ingestion. Treat it as tier‑1 infrastructure, not a convenience wrapper.
  • v1.30.0 moves the package from "fire-and-forget HTTP wrapper" toward "idempotent payment orchestration": a persisted checkout_key with a unique index, provider-level Idempotency-Key, and typed failure signalling (ProviderUnavailable). That is the right direction — it aligns with how Mollie itself expects to be integrated (idempotency keys + webhook redelivery).
  • The design correctly refuses to rely on lockForUpdate() alone (a no-op on SQLite) and instead uses a database constraint as the source of truth. Constraint-based concurrency rather than application-level locking is the more defensible pattern for a package that must run on MySQL, Postgres, and SQLite.

Integration feasibility

  • Two changes are genuinely additive and low-friction: the payments.checkout_key column (nullable, unique) and the extra idempotencyKey key in the createPayment() payload.
  • The behavioural change is in the webhook contract: non-200 (specifically 503) is now a deliberate, load-bearing signal. Any infrastructure between Mollie and the app that normalises responses — reverse proxies, WAFs, API gateways, error pages, health-check middleware, or a global exception handler that returns 200 with an error body — will silently reintroduce the original bug. This must be verified end to end, not assumed.
  • Custom PaymentGateway implementations are the main feasibility unknown. Implementers that forward the payload verbatim into a provider request body will now leak idempotencyKey into the body of a third-party API, which typically yields a 400 and a failed payment. Implementers with strict payload validation may throw. This is a contract change delivered in a minor version — technically semver-compatible, practically a breaking change for that subset.

Technical risk

  • Retry-window exhaustion (high). Mollie redelivers roughly 10 times over ~26 hours. A ProviderUnavailable condition that outlives that window (extended Mollie outage, expired credentials, DNS/TLS fault, self-inflicted 429 loop) leaves paid-but-unfulfilled orders with no further automatic signal. The 503 fix converts a silent data-integrity bug into a bounded, but still real, stranded-order risk.
  • Feedback loop under rate limiting (medium-high). If fetch() is being throttled (429) and the webhook answers 503, Mollie retries, generating more calls to a provider that is already refusing them. Absent explicit backoff/jitter and a circuit breaker inside fetch(), the failure mode is self-amplifying within the retry budget.
  • Idempotency key false positives (medium). The dedupe window is 10 minutes and the key is derived from address, products, quantities, discount, return URL, and methods. Two legitimately distinct purchases that happen to match on all of those (same buyer, same basket, same address, twice in ten minutes — a plausible B2B or repeat-gift scenario) collapse onto one payment. There must be an explicit escape hatch; otherwise the fix trades duplicate charges for blocked orders.
  • Key canonicalisation false negatives (medium). Product ordering, quantity formatting, discount representation, and URL normalisation (trailing slash, query-string order, scheme) all affect the hash. Any instability produces a different key and therefore no dedupe at all — the original double-charge bug survives for a subset of traffic. The uniqueness guarantee is only as good as the canonicaliser, which the notes do not describe.
  • Schema change on a hot table (medium). A unique index on payments is required. Laravel's default index creation is not concurrent on Postgres (CREATE INDEX takes an ACCESS EXCLUSIVE-adjacent lock and blocks writes) and on MySQL takes a brief metadata lock even with ALGORITHM=INPLACE. On a large payments table this is a write stall on the money path.
  • PII in a key (low-medium). The key is derived from a shipping/billing address. Whatever the derivation, the value persists on every payment row and appears in logs and support tooling. Confirm it is a salted hash and not a reversible encoding.
  • Losing-request behaviour (low, but unknown). "Two requests in flight end up on one row" implies the duplicate-key violation is caught — but the notes do not say the losing request returns the winner's checkout URL rather than an error page. A 500 on a double-click is a support ticket, not a fix.

Key questions

  1. Does fetch() apply bounded retry with exponential backoff and jitter before throwing ProviderUnavailable, or does a single 429 immediately become a 503 webhook response?
  2. What is the documented recovery path for orders whose 26-hour redelivery budget is exhausted — a reconciliation command, a scheduled sweep, a Mollie dashboard export?
  3. How is checkout_key canonicalised, and is there a supported way to force a new payment for a legitimate repeat purchase inside the ten-minute window?
  4. Does the losing request of a concurrent double-start receive the existing checkout URL (200) or an exception, and is that behaviour covered by a test?
  5. Is idempotencyKey additive-only, and does the package validate or ignore unknown keys for existing custom gateways? Is there a changelog entry aimed explicitly at PaymentGateway implementers?
  6. Is a 404 from Mollie mapped to 200 and a terminal payment state, or to 200 with the order left pending indefinitely?
  7. What is the vendor's release policy for behavioural changes to HTTP status semantics — minor or patch?

Integration Approach

Stack fit

  • Assumes Laravel with the standard migration/artisan workflow and a relational database. The package is explicitly multi-database (SQLite noted), so the same code must behave identically on SQLite in test suites and MySQL/Postgres in production — verify the duplicate-key path is not database-specific in its exception handling (QueryException codes differ across drivers).
  • Requires a queue-capable deployment for correctness, not just performance: webhook handling must complete fulfillment or enqueue it durably before returning 200. An inline, synchronous fulfillment path inside the webhook handler is incompatible with the retry model introduced here.
  • Benefits from existing observability: HTTP status distribution on the webhook route, application error tracking for ProviderUnavailable, and a scheduler for reconciliation.

Migration path

  1. Pin and audit first. Upgrade to v1.30.0 on a branch. Grep the codebase for every PaymentGateway implementation and classify each as (a) ignores unknown payload keys, (b) forwards payload into a request body, (c) validates payload shape strictly. Group (b) and (c) need code changes before deploy.
  2. Rehearse the migration on a production-sized copy. Measure lock duration for the nullable unique index on payments on the actual engine and version. If Postgres and the table is large, replace the generated index with CREATE UNIQUE INDEX CONCURRENTLY in a hand-written migration and mark the standard one as skipped. If MySQL and the table is very large, plan a gh-ost/pt-osc run or a low-traffic window.
  3. Deploy order: migrate, then code. New code writes and reads checkout_key; old code writes NULL. A nullable unique index tolerates multiple NULLs on MySQL, Postgres, and SQLite, so migration-before-deploy is safe in both directions — but code-before-migration is not. During a rolling deploy, mixed old/new pods can both create payments for the same basket; dedupe only takes effect once all pods run the new code. Confirm this is acceptable for the rollout window or gate checkout during the transition.
  4. Verify the webhook path end to end. Simulate ProviderUnavailable (block egress to the Mollie API, or point at an unreachable host in staging) and assert: the endpoint returns 503, the response is not intercepted or rewritten, the response is not cached, and fulfillment is not marked complete. Then restore connectivity and confirm redelivery produces exactly one fulfillment.
  5. Exercise idempotency explicitly. Concurrent double-submit test (two parallel Checkout::start() calls with identical baskets) asserting one row, one provider payment, identical checkout URL to both callers. Then a "same basket, 11 minutes later" test asserting a new payment is created — that is the escape hatch for repeat purchases and must be proven, not inferred.

Compatibility

  • Custom gateways: additive payload key, but see risk above — treat as breaking for a subset and handle it as a code change, not a config change.
  • Older payment rows: unaffected (NULL key), but they are invisible to the new dedupe logic. Historic duplicate-prevention coverage starts at deploy time.
  • Clients/consumers: no API surface change. Front-end double-click handling can stay as-is, and arguably should — client-side guards are now defence in depth rather than the only defence.
  • Downgrade path: reverting the code leaves an unused nullable column and index, which is harmless. Reverting the migration is not required and should not be attempted on a live table.

Sequencing

Pin → gateway audit → staging migration rehearsal with prod-sized data → observability and alerting in place (503 rate, ProviderUnavailable, unfulfilled-paid orders) → migrate production in a low-traffic window → deploy application code → verify webhook 503/redelivery in production with a single test transaction → enable reconciliation sweep → remove any interim manual workarounds.

Do not deploy the code change and the alerting change in separate weeks; the 503 behaviour is only operationally safe once someone is watching for it.

Operational Impact

Maintenance

  • Ongoing: keep the package current — this release is evidence the vendor fixes correctness bugs on the money path, and lagging versions carry known data-integrity defects. Budget a review of each release's changelog for webhook-status and PaymentGateway-contract changes specifically.
  • One-time: any custom gateway adapters must be maintained against the payload contract going forward. Document the contract locally so the next upgrade does not repeat the audit.
  • Debt to accept consciously: a bespoke checkout_key derivation (canonicalisation rules) is now business logic owned by this integration. If the rules are wrong, the bug is yours, not the vendor's.

Support

  • Expect a new class of support contact: "Mollie shows failed webhooks." The 503s are correct behaviour but visible in the Mollie dashboard and in any uptime monitor watching the webhook route. Pre-empt this with a runbook entry explaining that webhook failures are retried for ~26 hours and that persistent 503s indicate a provider-side or credential problem.
  • Second class: "I ordered twice and only got one order." This is the dedupe working as designed. Front-line support needs the ten-minute rule and the force-new-payment path before this ships, not after.
  • Third class: "charged but nothing shipped" — the original bug. This should trend to zero, and if it does not, the canonicaliser or the retry-window exhaustion is the cause. Give support a query to find paid-but-unfulfilled orders.

Scaling

  • The unique index adds write cost to the payments insert path — negligible at normal volumes, but it is a hard serialisation point on a hot row-key space during traffic spikes (flash sales, campaign launches). Load-test the checkout endpoint, not just the webhook, at projected peak.
  • Webhook throughput becomes bursty rather than steady: a provider incident produces a redelivery wave (10 attempts per event). The handler must be cheap and must enqueue rather than fulfil inline, or a post-incident retry storm becomes a second incident.
  • Dedupe correctness does not degrade with volume; dedupe effectiveness depends on how many legitimate repeat purchases fall inside the ten-minute window, which scales with conversion volume. Track the rate of "start blocked by existing open payment" as a product metric, not just an error metric.

Failure modes

Mode Signal Consequence Mitigation
Provider unavailable, transient 503 on webhook, ProviderUnavailable logged Order delayed, redelivered within ~26h Alert on 503 rate; confirm queue drains post-incident
Provider unavailable, sustained >26h Redelivery stops, order stays pending Paid, unfulfilled, silent Scheduled reconciliation sweep against Mollie; alert on pending-but-paid age
Throttling loop (429 → 503 → retry) Rising 429s and 503s together Amplified load, extended outage Backoff/jitter and circuit breaker inside fetch()
Canonicalisation mismatch Double-click still produces two payments Original bug survives Explicit concurrency test in CI; monitor for duplicate-customer-payment patterns
False-positive dedupe Repeat purchase returns stale checkout URL Lost sale, confused buyer Escape hatch for forced new payment; document the 10-minute rule
Duplicate-key exception unhandled 500 on double-click Support tickets, abandoned carts Verify the losing request returns the winner's URL, in a test
Migration lock Write stall on payments during deploy Checkout unavailable Rehearse on prod-sized data; use concurrent index creation where supported
Proxy/middleware rewrites status Webhook always 200 in logs Silent reintroduction of the original defect End-to-end assertion of the 503 on the real ingress path

Ramp-up

  • Engineering: half a day to audit custom gateways and rehearse the migration; one to two days to wire the reconciliation sweep and the alerts. The reconciliation sweep is the single highest-value addition — it is the only defence that does not depend on Mollie's retry budget.
  • Support/ops: a one-page runbook covering the three contact classes above, the ten-minute dedupe rule, and the "failed webhooks are retried" explanation. Time-box to an hour's briefing; the cost of skipping it is misdiagnosed incidents.
  • Product/commercial: confirm the ten-minute repeat-purchase behaviour is acceptable for your buying patterns before rollout. For consumer retail it is almost certainly fine; for B2B reordering, invoicing flows, or gift purchases it may need an explicit "buy again" affordance that bypasses dedupe. This is a product decision surfaced by an infrastructure fix, and it should be made deliberately rather than discovered in support tickets.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings