Je vous avais proposé dans ce blog une application de commerce en ligne. Je me suis alors rendu compte que ce projet avait bien intéressé et fait émerger un nombre considérable de questionnements.
L'application a évolué entre Laravel 7 et Laravel 11. Ce genre de projet peut rapidement devenir volumineux. Donc au niveau des possibilités on a en gros :
- une interface prévues multi-languages
- application de la TVA
- pour les possibilités de paiement : chèque, virement, mandat administratif et carte bancaire (Stripe)
- pour la facturation : utilisation d'une application tierce (vosfactures.fr). Pourquoi ce choix ? La loi anti-fraude à la TVA impose trop de contraintes pour gérer soi-même les factures, je précise que je n'ai aucun lien commercial avec ce site, c'est juste que j'ai trouvé là tout ce dont j'avais besoin pour un tarif raisonnable
- gestion complète des commandes
- gestion du catalogue mais sans catégories (on part du principe qu'il y a peu de produits)
- gestion des clients
- gestion des pays d'expédition
- gestion des tarifs postaux (utilisation exclusive de Colissimo)
- ...
Voilà les grandes lignes de ce que je vous avais proposé de réaliser alors et que je vais poursuivre avec cette nouvelle version.
Je vais conserver la technologie utilisée avec Laravel 7. Si vous me suivez ici vous avez remarqué que j'ai opté depuis pas mal de temps maintenant pour Livewire. J'aimais bien Volt mais il se trouve qu'il a disparu Dans Livewire 4. Mais ce n'est pas tout à fait exact, en fait, dans son utilisation par défaut, Livewire adopte la startégie qui était utilisée par Volt.
Installation de Laravel
Dans un premier temps on va installer la dernière version de Laravel :

J'ai répondu non pour le boost de l'IA mais vous pouvez mettre oui si vous voulez. On attend tranquillement que tout se charge, puis :

Vous devriez tomber sur la page d'accueil :

Si ce n'est pas le cas recommencez tout !
Les utilisateurs
On a par défaut une migration pour la table users. Mais on ne va pas utiliser la vérification des emails, donc on supprime la colonne email_verified_at.
D'autre part on ajoute ces colonnes :
- firstname : pour le prénom
- newsletter : pour l'inscription à la lettre d'information
- admin : pour identifier les administrateurs (on n'aura donc que 2 rôles : les clients et les administrateurs)
- last_seen : pour connaître la date de la dernière visite
Ce qui nous donne cette migration :
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('firstname');
$table->string('email')->unique();
$table->boolean('newsletter');
$table->boolean('admin')->default(false);
$table->timestamp('last_seen')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
On va aussi compléter UserFactory en conséquence :
public function definition(): array
{
return [
'name' => fake()->lastName,
'firstname' => fake()->firstName,
'email' => fake()->unique()->safeEmail,
'password' => static::$password ??= Hash::make('password'),
'newsletter' => fake()->boolean(),
'created_at' => fake()->dateTimeBetween('-4 years', '-6 months'),
'remember_token' => Str::random(10),
];
}
Pour simplifier on aura le même mot de passe pour tous les utilisateurs générés : password. Les dates de création seront lissées dans le temps pour plus de réalisme.
Dans le modèle on complète l'attribut Fillable :
#[Fillable(['name', 'email', 'password', 'first_name', 'last_name', 'last_seen'])]
Les pays
Comme on peut expédier dans plusieurs pays il y a des différences au niveau des frais de port et des taxes. On a donc besoin d'une table (et d'un modèle associé) pour les pays :
php artisan make:model Country -m


On va se contenter de deux colonnes (nom et taxe) :
Schema::create('countries', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->decimal('tax');
$table->boolean('requires_siren')->default(false);
});
Et dans le modèle l'attribut Fillable :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'tax', 'requires_siren'])]
class Country extends Model
{
public $timestamps = false;
}
On met la propriété $timestamps à false parce qu'on ne met pas les colonnes des dates. On va aussi se passer de factory pour cette table.
On va avoir des taux de TVA différents selon le pays d'expédition. D'autre part, en prévision de la facturation électronique, on prévoit un champ pour savoir si le SIREN ou SIRET est obligatoire pour le pays.
Les adresses
Les utilisateurs pourront définir plusieurs adresses (facturation, expédition...). On crée donc une table et un modèle :
php artisan make:model Address -m


