
Dans le précédent article, on a permis au client de préciser sa commande : produits, adresses, moyen de paiement. On a mis en place le code nécessaire pour réaliser un paiement avec carte bancaire avec Stripe. On a aussi prévu les emails nécessaires pour la commande. Dans le présent article, on va compléter avec la confirmation de la commande.
Vous pouvez trouver le code dans ce dépôt Github.
La confirmation de la commande
Lorsque le client confirme sa commande, on a deux cas :
- Le paiement est effectué par carte bancaire : dans ce cas, on le dirige vers la page de Stripe et c'est au retour du paiement que la page de confirmation sera présentée. Si le paiement a réussi, il faudra générer la facture
- le paiement est effectué par un autre mode (chèque, virement ou mandat administratif) : dans ce cas, on ouvre directement la page de confirmation
On a besoin d'un nouveau composant :
php artisan make:volt order/confirmation --class
Avec ce code :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\Title;
use Illuminate\Support\Facades\Auth;
use App\Models\Order;
new #[Title('Order confirmation')]
class extends Component {
public Order $order;
public function mount($id): void
{
$this->order = Order::with('products', 'addresses', 'state')->findOrFail($id);
if (Auth::user()->id != $this->order->user_id) {
abort(403);
}
}
}; ?>
<div>
<x-card
class="flex items-center justify-center mt-6 bg-gray-100 w-full lg:max-w-[80%] lg:mx-auto" title="{{ trans('Your order is confirmed') }}" shadow separator>
<x-elements :order="$order" />
<br>
<x-card class="w-full sm:min-w-[50vw]" title="" shadow separator >
@if($order->state->slug === 'cheque')
<p>{{ trans('Please send us a check with:') }}</p>
<ul>
<li>- {{ trans('amount of payment:') }} <strong>{{ number_format($order->total + $order->shipping, 2, ',', ' ') }} €</strong></li>
<li>- {{ trans('payable to the order of') }} <strong>{{ $shop->name }}</strong></li>
<li>- {{ trans('to be sent to') }} <strong>{{ $shop->address }}</strong></li>
<li>- {{ trans('do not forget to indicate your order reference') }} <strong>{{ $order->reference }}</strong></li>
</ul>
<p>{{ $order->pick? trans('You can come and pick up your order as soon as the payment is received.') : trans('Your order will be shipped as soon as the payment is received.') }}</p>
@elseif($order->state->slug === 'mandat')
<p>{{ trans('You have chosen to pay by administrative mandate. This type of payment is reserved for administrations.') }}</p>
<p>{{ trans('You must send your administrative mandate to:') }}</p>
<p><strong>{{ $shop->name }}</strong></p>
<p><strong>{{ $shop->address }}</strong></p>
<p>{{ trans('You can also send it to us by email at this address:') }} <strong>{{ $shop->email }}</strong></p>
<p>{{ trans('Do not forget to indicate your order reference') }} <strong>{{ $order->reference }}</strong>.</p>
<p>{{ $order->pick ? trans('You can come and pick up your order as soon as the mandate is received.') : trans('Your order will be shipped as soon as this mandate is received.') }}</p>
@elseif($order->state->slug === 'virement')
<p>{{ trans('Please make a transfer to our account:') }}</p>
<ul>
<li>- {{ trans('amount of transfer:') }} <strong>{{ number_format($order->total + $order->shipping, 2, ',', ' ') }} €</strong></li>
<li>- {{ trans('account holder:') }} <strong>{{ $shop->holder }}</strong></li>
<li>- {{ trans('BIC:') }} <strong>{{ $shop->bic }}</strong></li>
<li>- {{ trans('IBAN:') }} <strong>{{ $shop->iban }}</strong></li>
<li>- {{ trans('bank:') }} <strong>{{ $shop->bank }}</strong></li>
<li>- {{ trans('bank address:') }} <strong>{{ $shop->bank_address }}</strong></li>
<li>- {{ trans('do not forget to indicate your order reference') }} <strong>{{ $order->reference }}</strong></li>
</ul>
<p>{{ $order->pick ? trans('You can come and pick up your order as soon as the payment is received.') : trans('Your order will be shipped as soon as the transfer is received.') }}</p>
@endif
</x-card>
</x-card>
</div>
Dans ce code, on traite les trois cas hors paiement par carte bancaire : mandat administratif, virement et chèque.
On ajoute des traductions :
"Please send us a check with:": "Veuillez nous envoyer un chèque avec :",
"You have chosen to pay by administrative mandate. This type of payment is reserved for administrations.": "Vous avez choisi de payer par mandat administratif. Ce type de paiement est réservé aux administrations.",
"Your order is confirmed": "Votre commande est confirmée",
"Your order will be shipped as soon as this mandate is received.": "Votre commande vous sera envoyée dès réception de votre mandat.",
"Your order will be shipped as soon as the transfer is received.": "Votre commande vous sera envoyée dès réception du virement.",
Dans tous les cas, on va avoir les références :
Le détail :
Les adresses :
Et ensuite la partie spécifique au mode de paiement, pour un mandat administratif, on a :
Pour un paiement par chèque :
Et pour un virement bancaire :
La facturation
Une application tierce
La réglementation anti fraude à la TVA est devenue très contraignante. On peut concevoir que ces fraudes sont sans doute fréquentes et répandues, mais comme toujours ce sont les agissements d'un petit nombre qui compliquent la vie de la majorité, c'est vrai dans tous les domaines. Toujours est-il qu'il faut respecter la réglementation en vigueur et on doit pouvoir fournir une attestation à l'administration qui prouve qu'on respecte bien les quatre conditions : inaliénabilité, sécurisation, conservation et archivage. Mais évidemment, on ne peut pas s'auto-certifier !
Finalement, le plus simple est de gérer la facturation avec une application tierce qui fournit cette certification. Après quelques recherches, j'ai opté pour Vosfactures. C'est une application simple et complète avec un bon rapport qualité-prix. Il existe même une option gratuite, mais limitée à trois documents par mois, ce qui n'est pas très réaliste pour une boutique en ligne parce qu'à ce niveau la rentabilité semble impossible. Par contre l'option basique à 4€ HT permet un nombre illimité de documents. Je précise que je n'ai aucun lien commercial avec ce site mis à part le fait que j'y suis un client. On peut s'inscrire gratuitement pendant un mois en utilisant toutes les fonctionnalités, ce qui permet de se faire une bonne idée des possibilités. Pour continuer ce tutoriel de façon efficace, vous pouvez donc prendre un compte gratuit.
Pour que les factures soient renseignées, il faut au moins dans l'application Vosfactures créer ce qu'ils appellent un département :
Vous pouvez accéder à un formulaire très complet en cliquant le petit bouton avec le pignon à droite. Là, vous pouvez préciser le nom de l'entreprise, son adresse, les mentions légales à mentionner en bas des documents... Si vous ne spécifiez pas tout ça, il y aura des trous dans vos factures, même pour vos essais !
Vosfacture propose une API très complète et documentée ici. On va voir dans cet article comment la mettre en œuvre. Une fois que vous avez un compte, vous pouvez récupérer URL et token à cette adresse : https://{votrecompte}.vosfactures.fr/api_tokens.
On va mémoriser ces informations dans le fichier .env :
FACTURES_TOKEN=votre token ici
FACTURES_URL=https://votre compte.vosfactures.fr/
FACTURES_TEST=true
N'oubliez pas d'ajouter la variable pour préciser que vous êtes en test ! Et évidemment on crée un fichier de configuration :
Avec ce simple code :
<?php
return [
'token' => env('FACTURES_TOKEN'),
'url' => env('FACTURES_URL'),
'test' => env('FACTURES_TEST'),
];
Un service pour la facture
Comme on va utiliser cette API de plusieurs points de la boutique, on crée un service :
Et voilà le code :
<?php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Http;
class Invoice
{
public function create(Order $order, $paid = false)
{
$order->load('addresses', 'products', 'addresses.country', 'user');
// Adresse de facturation
$addressOrder = $order->addresses->first();
$text = $addressOrder->address;
if($addressOrder->addressbis) {
$text .= "\n" . $addressOrder->addressbis;
}
$invoice = [
'kind' => 'vat',
'test' => config('invoice.test') ? 'true' : 'false',
'title' => $order->payment == 'mandat' ? 'Engagement juridique ' . $order->purchase_order : 'Commande référence ' . $order->reference,
'buyer_street' => $text,
'buyer_country' => $addressOrder->country->name,
'buyer_post_code' => $addressOrder->postal,
'buyer_city' => $addressOrder->city,
'buyer_company' => $addressOrder->professionnal ? '1' : '0',
'payment_type' => $order->payment_text,
'payment_to' => now()->endOfMonth()->addMonth()->format('Y-m-d'),
'status' => $paid ? 'Payé' : 'Créé',
];
// Bon de commande éventuel
if($order->payment === 'mandat') {
$invoice['oid'] = $order->purchase_order;
}
// Si la facture a été payée
if($paid) {
$invoice['paid'] = $order->totalOrder;
}
// Si c'est un professionnel
if($addressOrder->professionnal) {
$invoice['buyer_name'] = "$addressOrder->company";
if(isset($addressOrder->name)) {
$invoice['buyer_first_name'] = $addressOrder->firstname;
$invoice['buyer_last_name'] = $addressOrder->name;
} else {
$invoice['buyer_first_name'] = $order->user->firstname;
$invoice['buyer_last_name'] = $order->user->name;
}
} else {
$invoice['buyer_first_name'] = $addressOrder->firstname;
$invoice['buyer_last_name'] = $addressOrder->name;
}
// Adresse et boîte postale
$text = $addressOrder->address;
if($addressOrder->addressbis) {
$text .= " " . $addressOrder->addressbis;
}
if($addressOrder->bp) {
$text .= " " . $addressOrder->bp;
}
$invoice['buyer_street'] = $text;
// S'il y a une adresse de livraison
if($order->addresses->count() === 2) {
$invoice['use_delivery_address'] = true;
$addressdelivery = $order->addresses->get(1);
$text = '';
if(isset($addressdelivery->name)) {
$text .= "$addressdelivery->civility $addressdelivery->name $addressdelivery->firstname \n";
}
if($addressdelivery->company) {
$text .= $addressdelivery->company . "\n";
}
$text .= $addressdelivery->address . "\n";
if($addressdelivery->addressbis) {
$text .= $addressdelivery->addressbis . "\n";
}
if($addressdelivery->bp) {
$text .= 'BP ' . $addressdelivery->bp . "\n";
}
$text .= "$addressdelivery->postal $addressdelivery->city" . "\n";
$text .= $addressdelivery->country->name . "\n";
$invoice['delivery_address'] = $text;
}
// Taxe
if($order->pick) {
$tax = .2;
} else {
if(isset($addressdelivery)) {
$tax = $addressdelivery->country->tax;
} else {
$tax = $addressOrder->country->tax;
}
}
// Produits
$positions = [];
foreach($order->products as $product) {
array_push($positions, [
'name' => $product->name,
'quantity' => $product->quantity,
'tax' => $tax * 100,
'total_price_gross' => $product->total_price_gross,
]);
}
// Frais d'expédition
if($order->shipping > 0) {
array_push($positions, [
'name' => 'Frais d\'expédition',
'quantity' => 1,
'tax' => 0,
'total_price_gross' => $order->shipping,
]);
}
$invoice['positions'] = $positions;
// Envoi
return Http::post(config('invoice.url') . 'invoices.json', [
'api_token' => config('invoice.token'),
'invoice' => $invoice,
]);
}
}
Je ne commente pas le code qui est relativement simple.
Cas du paiement par carte bancaire
Je rappelle qu'il faut bien préciser vos clés publiques et secrètes de test pour Stripe dans votre configuration, comme je l'ai signalé dans le précédent article. Pour vos tests, vous trouvez une page dans la documentation avec des numéros de cartes pour toutes les situations possibles. Je vous conseille aussi de créer un environnement de test.
Lorsque le client choisit le paiement par carte bancaire, on crée une session Checkout et on dirige le client vers la page de paiement générée automatiquement par Stripe à partir des données qu'on lui envoie.
Je vous rappelle que vous trouvez les cartes de test dans la documentation de Stripe.
On a besoin d'un nouveau composant :
php artisan make:volt order/card --class
Avec ce code :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\Title;
use App\Models\{ Order, Shop, State, Payment };
use Illuminate\Support\Facades\Auth;
use App\Services\Invoice;
use Illuminate\Support\Facades\Mail;
use App\Mail\Ordered;
use Stripe\Stripe;
use Stripe\Checkout\Session as CheckoutSession;
use Stripe\Exception\ApiErrorException;
use Illuminate\Support\Facades\Log;
new #[Title('Card')]
class extends Component {
public Order $order;
public Shop $shop;
public bool $paid = false;
public function mount($id, Invoice $invoice): void
{
$this->order = Order::findOrFail($id);
$this->shop = Shop::firstOrFail();
if (Auth::user()->id != $this->order->user_id) {
abort(403);
}
if (session()->has('checkout_session_id')) {
Stripe::setApiKey(config('stripe.secret_key'));
try {
$session = CheckoutSession::retrieve(session('checkout_session_id'));
} catch (ApiErrorException $e) {
Log::error('Stripe API Error: ' . $e->getMessage());
abort(500, trans('An error occurred while processing your payment.'));
}
if($session->payment_status === 'paid') {
$this->paid = true;
$this->order->state_id = State::whereSlug('paiement_ok')->first()->id;
$this->order->save();
$payment = new Payment(['payment_id' => $session->payment_intent,]);
$this->order->payment_infos()->save($payment);
// Création de la facture
$response = $invoice->create($this->order, true);
if($response->successful()) {
$data = json_decode($response->body());
$this->order->invoice_id = $data->id;
$this->order->invoice_number = $data->number;
$this->order->save();
}
} else {
$this->order->state_id = State::whereSlug('erreur')->first()->id;
$this->order->save();
}
session()->forget('checkout_session_id');
Mail::to(Auth::user())->send(new Ordered($this->shop, $this->order));
} else {
abort(404);
}
}
}; ?>
<div>
<x-card
class="flex items-center justify-center mt-6 bg-gray-100 w-full lg:max-w-[80%] lg:mx-auto"
title=""
shadow
separator>
@if($paid)
<x-badge value="{!! trans('Your payment has been validated') !!}" class="p-3 badge-success" /><br><br>
<p>{{ $order->pick ? trans('You can come and pick up your order') : trans('Your order will be shipped to you') }}</p>
<p>{{ trans('You can retrieve your invoice from your account') }}</p>
@else
<x-badge value="{!! trans('Your payment has not been validated') !!}" class="p-3 badge-error" /><br><br>
<p>{{ trans('Please contact us') }}</p>
@endif
</x-card>
</div>
On ajoute les traductions :
"Your payment has been validated": "Votre paiement a été validé",
"You can come and pick up your order": "Vous pouvez venir chercher votre commande",
"Your order will be shipped to you": "Votre commande va vous être envoyée",
"Your payment has not been validated": "Votre paiement n'a pas été validé",
"Please contact us": "Veuillez nous contacter",
"Phone us": "Nous téléphoner",
"You can retrieve your invoice from your account": "Vous pouvez retrouver votre facture dans votre compte",
"An error occurred while processing your payment.": "Une erreur s'est produite lors du traitement de votre paiement.",
La route :
Route::middleware('auth')->group(function () {
Route::prefix('order')->group(function () {
...
Volt::route('/card/{id}', 'order.card')->name('order.card');
});
Si le paiement réussit, ce qui est normalement le cas puisque Stripe nous redirige sur cette page en cas de réussite du paiement. On a ce simple message :
Et la facture est générée sur le site vosfactures.fr.
Dans le cas contraire, on a ce message :
Téléchargement de la facture
Lorsqu'on a créé la page de visualisation des données d'une commande dans le compte du client, on a laisser en attente la fonction pour télécharger une facture. On va maintenant compléter le composant account/orders/show :
use Illuminate\Support\Facades\Storage;
...
public function invoice()
{
// Récupération du pdf
$url = config('invoice.url') . 'invoices/' . (string)$this->order->invoice_id . '.pdf?api_token=' . config('invoice.token');
$contents = file_get_contents($url);
$name = (string)$this->order->invoice_id . '.pdf';
Storage::disk('invoices')->put($name, $contents);
// Envoi
return response()->download(storage_path('app/invoices/' . $name))->deleteFileAfterSend();
}
Dans config.filesystem, on crée le disque pour les factures :
'disks' => [
...
'invoices' => [
'driver' => 'local',
'root' => storage_path('app/invoices'),
],
Le client dispose d'un bouton pour charger la facture :
Conclusion
On en a terminé avec la commande et avec la partie frontale de notre boutique. Il nous reste encore un gros travail pour la partie administration.
Par bestmomo
Nombre de commentaires : 2