1+ <?php
2+
3+ namespace App \Http \Controllers \Api ;
4+
5+ use App \Http \Controllers \Controller ;
6+ use App \Models \Cart ;
7+ use App \Services \StripeService ;
8+ use Illuminate \Http \Request ;
9+ use Illuminate \Http \JsonResponse ;
10+ use Illuminate \Support \Facades \DB ;
11+ use Illuminate \Support \Facades \Log ;
12+
13+ class CheckoutController extends Controller
14+ {
15+ private StripeService $ stripeService ;
16+
17+ public function __construct (StripeService $ stripeService )
18+ {
19+ $ this ->stripeService = $ stripeService ;
20+ }
21+
22+ /**
23+ * Initiate checkout process
24+ */
25+ public function initiate (Request $ request ): JsonResponse
26+ {
27+ try {
28+ $ validated = $ request ->validate ([
29+ 'cart_id ' => 'nullable|exists:carts,id ' ,
30+ 'success_url ' => 'nullable|url ' ,
31+ 'cancel_url ' => 'nullable|url ' ,
32+ ]);
33+
34+ // Get or create cart for current user/session
35+ $ cart = $ this ->getCartForCheckout ($ request , $ validated ['cart_id ' ] ?? null );
36+
37+ if (!$ cart || $ cart ->items ->isEmpty ()) {
38+ return response ()->json ([
39+ 'success ' => false ,
40+ 'message ' => 'Cart is empty or not found '
41+ ], 404 );
42+ }
43+
44+ // Validate cart items are still available
45+ foreach ($ cart ->items as $ item ) {
46+ if (!$ item ->isValid ()) {
47+ return response ()->json ([
48+ 'success ' => false ,
49+ 'message ' => 'Some items in cart are no longer available ' ,
50+ 'invalid_items ' => [$ item ->id ]
51+ ], 422 );
52+ }
53+
54+ if (!$ item ->productVariant ->hasStock ($ item ->quantity )) {
55+ return response ()->json ([
56+ 'success ' => false ,
57+ 'message ' => 'Insufficient stock for item: ' . $ item ->productVariant ->product ->name ,
58+ 'insufficient_stock_items ' => [
59+ 'id ' => $ item ->id ,
60+ 'product ' => $ item ->productVariant ->product ->name ,
61+ 'requested ' => $ item ->quantity ,
62+ 'available ' => $ item ->productVariant ->stock_quantity
63+ ]
64+ ], 422 );
65+ }
66+ }
67+
68+ // Create payment intent
69+ $ metadata = [
70+ 'cart_id ' => $ cart ->id ,
71+ 'user_id ' => $ cart ->user_id ,
72+ 'item_count ' => $ cart ->item_count ,
73+ ];
74+
75+ if (isset ($ validated ['success_url ' ])) {
76+ $ metadata ['success_url ' ] = $ validated ['success_url ' ];
77+ }
78+
79+ if (isset ($ validated ['cancel_url ' ])) {
80+ $ metadata ['cancel_url ' ] = $ validated ['cancel_url ' ];
81+ }
82+
83+ $ paymentIntent = $ this ->stripeService ->createPaymentIntent ($ cart , $ metadata );
84+
85+ // Update cart with payment intent ID
86+ $ cart ->update (['payment_intent_id ' => $ paymentIntent ->id ]);
87+
88+ return response ()->json ([
89+ 'success ' => true ,
90+ 'data ' => [
91+ 'payment_intent_id ' => $ paymentIntent ->id ,
92+ 'client_secret ' => $ paymentIntent ->client_secret ,
93+ 'amount ' => $ paymentIntent ->amount ,
94+ 'currency ' => $ paymentIntent ->currency ,
95+ 'cart_id ' => $ cart ->id ,
96+ 'item_count ' => $ cart ->item_count ,
97+ 'subtotal ' => $ cart ->subtotal ,
98+ ],
99+ 'message ' => 'Checkout initiated successfully '
100+ ]);
101+
102+ } catch (\Illuminate \Validation \ValidationException $ e ) {
103+ return response ()->json ([
104+ 'success ' => false ,
105+ 'message ' => 'Validation failed ' ,
106+ 'errors ' => $ e ->errors ()
107+ ], 422 );
108+
109+ } catch (\Stripe \Exception \ApiErrorException $ e ) {
110+ Log::error ('Stripe checkout initiation failed ' , [
111+ 'error ' => $ e ->getMessage (),
112+ 'cart_id ' => $ validated ['cart_id ' ] ?? null
113+ ]);
114+
115+ return response ()->json ([
116+ 'success ' => false ,
117+ 'message ' => 'Payment service unavailable ' ,
118+ 'error ' => 'Payment processing failed '
119+ ], 503 );
120+
121+ } catch (\Exception $ e ) {
122+ Log::error ('Checkout initiation failed ' , [
123+ 'error ' => $ e ->getMessage (),
124+ 'cart_id ' => $ validated ['cart_id ' ] ?? null
125+ ]);
126+
127+ return response ()->json ([
128+ 'success ' => false ,
129+ 'message ' => 'Failed to initiate checkout ' ,
130+ 'error ' => $ e ->getMessage ()
131+ ], 500 );
132+ }
133+ }
134+
135+ /**
136+ * Process payment confirmation
137+ */
138+ public function process (Request $ request ): JsonResponse
139+ {
140+ try {
141+ $ validated = $ request ->validate ([
142+ 'payment_intent_id ' => 'required|string ' ,
143+ 'cart_id ' => 'nullable|exists:carts,id ' ,
144+ ]);
145+
146+ $ paymentIntentId = $ validated ['payment_intent_id ' ];
147+ $ cartId = $ validated ['cart_id ' ];
148+
149+ // Get cart if provided
150+ $ cart = null ;
151+ if ($ cartId ) {
152+ $ cart = Cart::find ($ cartId );
153+ }
154+
155+ // Confirm payment intent
156+ $ paymentIntent = $ this ->stripeService ->confirmPaymentIntent ($ paymentIntentId );
157+
158+ if ($ paymentIntent ->status !== 'succeeded ' ) {
159+ return response ()->json ([
160+ 'success ' => false ,
161+ 'message ' => 'Payment not completed ' ,
162+ 'payment_status ' => $ paymentIntent ->status
163+ ], 402 );
164+ }
165+
166+ // Process successful payment
167+ $ order = null ;
168+ if ($ cart ) {
169+ $ order = $ this ->processSuccessfulPayment ($ cart , $ paymentIntent );
170+ }
171+
172+ return response ()->json ([
173+ 'success ' => true ,
174+ 'data ' => [
175+ 'payment_intent_id ' => $ paymentIntent ->id ,
176+ 'payment_status ' => $ paymentIntent ->status ,
177+ 'order_id ' => $ order ?->id,
178+ 'order_number ' => $ order ?->order_number,
179+ ],
180+ 'message ' => 'Payment processed successfully '
181+ ]);
182+
183+ } catch (\Illuminate \Validation \ValidationException $ e ) {
184+ return response ()->json ([
185+ 'success ' => false ,
186+ 'message ' => 'Validation failed ' ,
187+ 'errors ' => $ e ->errors ()
188+ ], 422 );
189+
190+ } catch (\Stripe \Exception \CardException $ e ) {
191+ return response ()->json ([
192+ 'success ' => false ,
193+ 'message ' => 'Payment declined ' ,
194+ 'error ' => $ e ->getError ()->message
195+ ], 402 );
196+
197+ } catch (\Stripe \Exception \ApiErrorException $ e ) {
198+ Log::error ('Stripe payment processing failed ' , [
199+ 'payment_intent_id ' => $ validated ['payment_intent_id ' ] ?? null ,
200+ 'error ' => $ e ->getMessage ()
201+ ]);
202+
203+ return response ()->json ([
204+ 'success ' => false ,
205+ 'message ' => 'Payment processing failed ' ,
206+ 'error ' => 'Payment service error '
207+ ], 503 );
208+
209+ } catch (\Exception $ e ) {
210+ Log::error ('Payment processing failed ' , [
211+ 'error ' => $ e ->getMessage (),
212+ 'payment_intent_id ' => $ validated ['payment_intent_id ' ] ?? null
213+ ]);
214+
215+ return response ()->json ([
216+ 'success ' => false ,
217+ 'message ' => 'Failed to process payment ' ,
218+ 'error ' => $ e ->getMessage ()
219+ ], 500 );
220+ }
221+ }
222+
223+ /**
224+ * Handle Stripe webhooks
225+ */
226+ public function webhook (Request $ request ): JsonResponse
227+ {
228+ try {
229+ $ payload = $ request ->getContent ();
230+ $ signature = $ request ->header ('Stripe-Signature ' );
231+ $ webhookSecret = config ('services.stripe.webhook_secret ' );
232+
233+ if (!$ signature ) {
234+ return response ()->json ([
235+ 'success ' => false ,
236+ 'message ' => 'Missing Stripe signature '
237+ ], 400 );
238+ }
239+
240+ $ result = $ this ->stripeService ->handleWebhook ($ payload , $ signature , $ webhookSecret );
241+
242+ switch ($ result ['event ' ]->type ) {
243+ case 'payment_intent.succeeded ' :
244+ $ this ->stripeService ->processPaymentSuccess ($ result ['event ' ]->data );
245+ break ;
246+
247+ case 'payment_intent.payment_failed ' :
248+ $ this ->stripeService ->processPaymentFailure ($ result ['event ' ]->data );
249+ break ;
250+
251+ default :
252+ Log::info ('Unhandled webhook event ' , [
253+ 'event_type ' => $ result ['event ' ]->type
254+ ]);
255+ }
256+
257+ return response ()->json ([
258+ 'success ' => true ,
259+ 'message ' => 'Webhook processed successfully '
260+ ]);
261+
262+ } catch (\UnexpectedValueException $ e ) {
263+ return response ()->json ([
264+ 'success ' => false ,
265+ 'message ' => 'Invalid webhook payload '
266+ ], 400 );
267+
268+ } catch (\Stripe \Exception \SignatureVerificationException $ e ) {
269+ return response ()->json ([
270+ 'success ' => false ,
271+ 'message ' => 'Invalid webhook signature '
272+ ], 400 );
273+
274+ } catch (\Exception $ e ) {
275+ Log::error ('Webhook processing failed ' , [
276+ 'error ' => $ e ->getMessage ()
277+ ]);
278+
279+ return response ()->json ([
280+ 'success ' => false ,
281+ 'message ' => 'Webhook processing failed '
282+ ], 500 );
283+ }
284+ }
285+
286+ /**
287+ * Get payment intent status
288+ */
289+ public function paymentStatus (Request $ request , string $ paymentIntentId ): JsonResponse
290+ {
291+ try {
292+ $ paymentIntent = $ this ->stripeService ->getPaymentIntent ($ paymentIntentId );
293+
294+ return response ()->json ([
295+ 'success ' => true ,
296+ 'data ' => [
297+ 'payment_intent_id ' => $ paymentIntent ->id ,
298+ 'status ' => $ paymentIntent ->status ,
299+ 'amount ' => $ paymentIntent ->amount ,
300+ 'currency ' => $ paymentIntent ->currency ,
301+ 'last_payment_error ' => $ paymentIntent ->last_payment_error ,
302+ ],
303+ 'message ' => 'Payment status retrieved successfully '
304+ ]);
305+
306+ } catch (\Stripe \Exception \ApiErrorException $ e ) {
307+ return response ()->json ([
308+ 'success ' => false ,
309+ 'message ' => 'Failed to retrieve payment status ' ,
310+ 'error ' => 'Payment not found '
311+ ], 404 );
312+
313+ } catch (\Exception $ e ) {
314+ return response ()->json ([
315+ 'success ' => false ,
316+ 'message ' => 'Failed to retrieve payment status ' ,
317+ 'error ' => $ e ->getMessage ()
318+ ], 500 );
319+ }
320+ }
321+
322+ /**
323+ * Get or create cart for checkout
324+ */
325+ private function getCartForCheckout (Request $ request , ?int $ cartId = null ): ?Cart
326+ {
327+ if ($ cartId ) {
328+ $ cart = Cart::find ($ cartId );
329+ if ($ cart && $ cart ->is_active ) {
330+ return $ cart ;
331+ }
332+ }
333+
334+ // Get cart for current user/session
335+ $ user = auth ()->user ();
336+ if ($ user ) {
337+ return Cart::findOrCreateForUser ($ user );
338+ }
339+
340+ $ sessionId = $ request ->session ()->getId ();
341+ return Cart::findOrCreateForUser (null , $ sessionId );
342+ }
343+
344+ /**
345+ * Process successful payment and create order
346+ */
347+ private function processSuccessfulPayment (Cart $ cart , $ paymentIntent ): Order
348+ {
349+ return DB ::transaction (function () use ($ cart , $ paymentIntent ) {
350+ // Create order
351+ $ order = Order::create ([
352+ 'user_id ' => $ cart ->user_id ,
353+ 'order_number ' => Order::generateOrderNumber (),
354+ 'status ' => 'paid ' ,
355+ 'subtotal ' => $ cart ->subtotal ,
356+ 'tax_amount ' => 0 , // Placeholder
357+ 'shipping_amount ' => 0 , // Placeholder
358+ 'total_amount ' => $ cart ->subtotal ,
359+ 'currency ' => 'USD ' ,
360+ 'payment_status ' => 'paid ' ,
361+ 'shipping_status ' => 'pending ' ,
362+ ]);
363+
364+ // Create order items from cart items
365+ foreach ($ cart ->items as $ cartItem ) {
366+ $ order ->items ()->create ([
367+ 'product_variant_id ' => $ cartItem ->product_variant_id ,
368+ 'quantity ' => $ cartItem ->quantity ,
369+ 'price_at_time ' => $ cartItem ->price_at_time ,
370+ 'line_total ' => $ cartItem ->quantity * $ cartItem ->price_at_time ,
371+ ]);
372+
373+ // Decrement stock
374+ $ cartItem ->productVariant ->decrement ('stock_quantity ' , $ cartItem ->quantity );
375+ }
376+
377+ // Create payment record
378+ $ order ->payments ()->create ([
379+ 'payment_intent_id ' => $ paymentIntent ->id ,
380+ 'amount ' => $ paymentIntent ->amount / 100 , // Convert from cents
381+ 'currency ' => $ paymentIntent ->currency ,
382+ 'status ' => 'succeeded ' ,
383+ 'payment_method ' => $ paymentIntent ->payment_method_types [0 ] ?? null ,
384+ 'metadata ' => $ paymentIntent ->metadata ?? [],
385+ ]);
386+
387+ // Clear the cart
388+ $ cart ->items ()->delete ();
389+
390+ return $ order ;
391+ });
392+ }
393+ }
0 commit comments