On va avoir besoin de pas mal de colonnes :
Schema::create('addresses', function (Blueprint $table) {
$table->id();
$table->boolean('professionnal')->default(false);
$table->enum('civility', ['Mme', 'M.']);
$table->string('name', 100)->nullable();
$table->string('firstname', 100)->nullable();
$table->string('company', 100)->nullable();
$table->string('buyer_tax_no', 20)->nullable();
$table->string('siren', 14)->nullable();
$table->string('address');
$table->string('addressbis')->nullable();
$table->string('bp', 100)->nullable();
$table->string('postal', 10);
$table->string('city', 100);
$table->string('phone', 25);
$table->timestamps();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('country_id')->constrained()->onDelete('cascade');
});
Voyons ça en détail :
- professionnal : indique si c'est une adresse professionnelle (dans ce cas nom et prénoms sont facultatifs mais company obligatoire)
- civility : monsieur ou madame (optionnel)
- name : le nom (optionnel si adresse professionnelle)
- firstname : le prénom (optionnel si adresse professionnelle)
- company : la société (que si adresse professionnelle)
- buyer_tax_no : le numéro de TVA de l'acheteur professionnel
- siren : le SIREN ou SIRET de l'acheteur professionnel
- address : l'adresse
- addressbis : complément optionnel de l'adresse
- bp : boîte postale optionnelle
- postal : code postal
- city : ville
- phone : téléphone
On a aussi deux clés étrangères pour les relations avec l'utilisateur et le pays. On va créer une factory pour créer des adresses :
php artisan make:factory AddressFactory

Avec ce code qui permet de générer des adresses assez variées :
<?php
namespace Database\Factories;
use App\Models\Address;
use Illuminate\Database\Eloquent\Factories\Factory;
use Random\RandomException;
/**
* @extends Factory<Address>
*/
class AddressFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
* @throws RandomException
*/
public function definition(): array
{
$professionnal = fake()->boolean();
$countryId = mt_rand(1, 4); // 1=France, 2=Belgique, 3=Suisse, 4=Canada
// Génération du nom (personne ou entreprise)
$name = !$professionnal || fake()->boolean() ? fake()->lastName : null;
$firstName = !$professionnal || fake()->boolean() ? fake()->firstName : null;
// Génération du numéro de TVA selon le pays
$buyerTaxNo = null;
if ($professionnal) {
switch ($countryId) {
case 1: // France
$buyerTaxNo = 'FR' . str_pad(random_int(0, 99), 2, '0', STR_PAD_LEFT) . ' ' . str_pad(random_int(0, 999999999), 9, '0', STR_PAD_LEFT);
break;
case 2: // Belgique
$buyerTaxNo = 'BE ' . str_pad(random_int(0, 999), 3, '0', STR_PAD_LEFT) . '.' . str_pad(random_int(0, 999), 3, '0', STR_PAD_LEFT) . '.' . str_pad(random_int(0, 999), 3, '0', STR_PAD_LEFT);
break;
case 3: // Suisse
$buyerTaxNo = 'CHE-' . str_pad(random_int(0, 999999999), 9, '0', STR_PAD_LEFT) . ' TVA';
break;
case 4: // Canada
$buyerTaxNo = 'CA' . str_pad(random_int(0, 999999999), 9, '0', STR_PAD_LEFT);
break;
}
}
// Génération du SIREN/SIRET ou équivalent pour la France
$siren = null;
if ($professionnal && $countryId == 1) {
$siren = str_pad(random_int(0, 999999999), 9, '0', STR_PAD_LEFT);
}
return [
'professionnal' => $professionnal,
'civility' => fake()->boolean() ? 'Mme' : 'M.',
'name' => $name,
'firstname' => $firstName,
'company' => $professionnal ? fake()->company : null,
'buyer_tax_no' => $buyerTaxNo,
'siren' => $siren,
'address' => fake()->streetAddress,
'addressbis' => fake()->boolean() ? fake()->secondaryAddress : null,
'bp' => fake()->boolean() ? fake()->numberBetween(100, 900) : null,
'postal' => fake()->numberBetween(10000, 90000),
'city' => fake()->city,
'country_id' => $countryId,
'phone' => fake()->numberBetween(1000000000, 9000000000),
];
}
}
Il faut renseigner l'attribut Fillable dans le modèle Address. Mais aussi signaler qu'on utilise une factory :
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'firstname', 'professionnal', 'buyer_tax_no', 'siren', 'civility', 'company', 'address', 'addressbis', 'bp', 'postal', 'city', 'phone', 'country_id'])]
class Address extends Model
{
use HasFactory;
}
Les produits
Évidemment on va avoir des produits :
php artisan make:model Product -m


