feat: Add unit test to basket v1 api call + refactor basket update + handle empty basket return

This commit is contained in:
Vincent BRIDIER 2025-11-18 18:56:19 +01:00
parent e9fa15d0f8
commit f542100fca
12 changed files with 300 additions and 94 deletions

View file

@ -3,13 +3,10 @@
namespace App\Http\Controllers\Api\v1;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Models\Campagne;
use App\Models\Coupon;
use App\Enums\LogActions;
use App\Http\Controllers\Controller;
use App\Models\Basket;
use App\Models\BasketContent;
use App\Models\Customers;
use App\Traits\CouponTrait;
use App\Traits\ThanksTrait;
@ -128,36 +125,7 @@ class BasketController extends Controller
$customer = Customers::where('hashcode', $request->customer_hashcode)->first();
// Retrieve or create basket
$basket = Basket::where(['campagne_id' => $refcamp, 'customer_id' => $customer->id])->first();
if ($basket) {
$camp = Campagne::where('camp_id', $basket->campagne_id)->first();
foreach ($coupons as $couponCode => $quantity) {
$cp = $camp->coupons()->where('coupon_code', $couponCode)->first();
if ($cp) {
$basketContent = BasketContent::where(['basket_id' => $basket->id, 'coupon_id' => $cp->coupon_id])->first();
if ($basketContent) {
$basketContent->quantity += $quantity;
$basketContent->save();
} else {
$this->addContentToBasket($basket, $cp, $quantity);
}
}
}
} else {
$basket = new Basket();
$basket->uuid = (string) Str::uuid();
$basket->campagne()->associate($refcamp);
$basket->customer()->associate($customer);
$basket->save();
foreach ($coupons as $couponCode => $quantity) {
$cp = $campagne->coupons()->where('coupon_code', $couponCode)->first();
if ($cp) {
$this->addContentToBasket($basket, $cp, $quantity);
}
}
}
$basket = Basket::updateContent($campagne, $coupons, $customer);
// Log success
$this->writeLogSuccessBasketCreated(
@ -173,15 +141,4 @@ class BasketController extends Controller
'Panier' => $basket
]);
}
private function addContentToBasket(Basket $basket, Coupon $coupon, int $quantity): void
{
if ($quantity > 0) {
$newBasketContent = new BasketContent();
$newBasketContent->basket_id = $basket->id;
$newBasketContent->coupon_id = $coupon->coupon_id;
$newBasketContent->quantity = $quantity;
$newBasketContent->save();
}
}
}

View file

@ -9,8 +9,6 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Coupons\PrintRequest;
use App\Http\Resources\StatusResource;
use App\Models\Customers;
use App\Models\PrintedCoupons;
use App\Models\Statistique;
use App\Traits\CouponTrait;
use App\Traits\GenerateBR;
use PDF;

View file

@ -22,6 +22,10 @@ class BasketController extends Controller
return $content->quantity - $content->printed;
});
if (!$totalQuantity) {
return view('fo.basket.empty', compact('basket'));
}
return view('fo.basket.show', compact('basket', 'totalQuantity'));
}
@ -78,6 +82,10 @@ class BasketController extends Controller
}
}
if (empty($coupons)) {
return redirect()->route('fo.basket.show', $basket);
}
// $coupons contains all printed coupons
$pdf = PDF::loadView('pdf.coupons', ['coupons' => $coupons]);
return $pdf->download(sprintf("coupons_%s_%s_%s.pdf", $crmInfos->firstname, $crmInfos->lastname, date('d-m-Y')));

View file

