<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruhani Soft</title>
    <description>The latest articles on DEV Community by Ruhani Soft (@ruhanisoft).</description>
    <link>https://dev.to/ruhanisoft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4020101%2Fb74ff314-f666-4722-b66c-679b7ad91a7a.png</url>
      <title>DEV Community: Ruhani Soft</title>
      <link>https://dev.to/ruhanisoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ruhanisoft"/>
    <language>en</language>
    <item>
      <title>How to Build an MVP in 2026: A Practical Guide to Scoping, Building, and Learning Fast</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:06:38 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/how-to-build-an-mvp-in-2026-a-practical-guide-to-scoping-building-and-learning-fast-4k3l</link>
      <guid>https://dev.to/ruhanisoft/how-to-build-an-mvp-in-2026-a-practical-guide-to-scoping-building-and-learning-fast-4k3l</guid>
      <description>&lt;p&gt;Most startups do not fail because they could not build the product. They fail because they built the wrong product, or built too much of it before anyone used it. A minimum viable product (MVP) exists to prevent exactly that.&lt;/p&gt;

&lt;p&gt;This guide walks through how to go from an idea to a focused first release: what to include, what to cut, how to build it, and how to learn from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an MVP actually is
&lt;/h2&gt;

&lt;p&gt;An MVP is a usable first version of a product, built around one specific assumption you need to test. Users should be able to complete the central task, and everything else waits.&lt;/p&gt;

&lt;p&gt;Two things people get wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Minimum" does not mean "bad".&lt;/strong&gt; The scope is small, but the experience of that small scope should still work well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MVP is not a demo.&lt;/strong&gt; A demo shows an idea. An MVP is something real users try in a real context, so you can see what they actually do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MVP vs prototype vs full product
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prototype&lt;/th&gt;
&lt;th&gt;MVP&lt;/th&gt;
&lt;th&gt;Full product&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Goal&lt;/td&gt;
&lt;td&gt;Explore an idea or interface&lt;/td&gt;
&lt;td&gt;Test a core assumption with real users&lt;/td&gt;
&lt;td&gt;Serve a proven market at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real users?&lt;/td&gt;
&lt;td&gt;Usually feedback sessions&lt;/td&gt;
&lt;td&gt;Yes, in a defined context&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working backend?&lt;/td&gt;
&lt;td&gt;Often faked&lt;/td&gt;
&lt;td&gt;Yes, for the core flow&lt;/td&gt;
&lt;td&gt;Complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical question&lt;/td&gt;
&lt;td&gt;"Does this make sense?"&lt;/td&gt;
&lt;td&gt;"Will people use and value this?"&lt;/td&gt;
&lt;td&gt;"How do we grow and improve it?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are not sure which you need, ask what you most need to learn. If it is "do people understand this?", a prototype may be enough. If it is "will people actually use this?", you need an MVP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write down your riskiest assumption
&lt;/h2&gt;

&lt;p&gt;Every product idea is a stack of assumptions. Pick the one that would kill the idea if it were false. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Clinics will pay to replace paper appointment books."&lt;/li&gt;
&lt;li&gt;"Freelancers will switch from spreadsheets to a dedicated invoicing tool."&lt;/li&gt;
&lt;li&gt;"Shoppers will reorder from us if reordering takes one tap."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write it as a single sentence. Your MVP exists to test that sentence, and any feature that does not help test it is a candidate for cutting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Define the user and the one journey
&lt;/h2&gt;

&lt;p&gt;Be specific about who the first user is. "Small businesses" is too broad. "Owners of single-location restaurants who take phone orders" is a real person.&lt;/p&gt;

&lt;p&gt;Then map the single journey that delivers value:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How does the user arrive?&lt;/li&gt;
&lt;li&gt;What is the one task they came to do?&lt;/li&gt;
&lt;li&gt;What is the moment they get value?&lt;/li&gt;
&lt;li&gt;What would make them come back?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything on that path is in scope. Everything off it is not, at least for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Prioritize ruthlessly
&lt;/h2&gt;

&lt;p&gt;Take your full feature wishlist and sort it into three buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Must have:&lt;/strong&gt; the product cannot test the assumption without it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should have:&lt;/strong&gt; it improves the experience but the core still works without it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Later:&lt;/strong&gt; good ideas that belong on the roadmap, not in release one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick test for each feature: &lt;em&gt;"If we launched without this, could users still complete the core journey?"&lt;/em&gt; If yes, it is not a must have.&lt;/p&gt;

&lt;p&gt;Common items that can usually wait:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced admin dashboards and analytics&lt;/li&gt;
&lt;li&gt;Multiple user roles and complex permissions&lt;/li&gt;
&lt;li&gt;Native apps on every platform (start with one)&lt;/li&gt;
&lt;li&gt;Automated emails beyond the essentials&lt;/li&gt;
&lt;li&gt;Theme customization and settings pages&lt;/li&gt;
&lt;li&gt;Integrations nobody has asked for yet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Choose a stack you can move fast in
&lt;/h2&gt;

&lt;p&gt;For an MVP, the best technology is usually the one your team already knows well and that has mature tooling for your problem. A few guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer boring, proven technology.&lt;/strong&gt; You want to spend time on the product, not on debugging a new framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy or reuse before you build.&lt;/strong&gt; Authentication, payments, email, and file storage are solved problems. Use established libraries and services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a foundation that can grow.&lt;/strong&gt; An MVP can become the long-term product if the foundation fits later needs. Think one step ahead, but do not build for ten steps ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start on one platform.&lt;/strong&gt; A responsive web app or a single mobile platform is usually enough to learn from.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Build in small, demonstrable increments
&lt;/h2&gt;

&lt;p&gt;Long silent build phases are risky. Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work in short cycles (one or two weeks)&lt;/li&gt;
&lt;li&gt;End each cycle with a &lt;strong&gt;working demo&lt;/strong&gt;, not a status report&lt;/li&gt;
&lt;li&gt;Review it with the people who know the users&lt;/li&gt;
&lt;li&gt;Adjust scope based on what you see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the project honest. If something turns out harder than expected, you find out early, while there is still room to change the plan.&lt;/p&gt;

&lt;p&gt;Good habits that cost little:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep a simple written scope document and update it when scope changes&lt;/li&gt;
&lt;li&gt;Track decisions ("we cut X because Y") so they do not get relitigated&lt;/li&gt;
&lt;li&gt;Deploy early to a real environment, not only to a developer laptop&lt;/li&gt;
&lt;li&gt;Add basic error monitoring before launch, so you see failures users do not report&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Decide what you will measure before you launch
&lt;/h2&gt;

