Merge branch 'develop' into trunk

This commit is contained in:
Vincent BRIDIER 2025-08-07 18:08:21 +02:00
commit 83c4e4d7c4
No known key found for this signature in database
GPG key ID: 8571BFA20420D512
22 changed files with 266 additions and 59 deletions

View file

@ -180,13 +180,13 @@ class Handler extends ExceptionHandler
): JsonResponse {
if (Helper::isRequestForApiV2($request)) {
return (new V2ApiResponse(
ApiStatus::KO,
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO
))->toResponse($request)->setStatusCode($httpCode);
}
return (new V4ApiResponse(
ApiStatus::KO,
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO
))->toResponse($request)->setStatusCode($httpCode);
}
}

View file

@ -36,7 +36,7 @@ class OptinController extends Controller
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
$q->whereKey($service->brand->getKey());
})
->firstOrNew();

View file

@ -154,6 +154,7 @@ class AnswerController extends Controller
*/
public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse
{
$answer->singleOptins()->detach();
if ($answer->deleteOrFail()) {
return redirect()->route('bo.questions.answers.index', $question)
->with('success', trans(

View file

@ -31,11 +31,11 @@ class SingleOptinController extends Controller
$query = SingleOptin::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
fn($q) => $q->whereHas('brand', fn($q) => $q->whereKey($request->brand)),
)
->when(
$request->filled('answer'),
fn($q) => $q->whereHas('answers', fn($q) => $q->where('id', $request->answer))
fn($q) => $q->whereHas('answers', fn($q) => $q->whereKey($request->answer)),
);
if ($request->filled('search')) {
@ -57,7 +57,7 @@ class SingleOptinController extends Controller
$questionModels = Question::getAllModelsFromBrand($request->brand);
if ($request->filled('question')) {
$answerModels = Answer::getAllModelsFromQuestion($request->question);
$answerModels = Answer::getAllAnswersFromQuestion($request->question);
}
}
@ -78,6 +78,7 @@ class SingleOptinController extends Controller
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
$singleoptin->answers()->detach();
if ($singleoptin->deleteOrFail()) {
return redirect()->route('bo.single_optins.index')
->with('success', trans(

View file

@ -57,9 +57,8 @@ class ApiResponse
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
if (Helper::isRequestForApiV2($request)) {
return $response->setData(new V2ApiResponse((object)$data, $status));
return $response->setData(new V2ApiResponse($status, (object)$data));
}
return $response->setData(new V4ApiResponse((object)$data, $status));
return $response->setData(new V4ApiResponse($status, (object)$data));
}
}

View file

@ -31,7 +31,7 @@ class Authenticate extends Middleware
}
}
}
return route('login');
return route('bo.login');
}
return null;

View file

@ -3,9 +3,12 @@
namespace App\Http\Requests\Bo\Brands;
use Illuminate\Foundation\Http\FormRequest;
use Uwil\LaravelCommon\Traits\Requests\HasPicture;
class StoreBrandRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
@ -16,6 +19,16 @@ class StoreBrandRequest extends FormRequest
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->mergePicture('logo');
}
/**
* Get the validation rules that apply to the request.
*
@ -23,9 +36,16 @@ class StoreBrandRequest extends FormRequest
*/
public function rules(): array
{
return [
'name' => 'required|string|min:5|max:255',
$rules = [
'name' => 'required|unique:brands,name|string|min:5|max:255',
'url' => 'required|url|min:5|max:255',
'url_claim' => 'required|url|min:5|max:255',
'domain' => 'required|unique:brands,domain|string|min:5|max:255',
];
return \array_merge(
$rules,
$this->pictureRules('logo', 550, 550, 550, 550),
);
}
/**
@ -36,7 +56,11 @@ class StoreBrandRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom de la marque'),
'name' => trans('validation.attributes.name'),
'logo' => trans('validation.custom.logo'),
'url' => trans('validation.custom.url'),
'url_claim' => trans('validation.custom.url_claim'),
'domain' => trans('validation.custom.domain'),
];
}
}

View file

@ -4,4 +4,34 @@ namespace App\Http\Requests\Bo\Brands;
class UpdateBrandRequest extends StoreBrandRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
/** @var integer $brandModelKey */
$brandModelKey = request()->route()->parameter('brand')->getKey();
$rules = [
'name' => [
'required',
"unique:brands,name,$brandModelKey",
'string',
'min:5',
'max:255',
],
'domain' => [
'required',
"unique:brands,domain,$brandModelKey",
'string',
'min:5',
'max:255',
],
];
return \array_merge(
parent::rules(),
$rules,
);
}
}

View file

