Laravel 7

Shopping : la facture

Dans cet article nous allons aborder la partie création de facture. C’est un chapitre important pour une boutique en ligne. Il faut se poser une question de base : est-ce qu’on génère et mémorise la facture directement dans la boutique ou est-ce qu’on utilise un service tiers ?

La réglementation anti fraude à la TVA est devenue très contraignante. On peut concevoir que ces fraudes sont sans doutes 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 4 conditions : inaliénabilité, sécurisation, conservation et archivage. Mais évidemment on ne peut pas s’auto-certifier !

Vous pouvez télécharger un ZIP du projet ici.

VosFactures

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 à 3 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 à préciser en bas des documents…

Si vous ne précisez 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 information 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

<?php

return [
  'token' => env('FACTURES_TOKEN'),
  'url' => env('FACTURES_URL'),
  'test' => env('FACTURES_TEST'),
];

Je rappelle que c’est nécessaire si vous créez un cache pour la configuration.

Un service pour la facture

Comme on va utiliser cette API de plusieurs points de la boutique on va créer un service :

Je rappelle qu’il n’existe pas de commande Artisan pour créer un service. C’est une façon d’organiser le code pour rendre les contrôleurs plus légers, d’autant qu’on peut injecter ce service facilement dans une méthode.

Le code du service va être un peu chargé parce qu’on doit envoyer pas mal d’informations :

<?php

namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Http;

class Facture
{
    public function create(Order $order, $paid = false)
    {
        $order->load('adresses', 'products', 'adresses.country', 'user');

        // Adresse de facturation
        $addressOrder = $order->adresses->first();

        $text = $addressOrder->address;
        if($addressOrder->addressbis) {
            $text .= "\n" . $addressOrder->addressbis;
        }

        $invoice = [
            'kind' => 'vat',
            'test' => config('invoice.test') ? 'true' : 'false',
            'title' => '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',
            'buyer_email' => $order->user->email,
            'buyer_phone' => $addressOrder->phone,
            '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_date'] = now()->format('Y-m-d');
            $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_name'] = "$addressOrder->name $addressOrder->firstname";
            $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->adresses->count() === 2) {
            $invoice['use_delivery_address'] = true;
            $addressdelivery = $order->adresses->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";
            $text .= $addressdelivery->phone;
            $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,
        ]);

    }
}

On commence par récupérer toutes les informations dans la base :

$order->load('adresses', 'products', 'adresses.country', 'user');

L’adresse de facturation est la première :

$addressOrder = $order->adresses->first();

On récupère le texte de l’adresse avec son complément éventuel :

$text = $addressOrder->address;
if($addressOrder->addressbis) {
    $text .= "\n" . $addressOrder->addressbis;
}

Ensuite on prépare un certain nombre de données :

$invoice = [
    'kind' => 'vat',
    'test' => config('invoice.test') ? 'true' : 'false',
    'title' => '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',
    'buyer_email' => $order->user->email,
    'buyer_phone' => $addressOrder->phone,
    'payment_type' => $order->payment_text,
    'payment_to' => now()->endOfMonth()->addMonth()->format('Y-m-d'),
    'status' => $paid ? 'Payé' : 'Créé',
];

Pour le paiement on prévoit ici 30 jours fin de mois mais vous pouvez prévoir évidemment autre chose.

S’il y a eu un bon de commande il faut ajouter le numéro dans la facture :

// Bon de commande éventuel
if($order->payment === 'mandat') {
    $invoice['oid'] = $order->purchase_order;
}

S’il y a eu un paiement il faut aussi le préciser, avec sa valeur :

// Si la facture a été payée
if($paid) {
    $invoice['paid'] = $order->totalOrder;
}

Ensuite on a du code que je ne détaille pas concernant les adresses.

On précise ensuite la TVA :

// Taxe
if($order->pick) {
    $tax = .2;
} else {
    if(isset($addressdelivery)) {
        $tax = $addressdelivery->country->tax;
    } else {
        $tax = $addressOrder->country->tax;
    }
}

On renseigne après tous les produits commandés :

// 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,
    ]);
}

Les frais d’expédition éventuels :

// 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,
  ]);
}

Et enfin on envoi tout en profitant des nouvelles possibilités de Laravel 7 avec le client HTTP :