&lt;p&gt;If you do not define success up front, every result looks like a win or a loss depending on your mood. Pick a small number of signals tied to your riskiest assumption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Activation:&lt;/strong&gt; what share of new users complete the core task?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention:&lt;/strong&gt; do they come back after a week?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qualitative feedback:&lt;/strong&gt; what do they say when you ask, and where do they get stuck?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Willingness to pay:&lt;/strong&gt; do they pay, or agree to, when asked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five conversations with real users will often teach you more than a dashboard with a hundred vanity numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Launch small, then learn
&lt;/h2&gt;

&lt;p&gt;Do not wait for a big public launch. Give the MVP to a small group first: early adopters, a pilot customer, a waitlist. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Watch how they actually use it&lt;/li&gt;
&lt;li&gt;Talk to them, especially the ones who dropped off&lt;/li&gt;
&lt;li&gt;Sort feedback into patterns, not one-off requests&lt;/li&gt;
&lt;li&gt;Decide: &lt;strong&gt;persevere, pivot, or stop&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat the result as data, not a verdict on you. A failed assumption found in six weeks is far cheaper than one found after eighteen months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common MVP mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Building for everyone.&lt;/strong&gt; A product for all users fits no one well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope creep disguised as "just one more feature".&lt;/strong&gt; Each small addition delays learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping real users.&lt;/strong&gt; Friends and family are polite. Target users are honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overengineering.&lt;/strong&gt; Microservices and multi-region infrastructure are rarely needed for a first release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the post-launch plan.&lt;/strong&gt; The MVP is the start of the learning loop, not the end of the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No single owner for decisions.&lt;/strong&gt; Someone has to be able to say "that goes in version two."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How long does an MVP take, and what does it cost?
&lt;/h2&gt;

&lt;p&gt;Anyone who quotes a fixed number before understanding your product is guessing. Timing and cost depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The number and complexity of core features&lt;/li&gt;
&lt;li&gt;Third-party integrations (payments, maps, messaging, hardware)&lt;/li&gt;
&lt;li&gt;Design needs&lt;/li&gt;
&lt;li&gt;How quickly feedback and approvals come back&lt;/li&gt;
&lt;li&gt;Whether you need web, mobile, or both&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest way to get an estimate is to share your goals, your current setup, and the main problem you want to solve, then scope the work from there. Smaller, clearer scope almost always means faster, cheaper, and better learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you build in-house or hire a partner?
&lt;/h2&gt;

&lt;p&gt;Both work. A rough way to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-house&lt;/strong&gt; fits when you already have engineers with capacity and the domain expertise to scope well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A development partner&lt;/strong&gt; fits when you need to move without hiring a full team, want an outside view on scope, or need specific skills for a short period.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you go with a partner, look for one who asks hard questions about scope before quoting, shows working demos regularly, and is comfortable saying "that is not needed yet." Teams that focus on &lt;a href="https://ruhanisoft.com/mvp-development.php" rel="noopener noreferrer"&gt;MVP development for startups&lt;/a&gt; typically structure the work around exactly this flow: define the essentials, build in reviewed increments, then plan for feedback.&lt;/p&gt;

&lt;p&gt;If you are an early-stage team still figuring out how to work with outside developers, resources written for &lt;a href="https://ruhanisoft.com/startups.php" rel="noopener noreferrer"&gt;startups and founders&lt;/a&gt; are a useful place to start.&lt;/p&gt;

&lt;p&gt;And whichever route you choose, ask any partner to explain their process up front. A clear, documented &lt;a href="https://ruhanisoft.com/our-approach.php" rel="noopener noreferrer"&gt;approach to planning and delivery&lt;/a&gt; is one of the best signs that a project will stay on scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple MVP checklist
&lt;/h2&gt;

&lt;p&gt;Before you start building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] One-sentence riskiest assumption&lt;/li&gt;
&lt;li&gt;[ ] One clearly defined first user&lt;/li&gt;
&lt;li&gt;[ ] One core journey mapped end to end&lt;/li&gt;
&lt;li&gt;[ ] Must-have list that fits in one page&lt;/li&gt;
&lt;li&gt;[ ] "Later" list captured so ideas are not lost&lt;/li&gt;
&lt;li&gt;[ ] Success signals defined before launch&lt;/li&gt;
&lt;li&gt;[ ] A small group of real users lined up to try it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Core journey tested end to end on a real environment&lt;/li&gt;
&lt;li&gt;[ ] Error monitoring in place&lt;/li&gt;
&lt;li&gt;[ ] A way for users to give feedback easily&lt;/li&gt;
&lt;li&gt;[ ] A plan for the first two weeks of post-launch review&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;An MVP is not about shipping less. It is about learning sooner. Pick one assumption, build the smallest real product that tests it, put it in front of actual users, and let what they do decide what comes next.&lt;/p&gt;

&lt;p&gt;What was the hardest feature you had to cut from your first release? Share it in the comments.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Laravel Payment Gateway Integration in 2026: A Production Guide to Webhooks, Idempotency, and Local Gateways</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:04:31 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/laravel-payment-gateway-integration-in-2026-a-production-guide-to-webhooks-idempotency-and-local-e8e</link>
      <guid>https://dev.to/ruhanisoft/laravel-payment-gateway-integration-in-2026-a-production-guide-to-webhooks-idempotency-and-local-e8e</guid>
      <description>&lt;p&gt;Integrating a payment gateway in Laravel looks easy in the tutorial: redirect the user, get a callback, mark the order as paid. Then production happens. Customers close the tab before the redirect. Webhooks arrive twice, or out of order, or never. A refund comes in while the order is still "pending". Your finance team asks why the ledger and the gateway dashboard disagree.&lt;/p&gt;

&lt;p&gt;This guide covers how to build payment integrations that survive all of that. It is based on patterns that hold up across very different gateways, from Stripe and PayPal to mobile financial services like bKash and Nagad.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A gateway-agnostic architecture for Laravel&lt;/li&gt;
&lt;li&gt;A payment state machine that prevents illegal transitions&lt;/li&gt;
&lt;li&gt;Secure webhook handling with signature verification&lt;/li&gt;
&lt;li&gt;Idempotency, so double-delivery never double-charges or double-fulfills&lt;/li&gt;
&lt;li&gt;Reconciliation jobs for the webhooks you miss&lt;/li&gt;
&lt;li&gt;Testing payment flows without hitting real sandboxes&lt;/li&gt;
&lt;li&gt;Notes on local gateways such as bKash, Nagad, and SSLCommerz&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The mistakes that cost real money
&lt;/h2&gt;

&lt;p&gt;Before the code, here are the failure modes behind most payment incidents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the browser redirect.&lt;/strong&gt; The success URL is not proof of payment. Anyone can visit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fulfilling an order inside the callback request.&lt;/strong&gt; If it times out halfway, you get a paid order that was never fulfilled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not handling duplicate webhooks.&lt;/strong&gt; Gateways use at-least-once delivery. Duplicates are normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing money as floats.&lt;/strong&gt; Use integers in minor units (cents, poisha).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No reconciliation.&lt;/strong&gt; Webhooks fail. Without a safety net, you will have silent mismatches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway logic scattered across controllers.&lt;/strong&gt; Adding the second gateway becomes a rewrite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything below is designed to remove these.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture: one interface, many gateways
&lt;/h2&gt;

