Skip to main content

Documentation

No results found.
Features

Payment Links

Payment Links is the lightweight "get paid" card: create a shareable link (and matching QR code) for a fixed or open ("payer chooses") amount — without setting up the full E-Commerce store. Visitors open /pay/{token}, se...

Payment Links is the lightweight "get paid" card: create a shareable link (and matching QR code) for a fixed or open ("payer chooses") amount — without setting up the full E-Commerce store. Visitors open /pay/{token}, see the title, description, and amount, and pay through the same embedded Stripe checkout the rest of the CMS uses. Every payment lands in a log with optional payer receipts and an owner notification.


The problem

Plenty of payments don't need a product catalog: a deposit for a job, a consultation fee, a class registration, a "pay what you owe" balance, a tip jar. Setting up the full shop — products, categories, cart, checkout settings — for a one-off charge is overkill, and sending clients to a third-party payment page means another tool, another fee, and a URL that isn't yours.

The fix

A self-contained feature module under app/Features/PaymentLinks/: a dashboard page to create and manage links, a tokenized public pay page served from the install's own domain, embedded Stripe checkout (shared site-wide stripe.key / stripe.secret credentials), and a payments log — Stripe's fees the only cut taken.

Like every addon, it's structured as a feature module gated by feature:payment_links middleware. The service provider boots unconditionally; routes 404 and the sidebar entry hides when the feature is off. Toggling it on runs the module's migrations. Disabling preserves all data.

What the dashboard gets

One page under Dashboard → Payment Links (Manager and up) plus an admin-only Settings page:

  • Payment Links — the link list: title, amount (or Open amount with its minimum), Active/Archived status badge, total collected (paid payments, net of refunds), payment count (paid payments), and created date. Row actions: Copy URL, QR code (an inline SVG in a modal, ready to screenshot or print), Open page, Edit, Archive/Restore, and Delete. Links with recorded payments — including a bank payment still clearing — can't be deleted; archive them instead to keep the payment history. Below the list, a Recent payments section shows the last 20 checkouts across every link: link title, payer name/email (when Stripe provides them), amount (badged Refunded / Partially refunded), and date — or Payment processing / Payment failed for a bank payment.
  • Create / edit modal — title, description (shown to the payer above the checkout), amount mode (Fixed amount or Payer chooses the amount with an optional minimum), currency, and a custom success message shown after payment (and included in the receipt email).
  • Settings (admin only) — the Stripe webhook signing secret (with the endpoint URL to paste into Stripe), a payer-receipts toggle, and the owner-notification address (falls back to the Business settings email).

The dashboard also gets a Payment Links card: total collected in the last 30 days (paid, net of refunds) plus the payment count.

What the payer sees

/pay/{token} (a 40-character random token, no login needed) shows the link's title, description, and amount:

  • Fixed amount — the embedded Stripe checkout renders immediately.
  • Open amount — a simple amount form first (with the minimum shown, floor $1, sanity cap 1,000,000.00); submitting reloads the page with the chosen amount and renders the checkout. Amounts are validated server-side against the link's floor, so the form can't be bypassed.
  • Archived links — a friendly "This payment link is no longer active." notice (not a 404, so old printed QR codes degrade gracefully).
  • Stripe not configured — a "payments unavailable" notice instead of a broken checkout.

After paying, the return page confirms the amount and shows the link's success message (or a default thank-you).

Payments, receipts, and the webhook

Payment recording mirrors the invoicing engine: the Stripe webhook (/pay/stripe/webhook, verified against the stripe.webhook_secret.payment_links signing secret, subscribed to checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed and charge.refunded) and the return page both run the same recorder, deduped by an atomic claim on the checkout session id — exactly one of them lands the payment and sends the emails. Sessions from other features' checkouts are acknowledged and ignored; only sessions stamped with payment_link_id metadata are processed.

When a payment lands:

  • The payer's name and email are captured from Stripe's checkout details.
  • A short receipt goes to the payer (when Stripe gave us their email and receipts are enabled) including the link's success message.
  • An owner notification goes to the configured address (Settings → Notify on payment, falling back to the Business settings email).

Both emails send through the shared Marketing transport; a send failure is reported and swallowed — it never breaks payment recording.

Bank payments (ACH)

A payer can use a US bank account (ACH) when your Stripe account offers it. That checkout completes days before the money arrives, so the payment is recorded at completion as Payment processing (payment_processing_at, no paid_at): it isn't counted as collected anywhere, and no receipt or owner notification goes out yet. The return page says the payment is processing (a reload never offers the payment form again), and that payer's own visit to the pay page shows the same note instead of a second checkout until it settles. The note promises an email when the payment clears only while payer receipts are on — that receipt is the email; with receipts off it promises none. A checkout the payer already finished is never replaced: if their next visit finds it unreported (Stripe's completed event not yet delivered), it is recorded there and then — processing, so the note shows, or paid. When the debit clears (checkout.session.async_payment_succeeded) the same row is settled — once — and the emails go out as for a card. If it bounces (checkout.session.async_payment_failed) the row is marked Payment failed (payment_failed_at) and the payer gets one email saying it didn't go through, with a Try again button back to the link while it still takes payments. Either outcome can arrive before Stripe's completed event; the unique session row makes the late one a no-op. If Stripe never says how it ended (the webhook isn't subscribed to the two checkout.session.async_payment_* events, or Stripe gave up delivering them), the payment is checked against Stripe once it has been processing for a day — php artisan payments:reconcile-processing, every six hours — and settled or failed exactly as the webhook would have, with a warning in the log.

Refunds

Refund in the Stripe dashboard: charge.refunded records the running refunded total on the payment (amount_refunded_cents, stamped refunded_at once the whole payment has gone back). A redelivered or out-of-order event never lowers it. Collected totals are net of refunds.

QR codes

Every link's QR code (the same BaconQrCode SVG stack the restaurant table and ticketing check-in QRs use) encodes its public URL — print it on an invoice, a flyer, or a counter card and get paid on scan.

Settings reference

Setting Default Meaning
payment_links.receipts_enabled true Email a receipt to the payer when Stripe provides their address
payment_links.notification_email (Business settings email) Address notified when a link is paid
payment_links.currency (shop currency, else usd) Default currency for new links
stripe.webhook_secret.payment_links — Signing secret for the /pay/stripe/webhook endpoint

Stripe API keys are the shared site-wide pair under Settings → API Keys (stripe.key / stripe.secret).