Sync 51 Transactions PDF + multiple fixes

This commit is contained in:
Jérémy Munsch 2023-03-22 17:17:46 +01:00
parent 59ce99524a
commit e7e5ce5cfe
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
29 changed files with 1154 additions and 112 deletions

View file

@ -57,6 +57,8 @@ URL : /bo
Les pages statiques sont disponible a tout moment: `$StaticPageGlobal`
# Fonts
- Robot fonts are used in PDF generation documents.
# STRIPE
php artisan cashier:webhook

View file

@ -2,17 +2,17 @@
namespace App\Auth;
use App\Models\Customer;
use App\Models\Customer\Customer;
use Illuminate\Support\Str;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider as UserProviderContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
/**
* Overload EloquentUserProvider to handle
* Soft deleted Customers
*/
class ReactivateCustomerProvider extends EloquentUserProvider implements UserProviderContract
class ReactivateCustomerProvider extends EloquentUserProvider implements UserProvider
{
/**
* Retrieve a customer by the given credentials.
@ -46,15 +46,15 @@ class ReactivateCustomerProvider extends EloquentUserProvider implements UserPro
/**
* Validate a customer against the given credentials.
*
* @param UserContract $customer
* @param array $credentials
* @param \Illuminate\Contracts\Auth\Authenticatable $customer
* @param array $credentials
* @return boolean
* @throws \ErrorException If model provided is not a Customer or connection to server fails.
*/
public function validateCredentials(UserContract $customer, array $credentials)
public function validateCredentials(Authenticatable $customer, array $credentials)
{
/** @var \App\Models\Customer $customer */
if (\get_class($customer) !== Customer::class) {
/** @var \App\Models\Customer\Customer $customer */
if ($customer instanceof Customer) {
throw new \ErrorException('Cannot authenticate any thing else than ' . Customer::class);
}

View file

@ -23,28 +23,6 @@ class Handler extends ExceptionHandler
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $e
* @return void
*
* @phpcs:ignore Squiz.Commenting.FunctionComment.EmptyThrows
* @throws \Exception
*/
public function report(\Throwable $e)
{
// phpcs:enable
if (get_class($e) == RedirectTo::class) {
abort(redirect($e->url));
}
if ($this->shouldntReport($e)) {
return;
}
parent::report($e);
}
/**
* Register the exception handling callbacks for the application.
*

View file

@ -89,11 +89,10 @@ class OrderController extends Controller
*
* @param \App\Models\Shop\Order $order
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function show(Order $order): View
{
// phpcs:enable
$order->load('transactions');
return view('back::pages.orders.show', compact('order'));
}
@ -213,6 +212,7 @@ class OrderController extends Controller
$this->authorize('prepare', $order);
return DB::transaction(function () use ($request, $order) {
// * Stockage de la préparation de la commande.
// * FIXME: Same code here as in Models/Shop/Order::getPartialRefundedBasket.
// ! Ici on veux réutiliser le cart calculator.
$cart = ToolboxHelper::createVirtualCart(\collect($order->basket)
->map(function (object $basketItem) {
@ -227,11 +227,11 @@ class OrderController extends Controller
// * Update virtual basket with actual content
\collect($order->basket)->each(function (object $basketItem) use ($request, $cart) {
$newQty = \intval($request->validated()['items'][$basketItem->id]);
/** @var \Gloudemans\Shoppingcart\CartItem $cartItem */
$cartItem = $cart->search(function (CartItem $cartItem) use ($basketItem) {
return $cartItem->id === $basketItem->id;
})->first();
$newQty = \intval($request->validated()['items'][$basketItem->id]);
// * Update Qty or remove item from the cart.
if (!$newQty) {
$cart->remove($cartItem->rowId);

View file

@ -2,7 +2,10 @@
namespace App\Http\Controllers;
use App\Enums\Order\TransactionType;
use App\Models\Payment\Transaction;
use App\Models\Shop\DeliveryMode;
use App\Models\Shop\Order;
use App\Models\Shop\PaymentMode;
use Gloudemans\Shoppingcart\Cart;
use Gloudemans\Shoppingcart\CartItem;
@ -14,6 +17,8 @@ use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class Controller extends BaseController
{
@ -30,7 +35,7 @@ class Controller extends BaseController
{
$this->middleware(function (Request $request, callable $next) {
$reponse = $next($request);
if (\request()->is('bo*') and !\request()->ajax()) {
if (\request()->routeIs('bo.*') and !\request()->ajax()) {
if (!DeliveryMode::getCached()->where('status', true)->count()) {
Session::flash('bo.warning', trans('Aucun mode de livraison n\'est disponible'));
}
@ -175,4 +180,40 @@ class Controller extends BaseController
});
return $basket;
}
/**
* Generate and download transaction invoice
*
* ! Appliable for both Fo and Bo controllers
*
* @param \App\Models\Shop\Order $order
* @param \App\Models\Payment\Transaction $transaction
* @return \Symfony\Component\HttpFoundation\Response
*/
public function downloadTransactionInvoice(Order $order, Transaction $transaction): Response
{
$this->authorize('download', $transaction);
$title = trans('Commande du :date - :orderRef - :transRef', [
'date' => $transaction->created_at->isoFormat('DD\/MM\/YYYY'),
'orderRef' => $order->reference,
'transRef' => $transaction->reference,
]);
$sluggedTitle = Str::slug(\implode(' ', [
config('app.name'),
$transaction->type->equals(TransactionType::debit()) ? trans('Facture') : trans('Avoir'),
$transaction->created_at->isoFormat('YYYY-MM-DD'),
]));
$transaction->generateInvoice($title);
return \response()->streamDownload(function () use ($transaction, $title) {
echo $transaction->generateInvoice($title)->output();
}, "{$sluggedTitle}.pdf", [
'Cache-Control' => 'public, must-revalidate, max-age=0',
'Content-Description' => 'File Transfer',
'Content-Type' => 'application/pdf',
'Expires' => 'Sat, 26 Jul 1997 05:00:00 GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
// For glory.
'X-Generator' => 'mPDF'
]);
}
}

View file

@ -216,7 +216,7 @@ class CustomerController extends Controller
$customer = auth('frontend')->user();
$orders = $customer->orders()->orderBy('created_at', 'DESC')->paginate();
return view('theme::pages.customer.my-orders', compact('orders'));
return view('theme::pages.customer.orders', compact('orders'));
}
/**
@ -228,7 +228,7 @@ class CustomerController extends Controller
public function orderDetail(Order $order)
{
$order->load('timeslot.shop');
return view('theme::pages.customer.order-detail', compact('order'));
return view('theme::pages.customer.orders.show', compact('order'));
}
// * --- END Backoffice ---

View file

@ -47,7 +47,7 @@ class StoreProductRequest extends UpdateMiniCrudRequest
public function validateResolved()
{
// * Minicrud Hack to allow storage, apply only on store action
if (\get_class($this) === self::class) {
if (\get_class($this) instanceof self) {
if (!\array_key_exists(\strval($this->type), Product::fetchTypesInputs())) {
throw ValidationException::withMessages([
'type' => trans('type de produit invalide')
@ -141,7 +141,7 @@ class StoreProductRequest extends UpdateMiniCrudRequest
*
* @return array
*/
public function attributes()
public function attributes(): array
{
return [
'name' => trans('nom'),

View file

@ -128,7 +128,7 @@ abstract class XmlJobs extends XArcadiaFlux
}
} catch (InvalidXmlException $e) {
$message = $e->getMessage();
if (get_class($e) === QueryException::class) {
if ($e instanceof QueryException) {
$message = implode(' ', $e->errorInfo);
}
$this->log(

View file

@ -0,0 +1,64 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Container\Container;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
class CollectionHelper
{
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginate(Collection $results, int $pageSize): LengthAwarePaginator
{
$page = Paginator::resolveCurrentPage('page');
return self::paginateManually($results, $pageSize, $page);
}
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @param integer $page
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginateManually(Collection $results, int $pageSize, int $page = 1): LengthAwarePaginator
{
$total = $results->count();
return self::paginator($results->forPage($page, $pageSize), $total, $pageSize, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]);
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param integer $total
* @param integer $perPage
* @param integer $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected static function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'currentPage' => $currentPage,
'options' => $options
]);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models\Payment;
use App\Enums\GeneralConfigType;
use App\Enums\Order\PaymentModeEnum;
use App\Enums\Order\PaymentStatus;
use App\Enums\Order\TransactionStatus;
@ -10,13 +11,16 @@ use App\Enums\Order\ValidationStatus;
use App\Lib\Helpers\ToolboxHelper;
use App\Lib\Payments\PaymentAdapter;
use App\Models\Shop\Order;
use App\Models\Site\GeneralConfig;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Kwaadpepper\Enum\Traits\CastsEnums;
use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Money\Currency;
use Money\Money;
use Mpdf\Config\FontVariables;
/**
* Transaction
@ -86,6 +90,16 @@ class Transaction extends Model
});
}
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'reference';
}
// * METHODS
/**
@ -256,6 +270,120 @@ class Transaction extends Model
$this->attributes['foreign_refund_id'] = $foreign_refund_id;
}
/**
* Generate Invoice
*
* @param string $title
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf
*/
public function generateInvoice(string $title): LaravelMpdf
{
/** @var \App\Models\GeneralConfig */
$config = GeneralConfig::getCached()->where('type', GeneralConfigType::global())->firstOrFail();
$additionalData = $config->data;
/** @var \App\Models\Shop\Order */
$order = $this->order;
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdfWrapper */
$mpdf = app('laravel-mpdf');
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontData = $defaultFontConfig['fontdata'];
$isCompleteRefunded = false;
$isRefunded = false;
if ($this->type->equals(TransactionType::credit())) {
$zeroMoney = new Money(0, app('phpMoneyCurrency'));
/** */
$isCompleteRefunded = (new Money($order->total_ttc, app('phpMoneyCurrency')))
->subtract($this->amount)->lessThanOrEqual($zeroMoney) or
\optional($order->transactions()->orderBy('created_at')->where('type', TransactionType::credit())
->latest()->pluck('id')->last())->id === $this->id;
$isRefunded = $isCompleteRefunded ?: $this->type->equals(
TransactionType::credit()
);
}
/**
* * Because of Stripe reusing transactions, therefore use prefix.
*/
$transactionReferenceSuffix = $order->payment_status->equals(PaymentStatus::refunded()) ? '_1' : '';
// * Transaction is Completely Refund.
$invoiceBasketItems = $isCompleteRefunded ? \collect($order->basket) : null;
// * Transaction is Refund Partial.
$invoiceBasketItems = $invoiceBasketItems ?? ($isRefunded ? $order->getPartialRefundedBasket() : null);
// * Transaction is Completed.
$invoiceBasketItems = $invoiceBasketItems ?? \collect($order->basket);
/**
* 7.5pt 10px
* 9pt 12px
* 10.5pt 14px
* 12pt 16px
* 13.5pt 18px
*/
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */
$pdf = $mpdf->loadView(
'back::documents.order-invoice',
$this->toArray(),
[
'title' => $title,
'billing' => !empty($order->billing->firstname) ? $order->billing : $order->shipping,
'shipping' => $order->shipping,
'logo' => \public_path(resize($additionalData->logo->value ?? '', 'BoutiqueLogo')),
'introLines' => \collect($additionalData)->map(function (object $input, string $inputName) {
return Str::substr($inputName, 0, -1) === 'invoice_line_' ? $input->value : null;
})->filter()->all(),
'orderReference' => $order->reference,
'transactionReference' => $this->reference . $transactionReferenceSuffix,
'date' => $this->updated_at,
'transactionType' => $this->type,
'basket' => $invoiceBasketItems,
'totalTtc' => $invoiceBasketItems->sum('subtotal'),
'totalTaxes' => $invoiceBasketItems->sum('tax')
],
[
'mode' => 'utf-8',
'format' => 'A4',
// * 10.5pt = 14px
'default_font_size' => 10.5,
'default_font' => 'roboto',
'margin_left' => 5,
'margin_right' => 5,
'margin_top' => 5,
'margin_bottom' => 5,
'margin_header' => 0,
'margin_footer' => 5,
'orientation' => 'P',
'title' => $title,
'author' => \config('app.name'),
'watermark' => '',
'show_watermark' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'custom_font_dir' => '',
'custom_font_data' => [],
'auto_language_detection' => false,
'temp_dir' => rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR),
'pdfa' => false,
'pdfaauto' => false,
'fontdata' => $fontData + [
'roboto' => [
'R' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Regular.ttf'),
'B' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Bold.ttf'),
'I' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Italic.ttf'),
'BI' => \resource_path('assets/fonts/bo/roboto/RobotoMono-BoldItalic.ttf')
]
]
]
);
return $pdf;
}
// * GETTER
/**

View file

@ -17,8 +17,10 @@ use App\Models\Payment\Transaction;
use App\Notifications\Order\OrderValidationStatusChanged;
use App\Notifications\Order\OrderDeliveryStatusChanged;
use Gloudemans\Shoppingcart\Cart;
use Gloudemans\Shoppingcart\CartItem;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
@ -166,6 +168,16 @@ class Order extends Model
});
}
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'reference';
}
// * RELATIONSHIPS
/**
@ -451,4 +463,52 @@ class Order extends Model
$this->total_ht = \intval($this->total_ttc - $basket->tax());
$this->total_weight = \intval($basket->weight());
}
/**
* Generate partial refunded items basket
*
* ! (diff de basket - shipped basket)
*
* @return \Illuminate\Support\Collection<\Gloudemans\Shoppingcart\CartItem>
*/
public function getPartialRefundedBasket(): Collection
{
$shippedBastetItems = collect($this->shipped_basket);
// * FIXME: Same code here as in Bo/OrderController::prepareSubmit.
// ! Ici on veux réutiliser le cart calculator (diff de basket - shipped basket).
$cart = ToolboxHelper::createVirtualCart(\collect($this->basket)
->map(function (object $basketItem) use ($shippedBastetItems) {
if (!$shippedBastetItems->contains('id', $basketItem->id)) {
return null;
}
$cartItem = ToolboxHelper::basketItemObjectToCartItem($basketItem);
$cartItem->associate(Product::query()->where('id', $cartItem->id)->first());
return $cartItem;
})->filter());
// * Update virtual basket with actual content
collect($this->shipped_basket)->each(function (object $basketItem) use ($cart) {
/** @var \Gloudemans\Shoppingcart\CartItem $cartItem */
$cartItem = $cart->search(function (CartItem $cartItem) use ($basketItem) {
return $cartItem->id === $basketItem->id;
})->first();
$newQty = max($cartItem->qty - $basketItem->qty, 0);
// * Update Qty or remove item from the cart.
if (!$newQty) {
$cart->remove($cartItem->rowId);
return;
}
$cartItem->setQuantity($newQty);
// ! https://github.com/bumbummen99/LaravelShoppingcart/pull/181
// * Note Lib is is actually refactoring for laravel 9 php8 only
// * https://github.com/bumbummen99/LaravelShoppingcart/pull/145
// ! These lines are required to prevent amount bug.
$cartItem->setTaxRate($basketItem->options->tax_rate / 100);
$cartItem->setDiscountRate($basketItem->options->discount_rate);
});
$virtualOrder = new self();
$virtualOrder->assignBasket($cart);
return collect($virtualOrder->basket);
}
}

View file

@ -9,6 +9,15 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Kwaadpepper\Enum\Traits\CastsEnums;
/**
* GeneralConfig
*
* @property \App\Enums\GeneralConfigType $type The general type.
* @property object $data The general config data array.
*
* @package App\Models
*
*/
class GeneralConfig extends Model
{
use CastsEnums;

View file

@ -62,7 +62,7 @@ class OrderCreated extends Notification
\trans(
'Vous pouvez également consulter l\'avancement de votre commande dans votre espace sur la boutique.'
),
\route('account.order-detail', ['reference' => $this->order->reference])
\route('account.orders.show', ['reference' => $this->order->reference])
);
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Policies;
use App\Enums\Order\TransactionStatus;
use App\Models\Customer\Customer;
use App\Models\Payment\Transaction;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class TransactionPolicy extends Policy
{
/**
* Can a user order things ?
*
* ! Only owner customer or a Bo user
* ! + a validated transaction.
*
* @param \App\Models\Customer\Customer|\App\Models\User|null $user
* @param \App\Models\Payment\Transaction|null $transaction
*
* @return \Illuminate\Auth\Access\Response
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
*/
public function download(
$user = null,
Transaction $transaction = null
): Response {
if (!$transaction or !$transaction->status->equals(TransactionStatus::validated())) {
return Response::deny();
}
if ($user and $user instanceof User) {
return Response::allow();
}
if (
$user and $user instanceof Customer and
$transaction->order->customer and
!\is_null($user->hashcode) and
$transaction->order->customer->hashcode === $user->hashcode
) {
return Response::allow();
}
return Response::deny();
}
}

View file

@ -15,10 +15,13 @@ use App\Models\Merchandise\Product;
use App\Models\Merchandise\Range;
use App\Models\Site\StaticPage;
use App\Models\Merchandise\Tag;
use App\Models\Payment\Transaction;
use BadMethodCallException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Kwaadpepper\Enum\Exceptions\EnumNotRoutableException;
@ -81,6 +84,20 @@ class RouteServiceProvider extends ServiceProvider
Route::bind('reference', function ($value) {
return Order::query()->where('reference', $value)->firstOrFail();
});
Route::bind('transactionReference', function ($value, RoutingRoute $previousRoute) {
$previousOrder = $previousRoute->parameter('reference');
if (
!$previousRoute->hasParameter('reference') or
!\is_object($previousOrder) or
!($previousOrder instanceof Order)
) {
\abort(404);
}
return Transaction::query()->where('reference', $value)
->whereHas('order', function (Builder $q) use ($previousOrder) {
return $q->where('id', $previousOrder->id);
})->firstOrFail();
});
Route::bind('static_page_front', function ($value) {
return StaticPage::where('slug', $value)->firstOrFail();

View file

@ -17,6 +17,7 @@
"ext-intl": "*",
"axlon/laravel-postal-code-validation": "^3.3",
"bumbummen99/shoppingcart": "^4.0",
"carlos-meneses/laravel-mpdf": "^2.1",
"diglactic/laravel-breadcrumbs": "^6.1",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",

378
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": "cd11a30c4de1fb87def9102b9a4e214f",
"content-hash": "2b6013b98e0676dfa66fe4b2ea394475",
"packages": [
{
"name": "asm89/stack-cors",
@ -260,6 +260,62 @@
},
"time": "2022-05-15T15:32:38+00:00"
},
{
"name": "carlos-meneses/laravel-mpdf",
"version": "2.1.11",
"source": {
"type": "git",
"url": "https://github.com/mccarlosen/laravel-mpdf.git",
"reference": "cb52e5c17df9caa340eaed8566cfb1e3c7b37c33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mccarlosen/laravel-mpdf/zipball/cb52e5c17df9caa340eaed8566cfb1e3c7b37c33",
"reference": "cb52e5c17df9caa340eaed8566cfb1e3c7b37c33",
"shasum": ""
},
"require": {
"mpdf/mpdf": "^8.0",
"php": "^5.6|^7.0|^8.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Mccarlosen\\LaravelMpdf\\LaravelMpdfServiceProvider"
],
"aliases": {
"PDF": "Mccarlosen\\LaravelMpdf\\Facades\\LaravelMpdf"
}
}
},
"autoload": {
"psr-4": {
"Mccarlosen\\LaravelMpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Laravel Mpdf: Using Mpdf in Laravel to generate Pdfs.",
"keywords": [
"laravel",
"mpdf",
"pdf"
],
"support": {
"issues": "https://github.com/mccarlosen/laravel-mpdf/issues",
"source": "https://github.com/mccarlosen/laravel-mpdf/tree/2.1.11"
},
"funding": [
{
"url": "https://www.paypal.me/mccarlosen",
"type": "custom"
}
],
"time": "2022-12-28T02:20:09+00:00"
},
{
"name": "clue/stream-filter",
"version": "v1.6.0",
@ -3283,6 +3339,142 @@
],
"time": "2023-02-06T13:44:46+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.1.4",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "add590e93b7502efafd9839a68cff99f3497b318"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/add590e93b7502efafd9839a68cff99f3497b318",
"reference": "add590e93b7502efafd9839a68cff99f3497b318",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0",
"php-http/message-factory": "^1.0",
"psr/http-message": "^1.0",
"psr/log": "^1.0 || ^2.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "http://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2022-12-15T11:24:39+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.66.0",
@ -3629,6 +3821,56 @@
},
"time": "2022-01-27T09:35:39+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "phenx/php-font-lib",
"version": "0.5.4",
@ -5403,6 +5645,78 @@
],
"time": "2023-02-01T10:56:52+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.3.7",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "bccc892d5fa1f48c43f8ba7db5ed4ba6f30c8c05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/bccc892d5fa1f48c43f8ba7db5ed4ba6f30c8c05",
"reference": "bccc892d5fa1f48c43f8ba7db5ed4ba6f30c8c05",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^5.6 || ^7.0 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "~5.7",
"setasign/fpdf": "~1.8",
"setasign/tfpdf": "1.31",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "~6.2"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.3.7"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2023-02-09T10:38:43+00:00"
},
{
"name": "spatie/browsershot",
"version": "3.40.2",
@ -9938,65 +10252,6 @@
},
"time": "2022-09-07T15:32:08+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v5.11.0",
@ -11852,7 +12107,8 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.3|^8.0"
"php": "^7.3|^8.0",
"ext-intl": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"

View file

@ -67,6 +67,15 @@ return [
'keepRatio' => true,
'trim' => true
],
'foBoutiqueLogo' => [
'height' => 100,
'width' => 100,
'format' => 'png',
'inCanvas' => true,
'resize' => true,
'keepRatio' => true,
'trim' => true
],
'foPaymentButtonLogo' => [
'height' => 25,
'width' => 25,

View file

@ -0,0 +1,62 @@
<?php
use App\Enums\GeneralConfigType;
use App\Enums\InputType;
use App\Models\Site\GeneralConfig;
use Illuminate\Database\Migrations\Migration;
class AddInvoiceHeaderToGeneralConfigs extends Migration
{
/**
* Run the migrations.
*
* @return void
* @throws \Error If general config is missing.
*/
public function up()
{
/** @var App\Models\Site\GeneralConfig|null */
$generalConfig = GeneralConfig::query()->where('type', GeneralConfigType::global())->first();
if (!$generalConfig) {
throw new \Error('missing \'global\' general config');
}
$data = $generalConfig->data;
for ($i = 1; $i <= 5; $i++) {
if (empty($data->{"invoice_line_{$i}"})) {
$data->{"invoice_line_{$i}"} = [
'attribute' => "Facture ligne {$i}",
'label' => "Facture ligne {$i}",
'inputType' => InputType::string(),
'value' => '',
'required' => false,
'validate' => 'nullable|string|min:0|max:70',
'helper' => "Le texte qui sera affiché sur la facture en entête l.{$i}",
];
}
}
$generalConfig->data = $data;
$generalConfig->saveOrFail();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
/** @var App\Models\Site\GeneralConfig|null */
$generalConfig = GeneralConfig::query()->where('type', GeneralConfigType::global())->first();
if (!$generalConfig) {
return;
}
$data = $generalConfig->data;
for ($i = 1; $i <= 5; $i++) {
if (!empty($data->{"invoice_line_{$i}"})) {
unset($data->{"invoice_line_{$i}"});
}
}
$generalConfig->data = $data;
$generalConfig->saveOrFail();
}
}

Binary file not shown.

Binary file not shown.

@ -1 +1 @@
Subproject commit ab2545ed02b9a1f1eea6809c876e752856eadf34
Subproject commit 33947ec0111809cefa8f29440708b77a0d01f550

View file

@ -0,0 +1,358 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{ $title }}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
html,
body {
/** page margins : 5mm */
position: relative;
width: 200mm;
height: 287mm;
margin: 0;
padding: 0;
}
body {
color: #333333;
background-color: #FFFFFF;
}
table {
border-collapse: collapse;
}
#main-table {
margin: auto;
height: 274mm;
width: 100%;
/** Test Mise en page */
/* border-color: red;
border-width: 1px;
border-style: solid; */
}
/* 1pt=0,352778mm, 1pt=1.3333333333333333px font-size=10.5pt=13,9999999965px=3,704169mm*/
/* LOGO + INTRO LINES */
#top-row {
height: 132.28px; /* 35mm */
}
#addresses-row {
height: 170.078px; /* 45mm */
}
#billing-box {
float: right;
width: 321.259px; /* 85mm */
margin-right: 56.69px; /* 15mm */
padding-top: 3rem;
/** Test Mise en page */
/* background-color: rgb(253, 184, 255); */
}
#shipping-box {
height: 6rem; /* 7 lines */
margin-top: 0.5rem;
margin-bottom: 0.5rem;
text-align: center;
/** Test Mise en page */
/* background-color: rgb(215, 255, 135); */
}
#logo-container {
/** Test Mise en page */
/* border: 1px solid blue; */
}
#intro-lines-container {
/** Test Mise en page */
/* border: 1px solid purple; */
}
.logo-container-pages {
/** Test Mise en page */
/* border: 1px solid blue; */
}
.intro-lines-container-pages {
/** Test Mise en page */
/* border: 1px solid purple; */
}
#intro-lines {
font-size: 0.8rem;
}
.font-typed {
font-family: serif;
}
.font-upper {
text-transform: uppercase;
}
.font-spaced {
letter-spacing: 6px;
}
.font-amount {
font-family: serif;
text-transform: uppercase;
text-align: right;
}
.page-footer {
margin: auto;
/** Test Mise en page */
/* border-color: red;
border-width: 1px;
border-style: solid; */
}
.page-footer td {
padding-top: 2mm;
border-top:double medium #CCCCCC;
}
td.bold-cell,
th.bold-cell {
padding: 1mm;
border: 1px solid #333333;
background-color: #dadada;
font-weight: bold;
text-align: center;
}
td.bordered-cell,
th.bordered-cell {
border: 1px solid #333333;
}
table.bordered,
table.bordered thead,
table.bordered body,
table.bordered td,
table.bordered th {
border: 1px solid;
border-color: #333333;
}
table.bordered td,
table.bordered th {
padding-left: 1mm;
padding-right: 1mm;
}
table.bordered td {
border-top: none;
border-bottom: none;
border-color: #333333;
}
@page {
footer: page-footer;
}
</style>
</head>
@php
$customerIsRefunded = !$transactionType->equals(\App\Enums\Order\TransactionType::debit());
$amountPrefix = $customerIsRefunded ? '- ' : '';
@endphp
<body>
<div id="main-table" cellpadding="0" cellspacing="0">
{{-- LOGO and INTROLINES FROM GENERAL CONFIGS --}}
<div id="top-row">
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
{{-- LOGO --}}
<td id="logo-container" valign="middle" width="25%" style="text-align: center;height: 30mm;">
<img style="width: 26mm;" src="{{ $logo }}" alt="{{ config('app.name') }}"/>
</td>
{{-- INTROLINES --}}
<td id="intro-lines-container" width="75%" style="text-align: center;">
<h1>{{ config('app.name') }}</h1>
<p id="intro-lines">
@foreach ($introLines as $introLine)
<span>{{ $introLine }}</span>
@if(!$loop->last)<br />@endif
@endforeach
</p>
</td>
</tr>
</table>
</div>
{{-- SHIPPING AND BILLING --}}
<div id="billing-box" class="font-typed font-upper">
<strong>{{ Str::limit(Str::transliterate($billing->lastname) . ' '. Str::transliterate($billing->firstname), 37, '.') }}</strong><br/>
{{ Str::limit(Str::transliterate($billing->line1), 37, '.') }}<br/>
@if(!empty($billing->line2)){{ Str::limit(Str::transliterate($billing->line2), 37, '.') }}<br/>@endif
{!! Str::limit(Str::transliterate(clean($billing->postal_code)) . '&nbsp;&nbsp;'. Str::transliterate(clean($billing->city)), 37, '.') !!}
</div>
<div id="addresses-row">
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
{{-- TYPE --}}
<td width="80%" class="bold-cell font-spaced">
<h2>@if(!$customerIsRefunded){{
Str::upper(__('Facture'))
}}@else{{
Str::upper(__('Avoir'))
}}@endif</h2>
</td>
{{-- BILLING --}}
<td rowspan="4" valign="middle">&nbsp;</td>
</tr>
<tr><td>&nbsp;</td></tr>
<tr><td class="bold-cell"><h3>{{ Str::upper(__('Adresse de livraison')) }}</h3></td></tr>
{{-- SHIPPING --}}
<tr><td id="shipping-box" class="bordered-cell font-typed font-upper">
<strong>{{ Str::transliterate($shipping->lastname) }} {{ Str::transliterate($shipping->firstname) }}</strong><br/>
{{ Str::transliterate($shipping->line1) }}<br/>
@if(!empty($shipping->line2)){{ Str::transliterate($shipping->line2) }}<br/>@endif
{{ Str::transliterate($shipping->postal_code) }} {{ Str::transliterate($shipping->city) }}
</td></tr>
</table>
</div>
{{-- DATE AND REFERENCES --}}
<table class="bordered" width="100%" cellspacing="0" cellpadding="0">
<tr>
<td class="bold-cell">{{ __('Date') }}</td>
<td class="font-typed font-upper" style="text-align: center;">{{
Carbon\Carbon::parse($created_at)->isoFormat('DD\/MM\/YYYY')
}}</td>
<td class="bold-cell">{{ __('N° Commande') }}</td>
<td class="font-typed font-upper" style="text-align: center;">{{ $orderReference }}</td>
<td class="bold-cell">{{ __('N° Facture') }}</td>
<td class="font-typed font-upper" style="text-align: center;">{{ $transactionReference }}</td>
</tr>
</tr>
</table>
@php
$basket = collect($basket);
$hasTaxes = !empty($totalTaxes) ? true : false;
$columns = 4 + ($hasTaxes ? 1 : 0);
$page = 1;
$perPage = 15;
$perPageAfterFirstPage = 22;
$lastPage = \App\Lib\Helpers\CollectionHelper::paginateManually(collect($basket), $perPage)->lastPage();
@endphp
@while($lastPage >= $page)
@php $basketPagedList = \App\Lib\Helpers\CollectionHelper::paginateManually(collect($basket), $perPage, $page); @endphp
@if($page !== 1)
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
{{-- LOGO --}}
<td class="logo-container-pages" valign="middle" width="25%" style="text-align: center;height: 15mm;">
<img style="width: 16mm;" src="{{ $logo }}" alt="{{ config('app.name') }}"/>
</td>
{{-- APPNAME --}}
<td class="intro-lines-container-pages" width="75%" style="text-align: center;">
<h2>{{ config('app.name') }}</h2>
</td>
</tr>
</table>
@endif
<table class="bordered" width="100%" cellspacing="0" cellpadding="2" @if($page !== 1) style="margin-top: 5mm;" @endif>
{{-- TABLE HEADER --}}
<thead>
<tr>
<th width="40%" class="bold-cell font-upper"><strong>{{ __('Désignation') }}</strong></th>
<th class="bold-cell font-upper"><strong>{{ __('Qte') }}</strong></th>
<th class="bold-cell font-upper"><strong>{{ __('Prix U. TTC') }}</strong></th>
@if($hasTaxes)
<th class="bold-cell font-upper"><strong>{{ __('Tva') }}</strong></th>
@endif
<th class="bold-cell font-upper"><strong>{{ __('S.Tot TTC') }}</strong></th>
</tr>
</thead>
<tbody>
@foreach($basketPagedList as $basketItem)
<tr>
<td valign="top" class="font-typed">
<strong>{{ Str::limit($basketItem->name, 45, '.') }}</strong><br />
<i>{{ $basketItem->options->reference }}</i>
</td>
<td valign="top" class="font-amount">{{ $amountPrefix }}{{ $basketItem->qty }}</td>
<td valign="top" class="font-amount">{{ \App\Lib\Helpers\ToolboxHelper::fmtCurrency($basketItem->options->price_ttc, '') }}</td>
@if($hasTaxes)
<td valign="top" class="font-amount">{{ \App\Lib\Helpers\ToolboxHelper::fmtCurrency($basketItem->tax, '') }}</td>
@endif
<td valign="top" class="font-amount">{{ $amountPrefix }}{{ \App\Lib\Helpers\ToolboxHelper::fmtCurrency($basketItem->subtotal, '') }}</td>
</tr>
@endforeach
@for ($i = $perPage - $basketPagedList->count(); $i > 0; $i--)
<tr>
@for ($k = $columns; $k > 0; $k--)
<td valign="top">&nbsp;<br>&nbsp;</td>
@endfor
</tr>
@endfor
</tbody>
</table>
@php $page++; $perPage = $perPageAfterFirstPage; $lastPage = \App\Lib\Helpers\CollectionHelper::paginateManually(collect($basket), $perPage)->lastPage(); @endphp
@if($basketPagedList->currentPage() < $basketPagedList->lastPage())
<pagebreak>
@endif
@endwhile
<table width="100%" cellspacing="0" cellpadding="2">
<tr>
<td style="width:50%;"><strong>&nbsp;</strong></td>
<td>
<table width="100%" cellspacing="0" cellpadding="2">
@if($hasTaxes)
<tr>
<td align="right"><u>{{ __('Tva') }}</u></td>
<td align="right" class="font-amount">{{
\App\Lib\Helpers\ToolboxHelper::fmtCurrency($totalTaxes)
}}</td>
</tr>
@endif
<tr>
<td align="right"><strong>{{ __('Total TTC') }}</strong></td>
<td align="right" class="font-amount"><strong>{{ $amountPrefix }}{{
\App\Lib\Helpers\ToolboxHelper::fmtCurrency($totalTtc)
}}</strong></td>
</tr>
</table>
</td>
</tr>
</table>
@if(!$hasTaxes)
<table width="100%" style="height: 10mm;">
<tr>
<td style="text-align:justify;">
{{ __('TVA non applicable, article 293B du code général des impôts') }}</td>
</tr>
</table>
@endif
</div>
<htmlpagefooter name="page-footer">
<table width="100%" cellspacing="0" cellpadding="2" class="page-footer">
<tr>
<td width="16%" style="text-align: right;" valign="top">
{{ __('N° Commande') }}&nbsp;:<br/>
{{ __('N° Facture') }}&nbsp;&nbsp;:
</td>
<td width="22%">
<span>{{ $orderReference }}</span><br/>
<span>{{ $transactionReference }}</span>
</td>
<td width="45%" style="text-align: right;">&nbsp;</td>
<td width="7%" style="text-align: right;">
<span>{{ __('date') }}&nbsp;:</span>
<span>{{ __('page') }}&nbsp;:</span>
</td>
<td valign="bottom" width="10%" style="text-align: left;">
<span>{{ Carbon\Carbon::parse($created_at)->isoFormat('DD\/MM\/YY') }}</span>
<span>{PAGENO}&nbsp;/&nbsp;{nbpg}</span>
</td>
</tr>
</table>
</htmlpagefooter>
</body>
</html>