On prévoit ces colonnes :
- name : le nom
- price : le prix TTC
- weight : le poids pour le calcul des frais de port
- active : pour savoir si le produit est actif et doit être proposé à la vente
- quantity : la quantité disponible
- quantity_alert : la quantité minimum qui déclenche une alerte à l'administrateur
- image : le nom de l'image du produit qui complètera le lien vers l'image
- description : la description du produit
Ce qui donne :
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('price');
$table->decimal('weight');
$table->boolean('active')->default(false);
$table->integer('quantity')->defaut(0);
$table->integer('quantity_alert')->default(10);
$table->string('image')->nullable();
$table->text('description');
$table->timestamps();
});
Et l'attribut Fillable dans le modèle Product :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'price', 'quantity', 'weight', 'active', 'quantity_alert', 'image', 'description'])]
class Product extends Model
Les états
Une commande passe par plusieurs états, on crée donc aussi une table (et un modèle associé) :
php artisan make:model State -m


Avec 4 colonnes :
Schema::create('states', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('slug', 50);
$table->string('color', 20);
$table->integer('indice');
});
Voyons ça :
- name : le nom de l'état à afficher
- slug : pour référencer l'état hors affichage
- color : un code couleur pour renforcer l'affichage de l'état
- indice : le degré de l'état. Selon l'état on ne disposera que d'un certain nombre d'autres états possibles. Par exemple si on a déjà confirmé un paiement, on ne pourra pas changer de mode de paiement. Cet indice va simplifier le codage.
Et l'attribut Fillable dans le modèle State :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'slug', 'color', 'indice'])]
class State extends Model
On met la propriété $timestamps à false parce qu'on ne met pas les colonnes des dates. On ne crée pas de factory pour cette table.
Les commandes
Les commandes constituent la partie la plus importante de l'application :
php artisan make:model Order -m


