Skip to content

Commit 0b5224e

Browse files
committed
implemented payment with stripe
1 parent 475671e commit 0b5224e

11 files changed

Lines changed: 343 additions & 28 deletions

File tree

.github/workflows/deploy.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,14 @@ jobs:
5454

5555
DB_PASSWORD_PROD: ${{ secrets.PROD_DB_PASSWORD }}
5656
REDIS_PASSWORD_PROD: ${{ secrets.PROD_REDIS_PASSWORD }}
57+
JWT_SECRET_PROD: ${{ secrets.PROD_JWT_SECRET }}
58+
ORDER_SERVICE_KEY_PROD: ${{ secrets.PROD_ORDER_SERVICE_KEY }}
59+
STRIPE_SECRET_KEY_PROD: ${{ secrets.PROD_STRIPE_SECRET_KEY }}
60+
STRIPE_WEBHOOK_SECRET_PROD: ${{ secrets.PROD_STRIPE_WEBHOOK_SECRET }}
61+
5762
DB_PASSWORD_DEV: ${{ secrets.DEV_DB_PASSWORD }}
58-
REDIS_PASSWORD_DEV: ${{ secrets.DEV_REDIS_PASSWORD }}
63+
REDIS_PASSWORD_DEV: ${{ secrets.DEV_REDIS_PASSWORD }}
64+
JWT_SECRET_DEV: ${{ secrets.DEV_JWT_SECRET }}
65+
ORDER_SERVICE_KEY_DEV: ${{ secrets.DEV_ORDER_SERVICE_KEY }}
66+
STRIPE_SECRET_KEY_DEV: ${{ secrets.DEV_STRIPE_SECRET_KEY }}
67+
STRIPE_WEBHOOK_SECRET_DEV: ${{ secrets.DEV_STRIPE_WEBHOOK_SECRET }}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Http;
7+
use Stripe\StripeClient;
8+
9+
class CheckoutController extends Controller
10+
{
11+
private function userId(Request $request): int
12+
{
13+
return (int) $request->attributes->get('user_id');
14+
}
15+
16+
public function create(Request $request)
17+
{
18+
$data = $request->validate([
19+
'order_id' => ['required', 'integer', 'min:1'],
20+
]);
21+
22+
$orderId = (int) $data['order_id'];
23+
24+
// 1) Fetch order from order-service as the user (so ownership is enforced)
25+
$ordersBase = rtrim((string) config('services.orders.base_url'), '/'); // http://web/api/orders
26+
$orderUrl = $ordersBase . '/items/' . $orderId;
27+
28+
$authHeader = (string) $request->header('Authorization'); // reuse same user token
29+
$orderResp = Http::acceptJson()
30+
->withHeaders(['Authorization' => $authHeader])
31+
->get($orderUrl);
32+
33+
if (!$orderResp->successful()) {
34+
return response()->json(['message' => 'Order not found'], 404);
35+
}
36+
37+
$order = $orderResp->json('data');
38+
if (!$order || !isset($order['id'], $order['total_price'], $order['status'])) {
39+
return response()->json(['message' => 'Invalid order response'], 500);
40+
}
41+
42+
if ((string) $order['status'] === 'paid') {
43+
return response()->json(['message' => 'Order already paid'], 422);
44+
}
45+
46+
$amount = (int) $order['total_price']; // cents
47+
if ($amount <= 0) {
48+
return response()->json(['message' => 'Invalid order amount'], 422);
49+
}
50+
51+
// 2) Create Stripe Checkout Session
52+
$stripe = new StripeClient((string) config('services.stripe.secret'));
53+
54+
$frontend = rtrim((string) config('services.frontend.base_url'), '/'); // http://app.localhost
55+
$currency = (string) config('services.stripe.currency');
56+
57+
$session = $stripe->checkout->sessions->create([
58+
'mode' => 'payment',
59+
'success_url' => $frontend . '/checkout/success?order_id=' . $orderId . '&session_id={CHECKOUT_SESSION_ID}',
60+
'cancel_url' => $frontend . '/checkout/cancel?order_id=' . $orderId,
61+
'line_items' => [[
62+
'quantity' => 1,
63+
'price_data' => [
64+
'currency' => $currency,
65+
'unit_amount' => $amount,
66+
'product_data' => [
67+
'name' => 'Order #' . $orderId,
68+
],
69+
],
70+
]],
71+
'metadata' => [
72+
'order_id' => (string) $orderId,
73+
'user_id' => (string) $this->userId($request),
74+
],
75+
]);
76+
77+
return response()->json([
78+
'url' => $session->url,
79+
'session_id' => $session->id,
80+
]);
81+
}
82+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Http;
7+
use Stripe\Webhook;
8+
9+
class StripeWebhookController extends Controller
10+
{
11+
public function handle(Request $request)
12+
{
13+
$payload = $request->getContent();
14+
$sigHeader = (string) $request->header('Stripe-Signature', '');
15+
$secret = (string) config('services.stripe.webhook_secret');
16+
17+
try {
18+
$event = Webhook::constructEvent($payload, $sigHeader, $secret);
19+
} catch (\Throwable $e) {
20+
return response()->json(['message' => 'Invalid signature'], 400);
21+
}
22+
23+
if ($event->type === 'checkout.session.completed') {
24+
$session = $event->data->object;
25+
26+
$orderId = $session->metadata->order_id ?? null;
27+
28+
if ($orderId) {
29+
$ordersBase = rtrim((string) config('services.orders.base_url'), '/'); // http://web/api/orders
30+
$serviceKey = (string) config('services.orders.service_key');
31+
32+
$markPaidUrl = $ordersBase . '/internal/items/' . $orderId . '/mark-paid';
33+
34+
Http::acceptJson()
35+
->withHeaders(['X-Service-Key' => $serviceKey])
36+
->post($markPaidUrl);
37+
}
38+
}
39+
40+
return response()->json(['received' => true]);
41+
}
42+
}

app/Http/Middleware/JwtAuth.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use App\Services\JwtService;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
9+
class JwtAuth
10+
{
11+
public function handle(Request $request, Closure $next)
12+
{
13+
$authHeader = (string) $request->header('Authorization', '');
14+
15+
if (!str_starts_with($authHeader, 'Bearer ')) {
16+
return response()->json(['message' => 'Missing Bearer token'], 401);
17+
}
18+
19+
$token = trim(substr($authHeader, 7));
20+
21+
try {
22+
$claims = app(JwtService::class)->decode($token);
23+
$userId = $claims['sub'] ?? null;
24+
25+
if (!$userId) {
26+
return response()->json(['message' => 'Invalid token'], 401);
27+
}
28+
29+
$request->attributes->set('user_id', (int) $userId);
30+
31+
return $next($request);
32+
} catch (\Throwable) {
33+
return response()->json(['message' => 'Invalid or expired token'], 401);
34+
}
35+
}
36+
}

app/Services/JwtService.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Firebase\JWT\JWT;
6+
use Firebase\JWT\Key;
7+
8+
class JwtService
9+
{
10+
public function decode(string $token): array
11+
{
12+
$secret = (string) config('jwt.secret');
13+
if ($secret === '') {
14+
throw new \RuntimeException('JWT_SECRET not configured.');
15+
}
16+
17+
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
18+
return (array) $decoded;
19+
}
20+
}

bootstrap/app.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
health: '/up',
1313
)
1414
->withMiddleware(function (Middleware $middleware): void {
15-
//
15+
$middleware->alias([
16+
'jwt' => \App\Http\Middleware\JwtAuth::class,
17+
]);
1618
})
1719
->withExceptions(function (Exceptions $exceptions): void {
1820
//

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
"license": "MIT",
88
"require": {
99
"php": "^8.2",
10+
"firebase/php-jwt": "^7.0",
1011
"laravel/framework": "^12.0",
11-
"laravel/tinker": "^2.10.1"
12+
"laravel/tinker": "^2.10.1",
13+
"stripe/stripe-php": "^19.2"
1214
},
1315
"require-dev": {
1416
"fakerphp/faker": "^1.23",

composer.lock

Lines changed: 123 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/jwt.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
return [
3+
'secret' => env('JWT_SECRET'),
4+
];

0 commit comments

Comments
 (0)