@ -16,10 +16,10 @@ class ApiResponse extends JsonResource
protected $status;
/**
* @param \stdClass|null $data
* @param \App\Enums\ApiStatus $status
* @param \stdClass|null $data
*/
public function __construct(stdClass|null $data = null, ApiStatus $status)
public function __construct(ApiStatus $status, stdClass|null $data = null)
{
$this->status = $status;
$this->data = $data;

View file

@ -16,10 +16,10 @@ class ApiResponse extends JsonResource
protected $status;
/**
* @param \stdClass|null $data
* @param \App\Enums\ApiStatus $status
* @param \stdClass|null $data
*/
public function __construct(stdClass|null $data = null, ApiStatus $status)
public function __construct(ApiStatus $status, stdClass|null $data = null)
{
$this->status = $status;
$this->data = $data;
@ -36,8 +36,8 @@ class ApiResponse extends JsonResource
{
//phpcs:enable
return [
'status' => $this->status->value(),
'data' => (array)($this->data ?? []),
'status' => $this->status->value(),
'data' => (array)($this->data ?? []),
'version' => config('api.version')
];
}

View file

@ -49,19 +49,6 @@ class Answer extends Model
// * METHODS
/**
* Get all answers associated to the band.
*
* @param integer $questionModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAllModelsFromQuestion(int $questionModelKey): \Illuminate\Database\Eloquent\Collection
{
return self::getAnswers()->filter(
fn($model) => $model->question->getKey() === $questionModelKey
)->values();
}
/**
* Reset all cache elements related to this model.
*
@ -72,18 +59,22 @@ class Answer extends Model
{
$questionModelKey = $model->question->getKey();
Cache::forget("question-{$questionModelKey}-answers");
self::getAnswers();
self::getAllAnswersFromQuestion($questionModelKey);
}
/**
* Get all answers.
*
* @param integer $questionModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAnswers(): \Illuminate\Database\Eloquent\Collection
public static function getAllAnswersFromQuestion(int $questionModelKey): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember('answers', 86400, function () {
return self::query()->orderBy('answer', 'ASC')->get();
return Cache::remember("question-{$questionModelKey}-answers", 86400, function () use ($questionModelKey) {
return self::query()
->whereHas('question', fn($q) => $q->whereKey($questionModelKey))
->orderBy('answer', 'ASC')
->get();
});
}

View file

@ -7,6 +7,7 @@ use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Uwil\LaravelCommon\Helpers\FileHelper;
/**
* @property integer $id Brand id.
@ -54,14 +55,22 @@ class Brand extends Model
*/
protected static function booted(): void
{
static::creating(function (self $model) {
$model->logo = FileHelper::storeFile($model, $model->logo, true);
});
static::created(function () {
self::resetCache();
});
static::updated(function () {
self::resetCache();
static::updating(function (self $model) {
$model->logo = FileHelper::storeFile($model, $model->logo, true);
});
static::deleted(function () {
static::updated(function (self $model) {
self::resetCache();
FileHelper::removeOldFile($model, 'logo');
});
static::deleted(function (self $model) {
self::resetCache();
FileHelper::removeOldFile($model, 'logo');
});
}

View file

@ -22,10 +22,10 @@ class BrandFactory extends Factory
public function definition(): array
{
return [
'name' => $this->faker->company,
'name' => $this->faker->unique()->company,
'url' => $this->faker->url,
'url_claim' => $this->faker->url,
'domain' => $this->faker->domainName,
'domain' => $this->faker->unique()->domainName,
];
}
}

View file

@ -0,0 +1,52 @@
<?php
use App\Models\Brand;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Uwil\LaravelCommon\Helpers\FileHelper;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
// Set default value.
Brand::query()->get()->map(function (Brand $brandModel) {
$brandModel->logo = FileHelper::storeFile(
$brandModel,
new \SplFileInfo(
\resource_path('../database/factories/assets/brands/default-logo-550x550.jpg')
)
);
$urlExample = "https://example-{$brandModel->getKey()}.com";
$brandModel->url = $urlExample;
$brandModel->url_claim = $urlExample;
$brandModel->domain = parse_url($urlExample, PHP_URL_HOST);
$brandModel->saveOrFail();
});
// Set unique keys.
Schema::table('brands', function (Blueprint $table) {
$table->unique('name', 'brands_name_unique');
$table->unique('domain', 'brands_domain_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('brands', function (Blueprint $table) {
$table->dropUnique('brands_name_unique');
$table->dropUnique('brands_domain_unique');
});
}
};

View file

@ -233,5 +233,9 @@ return [
'type' => 'type',
'status' => 'status',
'label' => 'label',
'domain' => 'domain',
'logo' => 'logo',
'url' => 'url',
'url_claim' => 'claim url',
],
];

View file

@ -233,5 +233,9 @@ return [
'type' => 'type',
'status' => 'status',
'label' => 'label',
'domain' => 'domaine',
'logo' => 'logo',
'url' => 'lien',
'url_claim' => 'lien réclamation',
],
];

View file

@ -29,7 +29,7 @@
</div>
</div>
<div class="card-body">
<form action="{{ route('bo.brands.store') }}" method="POST">
<form action="{{ route('bo.brands.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.brands.form')
<div class="row">

View file

@ -29,7 +29,7 @@
</div>
</div>
<div class="card-body">
<form action="{{ route('bo.brands.update', $brandModel->id) }}" method="POST">
<form action="{{ route('bo.brands.update', $brandModel->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.brands.form')

View file

@ -2,22 +2,111 @@
{{-- TITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ __('Nom') }}</legend>
<legend>{{ str(__('validation.attributes.name'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'brandName',
'label' => __('Nom de la :model', ['model' => Str::singular(__('models.classes.brands'))]),
'label' => __('Nom de la :model', ['model' => str(__('models.classes.brands'))->singular()]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $brandModel ?? null,
'helper' => __('Doit décrire précisément la :model (3 caractères minimum, 255 maximum).', [
'model' => str(__('models.classes.brands'))->singular(),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- DOMAIN --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.custom.domain'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'brandDomain',
'label' => __(':field de la :model', [
'field' => str(__('validation.custom.domain'))->ucfirst(),
'model' => str(__('models.classes.brands'))->singular(),
]),
'hasWordCounter' => true,
'name' => 'domain',
'required' => true,
'model' => $brandModel ?? null,
'placeholder' => parse_url('https://example.com/', PHP_URL_HOST),
'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'),
])
</div>
</div>
</fieldset>
</div>
{{-- URL --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.custom.url'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'brandUrl',
'label' => __(':field de la :model', [
'field' => str(__('validation.custom.url'))->ucfirst(),
'model' => str(__('models.classes.brands'))->singular(),
]),
'hasWordCounter' => true,
'name' => 'url',
'required' => true,
'model' => $brandModel ?? null,
'placeholder' => 'https://example.com/',
'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'),
])
</div>
</div>
</fieldset>
</div>
{{-- URL_CLAIM --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.custom.url_claim'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'brandUrlClaim',
'label' => __(':field de la :model', [
'field' => str(__('validation.custom.url_claim'))->ucfirst(),
'model' => str(__('models.classes.brands'))->singular(),
]),
'hasWordCounter' => true,
'name' => 'url_claim',
'required' => true,
'model' => $brandModel ?? null,
'placeholder' => 'https://example.com/',
'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'),
])
</div>
</div>
</fieldset>
</div>
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.custom.url_claim'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar-picture', [
'id' => 'brandLogo',
'name' => 'logo',
'width' => 550,
'height' => 550,
'model' => $brandModel ?? null,
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
</div>
@push('scripts')

View file

@ -15,17 +15,20 @@
--}}
@php
$data = [
'id' => $id,
'name' => $name,
'helper' => $helper,
'width' => $width,
'height' => $height,
'value' => $model->{$name} ?? $value ?? '',
'preview' => $preview ?? false,
'browseEvent' => $browseEvent ?? false,
'required' => $required ?? false,
];
$data = [
'id' => $id,
'name' => $name,
'helper' => __('crud.minicrud.dimensions_must_be', [
'widthpx' => "{$width}px",
'heightpx' => "{$height}px",
]),
'width' => $width,
'height' => $height,
'value' => $model->{$name} ?? ($value ?? ''),
'preview' => $preview ?? false,
'browseEvent' => $browseEvent ?? false,
'required' => $required ?? false,
];
@endphp
<div class="image-input" data-json='@json($data)'></div>
@include('modules.input-error', ['inputName' => $name, 'helper' => null])

View file

@ -41,7 +41,7 @@ class SingleOptinTest extends ApiBase
/**
* Test set single optin on with answer associated.
* route: apiV2.optin
* route: apiV4.optin
*
* @return void
*/
@ -57,7 +57,7 @@ class SingleOptinTest extends ApiBase
// * Make sure a customer answer is set.
$firstAnswer = $questions->first()->answers->first();
$response = $this->callApiWithToken($service, 'post', route('apiV2.optin'), [
$response = $this->callApiWithToken($service, 'post', route('apiV4.optin'), [
'email' => 'singleoptin@hotmail.fr',
'opt_in' => 1,
'answer_id' => [$firstAnswer->getkey()],
@ -134,7 +134,7 @@ class SingleOptinTest extends ApiBase
public function testSingleOptinShouldFailIfAnswerIdDoesntExist(): void
{
$service = Service::query()->firstOrFail();
$response = $this->callApiWithToken($service, 'post', route('apiV2.optin'), [
$response = $this->callApiWithToken($service, 'post', route('apiV4.optin'), [
'email' => 'singleoptin@hotmail.fr',
'opt_in' => 1,
'answer_id' => [0],