diff --git a/app/Http/Controllers/StripeController.php b/app/Http/Controllers/StripeController.php new file mode 100644 index 0000000..09abb24 --- /dev/null +++ b/app/Http/Controllers/StripeController.php @@ -0,0 +1,125 @@ + ['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')); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..184e281 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,8 +11,12 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->validateCsrfTokens(except: [ + 'stripe/webhook', + ]); }) + + ->withExceptions(function (Exceptions $exceptions): void { // })->create(); diff --git a/composer.json b/composer.json index 956c553..8aa6241 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "laravel/framework": "^12.49", "laravel/tinker": "^2.11.0", "livewire/flux": "^2.11.1", - "livewire/livewire": "^4.1" + "livewire/livewire": "^4.1", + "stripe/stripe-php": "^19.3" }, "require-dev": { "fakerphp/faker": "^1.24.1", diff --git a/composer.lock b/composer.lock index 502d044..631ee18 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa16a4acb2dc4a2a7bb2eb1dd04b1907", + "content-hash": "fae50eff8a59cae5ee7908635763dacf", "packages": [ { "name": "bacon/bacon-qr-code", @@ -3721,6 +3721,65 @@ }, "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", "version": "v8.0.0", diff --git a/config/services.php b/config/services.php index 6a90eb8..1c2ef58 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,9 @@ return [ ], ], + 'stripe' => [ + 'secret' => env('STRIPE_SK'), + 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), + ], + ]; diff --git a/config/stripe.php b/config/stripe.php new file mode 100644 index 0000000..d6ac570 --- /dev/null +++ b/config/stripe.php @@ -0,0 +1,6 @@ + env('STRIPE_SK'), + 'pk' => env('STRIPE_PK'), +]; diff --git a/resources/views/invoices/show.blade.php b/resources/views/invoices/show.blade.php index ccb6fbb..0ea62d9 100644 --- a/resources/views/invoices/show.blade.php +++ b/resources/views/invoices/show.blade.php @@ -137,12 +137,33 @@ @endif @if($invoice->balance_due != 0) -
-

Payment

-

Please make payment to:

-

eBandroom

-

540 W. Louse Ave.

-

Vinita, OK 74301

+
+

Payment Options

+ +
+
+

Pay Online

+

Pay securely with your credit or debit card.

+
+ @csrf + +
+
+ +
+

Pay by Mail

+

Make check payable to:

+

eBandroom

+

540 W. Louse Ave.

+

Vinita, OK 74301

+
+
@endif diff --git a/resources/views/stripe/index.blade.php b/resources/views/stripe/index.blade.php new file mode 100644 index 0000000..0d438a4 --- /dev/null +++ b/resources/views/stripe/index.blade.php @@ -0,0 +1,4 @@ +
+ @csrf + +
diff --git a/resources/views/stripe/success.blade.php b/resources/views/stripe/success.blade.php new file mode 100644 index 0000000..1223813 --- /dev/null +++ b/resources/views/stripe/success.blade.php @@ -0,0 +1,32 @@ + + + + + + Payment Received - Invoice {{ $invoice->invoice_number }} + + + +
+
+ + + +
+ +

Payment Received

+

Thank you for your payment.

+ +
+

Invoice

+

{{ $invoice->invoice_number }}

+

{{ $invoice->client->name }}

+
+ + + View Invoice + +
+ + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index f1c0c7a..4e34c04 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ group(function () { Route::view('invoices', 'invoices.index')->name('invoices'); Route::get('invoices/{invoice}/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('dashboard', 'dashboard') -// ->middleware(['auth', 'verified']) -// ->name('dashboard'); - -// Route::view('clients', 'clients.index') -// ->middleware(['auth', 'verified']) -// ->name('clients.index'); +Route::get('invoices/{invoice}', CustomerInvoiceController::class)->name('invoices.show'); +// 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';