fix back card

This commit is contained in:
Nyavokevin 2025-09-15 13:00:04 +03:00
parent bd7ed0c30e
commit 153e700b8a
10 changed files with 56303 additions and 77 deletions

View File

@ -0,0 +1,248 @@
<?php
namespace App\Http\Controllers;
use App\Models\Payment;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
class WisePaymentController extends Controller
{
private $wiseClient;
private $wiseApiKey;
private $profileId;
public function __construct()
{
$this->wiseApiKey = env('WISE_API_KEY');
$this->profileId = env('WISE_PROFILE_ID'); // Your Wise business/profile ID
$this->wiseClient = new Client([
'base_uri' => 'https://api.wise.com/v1/',
'headers' => [
'Authorization' => 'Bearer ' . $this->wiseApiKey,
'Content-Type' => 'application/json',
],
]);
}
public function createTransfer(Request $request)
{
// Validate the incoming request data
$validatedData = $request->validate([
'amount' => 'required|numeric|min:0.01',
'source_currency' => 'required|string|size:3',
'target_currency' => 'required|string|size:3',
'recipient_name' => 'required|string|max:255',
'recipient_email' => 'required|email',
'recipient_account_number' => 'required|string',
'recipient_bank_code' => 'sometimes|string', // Needed for some countries
'recipient_address' => 'sometimes|array',
'reason' => 'sometimes|string|max:255',
]);
try {
// Create a new Payment record
$payment = new Payment([
'amount' => $validatedData['amount'],
'currency' => $validatedData['source_currency'],
'target_currency' => $validatedData['target_currency'],
'recipient_name' => $validatedData['recipient_name'],
'recipient_email' => $validatedData['recipient_email'],
'status' => 'pending',
'client_session_id' => $request->client_session_id ?? uniqid('wise_', true)
]);
$payment->save();
// Create recipient account
$recipient = $this->createRecipient($validatedData);
// Create quote
$quote = $this->createQuote($validatedData);
// Create transfer
$transfer = $this->createTransferWise($quote, $recipient, $validatedData);
// Fund the transfer
$this->fundTransfer($transfer['id']);
// Update the Payment record
$payment->wise_transfer_id = $transfer['id'];
$payment->wise_recipient_id = $recipient['id'];
$payment->wise_quote_id = $quote['id'];
$payment->status = 'processing';
$payment->save();
return response()->json([
'success' => true,
'message' => 'Transfer initiated successfully.',
'transfer_id' => $transfer['id'],
'payment_id' => $payment->id
]);
} catch (ClientException $e) {
$response = $e->getResponse();
$body = json_decode($response->getBody()->getContents(), true);
Log::error('Wise Client Error:', [
'code' => $e->getCode(),
'message' => $e->getMessage(),
'details' => $body
]);
if (isset($payment)) {
$payment->status = 'failed';
$payment->error_message = $e->getMessage();
$payment->save();
}
return response()->json([
'success' => false,
'message' => 'Wise API failed to process the request.',
'error' => $body['errors'] ?? $e->getMessage()
], 400);
} catch (\Exception $e) {
Log::error('General Wise API Error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
if (isset($payment)) {
$payment->status = 'failed';
$payment->error_message = $e->getMessage();
$payment->save();
}
return response()->json([
'success' => false,
'message' => 'An unexpected error occurred.',
'error' => $e->getMessage()
], 500);
}
}
public function handleWebhook(Request $request)
{
// Verify the webhook signature if needed
$payload = $request->all();
Log::info('Wise Webhook Received:', $payload);
if (isset($payload['data']['resource']['id'])) {
$transferId = $payload['data']['resource']['id'];
// Find the payment by transfer ID
$payment = Payment::where('wise_transfer_id', $transferId)->first();
if ($payment) {
$payment->status = $payload['data']['current_state'] ?? 'unknown';
$payment->save();
// You might want to trigger other actions based on status change
}
}
return response()->json(['status' => 'ok']);
}
// Helper methods to interact with Wise API
private function createRecipient($data)
{
$response = $this->wiseClient->post('accounts', [
'json' => [
'profile' => $this->profileId,
'accountHolderName' => $data['recipient_name'],
'currency' => $data['target_currency'],
'type' => 'email', // or 'sort_code', 'aba', 'iban' etc. based on country
'details' => [
'email' => $data['recipient_email'],
// Add more details based on account type and country
'legalType' => 'PRIVATE',
// 'accountNumber' => $data['recipient_account_number'],
// 'bankCode' => $data['recipient_bank_code'] ?? null,
]
],
]);
return json_decode($response->getBody(), true);
}
private function createQuote($data)
{
$response = $this->wiseClient->post('quotes', [
'json' => [
'profile' => $this->profileId,
'source' => $data['source_currency'],
'target' => $data['target_currency'],
'rateType' => 'FIXED', // or 'FLOAT'
'sourceAmount' => $data['amount'],
'type' => 'BALANCE_PAYOUT' // or 'BALANCE_CONVERSION'
],
]);
return json_decode($response->getBody(), true);
}
private function createTransferWise($quote, $recipient, $data)
{
$response = $this->wiseClient->post('transfers', [
'json' => [
'targetAccount' => $recipient['id'],
'quote' => $quote['id'],
'customerTransactionId' => uniqid('cti_', true),
'details' => [
'reference' => $data['reason'] ?? 'Payment for services',
'transferPurpose' => $data['reason'] ?? 'Payment for services',
]
],
]);
return json_decode($response->getBody(), true);
}
private function fundTransfer($transferId)
{
// Check if the transfer requires funding
$transfer = $this->wiseClient->get("transfers/{$transferId}");
$transferData = json_decode($transfer->getBody(), true);
if ($transferData['status'] === 'pending') {
// Fund the transfer from your balance
$this->wiseClient->post("transfers/{$transferId}/payments", [
'json' => [
'type' => 'BALANCE',
'profile' => $this->profileId
]
]);
}
}
// Additional helper method to check transfer status
public function checkTransferStatus($paymentId)
{
$payment = Payment::findOrFail($paymentId);
try {
$response = $this->wiseClient->get("transfers/{$payment->wise_transfer_id}");
$transferData = json_decode($response->getBody(), true);
$payment->status = $transferData['status'];
$payment->save();
return response()->json([
'success' => true,
'status' => $transferData['status']
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to check transfer status'
], 500);
}
}
}

View File

@ -22,7 +22,7 @@ class Payment extends Model
'draw_count',
'status',
'cards',
'appointment_date'
'appointment_date',
];
/**

View File

@ -10,6 +10,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.10",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.0",

2
composer.lock generated
View File

@ -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": "7a72790164b9b6dc081f7cbfde7e67d5",
"content-hash": "748c4d7177ae376a830b7c336264affd",
"packages": [
{
"name": "brick/math",

55809
public/back-card.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -61,14 +61,7 @@
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,

View File

@ -75,28 +75,8 @@ defineExpose({ setDrawnCards });
>
<div v-for="i in 8" :key="i" class="card" :style="{ transform: `rotate(${-3 + i}deg) translateZ(-${10 * i}px);` }">
<div class="card-back">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
<div class="card-inner-content">
<img src="cards/1.png" alt="Card Back" class="card-back-image" />
</div>
</div>
</div>
@ -108,43 +88,14 @@ defineExpose({ setDrawnCards });
<div v-for="(card, index) in drawnCards" :key="index" class="card-result-wrapper">
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)">
<div class="card-face card-unknown-front">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
<div class="card-inner-content">
<img src="cards/1.png" alt="Card Back" class="card-back-image" />
</div>
</div>
<div class="card-face card-known-back">
<img :src="card.image_url!" :alt="card.name" class="card-image" />
<div class="card-description-overlay">
<h3>{{ card.name }}</h3>
<p class="description">{{ card.description }}</p>
<p v-if="card.orientation" class="orientation">
{{ card.orientation === 'reversed' ? 'Inversée' : 'Droite' }}
</p>
<div v-if="card.symbolism" class="symbolism">
<p><strong>Numéro:</strong> {{ card.symbolism.numéro }}</p>
<p><strong>Planète:</strong> {{ card.symbolism.planète }}</p>
<p><strong>Élément:</strong> {{ card.symbolism.élément }}</p>
</div>
</div>
</div>
</div>
@ -158,6 +109,7 @@ defineExpose({ setDrawnCards });
<span class="truncate">Retourner à la sélection des cartes</span>
</button>
<button
v-if="clientSessionId"
@click="goToResult"
class="mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--midnight-blue)] px-8 text-base font-bold tracking-wide text-[var(--pure-white)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:shadow-[var(--spiritual-earth)]/30 hover:shadow-lg disabled:cursor-not-allowed disabled:bg-gray-400 disabled:hover:shadow-none"
>
@ -273,11 +225,28 @@ defineExpose({ setDrawnCards });
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
display: flex;
display: flex; /* Keep flexbox for centering if needed */
align-items: center; /* For centering .card-inner-content if it's smaller */
justify-content: center; /* For centering .card-inner-content if it's smaller */
background-color: var(--subtle-gold);
}
.card-inner-content {
width: calc(100% - 10px); /* Adjust 10px to control border thickness (e.g., 5px border on each side) */
height: calc(100% - 10px); /* Adjust this value as well */
background-color: white; /* Or whatever color you want inside the gold border */
border-radius: 14px; /* Slightly smaller to match outer radius with padding */
display: flex; /* Use flex to center the image if it's an <img> tag */
align-items: center;
justify-content: center;
border: 1px solid var(--subtle-gold);
background: radial-gradient(circle, var(--midnight-blue) 0%, #121a2c 100%);
overflow: hidden; /* Ensure image doesn't bleed out */
}
.card-back-image {
width: 100%;
height: 100%;
object-fit: cover; /* Or 'cover' depending on how you want the image to fill the inner content */
border-radius: 14px; /* Match inner content border-radius */
}
.card-result-wrapper {
@ -324,16 +293,22 @@ defineExpose({ setDrawnCards });
}
.card-unknown-front {
background: radial-gradient(circle, var(--midnight-blue) 0%, #121a2c 100%);
display: flex;
background-color: var(--subtle-gold);
display: flex; /* Keep flexbox for centering its *own* content */
align-items: center;
justify-content: center;
border: 1px solid var(--subtle-gold);
}
.card-known-back {
background-color: var(--subtle-gold);
display: flex;
justify-content: center;
align-items: center;
transform: rotateY(180deg); /* This face starts rotated, so it's hidden */
position: relative;
border-radius: 16px; /* Ensure this matches the outer card radius */
overflow: hidden;
}
.card-back-design::before {
@ -410,7 +385,11 @@ defineExpose({ setDrawnCards });
}
.card-back-info {
background: linear-gradient(145deg, var(--midnight-blue), #121a2c);
/* Replace the existing background property */
background-image: url('back-card.svg'); /* background-size: cover; /* This makes the image cover the entire element */
background-position: center; /* This centers the image in the element */
background-repeat: no-repeat; /* This prevents the image from repeating */
color: var(--pure-white);
transform: rotateY(180deg);
text-align: center;
@ -444,22 +423,39 @@ defineExpose({ setDrawnCards });
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6); /* Semi-transparent overlay for readability */
color: white;
/* Adjust background for a brighter, more subtle overlay */
background: rgba(255, 255, 255, 0.2); /* Lighter, more transparent white overlay */
/* You could also use a subtle gradient if you prefer: */
/* background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, rgba(255,255,255,0) 50%); */
color: white; /* Keep text white for contrast */
padding: 1rem;
/* Center the content (just the name) in the middle of the card */
display: flex;
flex-direction: column;
justify-content: flex-end; /* Align content to the bottom */
align-items: center;
justify-content: center; /* Center vertically */
align-items: center; /* Center horizontally */
text-align: center;
box-sizing: border-box;
pointer-events: none; /* Make overlay non-interactive so clicks go through to flip */
}
.card-description-overlay h3,
.card-description-overlay p {
margin: 0.2rem 0;
.card-description-overlay h3 {
margin: 0; /* Remove default margin */
font-size: 1.8rem; /* Make the name larger */
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); /* Add subtle shadow for readability */
color: var(--pure-white); /* Ensure it's clearly white */
letter-spacing: 1px; /* Add some letter spacing */
}
/* Hide these elements completely */
.card-description-overlay p.description,
.card-description-overlay p.orientation,
.card-description-overlay div.symbolism {
display: none;
}
.orientation {
font-style: italic;
margin-top: 0.5rem;

View File

@ -0,0 +1,170 @@
<template>
<div class="wise-payment-form">
<form @submit.prevent="submitTransfer">
<div class="form-group">
<label for="amount">Amount</label>
<input type="number" id="amount" v-model="formData.amount" step="0.01" min="0.01" required />
</div>
<div class="form-group">
<label for="source_currency">Source Currency</label>
<select id="source_currency" v-model="formData.source_currency" required>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<!-- Add more currencies as needed -->
</select>
</div>
<div class="form-group">
<label for="target_currency">Target Currency</label>
<select id="target_currency" v-model="formData.target_currency" required>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<!-- Add more currencies as needed -->
</select>
</div>
<div class="form-group">
<label for="recipient_name">Recipient Name</label>
<input type="text" id="recipient_name" v-model="formData.recipient_name" required />
</div>
<div class="form-group">
<label for="recipient_email">Recipient Email</label>
<input type="email" id="recipient_email" v-model="formData.recipient_email" required />
</div>
<div class="form-group">
<label for="recipient_account_number">Account Number</label>
<input type="text" id="recipient_account_number" v-model="formData.recipient_account_number" required />
</div>
<div class="form-group">
<label for="reason">Payment Reason</label>
<input type="text" id="reason" v-model="formData.reason" />
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Processing...' : 'Create Transfer' }}
</button>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="success" class="success-message">Transfer initiated successfully! Transfer ID: {{ transferId }}</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
error: null,
success: false,
transferId: null,
formData: {
amount: '',
source_currency: 'USD',
target_currency: 'EUR',
recipient_name: '',
recipient_email: '',
recipient_account_number: '',
reason: 'Payment for services',
},
};
},
methods: {
async submitTransfer() {
this.loading = true;
this.error = null;
this.success = false;
try {
const response = await axios.post('/api/wise/transfer', this.formData);
if (response.data.success) {
this.success = true;
this.transferId = response.data.transfer_id;
// You might want to redirect or show additional info
} else {
this.error = response.data.message || 'An error occurred';
}
} catch (error) {
if (error.response && error.response.data) {
this.error = error.response.data.message || 'An error occurred';
// Display detailed errors if available
if (error.response.data.error) {
if (Array.isArray(error.response.data.error)) {
this.error += ': ' + error.response.data.error.map((e) => e.message).join(', ');
} else if (typeof error.response.data.error === 'object') {
this.error += ': ' + JSON.stringify(error.response.data.error);
} else {
this.error += ': ' + error.response.data.error;
}
}
} else {
this.error = 'Network error or server unavailable';
}
} finally {
this.loading = false;
}
},
},
};
</script>
<style scoped>
.wise-payment-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input,
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
background-color: #4caf50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.error-message {
color: #d9534f;
margin-top: 15px;
}
.success-message {
color: #5cb85c;
margin-top: 15px;
}
</style>

View File

@ -10,3 +10,7 @@ Route::get('/user', function (Request $request) {
Route::get('/validate-payment', [App\Http\Controllers\StripeController::class, 'validatePayment']);
Route::get('/get-cards', [App\Http\Controllers\StripeController::class, 'getCards']);
Route::post('/wise/transfer', [App\Http\Controllers\WisePaymentController::class, 'createTransfer']);
Route::post('/wise/webhook', [App\Http\Controllers\WisePaymentController::class, 'handleWebhook']);

View File

@ -27,6 +27,11 @@ Route::get('/rendez-vous', [App\Http\Controllers\AppointmentController::class, '
Route::get('/resultat', [App\Http\Controllers\CardController::class, 'cartResult']);
Route::get('paiement', function () {
return Inertia::render('Checkout');
})->name('paiement');
Route::get('/success', function (Request $request) {
$clientSessionId = $request->query('client_session_id');