4 Commits

Author SHA1 Message Date
Valentin Lab
7a189abf0b fix: move `build directory to resources/shop` 2025-10-04 10:13:38 +02:00
Valentin Lab
34fc1c33bf fix: repair favicon links and provide one from https://www.jardinenvie.com 2025-10-04 09:41:15 +02:00
Valentin Lab
61e34b4f4e fix: finalize payments and clear cart after Paybox success
This captures the Paybox verification flow, duplicate-payment guard, and cart cleanup.
2025-10-04 09:17:53 +02:00
Valentin Lab
7fe2770d45 fix: do not call debugbar if not available (when in prod) 2025-09-29 11:32:19 +02:00
97 changed files with 188 additions and 40 deletions

View File

@@ -93,7 +93,8 @@ RUN mkdir -p /out \
--exclude=.editorconfig --exclude=phpunit.xml \ --exclude=.editorconfig --exclude=phpunit.xml \
--exclude=.travis.yml --exclude=composer.lock --exclude=.styleci.yml \ --exclude=.travis.yml --exclude=composer.lock --exclude=.styleci.yml \
--exclude=Makefile --exclude=.gitkeep --exclude=test \ --exclude=Makefile --exclude=.gitkeep --exclude=test \
artisan app build config database vendor public resources routes stubs bootstrap storage composer.json \ --exclude=resources/shop \
artisan app config database vendor public resources routes stubs bootstrap storage composer.json \
&& xz -T0 -9e /out/app.tar \ && xz -T0 -9e /out/app.tar \
&& mv /out/app.tar.xz /out/opensem-prod.tar.xz && mv /out/app.tar.xz /out/opensem-prod.tar.xz

View File