View file

@ -3,8 +3,6 @@
<div class="col">
<div class="h4">{{ __('Transactions') }}</div>
@foreach ($order->transactions as $transaction)
{{-- FIXME: Invoice download here --}}
{{-- <a class="btn btn-sm btn-info" href="{{ route('bo.orders.invoice', $order) }}" target="_blank" title="{{ __('Télécharger la facture') }}">{{ __('Facture') }}</a> --}}
<ol class="list-group list-group-numbered">
<li class="list-group-item d-flex flex-column flex-md-row justify-content-between align-items-start">
<div class="ms-2 me-auto mb-3">
@ -40,6 +38,11 @@
<span class="h5 font-imprima">{{ __('Montant :amount', [
'amount' => app('phpMoneyFormatter')->format($transaction->amount)
]) }}</span>
@can('download', $transaction)
<a href="{{ route(request()->routeIs('bo.*') ? 'bo.orders.invoice' : 'account.orders.invoice', ['reference' => $order, 'transactionReference' => $transaction]) }}" class="btn btn-primary" target="_blank" role="button">{{
__('Télécharger la facture')
}}</a>
@endcan
</div>
</li>
</ol>

View file

@ -83,13 +83,13 @@ Breadcrumbs::for('account.dashboard', function ($trail) {
$trail->parent('home');
$trail->push(__('Mon compte'), route('account.dashboard'));
});
Breadcrumbs::for('account.my-orders', function ($trail) {
Breadcrumbs::for('account.orders', function ($trail) {
$trail->parent('account.dashboard');
$trail->push(__('Mes commandes'), route('account.my-orders'));
$trail->push(__('Mes commandes'), route('account.orders'));
});
Breadcrumbs::for('account.order-detail', function ($trail, $reference) {
$trail->parent('account.my-orders');
$trail->push(__('Détail d\'une commande'), route('account.order-detail', ['reference', $reference]));
Breadcrumbs::for('account.orders.show', function ($trail, $reference) {
$trail->parent('account.orders');
$trail->push(__('Détail d\'une commande'), route('account.orders.show', ['reference', $reference]));
});
Breadcrumbs::for('account.email', function ($trail) {
$trail->parent('account.dashboard');

View file

@ -154,5 +154,9 @@ Route::prefix('bo')->name('bo.')->group(function () {
->name('orders.refund-submit');
Route::post('/orders/{order}/refund-submit', [OrderController::class, 'cancelRefundSubmit'])
->name('orders.cancel-refund-submit');
Route::get(
'/commandes/{reference}/transaction/{transactionReference}/invoice',
[OrderController::class, 'downloadTransactionInvoice']
)->name('orders.invoice');
});
});

View file

@ -35,8 +35,12 @@ Route::middleware(['auth:frontend', 'verified'])->group(function () {
Route::post('/profil', [CustomerController::class, 'updateInfos'])->name('update-infos');
// * Orders
Route::get('/commandes', [CustomerController::class, 'myOrders'])->name('my-orders');
Route::get('/commandes/{reference}', [CustomerController::class, 'orderDetail'])->name('order-detail');
Route::get('/commandes', [CustomerController::class, 'myOrders'])->name('orders');
Route::get('/commandes/{reference}', [CustomerController::class, 'orderDetail'])->name('orders.show');
Route::get(
'/commandes/{reference}/transaction/{transactionReference}/invoice',
[CustomerController::class, 'downloadTransactionInvoice']
)->middleware('throttle:2,1')->name('orders.invoice');
// * Security
Route::get('/securite/identification/email', [CustomerController::class, 'email'])->name('email');
@ -87,6 +91,7 @@ Route::get('/produits', [ProductController::class, 'products'])->name('products.
Route::get('/produit/{productSlug}', [ProductController::class, 'product'])
->middleware('redirect-slugs')->name('products.show');
Route::get('/produits/card/{productSlug}', [ProductController::class, 'ajaxProductsCard'])->name('products.card');
// * - Tags
Route::get('/produits/special', [TagController::class, 'tags'])->name('tags.index');
Route::get('/produits/{tagSlug}', [TagController::class, 'tag'])->name('tags.show');