diff --git a/WISE_INTEGRATION_UPDATES.md b/WISE_INTEGRATION_UPDATES.md new file mode 100644 index 0000000..cb48922 --- /dev/null +++ b/WISE_INTEGRATION_UPDATES.md @@ -0,0 +1,157 @@ +# Wise Payment Integration - Frontend Updates + +## Overview +Updated the KSA-ORACLE project's cardSelection component to support both Stripe and Wise payment methods, matching the implementation in the oracle project. + +## Changes Made + +### 1. **cardSelection.vue** (`resources/js/pages/cards/cardSelection.vue`) + +#### Added Wise Payment Support +- Updated `handleSelection()` function to accept a `provider` parameter (`'stripe' | 'wise'`) +- Added new `redirectToWisePayment()` function that: + - Creates a payment record via `/create-wise-payment` endpoint + - Stores the `clientSessionId` in sessionStorage + - Redirects to the Wise payment link + +#### Updated UI +- Changed single payment buttons to dual payment buttons for 6-card and 18-card options +- **6-card option (9.99 €)**: Now shows two buttons side-by-side: + - "Stripe" button (outlined style) + - "Wise" button (filled style) +- **18-card option (99€)**: Now shows two buttons side-by-side: + - "Stripe" button (outlined style) + - "Wise" button (filled style) + +### 2. **WiseController.php** (`app/Http/Controllers/WiseController.php`) + +Added complete Wise payment controller with: +- `createPaymentSession()` - Creates payment records and returns Wise payment links +- `handleWebhook()` - Processes Wise webhook notifications +- `validatePayment()` - Manual payment validation for redirect flow +- Webhook signature verification +- Support for different Wise event types + +Configuration: +- Uses environment variables for payment links per draw count +- Stores payments with `payment_provider = 'wise'` +- Tracks payments via `client_session_id` and `wise_session_id` + +### 3. **Routes** (`routes/web.php`) + +Added new Wise payment routes: +```php +Route::post('/create-wise-payment', [WiseController::class, 'createPaymentSession']); +Route::post('/wise/webhook', [WiseController::class, 'handleWebhook']); +Route::get('/wise/validate-payment', [WiseController::class, 'validatePayment']); +Route::get('/wise/verify', function () { + return Inertia::render('wise/VerifyPayment'); +})->name('wise.verify'); +``` + +Updated `/success` route to: +- Check for pending Wise payments +- Redirect to Pending page if payment is still processing +- Pass `paymentProvider` prop to success page + +### 4. **Frontend Views** + +Added new Vue components: +- **Pending.vue** (`resources/js/pages/payments/Pending.vue`) + - Shows "Payment is being processed" message + - Polls payment status for Wise payments + - Auto-redirects when payment succeeds + +- **VerifyPayment.vue** (`resources/js/pages/wise/VerifyPayment.vue`) + - Manual payment verification page + - Allows users to check payment status + +### 5. **TypeScript Actions** + +Added **WiseController.ts** (`resources/js/actions/App/Http/Controllers/WiseController.ts`) +- Type-safe route helpers for Wise endpoints +- Auto-generated from Laravel routes + +## Environment Configuration + +Add these variables to your `.env` file: + +```bash +# Wise Payment Links (Simplified Integration) +WISE_PAYMENT_LINK_6_CARDS=https://wise.com/pay/r/YOUR_6_CARDS_LINK +WISE_PAYMENT_LINK_18_CARDS=https://wise.com/pay/r/YOUR_18_CARDS_LINK + +# Wise Webhook Configuration +WISE_WEBHOOK_SECRET=your_webhook_secret_here +``` + +## User Experience Flow + +### Stripe Flow (unchanged) +1. User clicks "Stripe" button +2. Backend creates Stripe checkout session +3. User redirects to Stripe checkout page +4. After payment, redirects back to `/success` + +### Wise Flow (new) +1. User clicks "Wise" button +2. Backend creates payment record with `status='pending'` +3. Returns Wise payment link URL +4. Frontend stores `client_session_id` in sessionStorage +5. User redirects to Wise payment page +6. After payment on Wise, user returns to `/success` +7. If payment still pending, shows Pending page +8. Pending page polls backend until payment confirmed via webhook +9. When confirmed, redirects to success page + +## Webhook Setup + +To receive payment confirmations: + +1. Go to https://wise.com/settings/webhooks +2. Create a new webhook +3. Set URL to: `https://yourdomain.com/wise/webhook` +4. Select event types: + - `transfer_state_change` + - `balance_credit` +5. Copy the webhook secret to your `.env` file as `WISE_WEBHOOK_SECRET` + +## Testing + +To test the integration: + +1. Configure test payment links in `.env` +2. Start the development server +3. Navigate to the card selection page +4. Click "Wise" button for either 6 or 18 cards +5. Complete payment on Wise +6. Verify redirect back to success page + +## Payment Links Configuration + +Get your Wise payment links: + +1. Log into your Wise business account +2. Go to "Request payment" or "Payment Links" +3. Create payment links for: + - 9.99 EUR (6 cards) + - 15.90 EUR (18 cards) - *Update this value based on your pricing* +4. Copy the links and add to `.env` + +## Database Fields Used + +The following Payment model fields are used for Wise: +- `payment_provider` = 'wise' +- `wise_session_id` - Unique session identifier +- `client_session_id` - Client-side tracking ID +- `wise_payment_id` - Wise transaction ID (from webhook) +- `status` - Payment status (pending, succeeded, failed, etc.) +- `amount` - Payment amount +- `currency` - Payment currency (EUR) +- `draw_count` - Number of cards (6 or 18) + +--- + +**Integration Status**: ✅ Complete +**Last Updated**: October 14, 2025 +**Project**: KSA-ORACLE (/media/creator/6226b912-8ba7-45dc-88a2-4b10d3dd1655/kandra/successkey/KSA-ORACLE) diff --git a/app/Http/Controllers/WiseController.php b/app/Http/Controllers/WiseController.php new file mode 100644 index 0000000..f4162c2 --- /dev/null +++ b/app/Http/Controllers/WiseController.php @@ -0,0 +1,276 @@ +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); + } +} diff --git a/resources/js/components/landing/CallBookingSection.vue b/resources/js/components/landing/CallBookingSection.vue index 38aad86..9a1429b 100644 --- a/resources/js/components/landing/CallBookingSection.vue +++ b/resources/js/components/landing/CallBookingSection.vue @@ -23,11 +23,6 @@
- Consultation -