&lt;p&gt;If you ever expect to support more than one gateway, define a contract first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Payments\Contracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Payment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** Create a session/payment on the gateway and return where to send the customer. */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Payment&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;InitiatedPayment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** Verify the authenticity of an incoming webhook/IPN. */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** Extract a unique, stable event id from the webhook (used for idempotency). */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** Translate the gateway payload into our internal result. */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;parseWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;WebhookResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** Ask the gateway for the truth. Used by reconciliation and after redirects. */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetchStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Payment&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;GatewayStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Payment&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$amountMinor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;RefundResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep DTOs small and immutable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Payments\Contracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InitiatedPayment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$gatewayReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$redirectUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$clientPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebhookResult&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$gatewayReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nc"&gt;\App\Enums\PaymentStatus&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$amountMinor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a manager that resolves drivers, using Laravel's &lt;code&gt;Manager&lt;/code&gt; class or a simple map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Payments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Payments\Contracts\PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;InvalidArgumentException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GatewayManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$drivers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;drivers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvalidArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Unknown gateway [&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in a service provider, driven by config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/payments.php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'drivers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'stripe'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Payments\Gateways\StripeGateway&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sslcommerz'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Payments\Gateways\SslCommerzGateway&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bkash'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Payments\Gateways\BkashGateway&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// AppServiceProvider::register()&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GatewayManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GatewayManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payments.drivers'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your controllers now only know about &lt;code&gt;GatewayManager&lt;/code&gt; and &lt;code&gt;PaymentGateway&lt;/code&gt;. Adding the next gateway means writing one class and one config line.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Data model
&lt;/h2&gt;

&lt;p&gt;Two tables matter most: &lt;code&gt;payments&lt;/code&gt; and &lt;code&gt;webhook_events&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedBigInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amount_minor'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 1050 = 10.50&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedBigInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'refunded_minor'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'currency'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'meta'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'paid_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhook_events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payload'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processed_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;   &lt;span class="c1"&gt;// the idempotency guard&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unique index on &lt;code&gt;webhook_events&lt;/code&gt; is what makes duplicate delivery harmless. Do not rely on an &lt;code&gt;if (exists)&lt;/code&gt; check alone, because two requests can race past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A payment state machine
&lt;/h2&gt;

&lt;p&gt;Use a backed enum and explicitly define which transitions are legal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Enums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Processing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Refunded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'refunded'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;PartiallyRefunded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'partially_refunded'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @return array&amp;lt;self&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;allowedNext&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Processing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Processing&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;PartiallyRefunded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Refunded&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;PartiallyRefunded&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;PartiallyRefunded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Refunded&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Refunded&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;canTransitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;self&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedNext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the model, centralize transitions so nothing mutates &lt;code&gt;status&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PaymentStatus&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already there, treat as a no-op&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illegal payment transition'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'payment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'from'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'to'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because webhooks arrive out of order. A late &lt;code&gt;pending&lt;/code&gt; event must never overwrite &lt;code&gt;paid&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Starting a payment
&lt;/h2&gt;

&lt;p&gt;Create the local record first, then talk to the gateway. If the gateway call fails, you still have a record to inspect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Payment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Payments\GatewayManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StartPayment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;GatewayManager&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'order_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'gateway'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'amount_minor'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total_minor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'currency'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$initiated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateways&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$initiated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gatewayReference&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'payment_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'redirect'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$initiated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'payload'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$initiated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clientPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always pass your own &lt;code&gt;payment-&amp;gt;id&lt;/code&gt; (or a UUID) to the gateway as a merchant reference where supported. It makes debugging and reconciliation far easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Webhooks done right
&lt;/h2&gt;

&lt;p&gt;The webhook endpoint should do four things and nothing else: &lt;strong&gt;verify, deduplicate, persist, dispatch&lt;/strong&gt;. Heavy work goes to a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/webhooks/payments/{gateway}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PaymentWebhookController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withoutMiddleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Middleware\VerifyCsrfToken&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhooks.payments'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(In Laravel 11 and newer, exclude the path through &lt;code&gt;validateCsrfTokens(except: [...])&lt;/code&gt; in &lt;code&gt;bootstrap/app.php&lt;/code&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Jobs\ProcessPaymentWebhook&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Payments\GatewayManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentWebhookController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;GatewayManager&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateways&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$driver&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid signature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$inserted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhook_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insertOrIgnore&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'gateway'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'event_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$driver&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'payload'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'created_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'updated_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Duplicate delivery: acknowledge and stop.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$inserted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$eventDbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhook_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$driver&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;ProcessPaymentWebhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventDbId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return a 2xx quickly. Most gateways retry on anything else, which creates the duplicates you just protected against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signature verification
&lt;/h3&gt;

&lt;p&gt;Every serious gateway signs its webhooks, usually with HMAC. The pattern, using a Stripe-style scheme as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stripe-Signature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;parse_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$timestamp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Reject old requests to limit replay attacks.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// RAW body, not re-encoded JSON&lt;/span&gt;
        &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.webhook_secret'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details people get wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the &lt;strong&gt;raw body&lt;/strong&gt; (&lt;code&gt;$request-&amp;gt;getContent()&lt;/code&gt;), never &lt;code&gt;json_encode($request-&amp;gt;all())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare with &lt;code&gt;hash_equals()&lt;/code&gt; to avoid timing attacks.&lt;/li&gt;
&lt;li&gt;Check a timestamp tolerance where the scheme supports it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For gateways that do not sign webhooks at all (some regional ones only send an IPN with a transaction ID), treat the webhook as a &lt;strong&gt;hint&lt;/strong&gt;, then call the gateway's verification API server-to-server before changing any state. More on that in section 9.&lt;/p&gt;

&lt;h3&gt;
  
  
  The processing job
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Jobs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Enums\PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Payment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Payments\GatewayManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Bus\Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Bus\Dispatchable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessPaymentWebhook&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GatewayManager&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhook_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processed_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Http\Request&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;parseWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gatewayReference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lockForUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Webhook beat our own DB write. Let the queue retry later.&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amountMinor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amountMinor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount_minor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Amount mismatch on payment &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;\App\Events\PaymentSucceeded&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhook_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'processed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lockForUpdate()&lt;/code&gt; inside a transaction stops two workers from processing the same payment at once.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;amount check&lt;/strong&gt; defends against tampering and mismatched currency handling.&lt;/li&gt;
&lt;li&gt;Fulfillment hangs off a &lt;code&gt;PaymentSucceeded&lt;/code&gt; event, so the webhook job stays focused on payment state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Idempotency beyond webhooks
&lt;/h2&gt;