@@ -19,8 +19,8 @@ var jsSite = [
jsBootstrap, jsBootstrap,
'node_modules/jquery-serializejson/jquery.serializejson.min.js', 'node_modules/jquery-serializejson/jquery.serializejson.min.js',
'node_modules/currency.js/dist/currency.min.js', 'node_modules/currency.js/dist/currency.min.js',
'build/js/plugins/smooth_products/js/smoothproducts.min.js', 'resources/shop/js/plugins/smooth_products/js/smoothproducts.min.js',
'build/js/site.js', 'resources/shop/js/site.js',
] ]
var cssSite = [ var cssSite = [
@@ -28,8 +28,8 @@ var cssSite = [
'node_modules/@fortawesome/fontawesome-free/css/all.min.css', 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
'node_modules/animate.css/animate.min.css', 'node_modules/animate.css/animate.min.css',
'node_modules/icheck-bootstrap/icheck-bootstrap.min.css', 'node_modules/icheck-bootstrap/icheck-bootstrap.min.css',
'build/js/plugins/smooth_products/css/smoothproducts.css', 'resources/shop/js/plugins/smooth_products/css/smoothproducts.css',
'build/css/site.css', 'resources/shop/css/site.css',
] ]
var jsAdminLTE = [ var jsAdminLTE = [
@@ -41,15 +41,15 @@ var jsAdminLTE = [
] ]
var jsCoreInclude = [ var jsCoreInclude = [
'build/js/include/core/objectLength.js', 'resources/shop/js/include/core/objectLength.js',
'build/js/include/core/url.js', 'resources/shop/js/include/core/url.js',
'build/js/include/core/user.js', 'resources/shop/js/include/core/user.js',
'build/js/include/form/radio.js', 'resources/shop/js/include/form/radio.js',
'build/js/include/form/upload.js', 'resources/shop/js/include/form/upload.js',
'build/js/include/form/validator.js', 'resources/shop/js/include/form/validator.js',
'build/js/include/layout/animate.js', 'resources/shop/js/include/layout/animate.js',
'build/js/include/layout/scroll.js', 'resources/shop/js/include/layout/scroll.js',
'build/js/include/layout/tooltip.js', 'resources/shop/js/include/layout/tooltip.js',
] ]
var jsBundle = [ var jsBundle = [
@@ -84,7 +84,7 @@ var jsMain = [
var cssPrint = [ var cssPrint = [
// 'node_modules/bootstrap/dist/css/bootstrap.min.css', // 'node_modules/bootstrap/dist/css/bootstrap.min.css',
'cssIcons', 'cssIcons',
'build/print.css' 'resources/shop/print.css'
] ]
var cssBundle = [ var cssBundle = [
@@ -109,7 +109,7 @@ var cssIcons = [
var cssMain = [ var cssMain = [
cssBundle, cssBundle,
cssIcons, cssIcons,
'build/css/main.css', 'resources/shop/css/main.css',
] ]
var jsDataTables = [ var jsDataTables = [
@@ -251,31 +251,31 @@ module.exports = function(grunt) {
}, },
{ {
expand: true, expand: true,
cwd: 'build/fonts', cwd: 'resources/shop/fonts',
src: ['**'], src: ['**'],
dest: 'public/fonts/' dest: 'public/fonts/'
}, },
{ {
expand: true, expand: true,
cwd: 'build/img', cwd: 'resources/shop/img',
src: ['**'], src: ['**'],
dest: 'public/img/' dest: 'public/img/'
}, },
{ {
expand: true, expand: true,
cwd: 'build/lang', cwd: 'resources/shop/lang',
src: ['**'], src: ['**'],
dest: 'public/assets/lang/' dest: 'public/assets/lang/'
}, },
{ {
expand: true, expand: true,
cwd: 'build/plugins', cwd: 'resources/shop/plugins',
src: ['**'], src: ['**'],
dest: 'public/assets/plugins/' dest: 'public/assets/plugins/'
}, },
{ {
expand: true, expand: true,
cwd: 'build/assets/tpl', cwd: 'resources/shop/assets/tpl',
src: ['**'], src: ['**'],
dest: 'public/assets/tpl/' dest: 'public/assets/tpl/'
}, },
@@ -395,7 +395,7 @@ module.exports = function(grunt) {
}, },
{ {
expand: true, expand: true,
cwd: 'build/plugins/pdfjs/', cwd: 'resources/shop/plugins/pdfjs/',
src: ['**'], src: ['**'],
dest: 'public/assets/plugins/pdfjs', dest: 'public/assets/plugins/pdfjs',
}, },
@@ -461,7 +461,7 @@ module.exports = function(grunt) {
}, },
{ {
expand: true, expand: true,
cwd: 'build/js/include/plugins/datatables_lang/', cwd: 'resources/shop/js/include/plugins/datatables_lang/',
src: ['*.json'], src: ['*.json'],
dest: 'public/assets/plugins/datatables_lang', dest: 'public/assets/plugins/datatables_lang',
}, },
@@ -539,7 +539,7 @@ module.exports = function(grunt) {
}, },
{ {
expand: true, expand: true,
cwd: 'build/js/include/', cwd: 'resources/shop/js/include/',
src: ['boilerplate.js'], src: ['boilerplate.js'],
dest: 'public/assets/plugins', dest: 'public/assets/plugins',
}, },
@@ -548,7 +548,7 @@ module.exports = function(grunt) {
}, },
watch: { watch: {
dist: { dist: {
files: ['build/js/*', 'build/css/*'], files: ['resources/shop/js/*', 'resources/shop/css/*'],
// tasks: ['concat', 'copy'] // tasks: ['concat', 'copy']
tasks: ['concat'] tasks: ['concat']
} }

View File

@@ -24,7 +24,9 @@ class InvoiceController extends Controller
public function pdf($uuid) public function pdf($uuid)
{ {
\Debugbar::disable(); if (app()->bound('debugbar')) {
app('debugbar')->disable();
}
return InvoicePDF::getByUUID($uuid); return InvoicePDF::getByUUID($uuid);
} }

View File

@@ -3,13 +3,19 @@
namespace App\Http\Controllers\Shop; namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Repositories\Core\User\ShopCart;
use App\Repositories\Shop\Paybox as PayboxGateway;
use App\Repositories\Shop\Contents; use App\Repositories\Shop\Contents;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class PayboxController extends Controller class PayboxController extends Controller
{ {
public function accepted() public function accepted()
{ {
ShopCart::clear();
return view('paybox.paybox', ['content' => Contents::getPayboxConfirmedContent()]); return view('paybox.paybox', ['content' => Contents::getPayboxConfirmedContent()]);
} }
@@ -30,8 +36,20 @@ class PayboxController extends Controller
public function process(Request $request) public function process(Request $request)
{ {
$data = $request->all(); $invoiceId = $request->input('order_number');
return view('paybox.send', $data); if (! $invoiceId) {
Log::warning('Paybox callback missing order_number', ['payload' => $request->all()]);
return response('Missing order_number', 400);
}
$success = PayboxGateway::verifyPayment($invoiceId);
if (! $success) {
return response('KO', 400);
}
return response('OK');
} }
} }

View File

@@ -2,12 +2,15 @@
namespace App\Repositories\Shop; namespace App\Repositories\Shop;
use App\Models\Shop\Invoice;
use App\Models\Shop\InvoicePayment; use App\Models\Shop\InvoicePayment;
use App\Repositories\Core\DateTime;
use Bnb\PayboxGateway\Requests\Paybox\AuthorizationWithCapture; use Bnb\PayboxGateway\Requests\Paybox\AuthorizationWithCapture;
use Bnb\PayboxGateway\Requests\PayboxDirect\Capture; use Bnb\PayboxGateway\Requests\PayboxDirect\Capture;
use Bnb\PayboxGateway\Responses\Exceptions\InvalidSignature; use Bnb\PayboxGateway\Responses\Exceptions\InvalidSignature;
use Bnb\PayboxGateway\Responses\Paybox\Verify; use Bnb\PayboxGateway\Responses\Paybox\Verify;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class Paybox class Paybox
@@ -23,17 +26,141 @@ class Paybox
public static function verifyPayment($invoiceId) public static function verifyPayment($invoiceId)
{ {
$invoice = Invoices::get($invoiceId); $invoice = Invoices::get($invoiceId, ['order']);
$payboxVerify = App::make(Verify::class);
try { if (! $invoice) {
$success = $payboxVerify->isSuccess($invoice->total_shipped); Log::warning('Paybox callback received for unknown invoice', [
if ($success) { 'invoice_id' => $invoiceId,
// process order here after making sure it was real payment 'payload' => request()->all(),
} ]);
echo 'OK';
} catch (InvalidSignature $e) { return false;
Log::alert('Invalid payment signature detected');
} }
$payboxVerify = App::make(Verify::class);
try {
$isSuccessful = $payboxVerify->isSuccess($invoice->total_shipped);
} catch (InvalidSignature $e) {
Log::alert('Invalid payment signature detected', [
'invoice_id' => $invoiceId,
'payload' => request()->except('signature'),
]);
return false;
}
if (! $isSuccessful) {
Log::warning('Paybox payment verification failed', [
'invoice_id' => $invoiceId,
'response_code' => $payboxVerify->getResponseCode(),
'payload' => request()->except('signature'),
]);
return false;
}
return self::finalizeInvoicePayment($invoice);
}
protected static function finalizeInvoicePayment(Invoice $invoice)
{
$order = $invoice->order;
if (! $order) {
Log::error('Paybox payment cannot be finalized: missing related order', [
'invoice_id' => $invoice->id,
]);
return false;
}
$request = request();
$referenceParts = array_filter([
$request->input('call_number'),
$request->input('transaction_number'),
]);
$reference = $referenceParts ? implode('-', $referenceParts) : $request->input('authorization_number');
if (! $reference) {
$reference = 'paybox-'.$invoice->id;
}
$payload = $request->except('signature');
$existingPayment = InvoicePayment::where('invoice_id', $invoice->id)
->where('reference', $reference)
->first();
$shouldNotify = false;
$validatedTotal = InvoicePayment::where('invoice_id', $invoice->id)
->validated()
->sum('amount');
if (! $existingPayment && (float) $validatedTotal >= (float) $invoice->total_shipped) {
Log::info('Paybox payment ignored: invoice already fully settled', [
'invoice_id' => $invoice->id,
'order_id' => $order->id,
'reference' => $reference,
]);
return true;
}
DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify) {
$attributes = [
'payment_type' => 1,
'amount' => $invoice->total_shipped,
'date' => DateTime::getDate(),
'data' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'validated' => 1,
];
if ($existingPayment) {
$previousValidationState = (int) ($existingPayment->validated ?? 0);
$existingPayment->fill($attributes);
if ($existingPayment->isDirty()) {
$existingPayment->save();
}
if ($previousValidationState !== 1 && (int) $existingPayment->validated === 1) {
$shouldNotify = true;
}
} else {
InvoicePayment::create($attributes + [
'invoice_id' => $invoice->id,
'reference' => $reference,
]);
$shouldNotify = true;
}
Invoices::checkPayments($invoice->id);
$paidStatus = Orders::getStatusByName('Préparation');
if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) {
$order->status = $paidStatus;
$order->save();
}
});
if ($shouldNotify) {
try {
OrderMails::sendOrderConfirmed($order->id);
} catch (\Throwable $exception) {
Log::error('Unable to send order confirmation email after Paybox payment', [
'order_id' => $order->id,
'invoice_id' => $invoice->id,
'exception' => $exception->getMessage(),
]);
}
}
Log::info('Paybox payment finalized successfully', [
'invoice_id' => $invoice->id,
'order_id' => $order->id,
'reference' => $reference,
'notified' => $shouldNotify,
]);
return true;
} }
public static function getPreviousAuthorizedRequest($request) public static function getPreviousAuthorizedRequest($request)

View File

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

View File

View File

0
resources/shop/print.css Normal file
View File

View File

@@ -13,8 +13,8 @@
<meta name="description" content="Vente de semences, variété anciennes"> <meta name="description" content="Vente de semences, variété anciennes">
<meta name="keywords" content=""> <meta name="keywords" content="">
<link rel="icon" type="image/vnd.microsoft.icon" href="img/favicon.ico"> <link rel="icon" type="image/vnd.microsoft.icon" href="{{ asset('img/favicon.ico') }}">
<link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="{{ asset('img/favicon.ico') }}">
<link rel="stylesheet" href="/css/site.min.css?{{ date('Ymd') }}" type="text/css" media="all"> <link rel="stylesheet" href="/css/site.min.css?{{ date('Ymd') }}" type="text/css" media="all">
@stack('css') @stack('css')