@ -7,11 +7,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Basket extends Model
{
use HasFactory;
protected $fillable = [
'campagne_id'
];
/**
* Customer model relation
* @return BelongsTo
@ -38,4 +43,40 @@ class Basket extends Model
{
return $this->hasMany(BasketContent::class);
}
public static function updateContent(Campagne $campagne, array $coupons, Customers $customer)
{
$basket = self::where(['campagne_id' => $campagne->camp_id, 'customer_id' => $customer->id])->first();
if (!$basket) {
$basket = new Basket();
$basket->uuid = (string) Str::uuid();
$basket->campagne()->associate($campagne);
$basket->customer()->associate($customer);
$basket->save();
}
foreach ($coupons as $couponCode => $quantity) {
$associatedCoupon = $campagne->coupons()->where('coupon_code', $couponCode)->first();
if ($associatedCoupon) {
$basketContent = BasketContent::where(['basket_id' => $basket->id, 'coupon_id' => $associatedCoupon->coupon_id])->first();
if ($basketContent) {
$basketContent->quantity += $quantity;
$basketContent->save();
} else {
if ($quantity > 0) {
$newBasketContent = new BasketContent();
$newBasketContent->basket_id = $basket->id;
$newBasketContent->coupon_id = $associatedCoupon->coupon_id;
$newBasketContent->quantity = $quantity;
$newBasketContent->save();
}
}
}
}
return $basket;
}
}

View file

@ -3,11 +3,14 @@
namespace App\Models;
use App\Models\Panier;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Customers extends Model
{
use HasFactory;
/**
* @var string $primaryKey
*/

View file

@ -26,26 +26,26 @@ trait LogTrait
/**
* Write error log when there's a wrong or missing parameter in the request.
*
* @param string $refcamp
* @param string $refadherent
* @param string $refsite
* @param string $panier
* @param string|null $refcamp
* @param string|null $refadherent
* @param string|null $refsite
* @param string|null $panier
* @return void
*/
public function writeLogErrorWrongRequestParameters(
string $refcamp,
string $refadherent,
string $refsite,
string $panier
$refcamp,
$refadherent,
$refsite,
$panier
) {
$log = new Log();
$log->log_action = $this->logAction->value;
$log->log_source = $this->logSource;
$log->log_params = json_encode([
'refcamp' => $refcamp,
'refadherent' => $refadherent,
'refsite' => $refsite,
'panier' => $panier
'refcamp' => $refcamp ?? 'NO DATA',
'refadherent' => $refadherent ?? 'NO DATA',
'refsite' => $refsite ?? 'NO DATA',
'panier' => $panier ?? 'NO DATA'
]);
$log->log_statut = 'NOK';
$log->log_message = "Des paramètres de la requête sont mal renseignés ou non présents";

View file

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CustomersFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'id' => $this->faker->randomDigit(),
'hashcode' => $this->faker->uuid(),
'email' => $this->faker->unique()->safeEmail(),
'country' => $this->faker->randomElement([56,442, 250]), // ISO Country value for Belgium, Luxembourg and France
'created_at' => now(),
'updated_at' => now(),
];
}
}

View file

@ -0,0 +1,12 @@
@extends('fo.layout')
@section('title', $basket->campagne->site->site_libelle . ' - ' . __('Bons de Réduction'))
@section('content')
<div class="container mt-5 col-6">
<div class="text-center mb-4">
<img src="{{ asset($basket->campagne->site->site_logo) }}" alt="{{ $basket->campagne->site->site_libelle }}" class="img-fluid" style="max-width: 340px;">
<br><br>
<h1>{{ __('Coupon(s) non disponible') }}</h1>
</div>
</div>
@endsection

View file