&lt;p&gt;Webhooks are one place duplicates appear. Others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User double-clicks "Pay".&lt;/strong&gt; Disable the button client-side, and server-side reuse an existing pending payment for the same order instead of creating a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API retries to the gateway.&lt;/strong&gt; Stripe and several others support an &lt;code&gt;Idempotency-Key&lt;/code&gt; header. Use your payment ID as the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fulfillment.&lt;/strong&gt; Make &lt;code&gt;PaymentSucceeded&lt;/code&gt; listeners idempotent. For example, issue a license key only if the order does not already have one.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'Idempotency-Key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"payment-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good rule: any operation that has a side effect in the outside world (email, license, shipment) should be safe to run twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Reconciliation: your safety net
&lt;/h2&gt;

&lt;p&gt;Even with perfect code, webhooks get lost: your server was down, a firewall blocked the gateway, the queue backed up. Run a scheduled job that asks the gateway for the truth about stale payments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/console.php (Laravel 11+) or Console\Kernel::schedule()&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payments:reconcile'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyFiveMinutes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Console\Commands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Payment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Payments\GatewayManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Console\Command&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReconcilePayments&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'payments:reconcile'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Sync stale pending payments with their gateway'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GatewayManager&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chunkById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payments&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$gateways&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                        &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also build an admin view that lists payments stuck in &lt;code&gt;pending&lt;/code&gt; for more than an hour. Support teams will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Working with local and regional gateways
&lt;/h2&gt;

&lt;p&gt;Stripe-quality documentation is the exception, not the rule. Many regional gateways have inconsistent sandboxes, unusual auth flows, or webhooks that are only partially signed. The architecture above still works, but expect these differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token-based, multi-step flows.&lt;/strong&gt; Mobile wallets like bKash typically follow a sequence: obtain an auth token, create a payment, redirect the customer to approve it, then &lt;strong&gt;execute&lt;/strong&gt; the payment after the callback. The execute step is what actually captures the money, so your callback handler must call it and then verify the final status. Tokens expire, so cache them with a TTL slightly below their lifetime and refresh on failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypted payload signing.&lt;/strong&gt; Some gateways, Nagad being a well-known example, require you to encrypt and sign request payloads with RSA keys. Keep keys in secure storage, never in the repo, and write the crypto in one small, well-tested class so the rest of the driver stays readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect plus server validation.&lt;/strong&gt; Gateways like SSLCommerz redirect the customer back with a transaction identifier, and also send an IPN. The safe approach is to ignore what the redirect claims and call the gateway's validation endpoint with the transaction ID, then compare the &lt;strong&gt;amount and currency&lt;/strong&gt; in the validation response against your own record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetchStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Payment&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;GatewayStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.sslcommerz.validation_url'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'val_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'val_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'store_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.sslcommerz.store_id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'store_passwd'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.sslcommerz.store_password'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'format'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'VALID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'VALIDATED'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$amountOk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount_minor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GatewayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$valid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$amountOk&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;General rules for any gateway with weak webhooks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat inbound notifications as triggers, not as truth.&lt;/li&gt;
&lt;li&gt;Always verify server-to-server before marking a payment paid.&lt;/li&gt;
&lt;li&gt;Validate amount, currency, and your own merchant reference.&lt;/li&gt;
&lt;li&gt;Log the full request and response for every gateway call (with secrets masked). You will need this during disputes.&lt;/li&gt;
&lt;li&gt;Put timeouts and retries on every outbound HTTP call: &lt;code&gt;Http::timeout(15)-&amp;gt;retry(3, 200)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building for emerging markets and do not want to write each integration from scratch, it can be worth reusing battle-tested code. Studios such as &lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;Ruhanisoft&lt;/a&gt;, which maintain a library covering dozens of regional and international gateways, exist precisely because the per-gateway quirks are where most of the project time goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Testing without hitting real sandboxes
&lt;/h2&gt;

