Merge stripe integration branch

This commit is contained in:
Matt Young 2026-01-29 22:47:48 -06:00
commit 7394593e57
10 changed files with 273 additions and 17 deletions

View File

@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PaymentStatus;
use App\Enums\PaymentMethod;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\Http\Request;
use Stripe\Stripe;
use Stripe\Webhook;
class StripeController extends Controller
{
public function index()
{
return view('stripe.index');
}
public function checkout(Invoice $invoice)
{
Stripe::setApiKey(config('services.stripe.secret'));
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [
[
'price_data' => [
'currency' => 'usd',
'product_data' => [
'name' => "Invoice {$invoice->invoice_number}",
],
'unit_amount' => (int) ($invoice->balance_due * 100),
],
'quantity' => 1,
],
],
'mode' => 'payment',
'success_url' => route('stripe.success', $invoice),
'cancel_url' => route('invoices.show', $invoice),
'metadata' => [
'invoice_id' => $invoice->id,
],
]);
return redirect($session->url);
}
public function webhook(Request $request)
{
$payload = $request->getContent();
$signature = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$signature,
config('services.stripe.webhook_secret')
);
} catch (\Exception $e) {
return response('Invalid signature', 400);
}
if ($event->type === 'checkout.session.completed') {
$session = $event->data->object;
$invoice = Invoice::find($session->metadata->invoice_id);
if ($invoice) {
Stripe::setApiKey(config('services.stripe.secret'));
// Retrieve PaymentIntent with expanded charge and balance_transaction
$paymentIntent = \Stripe\PaymentIntent::retrieve([
'id' => $session->payment_intent,
'expand' => ['latest_charge.balance_transaction'],
]);
$feeAmount = $paymentIntent->latest_charge->balance_transaction->fee;
Payment::create([
'invoice_id' => $invoice->id,
'payment_date' => now(),
'status' => PaymentStatus::COMPLETED,
'payment_method' => PaymentMethod::CARD,
'reference' => $session->payment_intent,
'stripe_payment_intent_id' => $session->payment_intent,
'amount' => $session->amount_total / 100,
'fee_amount' => $feeAmount / 100,
]);
}
}
return response('OK', 200);
}
public function checkoutTutorial()
{
Stripe::setApiKey(config('stripe.sk'));
$session = \Stripe\Checkout\Session::create([
'line_items' => [
[
'price_data' => [
'currency' => 'usd',
'product_data' => [
'name' => 'send me money',
],
'unit_amount' => 3250, // in cents
],
'quantity' => 1,
],
],
'mode' => 'payment',
'success_url' => route('stripe.success'),
'cancel_url' => route('stripe.index'),
]);
return redirect()->away($session->url);
}
public function success(Invoice $invoice)
{
return view('stripe.success', compact('invoice'));
}
}

View File

@ -11,8 +11,12 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// $middleware->validateCsrfTokens(except: [
'stripe/webhook',
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
})->create(); })->create();

View File

@ -14,7 +14,8 @@
"laravel/framework": "^12.49", "laravel/framework": "^12.49",
"laravel/tinker": "^2.11.0", "laravel/tinker": "^2.11.0",
"livewire/flux": "^2.11.1", "livewire/flux": "^2.11.1",
"livewire/livewire": "^4.1" "livewire/livewire": "^4.1",
"stripe/stripe-php": "^19.3"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.24.1", "fakerphp/faker": "^1.24.1",

61
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "aa16a4acb2dc4a2a7bb2eb1dd04b1907", "content-hash": "fae50eff8a59cae5ee7908635763dacf",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -3721,6 +3721,65 @@
}, },
"time": "2025-12-14T04:43:48+00:00" "time": "2025-12-14T04:43:48+00:00"
}, },
{
"name": "stripe/stripe-php",
"version": "v19.3.0",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "462272ae7560ee29bb891763fd0967d5a77784e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/462272ae7560ee29bb891763fd0967d5a77784e5",
"reference": "462272ae7560ee29bb891763fd0967d5a77784e5",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=5.6.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.72.0",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^5.7 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Stripe\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stripe and contributors",
"homepage": "https://github.com/stripe/stripe-php/contributors"
}
],
"description": "Stripe PHP Library",
"homepage": "https://stripe.com/",
"keywords": [
"api",
"payment processing",
"stripe"
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v19.3.0"
},
"time": "2026-01-28T21:15:45+00:00"
},
{ {
"name": "symfony/clock", "name": "symfony/clock",
"version": "v8.0.0", "version": "v8.0.0",

View File

@ -35,4 +35,9 @@ return [
], ],
], ],
'stripe' => [
'secret' => env('STRIPE_SK'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
]; ];