Amplify Your Oracle

For deeper guidance, book a personalized consultation with Kris. @@ -35,23 +30,23 @@

-

Secure payment.

+

Secure payment

diff --git a/resources/js/components/landing/ManuscritSection.vue b/resources/js/components/landing/ManuscritSection.vue index 3141617..5f4b632 100644 --- a/resources/js/components/landing/ManuscritSection.vue +++ b/resources/js/components/landing/ManuscritSection.vue @@ -9,13 +9,11 @@

- - Beyond a Guide, - + Beyond a Guide, - A - Strategic - Manuscript + A + Strategic + Manuscript

diff --git a/resources/js/layouts/app/LandingLayout.vue b/resources/js/layouts/app/LandingLayout.vue index 17575f7..9fe85a8 100644 --- a/resources/js/layouts/app/LandingLayout.vue +++ b/resources/js/layouts/app/LandingLayout.vue @@ -30,7 +30,7 @@ onMounted(() => {
diff --git a/resources/js/pages/Agenda.vue b/resources/js/pages/Agenda.vue index 6198259..d63d95d 100644 --- a/resources/js/pages/Agenda.vue +++ b/resources/js/pages/Agenda.vue @@ -1,8 +1,5 @@ diff --git a/resources/js/pages/wise/VerifyPayment.vue b/resources/js/pages/wise/VerifyPayment.vue new file mode 100644 index 0000000..0d21091 --- /dev/null +++ b/resources/js/pages/wise/VerifyPayment.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/routes/web.php b/routes/web.php index 8dcec57..0c756b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,6 +24,14 @@ Route::post('/checkout-rendez-vous', [App\Http\Controllers\StripeController::cla Route::post('/stripe/webhook', [App\Http\Controllers\WebhookController::class, 'handleWebhook']); +// Wise payment routes +Route::post('/create-wise-payment', [App\Http\Controllers\WiseController::class, 'createPaymentSession']); +Route::post('/wise/webhook', [App\Http\Controllers\WiseController::class, 'handleWebhook']); +Route::get('/wise/validate-payment', [App\Http\Controllers\WiseController::class, 'validatePayment']); +Route::get('/wise/verify', function () { + return Inertia::render('wise/VerifyPayment'); +})->name('wise.verify'); + Route::get('/rendez-vous', [App\Http\Controllers\AppointmentController::class, 'index']); Route::get('/resultat', [App\Http\Controllers\CardController::class, 'cartResult']); @@ -46,7 +54,19 @@ Route::get('/success', function (Request $request) { if ($payment) { return Inertia::render('payments/Success', [ 'paymentSuccess' => true, - 'drawCount' => $payment->draw_count + 'drawCount' => $payment->draw_count, + 'paymentProvider' => $payment->payment_provider ?? 'stripe' + ]); + } + + // If payment not found as succeeded, check if it's pending (especially for Wise) + $pendingPayment = Payment::where('client_session_id', $clientSessionId)->first(); + + if ($pendingPayment && $pendingPayment->status === 'pending') { + return Inertia::render('payments/Pending', [ + 'message' => 'Payment is being processed. Please wait...', + 'clientSessionId' => $clientSessionId, + 'paymentProvider' => $pendingPayment->payment_provider ?? 'stripe' ]); }