&lt;p&gt;Sandboxes are slow, flaky, and sometimes down. Fake the HTTP layer and test your own logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'marks a payment as paid once, even if the webhook is delivered twice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'gateway'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'gateway_reference'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pi_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'amount_minor'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'evt_1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reference'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pi_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signedStripeHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// helper that builds a valid Stripe-Signature&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/webhooks/payments/stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/webhooks/payments/stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertPushed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessPaymentWebhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rejects a webhook with a bad signature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/webhooks/payments/stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'evt_2'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Stripe-Signature'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'t=1,v1=bad'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'never moves a paid payment back to pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBeFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also fake outbound gateway calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'api.gateway.test/*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'VALID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'amount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'50.00'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests worth writing first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Duplicate webhook delivery&lt;/li&gt;
&lt;li&gt;Invalid signature&lt;/li&gt;
&lt;li&gt;Out-of-order events&lt;/li&gt;
&lt;li&gt;Amount mismatch&lt;/li&gt;
&lt;li&gt;Gateway timeout during &lt;code&gt;initiate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reconciliation picks up a missed webhook&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  11. Security checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Webhook secrets and API keys live in &lt;code&gt;.env&lt;/code&gt; or a secrets manager, never in Git&lt;/li&gt;
&lt;li&gt;Webhook route excluded from CSRF, but &lt;strong&gt;protected by signature verification&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Raw-body HMAC with &lt;code&gt;hash_equals&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Amounts stored as integers, verified against your own record&lt;/li&gt;
&lt;li&gt;Card data never touches your server if you can use hosted fields or redirects (stay out of PCI scope)&lt;/li&gt;
&lt;li&gt;Webhook endpoint rate limited and allowlisted by IP where the gateway publishes ranges&lt;/li&gt;
&lt;li&gt;Sensitive fields masked in logs&lt;/li&gt;
&lt;li&gt;Refund and admin actions behind policies and audit logging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Going live checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sandbox and live credentials separated per environment&lt;/li&gt;
&lt;li&gt;Webhook URL registered in the &lt;strong&gt;live&lt;/strong&gt; dashboard, not just sandbox&lt;/li&gt;
&lt;li&gt;Queue workers supervised (Horizon or Supervisor) and restarting on deploy&lt;/li&gt;
&lt;li&gt;Reconciliation scheduled and the scheduler actually running&lt;/li&gt;
&lt;li&gt;Alerts on: webhook failures, payments stuck in pending, amount mismatches&lt;/li&gt;
&lt;li&gt;A runbook for "customer paid but order not fulfilled"&lt;/li&gt;
&lt;li&gt;One real low-value transaction tested end to end, including a refund&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Reliable payments in Laravel come down to a few habits: never trust the redirect, verify every webhook, make every step idempotent, model payment status as a state machine, and reconcile against the gateway on a schedule. Get those right once behind a clean gateway interface and each new provider becomes a small, predictable piece of work instead of a risky rewrite.&lt;/p&gt;

&lt;p&gt;If you would rather hand off the integration work, agencies and founders often bring in a specialist &lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;Laravel payment gateway integration team&lt;/a&gt; for the tricky regional gateways, then keep the rest of the product in-house. Whichever route you pick, use the checklists above to hold the result to a high standard.&lt;/p&gt;

&lt;p&gt;And if you are starting a new Laravel product from scratch, a solid base saves weeks. Browse some &lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;ready-made Laravel scripts and starter kits&lt;/a&gt; to see what a production-oriented foundation looks like.&lt;/p&gt;

&lt;p&gt;What is the strangest gateway behavior you have run into? Share it in the comments.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>payments</category>
    </item>
    <item>
      <title>How to Choose the Right Offshore Laravel Development Company in 2026</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:01:43 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/how-to-choose-the-right-offshore-laravel-development-company-in-2026-20l0</link>
      <guid>https://dev.to/ruhanisoft/how-to-choose-the-right-offshore-laravel-development-company-in-2026-20l0</guid>
      <description>&lt;p&gt;Laravel is still one of the most productive PHP frameworks for building SaaS products, marketplaces, and internal tools. That is why so many agencies and startups outsource Laravel work to offshore teams. The catch is that the gap between a great offshore partner and a bad one is enormous, and you usually find out only after the budget is burned.&lt;/p&gt;

&lt;p&gt;Here is a practical checklist for picking the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Look at the code, not the pitch deck
&lt;/h2&gt;

&lt;p&gt;Ask for a real code sample or a short paid trial task. Then check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure:&lt;/strong&gt; Are controllers thin? Is business logic in services, actions, or jobs?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eloquent usage:&lt;/strong&gt; Do they avoid N+1 queries (&lt;code&gt;with()&lt;/code&gt;, eager loading)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; Form Requests instead of validation scattered across controllers?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security basics:&lt;/strong&gt; Mass assignment protection, policies/gates, CSRF, and no raw queries with user input.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Red flag: N+1 query&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Better&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a team cannot explain why the second version matters, keep looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check that they know the Laravel ecosystem
&lt;/h2&gt;

&lt;p&gt;A serious Laravel team should be comfortable with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queues, Horizon, and scheduled jobs&lt;/li&gt;
&lt;li&gt;Sanctum/Passport for API auth&lt;/li&gt;
&lt;li&gt;Livewire, Inertia, or a Vue/React frontend&lt;/li&gt;
&lt;li&gt;Pest or PHPUnit testing&lt;/li&gt;
&lt;li&gt;Redis caching and database indexing&lt;/li&gt;
&lt;li&gt;Deployment with Forge, Envoyer, Docker, or plain CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ask which of these they used in their last three projects. Specific answers beat generic ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Payment integrations are a great stress test
&lt;/h2&gt;

&lt;p&gt;Payment gateways expose weak engineering quickly. Webhooks, idempotency, refunds, and failed-payment states are where sloppy code breaks. If your product sells in emerging markets, local gateway experience matters even more.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;Ruhanisoft&lt;/a&gt;, a Laravel/PHP studio, maintains a large library of gateway integrations, including Bangladesh mobile financial services like bKash and Nagad as well as SSLCommerz. Teams that have shipped this kind of integration repeatedly tend to handle webhook verification and reconciliation correctly from the start.&lt;/p&gt;

&lt;p&gt;Questions worth asking any vendor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you verify webhook signatures?&lt;/li&gt;
&lt;li&gt;How do you prevent double-processing the same payment?&lt;/li&gt;
&lt;li&gt;How do you test against sandbox environments?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Communication beats hourly rate
&lt;/h2&gt;

&lt;p&gt;Cheap rates get expensive when requirements get lost in translation. Evaluate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time zone overlap:&lt;/strong&gt; At least 3 to 4 shared working hours is ideal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Written English:&lt;/strong&gt; Read their emails and docs, not just their sales calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process:&lt;/strong&gt; Do they use Jira, Linear, or GitHub Projects? Do they do weekly demos?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proactivity:&lt;/strong&gt; Do they challenge unclear requirements or just say yes to everything?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A team that asks good questions in week one saves you weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Clarify ownership and IP up front
&lt;/h2&gt;

&lt;p&gt;Put these in the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full source code ownership transfers to you&lt;/li&gt;
&lt;li&gt;Access to the Git repo from day one, under your organization&lt;/li&gt;
&lt;li&gt;Documentation and a handover process&lt;/li&gt;
&lt;li&gt;NDA and confidentiality terms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never accept a setup where the vendor holds the only copy of the repo or the production credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Start small with a pilot
&lt;/h2&gt;

&lt;p&gt;Instead of committing to a 6-month build, run a 2 to 4 week pilot: one feature, one module, or one bug-fix sprint. Judge them on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Code quality&lt;/li&gt;
&lt;li&gt;Delivery against the estimate&lt;/li&gt;
&lt;li&gt;Communication&lt;/li&gt;
&lt;li&gt;How they handle feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. Understand the pricing model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed price&lt;/td&gt;
&lt;td&gt;Well-defined MVPs&lt;/td&gt;
&lt;td&gt;Scope disputes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly / T&amp;amp;M&lt;/td&gt;
&lt;td&gt;Evolving products&lt;/td&gt;
&lt;td&gt;Budget creep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated developer&lt;/td&gt;
&lt;td&gt;Long-term overflow work&lt;/td&gt;
&lt;td&gt;Needs your management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Agencies often prefer a white-label or dedicated-developer arrangement, where the offshore team works behind your brand. If you go this route, make sure the vendor is comfortable with NDAs and with working inside your tools and repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Red flags to walk away from
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No willingness to share code samples or references&lt;/li&gt;
&lt;li&gt;Guaranteed unrealistic timelines&lt;/li&gt;
&lt;li&gt;No mention of testing or code review&lt;/li&gt;
&lt;li&gt;Vague answers about who will actually work on your project&lt;/li&gt;
&lt;li&gt;Pressure to pay everything upfront&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The best offshore Laravel partner is not the cheapest or the biggest. It is the one that writes maintainable code, communicates clearly, and treats your product like their own. Use a pilot, verify their technical depth, and lock down ownership before you scale.&lt;/p&gt;

&lt;p&gt;If you are comparing options, studios like &lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;Ruhanisoft's Laravel development team&lt;/a&gt; are a good example of the kind of focused, PHP/Laravel-first shop worth shortlisting. Just run the same checklist on everyone, including them.&lt;/p&gt;

&lt;p&gt;What has your experience been with offshore Laravel teams? Share your lessons in the comments.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why We Chose Laravel to Build Products That Last</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Sun, 12 Jul 2026 05:34:25 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/why-we-chose-laravel-to-build-products-that-last-17na</link>
      <guid>https://dev.to/ruhanisoft/why-we-chose-laravel-to-build-products-that-last-17na</guid>
      <description>&lt;p&gt;Software is easier to build than ever.&lt;/p&gt;

&lt;p&gt;With AI generating code in seconds and countless frameworks available, shipping an application has become faster than anyone imagined. But building software that remains maintainable, secure, and scalable after months or years is still a different challenge.&lt;/p&gt;

&lt;p&gt;At Ruhani Soft, we focus on Laravel because it helps us solve that challenge consistently.&lt;/p&gt;

&lt;p&gt;Laravel is more than a PHP framework&lt;/p&gt;

&lt;p&gt;Laravel isn't just about elegant syntax.&lt;/p&gt;

&lt;p&gt;It's an ecosystem that allows teams to build production-ready applications with features like:&lt;/p&gt;

&lt;p&gt;Authentication and authorization&lt;br&gt;
Queues and background jobs&lt;br&gt;
Caching&lt;br&gt;
Scheduled tasks&lt;br&gt;
API development&lt;br&gt;
Testing&lt;br&gt;
Database migrations&lt;br&gt;
Strong security practices&lt;/p&gt;

&lt;p&gt;Instead of reinventing these building blocks for every project, we can spend more time solving real business problems.&lt;/p&gt;

&lt;p&gt;Our philosophy&lt;/p&gt;

&lt;p&gt;We believe software should be:&lt;/p&gt;

&lt;p&gt;Easy to maintain&lt;br&gt;
Fast to iterate&lt;br&gt;
Secure by default&lt;br&gt;
Well documented&lt;br&gt;
Built with long-term growth in mind&lt;/p&gt;

&lt;p&gt;Whether it's a SaaS platform, internal business software, fintech solution, or custom web application, these principles remain the same.&lt;/p&gt;

&lt;p&gt;AI changes development, but not engineering&lt;/p&gt;

&lt;p&gt;AI has dramatically increased developer productivity.&lt;/p&gt;

&lt;p&gt;It can generate code, explain unfamiliar APIs, create tests, and speed up repetitive tasks.&lt;/p&gt;

&lt;p&gt;But successful products still require thoughtful architecture, clean code, good UX, security, and careful technical decisions.&lt;/p&gt;

&lt;p&gt;We see AI as a powerful development partner—not a replacement for engineering.&lt;/p&gt;

&lt;p&gt;What we're building&lt;/p&gt;

&lt;p&gt;At Ruhani Soft, we're sharing our journey as we build:&lt;/p&gt;

&lt;p&gt;Laravel applications&lt;br&gt;
SaaS products&lt;br&gt;
Business automation tools&lt;br&gt;
Developer resources&lt;br&gt;
Open-source contributions&lt;/p&gt;

&lt;p&gt;We'll also publish lessons learned from real projects, performance optimization tips, deployment strategies, and practical Laravel techniques.&lt;/p&gt;

&lt;p&gt;If you're building with Laravel or planning your next product, we'd love to connect and exchange ideas.&lt;/p&gt;

</description>
      <category>ruhanisoft</category>
      <category>coding</category>
      <category>business</category>
      <category>software</category>
    </item>
    <item>
      <title>How Offshore PHP Development Helps Startups Build MVPs Faster</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Sun, 12 Jul 2026 05:33:48 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/how-offshore-php-development-helps-startups-build-mvps-faster-1pmm</link>
      <guid>https://dev.to/ruhanisoft/how-offshore-php-development-helps-startups-build-mvps-faster-1pmm</guid>
      <description>&lt;p&gt;Startups usually do not have unlimited time, budget, or development capacity.&lt;/p&gt;

&lt;p&gt;A product idea needs to be tested quickly. The first version does not need every feature, but it does need to be stable, usable, and clear enough for real users.&lt;/p&gt;

&lt;p&gt;This is where offshore PHP and Laravel development can be useful for startups building MVPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MVP speed matters
&lt;/h2&gt;

&lt;p&gt;An MVP helps a startup test an idea before spending too much time and money on a full product.&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the core features&lt;/li&gt;
&lt;li&gt;Launch quickly&lt;/li&gt;
&lt;li&gt;Get user feedback&lt;/li&gt;
&lt;li&gt;Improve based on real usage&lt;/li&gt;
&lt;li&gt;Avoid building unnecessary features too early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When development is slow, the startup loses time. When development is rushed without structure, the product becomes difficult to maintain.&lt;/p&gt;

&lt;p&gt;A good MVP needs both speed and clean execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel works well for startup MVPs
&lt;/h2&gt;

&lt;p&gt;Laravel is a practical framework for building MVPs because it gives developers a strong foundation out of the box.&lt;/p&gt;

&lt;p&gt;It supports many common product requirements, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;Admin dashboards&lt;/li&gt;
&lt;li&gt;Database management&lt;/li&gt;
&lt;li&gt;API development&lt;/li&gt;
&lt;li&gt;Payment integration&lt;/li&gt;
&lt;li&gt;Email notifications&lt;/li&gt;
&lt;li&gt;File uploads&lt;/li&gt;
&lt;li&gt;Role based access&lt;/li&gt;
&lt;li&gt;Background jobs&lt;/li&gt;
&lt;li&gt;Security features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps developers build faster without creating everything from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where offshore development helps
&lt;/h2&gt;

&lt;p&gt;Hiring a full in-house team can be expensive for an early stage startup. It can also take time to find, interview, onboard, and manage developers.&lt;/p&gt;

&lt;p&gt;Offshore development gives startups access to technical support without building a full internal team immediately.&lt;/p&gt;

&lt;p&gt;It can help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MVP planning&lt;/li&gt;
&lt;li&gt;Feature development&lt;/li&gt;
&lt;li&gt;Laravel backend development&lt;/li&gt;
&lt;li&gt;Admin panel creation&lt;/li&gt;
&lt;li&gt;API integration&lt;/li&gt;
&lt;li&gt;Payment gateway setup&lt;/li&gt;
&lt;li&gt;Bug fixing&lt;/li&gt;
&lt;li&gt;Product improvements after launch&lt;/li&gt;
&lt;li&gt;Long term maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For startups, this means the idea can move from concept to product faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What startups should avoid
&lt;/h2&gt;

&lt;p&gt;Offshore development works best when the project has clear direction.&lt;/p&gt;

&lt;p&gt;Startups should avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding too many features to the first version&lt;/li&gt;
&lt;li&gt;Building without a clear user problem&lt;/li&gt;
&lt;li&gt;Ignoring security and code quality&lt;/li&gt;
&lt;li&gt;Choosing only based on the lowest price&lt;/li&gt;
&lt;li&gt;Skipping documentation&lt;/li&gt;
&lt;li&gt;Delaying feedback until the product is complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small, focused MVP is usually better than a large unfinished product.&lt;/p&gt;

&lt;h2&gt;
  
  
  How RuhaniSoft supports startup MVPs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;RuhaniSoft&lt;/a&gt; helps startups and growing businesses build PHP and Laravel web applications, SaaS platforms, admin panels, and custom business software.&lt;/p&gt;

&lt;p&gt;For MVP projects, RuhaniSoft focuses on clean development, practical feature planning, scalable structure, and reliable delivery.&lt;/p&gt;

&lt;p&gt;The goal is to help startups launch a working product faster while keeping the codebase ready for future improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better way to build the first version
&lt;/h2&gt;

&lt;p&gt;The first version of a product should be simple, focused, and useful.&lt;/p&gt;

&lt;p&gt;Laravel gives startups a reliable technical base, while offshore development support helps reduce delivery pressure.&lt;/p&gt;

&lt;p&gt;For startups that need to launch a PHP or Laravel based MVP, working with an offshore development partner can be a practical way to move faster without losing control of quality.&lt;/p&gt;

&lt;p&gt;Learn more about RuhaniSoft here: &lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;https://ruhanisoft.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>startup</category>
      <category>webdev</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Why Growing Businesses Choose Laravel for Custom Web Applications</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Thu, 09 Jul 2026 03:57:12 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/why-growing-businesses-choose-laravel-for-custom-web-applications-1hac</link>
      <guid>https://dev.to/ruhanisoft/why-growing-businesses-choose-laravel-for-custom-web-applications-1hac</guid>
      <description>&lt;p&gt;When a business starts growing, basic websites and simple tools are often not enough anymore.&lt;/p&gt;

&lt;p&gt;Teams need custom dashboards, customer portals, reporting systems, SaaS platforms, payment workflows, and internal tools that match the way their business actually works.&lt;/p&gt;

&lt;p&gt;This is one reason Laravel is still a strong choice for custom web application development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel is built for practical business software
&lt;/h2&gt;

&lt;p&gt;Laravel gives developers a clean structure for building secure and maintainable applications.&lt;/p&gt;

&lt;p&gt;For business projects, this matters because the software usually needs to grow over time. A small project today can become a larger platform later with more users, more features, more integrations, and more data.&lt;/p&gt;

&lt;p&gt;Laravel works well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin panels&lt;/li&gt;
&lt;li&gt;SaaS applications&lt;/li&gt;
&lt;li&gt;CRM systems&lt;/li&gt;
&lt;li&gt;Booking platforms&lt;/li&gt;
&lt;li&gt;Finance dashboards&lt;/li&gt;
&lt;li&gt;Customer portals&lt;/li&gt;
&lt;li&gt;Inventory systems&lt;/li&gt;
&lt;li&gt;Reporting tools&lt;/li&gt;
&lt;li&gt;API based applications&lt;/li&gt;
&lt;li&gt;Internal business tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why businesses prefer custom web apps
&lt;/h2&gt;

&lt;p&gt;Off the shelf software can be useful, but it does not always fit every workflow.&lt;/p&gt;

&lt;p&gt;A custom web application allows a business to build around its own process instead of forcing the team to adjust to a generic tool.&lt;/p&gt;

&lt;p&gt;Custom software can help businesses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save time on repeated tasks&lt;/li&gt;
&lt;li&gt;Improve team coordination&lt;/li&gt;
&lt;li&gt;Reduce manual data entry&lt;/li&gt;
&lt;li&gt;Track business activity in one place&lt;/li&gt;
&lt;li&gt;Connect different tools through APIs&lt;/li&gt;
&lt;li&gt;Create better customer experiences&lt;/li&gt;
&lt;li&gt;Build features that competitors do not have&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why PHP and Laravel are still relevant
&lt;/h2&gt;

&lt;p&gt;PHP remains widely used for web development, and Laravel has made PHP development much cleaner and more productive.&lt;/p&gt;

&lt;p&gt;Laravel includes useful features for routing, authentication, database handling, queues, notifications, API development, validation, caching, and security.&lt;/p&gt;

&lt;p&gt;This makes it easier to build reliable applications without starting every feature from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a business should consider Laravel development
&lt;/h2&gt;

&lt;p&gt;A business should consider Laravel when it needs a web application that is more than a simple website.&lt;/p&gt;

&lt;p&gt;Some common signs include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The team is using spreadsheets for complex operations&lt;/li&gt;
&lt;li&gt;Customers need a portal or dashboard&lt;/li&gt;
&lt;li&gt;The business needs custom reporting&lt;/li&gt;
&lt;li&gt;Existing tools do not fit the workflow&lt;/li&gt;
&lt;li&gt;Manual work is slowing the team down&lt;/li&gt;
&lt;li&gt;Multiple systems need to be connected&lt;/li&gt;
&lt;li&gt;A SaaS product or MVP needs to be launched&lt;/li&gt;
&lt;li&gt;The company wants full control over features and data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How RuhaniSoft supports Laravel projects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;RuhaniSoft&lt;/a&gt; provides PHP and Laravel development services for businesses, startups, and agencies that need custom web software.&lt;/p&gt;

&lt;p&gt;The team works on scalable web applications, SaaS platforms, admin panels, API integrations, and long term software maintenance.&lt;/p&gt;

&lt;p&gt;For growing businesses, RuhaniSoft can help turn manual workflows into structured web applications that are easier to manage and improve over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building software that can grow
&lt;/h2&gt;

&lt;p&gt;The best custom applications are not only built for today. They are built with future changes in mind.&lt;/p&gt;

&lt;p&gt;A Laravel application can start as a small MVP, then grow into a full business platform with more modules, users, permissions, integrations, and automation.&lt;/p&gt;

&lt;p&gt;That makes Laravel a practical choice for businesses that want reliable software without overcomplicating the first version.&lt;/p&gt;

&lt;p&gt;Learn more about Laravel and PHP development services here: &lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;https://ruhanisoft.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdevv</category>
      <category>php</category>
      <category>business</category>
    </item>
    <item>
      <title>How Agencies Can Deliver Laravel Projects Faster With Offshore PHP Development</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Thu, 09 Jul 2026 03:55:37 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/how-agencies-can-deliver-laravel-projects-faster-with-offshore-php-development-5ha9</link>
      <guid>https://dev.to/ruhanisoft/how-agencies-can-deliver-laravel-projects-faster-with-offshore-php-development-5ha9</guid>
      <description>&lt;p&gt;Agencies often handle multiple client projects at the same time. Some projects need new features, some need bug fixes, and some need full Laravel application development from planning to launch.&lt;/p&gt;

&lt;p&gt;When the internal team is already busy, delivery can slow down. This is where offshore PHP and Laravel development support can help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel agencies need flexible development support
&lt;/h2&gt;

&lt;p&gt;Laravel is a strong framework for building business software, SaaS platforms, dashboards, admin panels, API systems, and custom web applications.&lt;/p&gt;

&lt;p&gt;But agency work is not always predictable.&lt;/p&gt;

&lt;p&gt;One month may have light work. The next month may bring multiple client deadlines at once. Hiring full time developers for every demand is not always practical.&lt;/p&gt;

&lt;p&gt;Offshore Laravel development helps agencies add skilled development capacity without increasing permanent team costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common situations where offshore support helps
&lt;/h2&gt;

&lt;p&gt;Agencies usually need extra support when they have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many active Laravel projects&lt;/li&gt;
&lt;li&gt;Urgent bug fixes or feature updates&lt;/li&gt;
&lt;li&gt;Client requests that need fast turnaround&lt;/li&gt;
&lt;li&gt;White label development requirements&lt;/li&gt;
&lt;li&gt;SaaS or dashboard projects that need backend expertise&lt;/li&gt;
&lt;li&gt;API integrations and payment gateway work&lt;/li&gt;
&lt;li&gt;Long term maintenance tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reliable offshore development partner can help keep projects moving while the agency focuses on client communication, sales, and strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good offshore Laravel support should include
&lt;/h2&gt;

&lt;p&gt;Not every offshore team is useful for agency work. The right partner should understand clean code, deadlines, communication, and client expectations.&lt;/p&gt;

&lt;p&gt;Important things to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong PHP and Laravel experience&lt;/li&gt;
&lt;li&gt;Clean and maintainable code&lt;/li&gt;
&lt;li&gt;Secure development practices&lt;/li&gt;
&lt;li&gt;Clear communication&lt;/li&gt;
&lt;li&gt;Flexible project capacity&lt;/li&gt;
&lt;li&gt;Ability to work with existing codebases&lt;/li&gt;
&lt;li&gt;Support for APIs, dashboards, SaaS features, and admin panels&lt;/li&gt;
&lt;li&gt;Long term maintenance availability&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How RuhaniSoft helps agencies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;RuhaniSoft&lt;/a&gt; helps agencies and growing businesses build scalable PHP and Laravel web applications.&lt;/p&gt;

&lt;p&gt;The team focuses on offshore development support for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laravel application development&lt;/li&gt;
&lt;li&gt;PHP web application development&lt;/li&gt;
&lt;li&gt;SaaS platforms&lt;/li&gt;
&lt;li&gt;Admin panels&lt;/li&gt;
&lt;li&gt;Custom business software&lt;/li&gt;
&lt;li&gt;API development&lt;/li&gt;
&lt;li&gt;Bug fixing and maintenance&lt;/li&gt;
&lt;li&gt;White label agency support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For agencies, this means extra technical capacity without the need to hire a full internal team for every project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this model works for agencies
&lt;/h2&gt;

&lt;p&gt;Offshore development works best when the agency already has client demand but needs more delivery power.&lt;/p&gt;

&lt;p&gt;Instead of rejecting projects or delaying deadlines, agencies can use a trusted development partner to handle technical execution.&lt;/p&gt;

&lt;p&gt;This helps agencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deliver projects faster&lt;/li&gt;
&lt;li&gt;Reduce workload pressure&lt;/li&gt;
&lt;li&gt;Accept more client work&lt;/li&gt;
&lt;li&gt;Keep costs more controlled&lt;/li&gt;
&lt;li&gt;Maintain long term client relationships&lt;/li&gt;
&lt;li&gt;Scale development capacity when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Laravel agencies need more than just developers. They need reliable technical partners who can understand deadlines, write clean code, and support long term project growth.&lt;/p&gt;

&lt;p&gt;If your agency needs PHP or Laravel development support, RuhaniSoft can help with offshore development services for scalable web applications.&lt;/p&gt;

&lt;p&gt;Learn more here: &lt;a href="https://ruhanisoft.com/" rel="noopener noreferrer"&gt;https://ruhanisoft.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>webdev</category>
      <category>sass</category>
    </item>
    <item>
      <title>How We Build Production Ready Laravel Applications That Scale</title>
      <dc:creator>Ruhani Soft</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:24:40 +0000</pubDate>
      <link>https://dev.to/ruhanisoft/how-we-build-production-ready-laravel-applications-that-scale-37mp</link>
      <guid>https://dev.to/ruhanisoft/how-we-build-production-ready-laravel-applications-that-scale-37mp</guid>
      <description>&lt;p&gt;When building a Laravel application, writing code is only the beginning.&lt;/p&gt;

&lt;p&gt;A production ready application needs to be secure, maintainable, performant, and easy to extend as the business grows. These are the principles we follow when building software at Ruhanisoft.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start With a Clean Architecture
&lt;/h2&gt;

&lt;p&gt;A well-structured codebase is easier to maintain than one filled with shortcuts.&lt;/p&gt;

&lt;p&gt;We organize business logic into dedicated services, keep controllers lightweight, and build reusable components wherever possible.&lt;/p&gt;

&lt;p&gt;A clean architecture makes new features faster to develop and reduces technical debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Optimize Performance From Day One
&lt;/h2&gt;

&lt;p&gt;Performance should not be an afterthought.&lt;/p&gt;

&lt;p&gt;We focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Efficient database queries&lt;/li&gt;
&lt;li&gt;Proper indexing&lt;/li&gt;
&lt;li&gt;Queue jobs for heavy tasks&lt;/li&gt;
&lt;li&gt;Caching where it provides real value&lt;/li&gt;
&lt;li&gt;Asset optimization&lt;/li&gt;
&lt;li&gt;Responsive user interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast applications create a better user experience and reduce infrastructure costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Build With Security in Mind
&lt;/h2&gt;

&lt;p&gt;Every application should include strong security practices from the start.&lt;/p&gt;

&lt;p&gt;This includes authentication, authorization, input validation, CSRF protection, secure file uploads, and regular dependency updates.&lt;/p&gt;

&lt;p&gt;Security is a feature, not an optional add-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make Software Easy to Customize
&lt;/h2&gt;

&lt;p&gt;Businesses rarely use software exactly as it ships.&lt;/p&gt;

&lt;p&gt;Flexible settings, modular components, reusable UI elements, and clear documentation make customization significantly easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Think Beyond Version 1
&lt;/h2&gt;

&lt;p&gt;The first release is only the beginning.&lt;/p&gt;

&lt;p&gt;A scalable application should support future integrations, new modules, payment gateways, localization, and business growth without requiring a complete rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach
&lt;/h2&gt;

&lt;p&gt;At Ruhanisoft, we build Laravel applications with long-term maintainability in mind.&lt;/p&gt;

&lt;p&gt;Our goal is simple:&lt;/p&gt;

&lt;p&gt;Build software that developers enjoy working on and businesses can confidently grow with.&lt;/p&gt;

&lt;p&gt;If you are interested in our products or want to learn more about our development approach, visit &lt;strong&gt;&lt;a href="https://ruhanisoft.com" rel="noopener noreferrer"&gt;https://ruhanisoft.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What practices do you consider essential for a production ready Laravel application?&lt;/p&gt;

</description>
      <category>php</category>
      <category>ruhanisoft</category>
      <category>laravel</category>
      <category>offshoredev</category>
    </item>
  </channel>
</rss>