// Envoi
return Http::post(config('invoice.url') . 'invoices.json', [
    'api_token' => config('invoice.token'),
    'invoice' => $invoice,
]);

Création de la facture

Dans le contrôleur PaymentController on avait juste laissé un commentaire pour la création de la facture, on va maintenant pouvoir utiliser notre service, je remets le code complet :

use App\Services\Facture;

...

public function __invoke(Request $request, Facture $facture, Order $order)
{
    $this->authorize('manage', $order);

    $state = null;

    if($request->payment_intent_id === 'error') {
        $state = 'erreur'; 
    } else {
        \Stripe\Stripe::setApiKey(config('stripe.secret_key'));
        $intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id);
        if ($intent->status === 'succeeded') {              
            $request->session()->forget($order->reference); 
            $order->payment_infos()->create(['payment_id' => $intent->id]);         
            $state = 'paiement_ok';
            // Création de la facture
            $response = $facture->create($order, true);  
            if($response->successful()) {
                $data = json_decode($response->body());
                $order->invoice_id = $data->id;
                $order->invoice_number = $data->number;
            }           
        } else {
            $state = 'erreur';     
        }
    }

    $order->state_id = State::whereSlug($state)->first()->id;              
    $order->save();
}

On crée la facture avec le service, ensuite on récupère l’identifaint de la facture qui nous sera utile pour aller la chercher avec l’API, ainsi que le numéro de la facture. On mémorise tout ça dans la commande.

Mise en oeuvre

Maintenant on va  voir si ça fonctionne…

On commande deux produits identiques :

On commande en utilisant un numéro de carte de test :

Le paiement réussit :

Sur le tableau de bord de VosFactures on trouve la facture qui apparaît bien comme payée :

Et on a bien la facture générée et disponible, on retrouve bien les deux produits, le bon prix, la référence de la commande, le paiement par carte bancaire :

Il existe plusieurs modèles de facture selon les goûts (une vingtaine) et on peut évidemment ajouter un logo.

Dans la table orders on a bien les renseignements désirés :

Conclusion

On a encore avancé avec la création de la facture. Il faudra encore prévoir que le client puisse aller la récupérer, on fera ça à partir de son compte personnel. Il faudra aussi pouvoir en générer une à partir de l’administration lorsque le paiement se fera par chèque ou virement.

Print Friendly, PDF & Email

11 commentaires

      • softcode

        Non il n’ya pas de renlentissement le site tourne très rapide comme toujours c’est juste quand je crée la facture apartir de cet api vosfacture que que je constate un ralentisement et après quelque seconde ça me donne la même erreur : cURL error 28: Resolving timed out after 10000 milliseconds,

        je constate aussi qu’à chaque fois que je fais appel à un service distant via un api quelconque cette erreur revient toujours : cURL error 28: Resolving timed out after 10000 milliseconds
        je me demande si c’est un problème lié à laravel ou à o2swich??

  • softcode

    Bonjour bestmomo, j’avais une préocupation à vous demander, de comment je peux faire pour que après validation du paiment, lors de l’enregistrement de la facture via l’api vosfacture, que l’utilisateur recoive un email comptenant la facture de sa commande sous format pdf?? Merci pour votre aide

    • bestmomo

      Salut,
      La création de la facture peur prendre un certain temps, ta question repose sur un petit souci de synchronisation. On ne peut pas récupérer la facture avant sa création et l’on ne sait pas quand elle va être créée. Quand on crée une facture avec l’API, on n’a pas de réponse. La documentation complète est ici. Il existe bien la possibilité d’envoyer la facture par email. Mais, je ne sais pas trop comment résoudre élégamment ce souci de synchro…

  • jeromeborg

    Salut,
    j’ai du mal a comprendre un concept, peux tu m’éclairer ?
    Dans le stripejs, a la validation, il appelle Route::name(‘commandes.payment’)->post(‘paiement/{order}’, ‘PaymentController’) avec pour parametre un id; //OK
    dans paymentcontroller on utilise la méthode magique __invoke // OK
    Mais a quel moment ou endroit appelles ton paymentcontroller comme une fonction ?
    Je bloque, j’ai mis du débuggage via un Storage::disk(‘public’)->append(, le fichier n’est jamais écrit, donc aucun événement ne déclenche le __invoke, dans pma le order n’est pas validé
    Merci

Laisser un commentaire