6
config/stripe.php Normal file
View File

@ -0,0 +1,6 @@
<?php
return [
'sk' => env('STRIPE_SK'),
'pk' => env('STRIPE_PK'),
];

View File

@ -137,12 +137,33 @@
@endif @endif
@if($invoice->balance_due != 0) @if($invoice->balance_due != 0)
<div class="mb-12 p-4 bg-gray-50 rounded"> <div class="mb-12 p-6 bg-gray-50 rounded-lg">
<h2 class="text-sm font-semibold text-gray-500 uppercase mb-2">Payment</h2> <h2 class="text-sm font-semibold text-gray-500 uppercase mb-4">Payment Options</h2>
<p class="text-gray-700">Please make payment to:</p>
<p class="text-gray-800 font-medium mt-1">eBandroom</p> <div class="flex flex-col md:flex-row gap-6">
<p class="text-gray-600">540 W. Louse Ave.</p> <div class="flex-1">
<p class="text-gray-600">Vinita, OK 74301</p> <h3 class="font-medium text-gray-800 mb-2">Pay Online</h3>
<p class="text-gray-600 text-sm mb-4">Pay securely with your credit or debit card.</p>
<form action="{{ route('stripe.checkout', $invoice) }}" method="POST">
@csrf
<button type="submit"
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"></path>
</svg>
Pay {{ formatMoney($invoice->balance_due) }} Now
</button>
</form>
</div>
<div class="flex-1">
<h3 class="font-medium text-gray-800 mb-2">Pay by Mail</h3>
<p class="text-gray-600 text-sm mb-2">Make check payable to:</p>
<p class="text-gray-800 font-medium">eBandroom</p>
<p class="text-gray-600">540 W. Louse Ave.</p>
<p class="text-gray-600">Vinita, OK 74301</p>
</div>
</div>
</div> </div>
@endif @endif

View File

@ -0,0 +1,4 @@
<form action="/checkout" method="POST">
@csrf
<button type="submit">Checkout</button>
</form>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Received - Invoice {{ $invoice->invoice_number }}</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-8">
<div class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h1 class="text-2xl font-bold text-gray-800 mb-2">Payment Received</h1>
<p class="text-gray-600 mb-6">Thank you for your payment.</p>
<div class="bg-gray-50 rounded-lg p-4 mb-6">
<p class="text-sm text-gray-500">Invoice</p>
<p class="text-lg font-semibold text-gray-800">{{ $invoice->invoice_number }}</p>
<p class="text-sm text-gray-500 mt-2">{{ $invoice->client->name }}</p>
</div>
<a href="{{ route('invoices.show', $invoice) }}"
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
View Invoice
</a>
</div>
</body>
</html>

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\CustomerInvoiceController; use App\Http\Controllers\CustomerInvoiceController;
use App\Http\Controllers\StripeController;
use App\Models\Invoice; use App\Models\Invoice;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -16,16 +17,14 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::view('invoices', 'invoices.index')->name('invoices'); Route::view('invoices', 'invoices.index')->name('invoices');
Route::get('invoices/{invoice}/edit', Route::get('invoices/{invoice}/edit',
fn(Invoice $invoice) => view('invoices.edit', compact('invoice')))->name('invoices.edit'); fn(Invoice $invoice) => view('invoices.edit', compact('invoice')))->name('invoices.edit');
Route::get('invoices/{invoice}', CustomerInvoiceController::class)->name('invoices.show');
Route::view('payments', 'payments.index')->name('payments'); Route::view('payments', 'payments.index')->name('payments');
}); });
// Route::view('dashboard', 'dashboard') Route::get('invoices/{invoice}', CustomerInvoiceController::class)->name('invoices.show');
// ->middleware(['auth', 'verified'])
// ->name('dashboard');
// Route::view('clients', 'clients.index')
// ->middleware(['auth', 'verified'])
// ->name('clients.index');
// Stripe
Route::get('stripe', [StripeController::class, 'index'])->name('stripe.index');
Route::post('/stripe/checkout/{invoice}', [StripeController::class, 'checkout'])->name('stripe.checkout');
Route::get('/stripe/success/{invoice}', [StripeController::class, 'success'])->name('stripe.success');
Route::post('stripe/webhook', [StripeController::class, 'webhook'])->name('stripe.webhook');
require __DIR__.'/settings.php'; require __DIR__.'/settings.php';