diff --git a/app/Http/Controllers/Bo/OrderController.php b/app/Http/Controllers/Bo/OrderController.php index b8f68e6..19eb0a6 100644 --- a/app/Http/Controllers/Bo/OrderController.php +++ b/app/Http/Controllers/Bo/OrderController.php @@ -508,7 +508,7 @@ class OrderController extends Controller $this->refundTransaction( $order, $order->total_ttc - $order->total_refund, - $order->getItemsForOrderCancelling() + $order->getItemForOrderCancelling() ); return \back()->with('bo.success', trans( ':model a été mis à jour', diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 9e30d41..0b5be33 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -236,4 +236,38 @@ class Controller extends BaseController 'X-Generator' => 'mPDF' ]); } + + /** + * Generate and download transaction invoice + * + * ! Appliable for both Fo and Bo controllers + * + * @param \App\Models\Shop\Order $order + * @return \Symfony\Component\HttpFoundation\Response + */ + public function downloadOrderInvoice(Order $order): Response + { + $this->authorize('download', $order); + $title = trans('Commande du :date - :orderRef - :transRef', [ + 'date' => $order->created_at->isoFormat('DD\/MM\/YYYY'), + 'orderRef' => $order->reference, + 'transRef' => trans('Toutes transactions'), + ]); + $sluggedTitle = Str::slug(\implode(' ', [ + config('app.name'), + trans('Facture'), + $order->created_at->isoFormat('YYYY-MM-DD'), + ])); + return \response()->streamDownload(function () use ($order, $title) { + echo $order->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' + ]); + } } diff --git a/app/Lib/Helpers/ToolboxHelper.php b/app/Lib/Helpers/ToolboxHelper.php index 777c300..9644dbc 100644 --- a/app/Lib/Helpers/ToolboxHelper.php +++ b/app/Lib/Helpers/ToolboxHelper.php @@ -3,9 +3,11 @@ namespace App\Lib\Helpers; use App\Enums\Discount\DiscountType; +use App\Enums\GeneralConfigType; use App\Enums\Product\TaxRate; use App\Models\Merchandise\Product; use App\Models\Shop\Order; +use App\Models\Site\GeneralConfig; use Gloudemans\Shoppingcart\Cart; use Gloudemans\Shoppingcart\CartItem; use Illuminate\Database\Eloquent\Model; @@ -18,7 +20,9 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; +use Mccarlosen\LaravelMpdf\LaravelMpdf; use Money\Money; +use Mpdf\Config\FontVariables; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ToolboxHelper @@ -532,4 +536,102 @@ class ToolboxHelper ] ], [], ['g-recaptcha-response' => 'reCaptcha']); } + + /** + * Generate Invoice + * + * @param \App\Models\Shop\Order $order + * @param \Illuminate\Support\Carbon $date + * @param string $title + * @param string $documentType + * @param object $items + * @param string $transactionReference + * @return \Mccarlosen\LaravelMpdf\LaravelMpdf + */ + public static function generateInvoice( + Order $order, + Carbon $date, + string $title, + string $documentType, + object $items, + string $transactionReference + ): LaravelMpdf { + /** @var \App\Models\Site\GeneralConfig */ + $config = GeneralConfig::getCached()->where('type', GeneralConfigType::global())->firstOrFail(); + + $additionalData = $config->data; + + /** @var \Mccarlosen\LaravelMpdf\LaravelMpdfWrapper */ + $mpdf = app('laravel-mpdf'); + + $defaultFontConfig = (new FontVariables())->getDefaults(); + $fontData = $defaultFontConfig['fontdata']; + + $invoiceBasketItems = \collect($items); + + /** + * 7.5pt 10px + * 9pt 12px + * 10.5pt 14px + * 12pt 16px + * 13.5pt 18px + */ + /** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */ + $pdf = $mpdf->loadView( + 'back::documents.order-invoice', + [ + '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' => $transactionReference, + 'date' => $date, + 'documentType' => $documentType, + '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; + } } diff --git a/app/Models/Payment/Transaction.php b/app/Models/Payment/Transaction.php index 37e15f9..23a5ec0 100644 --- a/app/Models/Payment/Transaction.php +++ b/app/Models/Payment/Transaction.php @@ -2,7 +2,6 @@ namespace App\Models\Payment; -use App\Enums\GeneralConfigType; use App\Enums\Order\DeliveryStatus; use App\Enums\Order\PaymentModeEnum; use App\Enums\Order\PaymentStatus; @@ -12,7 +11,6 @@ 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; @@ -21,7 +19,6 @@ use Kwaadpepper\Enum\Traits\CastsEnums; use Mccarlosen\LaravelMpdf\LaravelMpdf; use Money\Currency; use Money\Money; -use Mpdf\Config\FontVariables; /** * Transaction @@ -312,20 +309,9 @@ class Transaction extends Model */ public function generateInvoice(string $title): LaravelMpdf { - /** @var \App\Models\Site\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']; - /** * * Because of Stripe reusing transactions, therefore use prefix. */ @@ -333,73 +319,15 @@ class Transaction extends Model $invoiceBasketItems = \collect($this->items); - /** - * 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'), - 'isLastFullRefundTransaction' => $order->transactions()->where('id', '!=', $this->id)->pluck('items')->sum(function ($items) { - return \collect($items)->sum('subtotal'); - }) === \collect($this->items)->sum('subtotal') - ], - [ - '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 ToolboxHelper::generateInvoice( + $order, + $this->created_at, + $title, + $this->type->equals(TransactionType::debit()) ? + Str::ucfirst(\trans('facture')) : Str::ucfirst(\trans('avoir')), + $invoiceBasketItems, + $this->reference . $transactionReferenceSuffix ); - return $pdf; } // * GETTER diff --git a/app/Models/Shop/Order.php b/app/Models/Shop/Order.php index ac49835..9126ec1 100644 --- a/app/Models/Shop/Order.php +++ b/app/Models/Shop/Order.php @@ -24,8 +24,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Session; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Kwaadpepper\Enum\Traits\CastsEnums; +use Mccarlosen\LaravelMpdf\LaravelMpdf; use Money\Money; /** @@ -421,23 +423,12 @@ class Order extends Model * * @return \Illuminate\Support\Collection */ - public function getItemsForOrderCancelling(): Collection + public function getItemForOrderCancelling(): Collection { - // * Negate all items prices (already refunded) - $orderedItems = \collect($this->basket); - - /** @var \Illuminate\Support\Collection */ - $refundedTransactionsItems = $this->transactions()->where('type', TransactionType::credit()) - ->pluck('items'); - - $final = \collect($orderedItems); - $refundedTransactionsItems->each(function ($items) use (&$final) { - $final = $final->concat(\collect($items)); - }); - - $remainingToRefund = \intval($this->transactions()->where('id', '!=', $this->id)->pluck('items')->sum(function ($items) { - return \collect($items)->sum('subtotal'); - })); + $remainingToRefund = \intval($this->transactions() + ->where('id', '!=', $this->id)->pluck('items')->sum(function ($items) { + return \collect($items)->sum('subtotal'); + })); // * Créer un dummy product pour représenter la transaction. $cartItemOptions = \array_merge(Controller::$cartItemOptionsSkel, [ @@ -455,13 +446,34 @@ class Order extends Model $cartItem->setQuantity(1); // * Négation du prix - $basketItem = \json_decode($cartItem->toJson()); + $basketItem = \json_decode($cartItem->toJson()); $basketItem->price = $basketItem->price * -1; $basketItem->options->price_ttc = $basketItem->options->price_ttc * -1; $basketItem->subtotal = $basketItem->subtotal * -1; - // * On ajoute la ligne virtuelle en bas de la liste. - $final->add($basketItem); + return collect($basketItem)->values(); + } + + /** + * Fetch all transaction items + * ! For final bill. + * + * @return \Illuminate\Support\Collection + */ + public function getAllTransactionsItems(): Collection + { + // * Negate all items prices (already refunded) + $orderedItems = \collect($this->basket); + + /** @var \Illuminate\Support\Collection */ + $refundedTransactionsItems = $this->transactions()->where('type', TransactionType::credit()) + ->pluck('items'); + + $final = \collect($orderedItems); + $refundedTransactionsItems->each(function ($items) use (&$final) { + $final = $final->concat(\collect($items)); + }); + return $final->values(); } @@ -677,4 +689,22 @@ class Order extends Model $this->total_ht = \intval($this->total_ttc - $basket->tax()); $this->total_weight = \intval($basket->weight()); } + + /** + * Generate final Invoice + * + * @param string $title + * @return \Mccarlosen\LaravelMpdf\LaravelMpdf + */ + public function generateInvoice(string $title): LaravelMpdf + { + return ToolboxHelper::generateInvoice( + $this, + $this->created_at, + $title, + Str::ucfirst(\trans('facture')), + $this->getAllTransactionsItems(), + $this->reference + ); + } } diff --git a/app/Policies/OrderPolicy.php b/app/Policies/OrderPolicy.php index 16ee615..994823f 100644 --- a/app/Policies/OrderPolicy.php +++ b/app/Policies/OrderPolicy.php @@ -175,7 +175,9 @@ class OrderPolicy extends Policy $order->payment_status->equals(PaymentStatus::canceled()) or $order->payment_status->equals(PaymentStatus::failed()) ) { - return Response::deny('The order payment status is alread :status.', ['status' => $order->payment_status->label]); + return Response::deny('The order payment status is alread :status.', [ + 'status' => $order->payment_status->label + ]); } if (!$transaction) { return Response::deny('The order does not have a transaction associated.'); @@ -212,4 +214,29 @@ class OrderPolicy extends Policy } return Response::allow(); } + + /** + * Can a user order download final order invoice + * + * @param \App\Models\Customer\Customer|\App\Models\User|null $user + * @param \App\Models\Shop\Order|null $order + * + * @return \Illuminate\Auth\Access\Response + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + */ + public function download( + $user = null, + Order $order = null + ): Response { + if (!$order) { + return Response::deny(\trans('No order provided')); + } + if (!$order->transactions()->count()) { + return Response::deny('No transactions are available on this order'); + } + if (!\is_object($user) or !($user instanceof User)) { + return Response::deny(trans('Only authenticated users can do this')); + } + return Response::allow(); + } } diff --git a/app/Policies/TransactionPolicy.php b/app/Policies/TransactionPolicy.php index 35e6e6f..ab9b77b 100644 --- a/app/Policies/TransactionPolicy.php +++ b/app/Policies/TransactionPolicy.php @@ -11,7 +11,7 @@ use Illuminate\Auth\Access\Response; class TransactionPolicy extends Policy { /** - * Can a user order things ? + * Can a user download transaction invoice * * ! Only owner customer or a Bo user * ! + a validated transaction. @@ -27,12 +27,14 @@ class TransactionPolicy extends Policy $user = null, Transaction $transaction = null ): Response { - if ( - !$transaction or - !$transaction->status->equals(TransactionStatus::validated()) or - $transaction->needs_validation - ) { - return Response::deny(); + if (!$transaction) { + return Response::deny(\trans('No transaction provided')); + } + if (!$transaction->status->equals(TransactionStatus::validated())) { + return Response::deny(trans('Transaction is not validated')); + } + if ($transaction->needs_validation) { + return Response::deny(\trans('Transaction needs to be validated first')); } if ($user and $user instanceof User) { return Response::allow(); @@ -45,6 +47,6 @@ class TransactionPolicy extends Policy ) { return Response::allow(); } - return Response::deny(); + return Response::deny(\trans('Could not satisfy any conditions')); } } diff --git a/resources/theme b/resources/theme index d40a32f..f6dc1ac 160000 --- a/resources/theme +++ b/resources/theme @@ -1 +1 @@ -Subproject commit d40a32f3fd62cd3df89c556ba9b71299d38594b5 +Subproject commit f6dc1ac78237a08670d74721fe22b32f51ac2744 diff --git a/resources/views/documents/order-invoice.blade.php b/resources/views/documents/order-invoice.blade.php index 47de28f..e867ede 100644 --- a/resources/views/documents/order-invoice.blade.php +++ b/resources/views/documents/order-invoice.blade.php @@ -165,10 +165,6 @@ -@php -$customerIsRefunded = !$transactionType->equals(\App\Enums\Order\TransactionType::debit()); -@endphp -
@@ -207,11 +203,7 @@ $customerIsRefunded = !$transactionType->equals(\App\Enums\Order\TransactionType {{-- TYPE --}} -

@if(!$customerIsRefunded){{ - Str::upper(__('Facture')) - }}@else{{ - Str::upper(__('Avoir')) - }}@endif

+

{{ Str::upper($documentType) }}

{{-- BILLING --}}   @@ -241,7 +233,7 @@ $customerIsRefunded = !$transactionType->equals(\App\Enums\Order\TransactionType {{ __('Date') }} {{ - Carbon\Carbon::parse($created_at)->isoFormat('DD\/MM\/YYYY') + $date->isoFormat('DD\/MM\/YYYY') }} {{ __('N° Commande') }} {{ $orderReference }} @@ -371,7 +363,7 @@ $customerIsRefunded = !$transactionType->equals(\App\Enums\Order\TransactionType {{ __('page') }} : - {{ Carbon\Carbon::parse($created_at)->isoFormat('DD\/MM\/YY') }} + {{ $date->isoFormat('DD\/MM\/YY') }} {PAGENO} / {nbpg} diff --git a/resources/views/pages/orders/parts/transactions.blade.php b/resources/views/pages/orders/parts/transactions.blade.php index df8327e..41260a7 100644 --- a/resources/views/pages/orders/parts/transactions.blade.php +++ b/resources/views/pages/orders/parts/transactions.blade.php @@ -39,7 +39,7 @@ 'amount' => app('phpMoneyFormatter')->format($transaction->amount) ]) }} @can('download', $transaction) - {{ + {{ __('Télécharger la facture') }} @elseif ($transaction->needs_validation) @@ -54,3 +54,10 @@ @else {{ __('Aucune transaction') }} @endif +@can('download', $order) + +@endcan diff --git a/routes/web-bo.php b/routes/web-bo.php index d614a23..8fe1ccc 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -164,8 +164,12 @@ Route::prefix('bo')->name('bo.')->group(function () { Route::post('/orders/{order}/refund-submit', [OrderController::class, 'cancelRefundSubmit']) ->name('orders.cancel-refund-submit'); Route::get( - '/commandes/{reference}/transaction/{transactionReference}/invoice', + '/orders/{reference}/transaction/{transactionReference}/invoice', [OrderController::class, 'downloadTransactionInvoice'] + )->name('orders.transaction.invoice'); + Route::get( + '/orders/{reference}/invoice', + [OrderController::class, 'downloadOrderInvoice'] )->name('orders.invoice'); }); }); diff --git a/routes/web-fo.php b/routes/web-fo.php index cc13d17..ea95bdf 100644 --- a/routes/web-fo.php +++ b/routes/web-fo.php @@ -40,7 +40,7 @@ Route::middleware(['auth:frontend', 'verified'])->group(function () { Route::get( '/commandes/{reference}/transaction/{transactionReference}/invoice', [CustomerController::class, 'downloadTransactionInvoice'] - )->middleware('throttle:2,1')->name('orders.invoice'); + )->middleware('throttle:2,1')->name('orders.transaction.invoice'); // * Security Route::get('/securite/identification/email', [CustomerController::class, 'email'])->name('email');