On prévoie ces colonnes :
- reference : une référence unique pour la commande
- shipping : les frais de port
- total : le coût total TTC des produits
- tax : le taux de TVA appliqué aux produits (on ne prévoit pas de taux par produit)
- payment : le mode de paiement (carte, mandat, virement ou chèque)
- purchase_order : le numéro éventuel du bon de commande
- pick : si le produit est récupéré sur place
- invoice_id : l'identifiant de la facture (pour pouvoir la récupérer avec l'API)
- invoice_number : le numéro de la facture
On va aussi prévoir deux clés étrangères pour les utilisateurs et les états.
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('reference', 8);
$table->decimal('shipping');
$table->decimal('total');
$table->decimal('tax');
$table->enum('payment', [
'carte',
'mandat',
'virement',
'cheque'
]);
$table->string('purchase_order', 100)->nullable();
$table->boolean('pick')->default(false);
$table->integer('invoice_id')->nullable();
$table->string('invoice_number', 40)->nullable();
$table->foreignId('state_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
});
Dans le modèle on ajoute l'attribut Fillable et on indique la factory :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['shipping', 'tax', 'user_id', 'state_id', 'payment', 'reference', 'pick', 'total'])]
class Order extends Model
{
use HasFactory;
}
On ajoute la factory pour disposer de commande d'exemple :
php artisan make:factory OrderFactory
public function definition(): array
{
$pick = fake()->boolean();
$payment = ['carte', 'mandat', 'virement', 'cheque'][mt_rand(0, 3)];
if($payment === 'carte') {
$state_id = [4, 5, 6, 8, 9, 10][mt_rand(0, 5)];
} else if($payment === 'mandat') {
$state_id = [2, 6, 7, 8, 9, 10][mt_rand(0, 5)];
if($state_id > 6) {
$purchaseOrder = str()->random(6);
}
} else if($payment === 'virement') {
$state_id = [3, 6, 8, 9, 10][mt_rand(0, 4)];
} else if($payment === 'cheque') {
$state_id = [1, 6, 8, 9, 10][mt_rand(0, 4)];
}
if($payment === 'carte' && in_array($state_id, [8, 9, 10])) {
$invoice_id = fake()->numberBetween(10000, 90000);
$invoice_number = str()->random(6);
} else {
$invoice_id = null;
$invoice_number = null;
}
return [
'reference' => strtoupper(str()->random(8)),
'shipping' => $pick ? 0 : mt_rand (500, 1500) / 100,
'payment' => $payment,
'state_id' => $state_id,
'user_id' => mt_rand(1, 20),
'purchase_order' => $purchaseOrder ?? null,
'pick' => $pick,
'total' => 0,
'tax' => [0, .2][mt_rand(0, 1)],
'invoice_id' => $invoice_id,
'invoice_number' => $invoice_number,
'created_at' => fake()->dateTimeBetween('-2 years'),
];
}
Le code est assez chargé pour obtenir un minimum de cohérence, en particulier selon le mode de paiment et les états. Ca deviendra plus compréhensible quand on verra la population des tables.
Les adresses pour les commandes
Pour chaque commande on va avoir une ou deux adresses (s'il y a une adresse spécifique pour la livraison distincte de celle de la facturation). On ne peut pas se contenter de créer une relation avec la table des adresses parce que celles-ci peuvent changer ensuite et on doit figer les choses pour la commande et surtout la facture.
En conséquence on crée une table pour mémoriser ces adresses :
php artisan make:model OrderAddress -m


Les colonnes vont faire écho à celles de la table addresses :
Schema::create('order_addresses', function (Blueprint $table) {
$table->id();
$table->boolean('facturation')->default(true);
$table->boolean('professionnal')->default(false);
$table->enum('civility', ['Mme', 'M.']);
$table->string('name', 100)->nullable();
$table->string('firstname', 100)->nullable();
$table->string('company', 100)->nullable();
$table->string('buyer_tax_no', 20)->nullable();
$table->string('siren', 14)->nullable();
$table->string('address');
$table->string('addressbis')->nullable();
$table->string('bp', 100)->nullable();
$table->string('postal', 10);
$table->string('city', 100);
$table->string('phone', 25);
$table->timestamps();
$table->foreignId('order_id')->constrained()->onDelete('cascade');
$table->foreignId('country_id')->constrained()->onDelete('cascade');
});
La différence c'est qu'elle est liée à une commande au lieu d'un utilisateur. On ajoute l'attribut Fillable au modèle OderAddress :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'firstname', 'professionnal', 'buyer_tax_no', 'siren', 'civility', 'company', 'address', 'addressbis', 'bp', 'postal', 'city', 'phone', 'country_id', 'facturation'])]
class OrderAddress extends Model
On va se passer de factory pour cette table.
Les produits pour les commandes
Pour les produits des commandes on a le même raisonnement que pour les adresses, on ne peut pas se contenter d'une relation avec la table des produits parce qu'il peut y avoir des changements ultérieurs et on doit figer les choses. D'autre part, on doit mémoriser le prix total et la quantité commandée.
Alors on crée une table et un modèle :
php artisan make:model OrderProduct -m


Avec ces colonnes :
- name : le nom du produit
- total_price_gross : le prix TTC total en tenant compte de la quantité
- quantité : la quantité commandée pour ce produit
Schema::create('order_products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('total_price_gross');
$table->integer('quantity');
$table->timestamps();
$table->foreignId('order_id')->constrained()->onDelete('cascade');
});
Et évidemment on a la clé étrangère pour la liaison avec la commande. On complète la propriété $fillable du modèle OderProduct :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'total_price_gross', 'quantity'])]
class OrderProduct extends Model
Les frais de port
Le poids
Pour les frais de port on va avoir deux tables. Une première pour définir des plages de poids :
php artisan make:model Range -m


Avec juste une colonne pour le poids maximal :
Schema::create('ranges', function (Blueprint $table) {
$table->id();
$table->decimal('max');
});
Et dans le modèle Range :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['max'])]
class Range extends Model
{
public $timestamps = false;
}
On et la propriété $timestamps à false parce qu'on ne met pas les colonnes des dates.
Colissimo
On crée une table pour les tarifs Colissimo :
php artisan make:model Colissimo -m