@ -1,28 +1,22 @@
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<title>{{ $basket->campagne->site->site_libelle }} - {{ __('Bons de Réduction') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5 col-6">
<div class="text-center mb-4">
<img src="{{ asset($basket->campagne->site->site_logo) }}" alt="{{ $basket->campagne->site->site_libelle }}" class="img-fluid" style="max-width: 340px;">
</div>
@extends('fo.layout')
@section('title', $basket->campagne->site->site_libelle . ' - ' . __('Bons de Réduction'))
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title">Nombre de coupons :</h5>
<p class="card-text fs-3 fw-bold text-primary">{{ $totalQuantity }}</p>
</div>
@section('content')
<div class="container mt-5 col-6">
<div class="text-center mb-4">
<img src="{{ asset($basket->campagne->site->site_logo) }}" alt="{{ $basket->campagne->site->site_libelle }}" class="img-fluid" style="max-width: 340px;">
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title">Nombre de coupons :</h5>
<p class="card-text fs-3 fw-bold text-primary">{{ $totalQuantity }}</p>
</div>
@if($totalQuantity)
<div class="mb-3">
<h4>Liste des coupons :</h4>
<ul class="list-group">
</div>
@if($totalQuantity)
<div class="mb-3">
<h4>Liste des coupons :</h4>
<ul class="list-group">
@foreach ($basket->contents as $content)
@php
$remain = $content->quantity - $content->printed;
@ -34,16 +28,13 @@
</li>
@endif
@endforeach
</ul>
</div>
<div class="text-center">
<a class="btn btn-primary" href="{{ route('fo.basket.print', $basket) }}">{{ __('Récupérer') }}</a>
</div>
@endif
</ul>
</div>
<!-- Bootstrap JS (optionnel) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<div class="text-center">
<a class="btn btn-primary" href="{{ route('fo.basket.print', $basket) }}">{{ __('Récupérer') }}</a>
</div>
@endif
</div>
@endsection

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<title>@yield('title')</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
@yield('content')
<!-- Bootstrap JS (optionnel) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -11,11 +11,14 @@
|
*/
use Illuminate\Support\Facades\Route;
Route::prefix('v1')
->name('api.v1.')
->group(function () {
Route::get('/customer/mur', 'Api\v1\CustomerController@mur');
Route::get('/customer/basket', 'Api\v1\CustomerController@basket');
Route::get('/customer/basket/update', 'Api\v1\BasketController@updateBasket');
Route::get('/customer/basket/update', 'Api\v1\BasketController@updateBasket')->name('basket.update');
Route::resource('customer', 'Api\v1\CustomerController');
Route::get('/mur', 'Api\v1\MurController@show');
Route::get('/coupons/print', 'Api\v1\CouponsController@print');

View file

@ -0,0 +1,153 @@
<?php
namespace Tests\Feature\Api\V1;
use App\Models\Basket;
use App\Models\Campagne;
use App\Models\Coupon;
use App\Models\Customers;
use Illuminate\Support\Str;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class BasketTest extends TestCase
{
use RefreshDatabase;
public function test_updateBasket_missingRequiredParameters_returnsError()
{
$enpoint = route('api.v1.basket.update');
$response = $this->json('GET', $enpoint, [
// missing required params intentionally
]);
$response->assertStatus(200)
->assertJson([
'Statut' => 'KO',
'Code' => 101,
'Message' => 'Des paramètres de la requête sont mal renseignés ou non présents',
]);
}
public function test_updateBasket_invalidCampaign_returnsError()
{
$enpoint = route('api.v1.basket.update');
$response = $this->json('GET', $enpoint, [
'customer_hashcode' => 'hash123',
'refcamp' => 999999, // non existent campaign id
'refsite' => 1,
'panier' => '1)1000',
]);
$response->assertStatus(200)
->assertJson([
'Statut' => 'KO',
'Code' => 103,
'Message' => "La campagne n'existe pas ou n'est pas un panier",
]);
}
public function test_updateBasket_successfulBasketCreation_returnsSuccess()
{
$enpoint = route('api.v1.basket.update');
// Create a fake campaign
$campaign = Campagne::factory()->create([
'camp_published' => 1,
'camp_type' => 1,
'camp_siteId' => 1,
]);
// Create a fake customer
$customer = Customers::factory()->create([
'hashcode' => 'hash123',
]);
// Create a coupon in the campaign
$coupon = Coupon::factory()->create();
$campaign->coupons()->attach($coupon);
$response = $this->json('GET', $enpoint, [
'customer_hashcode' => 'hash123',
'refcamp' => $campaign->camp_id,
'refsite' => $campaign->camp_siteId,
'panier' => sprintf("(%d)%s", 2, $coupon->coupon_code),
]);
$response->assertStatus(200)
->assertJson([
'Statut' => 'OK',
]);
// Assert basket created for the customer and campaign
$this->assertDatabaseHas('baskets', [
'campagne_id' => $campaign->camp_id,
'customer_id' => $customer->id,
]);
$basket = Basket::where(['campagne_id' => $campaign->camp_id, 'customer_id' => $customer->id])->first();
$this->assertDatabaseHas('basket_contents', [
'quantity' => 2,
'printed' => 0,
'basket_id' => $basket->id,
'coupon_id' => $coupon->coupon_id
]);
}
public function test_updateBasket_successfulBasketUpdate_returnsSuccess()
{
$enpoint = route('api.v1.basket.update');
// Create a fake campaign
$campaign = Campagne::factory()->create([
'camp_published' => 1,
'camp_type' => 1,
'camp_siteId' => 1,
]);
// Create a fake customer
$customer = Customers::factory()->create([
'hashcode' => 'hash123',
]);
// Create a coupon in the campaign
$coupon = Coupon::factory()->create();
$campaign->coupons()->attach($coupon);
// Create a basket
$basket = new Basket();
$basket->uuid = (string) Str::uuid();
$basket->campagne()->associate($campaign);
$basket->customer()->associate($customer);
$basket->save();
$response = $this->json('GET', $enpoint, [
'customer_hashcode' => 'hash123',
'refcamp' => $campaign->camp_id,
'refsite' => $campaign->camp_siteId,
'panier' => sprintf("(%d)%s", 2, $coupon->coupon_code),
]);
$response->assertStatus(200)
->assertJson([
'Statut' => 'OK',
]);
// Assert basket created for the customer and campaign
$this->assertDatabaseHas('baskets', [
'campagne_id' => $campaign->camp_id,
'customer_id' => $customer->id,
]);
$this->assertDatabaseHas('basket_contents', [
'quantity' => 2,
'printed' => 0,
'basket_id' => $basket->id,
'coupon_id' => $coupon->coupon_id
]);
}
}