input('count'); $clientSessionId = Str::uuid(); // Define pricing and payment links for different draw counts $paymentOptions = [ 6 => [ 'amount' => 9.99, 'currency' => 'EUR', 'description' => 'Profilage - 6 cartes', 'payment_link' => env('WISE_PAYMENT_LINK_6_CARDS', 'https://wise.com/pay/r/JVNRSE21VZTj8rw'), ], 18 => [ 'amount' => 15.90, 'currency' => 'EUR', 'description' => 'Quadrige Doré - 18 cartes', 'payment_link' => env('WISE_PAYMENT_LINK_18_CARDS','https://wise.com/pay/r/W2k1NqQySdc9HW8'), ], ]; if (! isset($paymentOptions[$count])) { return response()->json(['error' => 'Invalid product selected.'], 400); } $option = $paymentOptions[$count]; try { // Store payment in database with pending status Payment::create([ 'amount' => $option['amount'], 'currency' => $option['currency'], 'wise_session_id' => $clientSessionId, 'client_session_id' => $clientSessionId, 'draw_count' => $count, 'status' => 'pending', 'payment_provider' => 'wise', ]); Log::info('Wise payment created', [ 'client_session_id' => $clientSessionId, 'amount' => $option['amount'], 'draw_count' => $count, ]); // Return the payment link URL return response()->json([ 'success' => true, 'paymentUrl' => $option['payment_link'], 'clientSessionId' => $clientSessionId, ]); } catch (\Exception $e) { Log::error('Wise payment creation failed: '.$e->getMessage()); return response()->json(['error' => 'Could not create payment session.'], 500); } } /** * Handle Wise webhook notifications */ public function handleWebhook(Request $request) { $payload = $request->all(); $signature = $request->header('X-Signature-SHA256'); // Verify webhook signature if (! $this->verifyWebhookSignature($request->getContent(), $signature)) { Log::error('Wise webhook signature verification failed'); return response()->json(['error' => 'Invalid signature'], 400); } try { $eventType = $payload['event_type'] ?? $payload['type'] ?? null; Log::info('Wise webhook received', ['event_type' => $eventType, 'payload' => $payload]); // Handle different Wise event types switch ($eventType) { case 'transfer_state_change': case 'transfers#state-change': $this->handleTransferStateChange($payload); break; case 'balance_credit': $this->handleBalanceCredit($payload); break; default: Log::info('Unhandled Wise webhook event type: '.$eventType); break; } return response()->json(['status' => 'success'], 200); } catch (\Exception $e) { Log::error('Wise webhook processing error: '.$e->getMessage(), ['exception' => $e]); return response()->json(['error' => 'Server error'], 500); } } /** * Handle transfer state change events */ private function handleTransferStateChange(array $payload) { $transferId = $payload['data']['resource']['id'] ?? null; $currentState = $payload['data']['current_state'] ?? $payload['data']['resource']['status'] ?? null; if (! $transferId) { Log::warning('Transfer ID not found in Wise webhook payload'); return; } // Find payment by Wise transfer ID $payment = Payment::where('wise_payment_id', $transferId) ->orWhere('wise_session_id', $payload['data']['resource']['customerTransactionId'] ?? null) ->first(); if (! $payment) { Log::warning('No payment record found for Wise transfer ID: '.$transferId); return; } // Update payment status based on transfer state switch ($currentState) { case 'outgoing_payment_sent': case 'funds_converted': case 'incoming_payment_waiting': // Payment is being processed $payment->update(['status' => 'processing']); break; case 'funds_refunded': // Payment was refunded $payment->update(['status' => 'refunded']); break; case 'bounced_back': case 'charged_back': // Payment failed or was charged back $payment->update(['status' => 'failed']); break; default: Log::info('Unhandled Wise transfer state: '.$currentState); break; } } /** * Handle balance credit events (payment received) */ private function handleBalanceCredit(array $payload) { $amount = $payload['data']['amount'] ?? null; $currency = $payload['data']['currency'] ?? null; $transactionId = $payload['data']['transaction_id'] ?? null; // Find payment by amount and currency (less reliable, but works for balance credits) $payment = Payment::where('amount', $amount) ->where('currency', $currency) ->where('status', '!=', 'succeeded') ->where('payment_provider', 'wise') ->first(); if ($payment) { $payment->update([ 'status' => 'succeeded', 'wise_payment_id' => $transactionId, ]); Log::info('Wise payment succeeded', ['payment_id' => $payment->id, 'transaction_id' => $transactionId]); } } /** * Verify Wise webhook signature */ private function verifyWebhookSignature(string $payload, ?string $signature): bool { if (! $signature) { return false; } $webhookSecret = env('WISE_WEBHOOK_SECRET'); if (! $webhookSecret) { Log::warning('WISE_WEBHOOK_SECRET not configured'); return true; // Allow in development if secret not set } $expectedSignature = hash_hmac('sha256', $payload, $webhookSecret); return hash_equals($expectedSignature, $signature); } /** * Validate payment status manually (for redirect flow) */ public function validatePayment(Request $request) { $clientSessionId = $request->query('client_session_id'); $payment = Payment::where('client_session_id', $clientSessionId) ->where('payment_provider', 'wise') ->first(); if (! $payment) { return response()->json([ 'success' => false, 'message' => 'Payment not found.', ], 404); } // Check if payment is succeeded if ($payment->status === 'succeeded') { return response()->json([ 'success' => true, 'drawCount' => $payment->draw_count, ]); } // If payment is still pending, check with Wise API if ($payment->status === 'pending' && $payment->wise_payment_id) { try { $wiseApiUrl = env('WISE_API_URL', 'https://api.wise.com'); $wiseApiToken = env('WISE_API_TOKEN'); $response = Http::withToken($wiseApiToken) ->get("{$wiseApiUrl}/v1/transfers/{$payment->wise_payment_id}"); if ($response->successful()) { $transferStatus = $response->json('status'); if ($transferStatus === 'outgoing_payment_sent') { $payment->update(['status' => 'succeeded']); return response()->json([ 'success' => true, 'drawCount' => $payment->draw_count, ]); } } } catch (\Exception $e) { Log::error('Wise payment validation failed: '.$e->getMessage()); } } return response()->json([ 'success' => false, 'message' => 'Payment not validated.', 'status' => $payment->status, ], 402); } }