Avec ces colonnes :
- price : le prix
- country_id : la clé étrangère pour le pays
- range_id : la clé étrangère pour la plage de poids
Schema::create('colissimos', function (Blueprint $table) {
$table->id();
$table->decimal('price');
$table->foreignId('country_id')->constrained()->onDelete('cascade');
$table->foreignId('range_id')->constrained()->onDelete('cascade');
});
Et évidemment dans le modèle Colissimo :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['price', 'country_id', 'range_id'])]
class Colissimo extends Model
La boutique
On doit aussi prévoir une table (et un modèle) pour tous les renseignements concernant la boutique :
php artisan make:model Shop -m


On aura de nombreuses colonnes (et a priori une seule ligne puisque une seule boutique) :
- name : nom de la boutique
- address : adresse de la boutique
- holder : nom officiel de la boutique
- email : email de contact
- bic : BIC pour les virements
- iban : l'IBAN pour les virements
- bank : nom de la banque
- bank_address : adresse de la banque
- phone : téléphone de contact
- facebook : lien vers la page Facebook
- linkedin : lien éventuel vers la page Linkedin
- home : texte pour la page d'accueil
- home_infos : texte d'information pour la page d'accueil
- home_shipping : texte d'information sur les frais de port pour la page d'accueil
- invoice : si la boutique doit générer des factures
- card : si la boutique accepte le paiement par carte bancaire
- transfer : si la boutique accepte le paiement par virement
- check : si la boutique accepte le paiement par chèque
- mandat : si la boutique accepte le paiement par mandat administratif
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('address');
$table->string('holder');
$table->string('email');
$table->string('bic');
$table->string('iban');
$table->string('bank');
$table->string('bank_address');
$table->string('phone', 25);
$table->string('facebook');
$table->string('linkedin')->nullable();
$table->text('home');
$table->text('home_infos');
$table->text('home_shipping');
$table->boolean('invoice')->default(true);
$table->boolean('card')->default(true);
$table->boolean('transfer')->default(true);
$table->boolean('check')->default(true);
$table->boolean('mandat')->default(true);
});
On crée aussi une factory :
php artisan make:factory ShopFactory

public function definition(): array
{
return [
'name' => fake()->sentence(2),
'address' => fake()->address,
'email' => fake()->email,
'phone' => fake()->phoneNumber,
'holder' => strtoupper(fake()->sentence(3)),
'bic' => strtoupper(str()->random(8)),
'iban' => fake()->iban,
'bank' => fake()->sentence(2),
'bank_address' => fake()->address,
'facebook' => fake()->url,
'home' => fake()->sentence(3),
'home_infos' => fake()->text,
'home_shipping' => fake()->text,
];
}
Et le modèle Shop :
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['name', 'address', 'email', 'holder', 'bic', 'iban', 'bank', 'bank_address', 'phone', 'facebook', 'linkedin', 'home', 'home_infos', 'home_shipping', 'invoice', 'card', 'transfer', 'check', 'mandat'])]
class Shop extends Model
{
use HasFactory;
public $timestamps = false;
}
Les pages
On va avoir des pages d'information (conditions de vente, mentions légale...). On crée donc une table pour mémoriser le contenu de ces pages :
php artisan make:model Page -m


On se contente de 3 colonnes classiques :
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('slug');
$table->string('title');
$table->text('text');
});
Et dans le modèle Page :
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['title', 'slug', 'text'])]
class Page extends Model
{
use HasFactory;
public $timestamps = false;
}
On crée une factory :
php artisan make:factory PageFactory
Avec ce code :
public function definition(): array
{
return [
'text' => fake()->paragraph(10),
];
}
Les paiements
Il ne nous reste plus qu'à prévoir une table pour mémoriser les identifiants de paiement des commandes dans le cas de paiement par carte bancaire :
php artisan make:model Payment -m


Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('payment_id');
$table->foreignId('order_id')->constrained()->onDelete('cascade');
});
Et dans le modèle Payment :
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['payment_id', 'order_id'])]
class Payment extends Model
Les notifications
Comme on va utiliser le système de notifications de Laravel on va aussi créer la table correspondante :
php artisan make:notifications-table

Là c'est Laravel qui s'occupera de tout !
Conclusion
On a maintenant toutes les tables et les factories nécessaires pour avoir des données d'exemple. Dans le prochain article on mettra en place les relations, quelques accessors et mutators et on complètera avec une population complète de la boutique. On aura ainsi tout en place pour commencer à coder.
Par bestmomo
Aucun commentaire