Compare commits
No commits in common. "develop" and "massive-bump" have entirely different histories.
develop
...
massive-bu
191 changed files with 1584 additions and 4192 deletions
|
|
@ -15,7 +15,7 @@ include:
|
|||
file: "build/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/docker-multistage.yml"
|
||||
file: "build/docker.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "test/test.yml"
|
||||
|
|
@ -313,16 +313,3 @@ npm-build:
|
|||
script:
|
||||
- echo "Frontend is built directly within Docker multi-stage build"
|
||||
|
||||
npm-security:
|
||||
stage: security
|
||||
script:
|
||||
- echo "Npm security would run bun security"
|
||||
dependencies:
|
||||
- bun-security
|
||||
needs:
|
||||
- job: bun-security
|
||||
optional: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
ARG PHP_8_5_TAG=latest
|
||||
|
||||
FROM composer:2.9 AS vendor
|
||||
|
||||
ARG GITLAB_USER
|
||||
|
|
@ -10,7 +8,7 @@ COPY database/ database/
|
|||
COPY composer.json composer.json
|
||||
COPY composer.lock composer.lock
|
||||
|
||||
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
|
||||
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
|
||||
RUN composer install \
|
||||
--ignore-platform-reqs \
|
||||
--no-interaction \
|
||||
|
|
@ -37,13 +35,13 @@ RUN echo "@ldc-web:registry=https://lab.s-web.fr/api/v4/packages/npm/" >> .npmrc
|
|||
|
||||
RUN bun install --frozen-lockfile && bun run prod
|
||||
|
||||
ARG PHP_8_5_TAG
|
||||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
|
||||
|
||||
ARG USER=www-data
|
||||
|
||||
COPY --chown=${USER}:${USER} . /app/srcs
|
||||
COPY --chown=${USER}:${USER} ./resources/favicon/ /app/srcs/public/
|
||||
COPY --chown=${USER}:${USER} ./resources/favicon/front/ /app/srcs/public/
|
||||
COPY --chown=${USER}:${USER} ./resources/favicon/back/ /app/srcs/public/back/
|
||||
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
|
||||
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
|
||||
|
||||
|
|
|
|||
51
RGPD.md
51
RGPD.md
|
|
@ -2,17 +2,16 @@
|
|||
|
||||
## Site
|
||||
|
||||
|
||||
### En base de données
|
||||
|
||||
| Table | Colonne | Contenu | Utilisation |
|
||||
| ----------- | -------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
|
||||
| customers | email | Adresse email | Identifiant du client |
|
||||
| customers | hashcode | Code unique de l'utilisateur en CRM | Identifiant du client en CRM |
|
||||
| customers | created_at | Date | Suivi d'activité |
|
||||
| customers | updated_at | Date | Suivi d'activité |
|
||||
| customers | last_login_at | Date | Suivi de la dernière connexion au compte client |
|
||||
| --- | --- | --- | --- |
|
||||
| Table | Colonne | Contenu | Utilisation |
|
||||
| ----------- | ------------------ | ----------------------------------------------- | -------------------------------------------------------- |
|
||||
| customers | email | Adresse email | Identifiant du client |
|
||||
| customers | hashcode | Code unique de l'utilisateur en CRM | Identifiant du client en CRM |
|
||||
| customers | created_at | Date | Suivi d'activité |
|
||||
| customers | updated_at | Date | Suivi d'activité |
|
||||
| customers | last_login_at | Date | Suivi de la dernière connexion au compte client |
|
||||
| --- | --- | --- | --- |
|
||||
| orders | employee.matricule | Matricule de l'employé | Identifiant unique de l'employé lié à la commande |
|
||||
| orders | employee.firstname | Prénom de l'employé | Affichage et personnalisation de la commande |
|
||||
| orders | employee.lastname | Nom de l'employé | Affichage et personnalisation de la commande |
|
||||
|
|
@ -27,23 +26,23 @@
|
|||
| orders | delivery_address.latitude | Latitude de l'adresse de livraison | Géolocalisation pour optimiser la livraison |
|
||||
| orders | delivery_address.longitude | Longitude de l'adresse de livraison | Géolocalisation pour optimiser la livraison |
|
||||
| orders | employee_matricule | Matricule de l'employé (colonne virtuelle) | Indexation et recherche rapide des commandes par employé |
|
||||
| --- | --- | --- | --- |
|
||||
| order_lines | product_id | Identifiant du produit présent dans la commande | Détail de la commande |
|
||||
| order_lines | quantité | Quantité du produit présent dans la commande | Détail de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| employees | customer_id | Identifiant client | Permet de relier un salarié à un compte client du site |
|
||||
| employees | matricule | Matricule | Matricule du salarié |
|
||||
| employees | nom | Nom | Nom du salarié |
|
||||
| employees | prenom | Prénom | Prénom du salarié |
|
||||
| employees | start_date | Date de début du contrat | Activation du compte |
|
||||
| employees | end_date | Date de fin du contrat | Désactivation du compte |
|
||||
| employees | etablissement | Identifiant établissement | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | site | Identifiant site | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | service | Identifiant service | Localiser l'employé pour le dépôt de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| users | first_name | Prénom | Affichage à différents endroits du BO |
|
||||
| users | last_name | Nom | Affichage à différents endroits du BO |
|
||||
| users | email | Adresse mail | Identifiant, sert à la connexion au BO |
|
||||
| --- | --- | --- | --- |
|
||||
| order_lines | product_id | Identifiant du produit présent dans la commande | Détail de la commande |
|
||||
| order_lines | quantité | Quantité du produit présent dans la commande | Détail de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| employees | customer_id | Identifiant client | Permet de relier un salarié à un compte client du site |
|
||||
| employees | matricule | Matricule | Matricule du salarié |
|
||||
| employees | nom | Nom | Nom du salarié |
|
||||
| employees | prenom | Prénom | Prénom du salarié |
|
||||
| employees | start_date | Date de début du contrat | Activation du compte |
|
||||
| employees | end_date | Date de fin du contrat | Désactivation du compte |
|
||||
| employees | etablissement | Identifiant établissement | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | site | Identifiant site | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | service | Identifiant service | Localiser l'employé pour le dépôt de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| users | first_name | Prénom | Affichage à différents endroits du BO |
|
||||
| users | last_name | Nom | Affichage à différents endroits du BO |
|
||||
| users | email | Adresse mail | Identifiant, sert à la connexion au BO |
|
||||
|
||||
### Formulaire de contact (marketing)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ namespace App\Actions\Common;
|
|||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use SortDirection;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
|
|
@ -32,17 +30,8 @@ final class ChangeModelOrder implements Action
|
|||
$goUp = $input->direction === SortDirection::Ascending;
|
||||
$currentOrder = $model->getOrder();
|
||||
|
||||
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true)
|
||||
|| method_exists($model, 'trashed');
|
||||
|
||||
/** @var Builder<Model&ModelWithOrder> $baseQuery */
|
||||
$baseQuery = $usesSoftDeletes
|
||||
// @phpstan-ignore method.notFound (withTrashed)
|
||||
? $model::query()->withTrashed()
|
||||
: $model::query();
|
||||
|
||||
/** @var (Model&ModelWithOrder)|null $adjacentModel */
|
||||
$adjacentModel = $baseQuery
|
||||
$adjacentModel = $model::query()
|
||||
->where('order', $goUp ? '<' : '>', $currentOrder)
|
||||
->orderBy('order', $goUp ? SortDirection::Descending : SortDirection::Ascending)
|
||||
->first();
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ChangeModelOrderPositionInput, bool>
|
||||
*/
|
||||
final class ChangeModelOrderPosition implements Action
|
||||
{
|
||||
private const int MAX_ORDER_VALUE = 4294967295;
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Common\ChangeModelOrderPositionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, ChangeModelOrderPositionInput::class);
|
||||
|
||||
$model = $input->model;
|
||||
$currentOrder = $model->getOrder();
|
||||
$targetOrder = $input->targetOrder;
|
||||
|
||||
if ($currentOrder === $targetOrder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true)
|
||||
|| method_exists($model, 'trashed');
|
||||
|
||||
/** @var Builder<Model&ModelWithOrder> $baseQuery */
|
||||
$baseQuery = $usesSoftDeletes
|
||||
// @phpstan-ignore method.notFound (withTrashed)
|
||||
? $model::query()->withTrashed()
|
||||
: $model::query();
|
||||
|
||||
/** @var int|null $maxOrder */
|
||||
$maxOrder = (clone $baseQuery)->max('order');
|
||||
if ($maxOrder !== null && $targetOrder > $maxOrder) {
|
||||
$targetOrder = (int) $maxOrder;
|
||||
}
|
||||
|
||||
if ($currentOrder === $targetOrder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deactivate timestamps
|
||||
$model->timestamps = false;
|
||||
|
||||
// Step 1: Temporarily park the moving model at MAX_ORDER_VALUE
|
||||
$model->setOrder(self::MAX_ORDER_VALUE);
|
||||
$model->saveQuietly();
|
||||
|
||||
if ($targetOrder < $currentOrder) {
|
||||
// Moving UP
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
||||
$itemsToShift = (clone $baseQuery)
|
||||
->where('order', '>=', $targetOrder)
|
||||
->where('order', '<', $currentOrder)
|
||||
->whereKeyNot($model->getKey())
|
||||
->orderBy('order', 'desc')
|
||||
->get();
|
||||
|
||||
foreach ($itemsToShift as $item) {
|
||||
$item->timestamps = false;
|
||||
$item->setOrder($item->getOrder() + 1);
|
||||
$item->saveQuietly();
|
||||
}
|
||||
} else {
|
||||
// Moving DOWN
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
||||
$itemsToShift = (clone $baseQuery)
|
||||
->where('order', '>', $currentOrder)
|
||||
->where('order', '<=', $targetOrder)
|
||||
->whereKeyNot($model->getKey())
|
||||
->orderBy('order', 'asc')
|
||||
->get();
|
||||
|
||||
foreach ($itemsToShift as $item) {
|
||||
$item->timestamps = false;
|
||||
$item->setOrder($item->getOrder() - 1);
|
||||
$item->saveQuietly();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Set the moving model to the target order
|
||||
$model->setOrder($targetOrder);
|
||||
$model->saveQuietly();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ChangeModelOrderPositionInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Model & ModelWithOrder $model,
|
||||
public int $targetOrder,
|
||||
) {
|
||||
Assert::isInstanceOf($this->model, Model::class);
|
||||
Assert::isInstanceOf($this->model, ModelWithOrder::class);
|
||||
Assert::greaterThanEq($this->targetOrder, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,32 +24,23 @@ final class UpdateCustomer implements Action
|
|||
#[\Override]
|
||||
public function __invoke(ActionInput $input): UpdateCustomerOutput
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateCustomerInput::class);
|
||||
Assert::isInstanceOf($input, UnsubscribeFromNewsletterInput::class);
|
||||
|
||||
$customer = $this->authService->getCustomerAsMandatory();
|
||||
|
||||
try {
|
||||
/** @var array{firstname:string,lastname:string,birth_date:string,phone?:string,address?:string,postcode:string,city:string,opt_in?:bool} $data */
|
||||
$data = [
|
||||
'firstname' => $input->firstname,
|
||||
'lastname' => $input->lastname,
|
||||
'birth_date' => $input->birthDate,
|
||||
'postcode' => $input->postcode,
|
||||
'city' => $input->city,
|
||||
'opt_in' => $input->optIn,
|
||||
];
|
||||
|
||||
if ($input->phone !== null) {
|
||||
$data['phone'] = $input->phone;
|
||||
}
|
||||
|
||||
if ($input->address !== null) {
|
||||
$data['address'] = $input->address;
|
||||
}
|
||||
|
||||
$result = $this->crmService->updateCustomerInfos(
|
||||
$customer,
|
||||
$data
|
||||
[
|
||||
'firstname' => $input->firstname,
|
||||
'lastname' => $input->lastname,
|
||||
'birth_date' => $input->birthDate,
|
||||
'phone' => $input->phone,
|
||||
'address' => $input->address,
|
||||
'postcode' => $input->postcode,
|
||||
'city' => $input->city,
|
||||
'opt_in' => $input->optIn,
|
||||
]
|
||||
);
|
||||
|
||||
return new UpdateCustomerOutput($result);
|
||||
|
|
|
|||
|
|
@ -35,13 +35,6 @@ class OrderLineProductCast implements CastsAttributes
|
|||
|
||||
$data = Arr::wrap(json_decode($value, true));
|
||||
|
||||
$taxRate = Arr::get($data, 'tax_rate');
|
||||
$taxRate = is_numeric($taxRate) ? (float) $taxRate : null;
|
||||
|
||||
if (is_null($taxRate)) {
|
||||
throw new \InvalidArgumentException('The tax rate cannot be null.');
|
||||
}
|
||||
|
||||
return new OrderLineProduct(
|
||||
incoId: new IncoId(Arr::integer($data, 'inco_id')),
|
||||
originalId: new ProductId(Uuid::fromString(
|
||||
|
|
@ -57,8 +50,14 @@ class OrderLineProductCast implements CastsAttributes
|
|||
price: new MoneyCast()->get($model, 'price', Arr::get($data, 'price'), []),
|
||||
discountedPrice: new MoneyCast()->get($model, 'discounted_price', Arr::get($data, 'discounted_price'), []),
|
||||
priceKgTtc: new MoneyCast()->get($model, 'price_kg_ttc', Arr::get($data, 'price_kg_ttc'), []),
|
||||
taxRate: TaxRate::fromPercent($taxRate),
|
||||
picture: is_string($picture = Arr::get($data, 'picture')) ? $picture : null,
|
||||
taxRate: match ((float) Arr::float($data, 'tax_rate')) {
|
||||
TaxRate::TVA_20->percent() => TaxRate::TVA_20,
|
||||
TaxRate::TVA_5_5->percent() => TaxRate::TVA_5_5,
|
||||
default => throw new \UnexpectedValueException(
|
||||
'Unhandled tax'
|
||||
)
|
||||
},
|
||||
picture: Arr::string($data, 'picture', null),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace App\Casts;
|
|||
|
||||
use Domain\Models\ValueObjects\OrderReference;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* @implements CastsAttributes<OrderReference,OrderReference>
|
||||
|
|
@ -19,7 +20,7 @@ class OrderReferenceCast implements CastsAttributes
|
|||
throw new \UnexpectedValueException('OrderReference cannot be null.');
|
||||
}
|
||||
|
||||
return OrderReference::fromString($value);
|
||||
return new OrderReference(Uuid::fromString($value));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $attributes */
|
||||
|
|
@ -29,6 +30,6 @@ class OrderReferenceCast implements CastsAttributes
|
|||
throw new \UnexpectedValueException('The given value is not an instance of OrderReference.');
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
return $value->value->toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ final readonly class DiscountProductDto implements \JsonSerializable, Arrayable
|
|||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public ?string $picture,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +23,6 @@ final readonly class DiscountProductDto implements \JsonSerializable, Arrayable
|
|||
return new self(
|
||||
id: (string) $product->id,
|
||||
name: (string) $product->name,
|
||||
picture: $product->picture,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -35,9 +33,8 @@ final readonly class DiscountProductDto implements \JsonSerializable, Arrayable
|
|||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'picture' => $this->picture,
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ class ProductsController extends Controller implements MassActions
|
|||
'brands' => $output->brands,
|
||||
'ranges' => $output->ranges,
|
||||
'tags' => $output->tags,
|
||||
'discounts' => $output->discounts,
|
||||
'searchFields' => $output->searchFields,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ abstract class AuthController extends Controller
|
|||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
// @phpstan-ignore assign.propertyType
|
||||
ResetPassword::$createUrlCallback = $this->createResetUrl(...);
|
||||
VerifyEmail::$createUrlCallback = $this->createValidationUrl(...);
|
||||
}
|
||||
|
|
@ -48,12 +50,8 @@ abstract class AuthController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
public function createResetUrl(mixed $notifiable, string $token): string
|
||||
public function createResetUrl(Customer $notifiable, string $token): string
|
||||
{
|
||||
if (! is_object($notifiable) || ! ($notifiable instanceof Customer)) {
|
||||
throw new \InvalidArgumentException('Invalid notifiable provided.');
|
||||
}
|
||||
|
||||
return url(
|
||||
URL::temporarySignedRoute(
|
||||
'fo.auth.reset-password',
|
||||
|
|
|
|||
|
|
@ -34,37 +34,35 @@ class CustomerController extends Controller
|
|||
{
|
||||
$this->authorize('manageOwnProfile', Customer::class);
|
||||
|
||||
$output = $this->executor->execute(
|
||||
$crmCustomer = $this->executor->execute(
|
||||
$getCrmCustomerQuery,
|
||||
new GetCrmCustomerQueryInput()
|
||||
);
|
||||
|
||||
abort_unless(
|
||||
$output->customer instanceof CrmCustomer,
|
||||
$crmCustomer->customer instanceof CrmCustomer,
|
||||
503,
|
||||
trans('An error occurred, please try again later.')
|
||||
);
|
||||
|
||||
return view('fo.customer.edit.infos', [
|
||||
'crmCustomer' => $output->customer,
|
||||
]);
|
||||
return view('fo.customer.edit.infos', compact('crmCustomer'));
|
||||
}
|
||||
|
||||
public function editInfosSubmit(UpdateInfosRequest $request, UpdateCustomer $updateCustomer): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageOwnProfile', Customer::class);
|
||||
|
||||
$output = $this->executor->execute(
|
||||
$result = $this->executor->execute(
|
||||
$updateCustomer,
|
||||
$request->toUpdateCustomerInput()
|
||||
);
|
||||
|
||||
return match (true) {
|
||||
$output->result instanceof CrmCustomer => back()
|
||||
return match ($result) {
|
||||
$result instanceof CrmCustomer => back()
|
||||
->with('success', trans('Your information has been saved.')),
|
||||
$output->result instanceof ValidationException => back()
|
||||
$result instanceof ValidationException => back()
|
||||
->withInput()
|
||||
->withErrors($output->result->getMessage()),
|
||||
->withErrors($result->getMessage()),
|
||||
default => back()
|
||||
->withInput()
|
||||
->withErrors(trans('An error occurred, please try again later.')),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace App\Http\Middleware;
|
|||
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Mews\Purifier\Purifier;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
|
|
@ -26,8 +25,7 @@ class HtmlSanitizer
|
|||
public function handle(Request $request, \Closure $next, string ...$params): Response
|
||||
{
|
||||
$inputs = $request->all();
|
||||
|
||||
\array_walk($inputs, function (null | string | array | UploadedFile &$input, string $name) use ($params): void {
|
||||
\array_walk($inputs, function (null | string | array &$input, string $name) use ($params): void {
|
||||
$this->cleanInput($input, $name, null, $params);
|
||||
});
|
||||
$request->merge($inputs);
|
||||
|
|
@ -36,22 +34,15 @@ class HtmlSanitizer
|
|||
}
|
||||
|
||||
/**
|
||||
* @param null|string|array<string,mixed>|\Illuminate\Http\UploadedFile $input
|
||||
* @param null|string|array<string,mixed> $input
|
||||
* @param string[] $params
|
||||
*/
|
||||
private function cleanInput(
|
||||
null | string | array | UploadedFile &$input,
|
||||
string $name,
|
||||
?string $rootName,
|
||||
array $params
|
||||
): void {
|
||||
private function cleanInput(null | string | array &$input, string $name, ?string $rootName, array $params): void
|
||||
{
|
||||
if (is_array($input)) {
|
||||
array_walk_recursive(
|
||||
$input,
|
||||
function (null | string | array | UploadedFile &$nestedInput, string $nestedName) use ($name, $params): void {
|
||||
$this->cleanInput($nestedInput, $nestedName, $name, $params);
|
||||
}
|
||||
);
|
||||
array_walk_recursive($input, function (null | string | array &$nestedInput, string $nestedName) use ($name, $params): void {
|
||||
$this->cleanInput($nestedInput, $nestedName, $name, $params);
|
||||
});
|
||||
}
|
||||
|
||||
if (! is_string($input)) {
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Bo;
|
||||
|
||||
use App\Actions\Common\ChangeModelOrderPositionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Infrastructure\Adapters\Services\AppAdminAuthService;
|
||||
|
||||
class ChangeSelectedOrderRequest extends FormRequest
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppAdminAuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->authService->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'order' => ['required', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function toChangeModelOrderPositionInput(Model & ModelWithOrder $model): ChangeModelOrderPositionInput
|
||||
{
|
||||
return new ChangeModelOrderPositionInput(
|
||||
model: $model,
|
||||
targetOrder: $this->integer('order'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ class IndexProductRequest extends FormRequest
|
|||
$tag = $this->resolveFilterValue('tag', $session);
|
||||
$range = $this->resolveFilterValue('range', $session);
|
||||
|
||||
$discount = $this->resolveFilterValue('discount', $session);
|
||||
$discount = $this->resolveBooleanFilter('discount', false, $session);
|
||||
$star = $this->resolveBooleanFilter('star', $starred, $session);
|
||||
|
||||
$search = $this->filled('search') ? $this->string('search')->value() : null;
|
||||
|
|
@ -85,7 +85,8 @@ class IndexProductRequest extends FormRequest
|
|||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'star' => $this->boolean('star'),
|
||||
'discount' => $this->boolean('discount'),
|
||||
'star' => $this->boolean('star'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ class IndexProductRequest extends FormRequest
|
|||
{
|
||||
|
||||
return [
|
||||
'discount' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'discount' => ['required', 'boolean'],
|
||||
'star' => ['required', 'boolean'],
|
||||
'brand' => ['sometimes', 'nullable', 'string', 'min:3', 'max:255'],
|
||||
];
|
||||
|
|
@ -104,7 +105,7 @@ class IndexProductRequest extends FormRequest
|
|||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'discount' => trans_choice('models.classes.discounts', 1),
|
||||
'discount' => trans('crud.helpers.discounted'),
|
||||
'star' => trans('crud.helpers.starred'),
|
||||
'description' => trans('validation.attributes.description'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class StoreShopRequest extends FormRequest
|
|||
{
|
||||
return [
|
||||
'name' => trans('validation.attributes.name'),
|
||||
'address' => trans('validation.custom.attributes.postal_address'),
|
||||
'address' => trans('validation.attributes.address'),
|
||||
'postcode' => trans('validation.attributes.postal_code'),
|
||||
'city' => trans('validation.attributes.city'),
|
||||
'phone' => trans('validation.attributes.phone'),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,18 @@ class UpdateShopPickupExceptionsRequest extends FormRequest
|
|||
];
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
#[\Override]
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'pickup-exceptions.*.uuid' => 'La fermeture exceptionnelle sélectionnée n\'est pas un identifiant valide.',
|
||||
'pickup-exceptions.*.exists' => 'La fermeture exceptionnelle sélectionnée n\'existe pas.',
|
||||
'order_deadline_at.*.before' => 'La date limite de commande doit être strictement antérieure au jour de remplacement.',
|
||||
'replacement_date.*.required_with' => 'Le jour de remplacement est obligatoire lorsqu\'une date limite de commande est définie.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{replacement_date: string|null, order_deadline_at: string|null}>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class RegisterRequest extends FormRequest
|
|||
'firstname' => trans('validation.attributes.first_name'),
|
||||
'lastname' => trans('validation.attributes.last_name'),
|
||||
'birth_date' => trans('validation.attributes.date_of_birth'),
|
||||
'address' => trans('validation.custom.attributes.postal_address'),
|
||||
'address' => trans('validation.attributes.address'),
|
||||
'phone' => trans('validation.attributes.phone'),
|
||||
'city' => trans('validation.attributes.city'),
|
||||
'postcode' => trans('validation.attributes.postal_code'),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use Domain\Models\ValueObjects\AttachedFile;
|
|||
use Domain\Models\ValueObjects\ContactFormData;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ContactFormRequest extends FormRequest
|
||||
{
|
||||
|
|
@ -18,22 +17,11 @@ class ContactFormRequest extends FormRequest
|
|||
return [
|
||||
'last_name' => ['required', 'string', 'min:2'],
|
||||
'first_name' => ['required', 'string', 'min:2'],
|
||||
'email' => [
|
||||
'required',
|
||||
Rule::email()
|
||||
->rfcCompliant()
|
||||
->preventSpoofing()
|
||||
->validateMxRecord(),
|
||||
],
|
||||
'email' => ['required', 'email:rfc,dns,spoof'],
|
||||
'subject' => ['required', 'string', 'min:2'],
|
||||
'order_ref' => ['nullable', 'string', 'regex:/^[0-9]+$/'],
|
||||
'message' => ['required', 'string', 'min:20', 'max:1900'],
|
||||
'file' => [
|
||||
'nullable',
|
||||
Rule::file()
|
||||
->max(5120)
|
||||
->types(['jpg', 'jpeg', 'png', 'pdf']),
|
||||
],
|
||||
'file' => ['nullable', 'file', 'mimes:jpg,jpeg,png,pdf', 'max:5120'],
|
||||
'ok_cgv' => ['accepted'],
|
||||
];
|
||||
}
|
||||
|
|
@ -79,13 +67,4 @@ class ContactFormRequest extends FormRequest
|
|||
file: $attachedFile,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
#[\Override]
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'subject' => trans('validation.custom.attributes.object'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class UpdateInfosRequest extends FormRequest
|
|||
'firstname' => trans('validation.attributes.first_name'),
|
||||
'lastname' => trans('validation.attributes.last_name'),
|
||||
'birth_date' => trans('validation.attributes.date_of_birth'),
|
||||
'address' => trans('validation.custom.attributes.postal_address'),
|
||||
'address' => trans('validation.attributes.address'),
|
||||
'phone' => trans('validation.attributes.phone'),
|
||||
'city' => trans('validation.attributes.city'),
|
||||
'postcode' => trans('validation.attributes.postal_code'),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ namespace App\Http\Resources\Fo\Ajax;
|
|||
|
||||
use App\Lib\Helpers\MoneyHelper;
|
||||
use App\Models\Product;
|
||||
use Domain\Services\PricingService;
|
||||
use Domain\Services\ProductAvailabilityService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
|
@ -21,10 +23,12 @@ class FilteredProductResource extends JsonResource
|
|||
#[\Override]
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$productRepository = resolve(EloquentProductRepository::class);
|
||||
$productRepository = resolve(EloquentProductRepository::class);
|
||||
$productAvailabilityService = resolve(ProductAvailabilityService::class);
|
||||
$pricingService = resolve(PricingService::class);
|
||||
|
||||
$domainProduct = $productRepository->toEntity($this->product);
|
||||
$productPricing = $domainProduct->pricing();
|
||||
$productPricing = $pricingService->calculateForProduct($domainProduct);
|
||||
$discount = $productPricing->hasDiscount ? $productPricing->discountedPriceTtc() : null;
|
||||
|
||||
return [
|
||||
|
|
@ -44,10 +48,11 @@ class FilteredProductResource extends JsonResource
|
|||
MoneyHelper::serialized($productPricing->priceKgTtc()) : null,
|
||||
'discountedPriceTtc' => Gate::allows('viewPrices', $this->product) ?
|
||||
MoneyHelper::serialized($discount) : null,
|
||||
'is_available' => $domainProduct->isOrderable(),
|
||||
'is_available' => $productAvailabilityService->isOrderable($domainProduct),
|
||||
'can_add_to_basket' => Gate::allows('addToBasket', $this->product),
|
||||
'showStrippedPrice' => (bool) $this->product->validDiscount?->is_old_price_displayed,
|
||||
'nutriscore' => $this->product->inco?->nutriscore?->value(),
|
||||
'showStrippedPrice' => $this->product->validDiscount()
|
||||
->value('is_old_price_displayed') ? true : false,
|
||||
'nutriscore' => $this->product->inco?->nutriscore?->value(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ class ExceptionsHelper
|
|||
*
|
||||
* @template T of mixed
|
||||
*
|
||||
* @param array<array-key, T> $params
|
||||
* @return array<string|T>
|
||||
* @param array<T> $params
|
||||
* @return array<T>
|
||||
*/
|
||||
private static function getAnonymizedSentryParams(array $params): array
|
||||
{
|
||||
|
|
@ -89,6 +89,7 @@ class ExceptionsHelper
|
|||
};
|
||||
}
|
||||
|
||||
// @phpstan-ignore return.type
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -277,10 +277,10 @@ final class StringEncoder
|
|||
|
||||
foreach ($bytes as $byte) {
|
||||
$parts[] = match (true) {
|
||||
// @phpstan-ignore argument.type (ASCII range check)
|
||||
$byte <= 0x7F => chr($byte),
|
||||
isset($overrides[$byte]) => $overrides[$byte],
|
||||
default => self::encodeLatin1ByteAsUtf8($byte),
|
||||
// @phpstan-ignore argument.type
|
||||
$byte <= 0x7F => chr($byte),
|
||||
isset($overrides[$byte]) => $overrides[$byte],
|
||||
default => self::encodeLatin1ByteAsUtf8($byte),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use Illuminate\Support\Facades\Storage;
|
|||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Propaganistas\LaravelPhone\PhoneNumber;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
|
@ -178,15 +179,16 @@ class ToolboxHelper extends ToolboxHelperCore
|
|||
*/
|
||||
public static function getOriginal(Model $model, string $attribute): mixed
|
||||
{
|
||||
|
||||
// @phpstan-ignore method.notFound (\Spatie\Translatable\HasTranslations could be absent)
|
||||
if (\in_array('Spatie\Translatable\HasTranslations', \class_uses_recursive($model)) && $model->isTranslatableAttribute($attribute)) {
|
||||
// @phpstan-ignore method.notFound (\Spatie\Translatable\HasTranslations could be absent)
|
||||
if (
|
||||
\in_array('Spatie\Translatable\HasTranslations', \class_uses_recursive($model)) && // @phpstan-ignore-next-line
|
||||
$model->isTranslatableAttribute($attribute)
|
||||
) {
|
||||
// @phpstan-ignore-next-line
|
||||
return $model->getTranslation(
|
||||
$attribute,
|
||||
// @phpstan-ignore method.notFound (\Spatie\Translatable\HasTranslations could be absent)
|
||||
// @phpstan-ignore-next-line
|
||||
$model->getLocale(),
|
||||
// @phpstan-ignore method.notFound (\Spatie\Translatable\HasTranslations could be absent)
|
||||
// @phpstan-ignore-next-line
|
||||
$model->useFallbackLocale()
|
||||
);
|
||||
}
|
||||
|
|
@ -214,7 +216,7 @@ class ToolboxHelper extends ToolboxHelperCore
|
|||
throw_unless(preg_match($localeRegex, $currentLocale), \InvalidArgumentException::class, 'Invalid locale identifier.');
|
||||
|
||||
/** @var array<string,string> $locales */
|
||||
$locales = Config::array('app.locales', []);
|
||||
$locales = config('app.locales', []);
|
||||
$locales = Collection::make($locales)
|
||||
->filter(fn (string $locale) => (bool) preg_match($localeRegex, $locale))
|
||||
->mapWithKeys(fn (string $locale) => [$locale => $locale]);
|
||||
|
|
@ -375,6 +377,25 @@ class ToolboxHelper extends ToolboxHelperCore
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validated enum value from request or return default value if validation fails
|
||||
*
|
||||
* @param int $default The default value to return if validation fails.
|
||||
* @param string $field The request field to validate.
|
||||
* @param class-string<\UnitEnum> $enumPath The enum class path to validate against.
|
||||
*/
|
||||
public static function getValidatedEnum(int $default, string $field, string $enumPath): int
|
||||
{
|
||||
try {
|
||||
return \intval(Validator::make(
|
||||
[$field => \request()->input($field)],
|
||||
[$field => new Enum($enumPath)]
|
||||
)->validated()[$field]);
|
||||
} catch (ValidationException) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Hex color to RGB
|
||||
*
|
||||
|
|
|
|||
|
|
@ -54,12 +54,8 @@ class Discount extends Model
|
|||
protected function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where(function (Builder $q): void {
|
||||
$q->whereNull('date_start')->orWhere('date_start', '<=', Date::now());
|
||||
})
|
||||
->where(function (Builder $q): void {
|
||||
$q->whereNull('date_end')->orWhere('date_end', '>=', Date::now());
|
||||
});
|
||||
->where('date_start', '<=', Date::now())
|
||||
->where('date_end', '>=', Date::now());
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\Product,$this> */
|
||||
|
|
|
|||
|
|
@ -147,11 +147,8 @@ class Product extends Model implements ModelWithOrder, ModelWithStar, ModelWithS
|
|||
$model->slug = Str::slug($model->name);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
||||
$model->picture_large = FileStorageHelper::storeFile($model, $model->picture_large, true);
|
||||
|
||||
if ($model->order === null) {
|
||||
$max = self::query()->max('order');
|
||||
$model->order = is_scalar($max) ? intval($max) + 1 : 1;
|
||||
}
|
||||
// @phpstan-ignore cast.int
|
||||
$model->order ??= (int) self::query()->max('order');
|
||||
}
|
||||
|
||||
private static function assertNameIsUnique(?string $name, ?string $id = null): void
|
||||
|
|
|
|||
|
|
@ -96,10 +96,9 @@ class Slide extends Model implements ModelWithOrder, ModelWithStatus
|
|||
private static function manageFields(self $model): void
|
||||
{
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
||||
|
||||
if ($model->order === null) {
|
||||
$max = self::query()->max('order');
|
||||
$model->order = is_scalar($max) ? intval($max) + 1 : 1;
|
||||
if (is_null($model->order)) {
|
||||
// @phpstan-ignore argument.type
|
||||
$model->order = intval(self::query()->max('order'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +110,7 @@ class Slide extends Model implements ModelWithOrder, ModelWithStatus
|
|||
if ($actualStatus !== $model->status) {
|
||||
resolve(Session::class)->flash('bo.info', \trans(
|
||||
'The publication status is updated according to the scheduled dates. '
|
||||
. 'To change the publication status, please edit or remove the corresponding dates.'
|
||||
. 'To change the publication status, please edit or remove the corresponding dates.'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ class Tag extends Model implements ModelWithOrder, ModelWithStatus
|
|||
self::assertLabelIsUnique($model->label, $model->id);
|
||||
$model->slug = Str::slug($model->label);
|
||||
|
||||
if ($model->order === null) {
|
||||
$max = self::query()->max('order');
|
||||
$model->order = is_scalar($max) ? intval($max) + 1 : 1;
|
||||
if (is_null($model->order)) {
|
||||
// @phpstan-ignore argument.type
|
||||
$model->order = intval(self::query()->max('order'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ use Illuminate\Bus\Queueable;
|
|||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class MissingOrderReminderNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
|
|
@ -31,7 +30,7 @@ class MissingOrderReminderNotification extends Notification implements ShouldQue
|
|||
public function __construct(
|
||||
private readonly CarbonInterface $pickupDay
|
||||
) {
|
||||
$appUrl = Config::string('app.url');
|
||||
$appUrl = config()->string('app.url');
|
||||
$appHost = parse_url($appUrl, \PHP_URL_HOST);
|
||||
|
||||
if (empty($appHost)) {
|
||||
|
|
@ -40,7 +39,8 @@ class MissingOrderReminderNotification extends Notification implements ShouldQue
|
|||
|
||||
$this->appUrl = $appUrl;
|
||||
$this->appHost = $appHost;
|
||||
$this->appName = Config::string('app.name', 'La Boutique de Cocotte');
|
||||
$this->appName = config()
|
||||
->string('app.name', 'La Boutique de Cocotte');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ use App\Models\User;
|
|||
use Domain\Enums\UserRole;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\ProductAvailabilityService;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class ProductPolicy
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductAvailabilityService $productService,
|
||||
private readonly ProductRepository $productRepository
|
||||
) {
|
||||
}
|
||||
|
|
@ -127,6 +129,6 @@ class ProductPolicy
|
|||
|
||||
$domainProduct = $this->productRepository->find($productId);
|
||||
|
||||
return $domainProduct && $domainProduct->isPublished();
|
||||
return $domainProduct && $this->productService->isPublished($domainProduct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,30 +8,25 @@ use App\Dto\SettingDto;
|
|||
use App\Models\Customer;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Domain\Enums\UserRole;
|
||||
|
||||
class SettingPolicy
|
||||
{
|
||||
public function view(User | Customer $user, Setting | SettingDto | null $_ = null): bool
|
||||
{
|
||||
// FIXME: The Arcadia refresh job dispatching is currently disabled and needs to be enabled when api will be available.
|
||||
return false;
|
||||
if ($user instanceof Customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if ($user instanceof Customer) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// return $user->role->isAtLeastEqualsTo(UserRole::Admin);
|
||||
return $user->role->isAtLeastEqualsTo(UserRole::Admin);
|
||||
}
|
||||
|
||||
public function update(User | Customer $user, Setting | SettingDto | null $_ = null): bool
|
||||
{
|
||||
// FIXME: The Arcadia refresh job dispatching is currently disabled and needs to be enabled when api will be available.
|
||||
return false;
|
||||
if ($user instanceof Customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if ($user instanceof Customer) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// return $user->role->isAtLeastEqualsTo(UserRole::Admin);
|
||||
return $user->role->isAtLeastEqualsTo(UserRole::Admin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ namespace App\Policies\Frontend;
|
|||
use App\Models\Customer;
|
||||
use App\Models\User;
|
||||
use Domain\Models\ValueObjects\BasketProduct;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\ProductAvailabilityService;
|
||||
|
||||
class BasketPolicy
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductRepository $productRepository
|
||||
private readonly ProductAvailabilityService $productService
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -27,8 +27,6 @@ class BasketPolicy
|
|||
return false;
|
||||
}
|
||||
|
||||
$domainProduct = $this->productRepository->find($product->id);
|
||||
|
||||
return $domainProduct && $domainProduct->isPublished();
|
||||
return $this->productService->isBasketProductPublished($product);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ use App\Models\Customer;
|
|||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\ProductAvailabilityService;
|
||||
|
||||
class ProductPolicy
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductAvailabilityService $productService,
|
||||
private readonly ProductRepository $productRepository
|
||||
) {
|
||||
}
|
||||
|
|
@ -20,14 +22,14 @@ class ProductPolicy
|
|||
{
|
||||
$domainProduct = $this->productRepository->find($product->getId());
|
||||
|
||||
return $domainProduct && $domainProduct->isPublished();
|
||||
return $domainProduct && $this->productService->isPublished($domainProduct);
|
||||
}
|
||||
|
||||
public function addToBasket(null | User | Customer $_, Product $product): bool
|
||||
{
|
||||
$domainProduct = $this->productRepository->find($product->getId());
|
||||
|
||||
return $domainProduct && $domainProduct->isOrderable();
|
||||
return $domainProduct && $this->productService->isOrderable($domainProduct);
|
||||
}
|
||||
|
||||
public function viewPrices(null | User | Customer $_, Product $product): bool
|
||||
|
|
@ -38,6 +40,6 @@ class ProductPolicy
|
|||
|
||||
$domainProduct = $this->productRepository->find($product->getId());
|
||||
|
||||
return $domainProduct && $domainProduct->isPublished();
|
||||
return $domainProduct && $this->productService->isPublished($domainProduct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use Domain\Ports\Repositories\ProductRepository;
|
|||
use Domain\Ports\Repositories\ShopperRepository;
|
||||
use Domain\Ports\Repositories\ShopRepository;
|
||||
use Domain\Ports\Services\AdminAuthenticationService;
|
||||
use Domain\Ports\Services\CsvGenerator;
|
||||
use Domain\Ports\Services\CustomerAuthenticationService;
|
||||
use Domain\Ports\Services\EmployeeImportStorageService;
|
||||
use Domain\Ports\Services\EmployeeStatisticsService;
|
||||
|
|
@ -30,7 +29,6 @@ use Infrastructure\Adapters\Repositories\EloquentShopRepository;
|
|||
use Infrastructure\Adapters\Repositories\SessionBasketRepository;
|
||||
use Infrastructure\Adapters\Services\AppAdminAuthService;
|
||||
use Infrastructure\Adapters\Services\AppCustomerAuthService;
|
||||
use Infrastructure\Adapters\Services\CsvGeneratorImpl;
|
||||
use Infrastructure\Adapters\Services\EloquentEmployeeImportStorageService;
|
||||
use Infrastructure\Adapters\Services\EloquentEmployeeStatisticsService;
|
||||
use Infrastructure\Adapters\Services\LaravelTransactionManager;
|
||||
|
|
@ -113,11 +111,6 @@ class DomainServiceProvider extends ServiceProvider
|
|||
EmployeeImportStorageService::class,
|
||||
EloquentEmployeeImportStorageService::class
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
CsvGenerator::class,
|
||||
CsvGeneratorImpl::class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Money\Currencies\ISOCurrencies;
|
||||
use Money\Currency;
|
||||
|
|
@ -30,8 +29,8 @@ class MoneyServiceProvider extends ServiceProvider
|
|||
$this->app->singleton(
|
||||
Currency::class,
|
||||
fn () => new Currency(
|
||||
// @phpstan-ignore argument.type (Should throw if not provided)
|
||||
Config::string(
|
||||
// @phpstan-ignore argument.type
|
||||
config(
|
||||
'app.currency',
|
||||
'EUR'
|
||||
)
|
||||
|
|
@ -41,7 +40,8 @@ class MoneyServiceProvider extends ServiceProvider
|
|||
$this->app->singleton(
|
||||
\NumberFormatter::class,
|
||||
fn () => new \NumberFormatter(
|
||||
Config::string('app.locale', 'fr_FR'),
|
||||
// @phpstan-ignore argument.type
|
||||
config('app.locale', 'fr_FR'),
|
||||
\NumberFormatter::CURRENCY
|
||||
)
|
||||
);
|
||||
|
|
@ -66,7 +66,9 @@ class MoneyServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->sharetoView();
|
||||
if (! $this->app->runningInConsole()) {
|
||||
$this->sharetoView();
|
||||
}
|
||||
}
|
||||
|
||||
private function sharetoView(): void
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use App\Models\Range;
|
|||
use App\Models\Slide;
|
||||
use App\Models\Tag;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\PricingService;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
|
|
@ -108,6 +109,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
fn (string $value) => $this->resolvePublishedProductWithPricing($value)
|
||||
);
|
||||
|
||||
// * CSP Route
|
||||
$this->cspReportRoute();
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +143,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
private function resolvePublishedProductWithPricing(string $value): ProductWithPricingDto
|
||||
{
|
||||
$domainProductRepository = resolve(ProductRepository::class);
|
||||
$pricingService = resolve(PricingService::class);
|
||||
$product = Product::query()
|
||||
->where('slug', $value)
|
||||
->published()
|
||||
|
|
@ -153,7 +156,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
$product->load(['inco', 'validDiscount', 'products']);
|
||||
|
||||
$domainProduct = $domainProductRepository->find($product->getId());
|
||||
$pricing = $domainProduct?->pricing();
|
||||
$pricing = $domainProduct ? $pricingService->calculateForProduct($domainProduct) : null;
|
||||
|
||||
if (! $pricing) {
|
||||
throw new NotFoundHttpException();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace App\Providers;
|
|||
use App\Models\Page;
|
||||
use App\Models\Product;
|
||||
use App\Services\EmployeeSyncService;
|
||||
use Domain\Services\ProductAvailabilityService;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
|
@ -22,13 +23,12 @@ class ViewServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(EloquentProductRepository $productRepository)
|
||||
{
|
||||
$this->setUpBladeMacros($productRepository);
|
||||
|
||||
if ($this->app->runningInConsole() && ! $this->app->runningUnitTests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setupPaginator();
|
||||
$this->setUpBladeMacros($productRepository);
|
||||
}
|
||||
|
||||
private function setupPaginator(): void
|
||||
|
|
@ -54,7 +54,8 @@ class ViewServiceProvider extends ServiceProvider
|
|||
{
|
||||
Blade::if(
|
||||
'orderable',
|
||||
fn (Product $product) => $productRepository->toEntity($product)->isOrderable()
|
||||
fn (Product $product) => resolve(ProductAvailabilityService::class)
|
||||
->isOrderable($productRepository->toEntity($product))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ final readonly class GetDiscountFormDataQuery implements Query
|
|||
|
||||
if ($input->discount !== null) {
|
||||
$selectedProds = Product::query()
|
||||
->select(['id', 'name', 'picture'])
|
||||
->select(['id', 'name'])
|
||||
->where('discount_id', $input->discount->id)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn (Product $p): DiscountProductDto => DiscountProductDto::fromModel($p));
|
||||
|
||||
$existingProds = Product::query()
|
||||
->select(['id', 'name', 'picture'])
|
||||
->select(['id', 'name'])
|
||||
->whereNotIn('id', $selectedProds->pluck('id'))
|
||||
->orderBy('name')
|
||||
->get()
|
||||
|
|
@ -46,7 +46,7 @@ final readonly class GetDiscountFormDataQuery implements Query
|
|||
} else {
|
||||
$selectedProds = collect();
|
||||
$existingProds = Product::query()
|
||||
->select(['id', 'name', 'picture'])
|
||||
->select(['id', 'name'])
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn (Product $p): DiscountProductDto => DiscountProductDto::fromModel($p));
|
||||
|
|
|
|||
|
|
@ -68,13 +68,7 @@ final readonly class GetBoPaginatedOrdersQuery implements Query
|
|||
$query->where('status', $input->status);
|
||||
}
|
||||
|
||||
$this->dataTable->sortQuery(
|
||||
Order::class,
|
||||
$query,
|
||||
defaultSortCol: 'date',
|
||||
defaultSortWay: \SortDirection::Descending,
|
||||
columnMap: ['date' => 'id'],
|
||||
);
|
||||
$this->dataTable->sortQuery(Order::class, $query);
|
||||
|
||||
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);
|
||||
/** @var LengthAwarePaginator<int, OrderDto> $ordersDtoPaginator */
|
||||
|
|
|
|||
|
|
@ -37,15 +37,9 @@ final readonly class GetBoTodayOrdersQuery implements Query
|
|||
|
||||
$query = Order::query()->with(['shop']);
|
||||
|
||||
$query->whereCreatedAtBetween(Date::now(), Date::now());
|
||||
$query->where('pickup_day', Date::now()->toDateString());
|
||||
|
||||
$this->dataTable->sortQuery(
|
||||
Order::class,
|
||||
$query,
|
||||
defaultSortCol: 'date',
|
||||
defaultSortWay: \SortDirection::Descending,
|
||||
columnMap: ['date' => 'id'],
|
||||
);
|
||||
$this->dataTable->sortQuery(Order::class, $query);
|
||||
|
||||
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ namespace App\Queries\Products;
|
|||
use App\Contracts\Query;
|
||||
use App\Contracts\QueryInput;
|
||||
use App\Dto\ProductDto;
|
||||
use App\Models\Discount;
|
||||
use App\Models\Product;
|
||||
use App\Models\Range;
|
||||
use App\Models\Tag;
|
||||
use App\Services\DataTableService;
|
||||
use Domain\Enums\ProductBrand;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Money\Formatter\IntlMoneyFormatter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
|
|
@ -24,7 +22,6 @@ final readonly class GetBoPaginatedProductsQuery implements Query
|
|||
{
|
||||
public function __construct(
|
||||
private DataTableService $dataTable,
|
||||
private IntlMoneyFormatter $moneyFormatter,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -56,12 +53,8 @@ final readonly class GetBoPaginatedProductsQuery implements Query
|
|||
$query->where('star', true);
|
||||
}
|
||||
|
||||
if ($input->discount !== null && $input->discount !== '') {
|
||||
if ($input->discount === '0') {
|
||||
$query->whereHas('discount');
|
||||
} else {
|
||||
$query->where('discount_id', $input->discount);
|
||||
}
|
||||
if ($input->discount) {
|
||||
$query->whereHas('discount');
|
||||
}
|
||||
|
||||
if (! empty($input->tag)) {
|
||||
|
|
@ -74,19 +67,13 @@ final readonly class GetBoPaginatedProductsQuery implements Query
|
|||
|
||||
$this->dataTable->sortQuery(Product::class, $query, ['inco']);
|
||||
|
||||
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);
|
||||
/** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator<array-key, \App\Dto\ProductDto> $productsDtoPaginator */
|
||||
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);
|
||||
/** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator<int, ProductDto> $productsDtoPaginator */
|
||||
$productsDtoPaginator = $paginator->through(fn (Product $product): ProductDto => ProductDto::fromModel($product));
|
||||
|
||||
$brands = ProductBrand::cases();
|
||||
$ranges = Range::query()->pluck('name', 'id');
|
||||
$tags = Tag::query()->pluck('label', 'id');
|
||||
$discounts = Discount::query()
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->mapWithKeys(fn (Discount $d): array => [
|
||||
$d->id => $this->formatDiscountLabel($d),
|
||||
]);
|
||||
$searchFields = str(collect([
|
||||
trans('validation.attributes.name'),
|
||||
trans('validation.custom.attributes.inco_id'),
|
||||
|
|
@ -97,27 +84,7 @@ final readonly class GetBoPaginatedProductsQuery implements Query
|
|||
brands: $brands,
|
||||
ranges: $ranges,
|
||||
tags: $tags,
|
||||
discounts: $discounts,
|
||||
searchFields: $searchFields,
|
||||
);
|
||||
}
|
||||
|
||||
private function formatDiscountLabel(Discount $discount): string
|
||||
{
|
||||
$formattedValue = $discount->value->isPercentage()
|
||||
? $discount->value->getPercentage() . ' %'
|
||||
: $this->moneyFormatter->format($discount->value->getAmount());
|
||||
|
||||
$label = "#{$discount->id} - {$discount->type->label()} ({$formattedValue})";
|
||||
|
||||
if ($discount->date_start !== null && $discount->date_end !== null) {
|
||||
$label .= " [{$discount->date_start->format('d/m/Y')} - {$discount->date_end->format('d/m/Y')}]";
|
||||
} elseif ($discount->date_start !== null) {
|
||||
$label .= ' [' . __('from :date', ['date' => $discount->date_start->format('d/m/Y')]) . ']';
|
||||
} elseif ($discount->date_end !== null) {
|
||||
$label .= ' [' . __('until :date', ['date' => $discount->date_end->format('d/m/Y')]) . ']';
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ final readonly class GetBoPaginatedProductsQueryInput implements QueryInput
|
|||
public ?string $search = null,
|
||||
public ?string $brand = null,
|
||||
public bool $starred = false,
|
||||
public ?string $discount = null,
|
||||
public bool $discount = false,
|
||||
public ?string $tag = null,
|
||||
public ?string $range = null,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,12 @@ final readonly class GetBoPaginatedProductsQueryOutput
|
|||
* @param list<ProductBrand> $brands
|
||||
* @param Collection<int|string, mixed> $ranges
|
||||
* @param Collection<int|string, mixed> $tags
|
||||
* @param Collection<int, string> $discounts
|
||||
*/
|
||||
public function __construct(
|
||||
public LengthAwarePaginator $products,
|
||||
public array $brands,
|
||||
public Collection $ranges,
|
||||
public Collection $tags,
|
||||
public Collection $discounts,
|
||||
public string $searchFields,
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ final readonly class GetFoProductIndexPageQuery implements Query
|
|||
|
||||
$tagList = Tag::query()
|
||||
->published()
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(fn (Tag $tag): TagDto => TagDto::fromModel($tag));
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use App\Dto\ProductWithPricingDto;
|
|||
use App\Models\Product;
|
||||
use App\Services\DataTableService;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\PricingService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ final readonly class GetPaginatedProductsQuery implements Query
|
|||
public function __construct(
|
||||
private DataTableService $dataTable,
|
||||
private ProductRepository $domainProductRepository,
|
||||
private PricingService $pricingService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ final readonly class GetPaginatedProductsQuery implements Query
|
|||
$result = $paginator->through(function (Product $product): ProductWithPricingDto {
|
||||
$domainProduct = $this->domainProductRepository->find($product->getId());
|
||||
Assert::notNull($domainProduct, "Domain product not found for ID {$product->id}");
|
||||
$pricing = $domainProduct->pricing();
|
||||
$pricing = $this->pricingService->calculateForProduct($domainProduct);
|
||||
|
||||
return new ProductWithPricingDto(
|
||||
product: $product,
|
||||
|
|
|
|||
|
|
@ -98,15 +98,11 @@ class DataTableService
|
|||
* @param class-string<TModel> $modelClass
|
||||
* @param TBuilder $query
|
||||
* @param array<int, string> $ignore Columns to ignore for sorting.
|
||||
* @param array<string, string> $columnMap Map virtual column names to database columns.
|
||||
*/
|
||||
public function sortQuery(
|
||||
string $modelClass,
|
||||
Builder $query,
|
||||
array $ignore = [],
|
||||
?string $defaultSortCol = null,
|
||||
SortDirection $defaultSortWay = SortDirection::Ascending,
|
||||
array $columnMap = [],
|
||||
array $ignore = []
|
||||
): Builder {
|
||||
$table = (new $modelClass())->getTable();
|
||||
$rName = $this->request->route()?->getName() ?? 'noroute';
|
||||
|
|
@ -119,22 +115,15 @@ class DataTableService
|
|||
$sort_col = $sort['sort_col'];
|
||||
$sort_way = $sort['sort_way'];
|
||||
|
||||
if ((! $sort_col || $sort_col === 'undefined') && $defaultSortCol !== null) {
|
||||
$sort_col = $defaultSortCol;
|
||||
$sort_way = $defaultSortWay;
|
||||
}
|
||||
|
||||
$this->persistSortSession($rName, $sort_col, $sort_way);
|
||||
|
||||
$dbCol = $columnMap[$sort_col] ?? $sort_col;
|
||||
|
||||
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $dbCol)) {
|
||||
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $sort_col)) {
|
||||
$direction = match ($sort_way) {
|
||||
SortDirection::Descending => 'desc',
|
||||
default => 'asc',
|
||||
};
|
||||
|
||||
$query->orderBy("{$table}.{$dbCol}", $direction);
|
||||
$query->orderBy("{$table}.{$sort_col}", $direction);
|
||||
} elseif ($this->hasTableColumn($table, self::ORDER_COLUMN)) {
|
||||
$query->orderBy(self::ORDER_COLUMN, 'asc');
|
||||
|
||||
|
|
@ -207,7 +196,7 @@ class DataTableService
|
|||
return array_values(array_filter(
|
||||
$cast::cases(),
|
||||
fn ($item): bool => str_contains(
|
||||
// @phpstan-ignore method.notFound (PhpStan cannot detect the label() method on BaseEnum cases)
|
||||
// @phpstan-ignore method.notFound
|
||||
mb_strtolower((string) $item->label()),
|
||||
$searchLower
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ namespace App\Traits\Controllers;
|
|||
|
||||
use App\Actions\Common\ChangeModelOrder;
|
||||
use App\Actions\Common\ChangeModelOrderInput;
|
||||
use App\Actions\Common\ChangeModelOrderPosition;
|
||||
use App\Contracts\Executor;
|
||||
use App\Http\Requests\Bo\ChangeOrderRequest;
|
||||
use App\Http\Requests\Bo\ChangeSelectedOrderRequest;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use App\Services\FlashMessageService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -38,27 +36,4 @@ trait ChangesModelOrder
|
|||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function changeSelectedOrder(
|
||||
Model & ModelWithOrder $model,
|
||||
ChangeSelectedOrderRequest $request,
|
||||
ChangeModelOrderPosition $action,
|
||||
Executor $executor,
|
||||
FlashMessageService $flashMessageService
|
||||
): RedirectResponse {
|
||||
$changed = $executor->execute(
|
||||
$action,
|
||||
$request->toChangeModelOrderPositionInput($model)
|
||||
);
|
||||
|
||||
if (! $changed) {
|
||||
$flashMessageService->error(trans('crud.messages.order_not_changed'));
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
$flashMessageService->success(trans('crud.messages.order_changed'));
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK') ?: 'local',
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'headquarters_phone_reception' => env('LDC_HEADQUARTERS_PHONE_RECEPTION') ?: '02 43 62 70 00',
|
||||
'headquarters_phone_reception' => env('LDC_HEADQUARTERS_PHONE_RECEPTION', '02 43 62 70 00'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "monthly", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'max_files' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'monthly' => [
|
||||
'driver' => 'monthly',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'max_files' => 3,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
'stdout' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDOUT_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stdout',
|
||||
],
|
||||
],
|
||||
|
||||
'k8s' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['daily', 'stdout'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -16,7 +16,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER') ?: 'log',
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -61,5 +61,5 @@ return [
|
|||
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate.
|
||||
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_TRACES_SAMPLE_RATE'),
|
||||
|
||||
'before_send' => [ExceptionsHelper::class, 'sentryEventHandler'],
|
||||
'before_send' => ExceptionsHelper::sentryEventHandler(...),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ return [
|
|||
* | "memcached", "redis", "dynamodb", "array"
|
||||
* |
|
||||
*/
|
||||
'driver' => env('SESSION_DRIVER') ?: 'file',
|
||||
'driver' => env('SESSION_DRIVER', 'file'),
|
||||
|
||||
/**
|
||||
* |--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ enum DiscountType: int
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::AMOUNT->name => 'Montant',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ enum EmployeeSite: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::slhp->name => 'SLHP',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum OrderStatus: int
|
|||
/**
|
||||
* Optional labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::WAITING->name => 'En attente',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ enum PriceShowingMode: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::authenticated->name => 'Client Connecté',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ enum ProductBrand: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::none->name => 'Sans marque',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum RepeatFrequency: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::WEEKLY->name => 'Toutes les semaines',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum SellUnit: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::PCE->name => 'Pièce',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ enum StaticPageSlug: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::CONTACT->name => 'contact',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum StatsCustomerType: int
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::nbSalarie->name => 'Nombre de salariés',
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ enum StatsShopType: int
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::nbActiveClients->name => 'Nombre de clients actifs',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Enums;
|
||||
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Services\PricingService;
|
||||
use Money\Money;
|
||||
use Shared\Enums\BaseEnum;
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ enum TaxRate: int
|
|||
/**
|
||||
* Optional labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::TVA_20->name => 'TVA 20%',
|
||||
|
|
@ -34,17 +34,6 @@ enum TaxRate: int
|
|||
};
|
||||
}
|
||||
|
||||
public static function fromPercent(float $percent): self
|
||||
{
|
||||
return match ($percent) {
|
||||
self::TVA_20->percent() => self::TVA_20,
|
||||
self::TVA_5_5->percent() => self::TVA_5_5,
|
||||
default => self::tryFrom((int) $percent) ?? throw new \UnexpectedValueException(
|
||||
sprintf('Unhandled tax rate: %s', $percent)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public function rate(): float
|
||||
{
|
||||
return $this->percent() / 100;
|
||||
|
|
@ -65,6 +54,6 @@ enum TaxRate: int
|
|||
|
||||
public function applyTo(Money $price): Money
|
||||
{
|
||||
return $price->multiply($this->multiplier(), Product::ROUNDING);
|
||||
return $price->multiply($this->multiplier(), PricingService::ROUNDING);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum InvoicingUnit: string
|
|||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::PCE->name => 'Pièce',
|
||||
|
|
|
|||
|
|
@ -15,19 +15,10 @@ enum OrderUnit: string
|
|||
case KGM = 'KGM';
|
||||
case COLIS = 'COLIS';
|
||||
|
||||
public function toRegateCode(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::COLIS => 'UEA',
|
||||
self::KGM => 'KGM',
|
||||
self::PCE => 'UB',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionnal labels definition
|
||||
*
|
||||
* @phpstan-ignore-next-line classConstant.unused (Base enum usage)
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::PCE->name => 'Pièce',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace Domain\Events\Dto;
|
|||
|
||||
use Domain\Enums\Units\OrderUnit;
|
||||
use Domain\Models\ValueObjects\IncoId;
|
||||
use Domain\Models\ValueObjects\OrderLine;
|
||||
use Money\Money;
|
||||
|
||||
final readonly class OrderPlacedLine
|
||||
|
|
@ -24,17 +23,4 @@ final readonly class OrderPlacedLine
|
|||
throw new \DomainException('Quantity must be greater than 0');
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromOrderLine(OrderLine $line): self
|
||||
{
|
||||
return new self(
|
||||
productName: $line->product->name,
|
||||
productIncoId: clone $line->product->incoId,
|
||||
productWeightIsVariable: $line->product->weight->isVariable,
|
||||
productPriceKgTtc: clone $line->product->priceKgTtc,
|
||||
productOrderUnit: $line->product->orderUnit,
|
||||
quantity: $line->quantity,
|
||||
totalTtc: clone $line->totalTtc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Events\Dto;
|
||||
|
||||
use Domain\Models\ValueObjects\DeliveryAddress;
|
||||
|
||||
final readonly class OrderPlacedShop
|
||||
{
|
||||
public function __construct(
|
||||
|
|
@ -27,14 +25,4 @@ final readonly class OrderPlacedShop
|
|||
throw new \DomainException('Shop city is required');
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromDeliveryAddress(DeliveryAddress $address): self
|
||||
{
|
||||
return new self(
|
||||
name: $address->name,
|
||||
address: $address->address,
|
||||
postcode: $address->postcode,
|
||||
city: $address->city,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Domain\Enums\OrderStatus;
|
||||
use Domain\Events\AnOrderHasBeenPlaced;
|
||||
use Domain\Events\Dto\OrderPlacedLine;
|
||||
use Domain\Events\Dto\OrderPlacedShop;
|
||||
use Domain\Models\ValueObjects\BasketProduct;
|
||||
use Domain\Models\ValueObjects\DeliveryAddress;
|
||||
use Domain\Models\ValueObjects\Entities\OrderId;
|
||||
use Domain\Models\ValueObjects\Entities\ShopId;
|
||||
|
|
@ -70,47 +65,4 @@ final class Order implements Entity
|
|||
|
||||
$this->status = OrderStatus::CANCELED;
|
||||
}
|
||||
|
||||
public static function fromBasket(
|
||||
OrderId $id,
|
||||
OrderReference $reference,
|
||||
Basket $basket,
|
||||
PickupTimeSlot $pickupTimeSlot,
|
||||
Shopper $shopper,
|
||||
Shop $shop
|
||||
): self {
|
||||
$lines = array_map(
|
||||
fn (BasketProduct $item) => OrderLine::fromBasketProduct($item),
|
||||
array_values($basket->items())
|
||||
);
|
||||
|
||||
return new self(
|
||||
id: $id,
|
||||
shopId: clone $shop->id(),
|
||||
reference: $reference,
|
||||
employee: OrderEmployee::fromShopper($shopper),
|
||||
deliveryAddress: DeliveryAddress::fromShop($shop),
|
||||
pickupTimeSlot: clone $pickupTimeSlot,
|
||||
status: OrderStatus::VALIDATED,
|
||||
total: clone $basket->totalTaxIncluded(),
|
||||
lines: $lines
|
||||
);
|
||||
}
|
||||
|
||||
public function toPlacedEvent(): AnOrderHasBeenPlaced
|
||||
{
|
||||
return new AnOrderHasBeenPlaced(
|
||||
orderId: $this->id(),
|
||||
reference: $this->reference,
|
||||
totalTtc: $this->getTotal(),
|
||||
byMatricule: $this->employee->matricule,
|
||||
employee: clone $this->employee,
|
||||
shop: OrderPlacedShop::fromDeliveryAddress($this->deliveryAddress),
|
||||
pickupDay: CarbonImmutable::instance($this->pickupTimeSlot->getAt()),
|
||||
lines: array_map(
|
||||
fn (OrderLine $line) => OrderPlacedLine::fromOrderLine($line),
|
||||
$this->lines
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace Domain\Models\Entities;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\WeekDay;
|
||||
use Domain\Enums\RepeatFrequency;
|
||||
use Domain\Models\ValueObjects\Entities\PickupExceptionId;
|
||||
use Shared\DDD\Entity;
|
||||
|
|
@ -28,9 +27,4 @@ final readonly class PickupExceptionOnDate implements Entity
|
|||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function isPickupDay(WeekDay $pickupDay = WeekDay::Friday): bool
|
||||
{
|
||||
return $this->date->isDayOfWeek($pickupDay->value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ declare(strict_types=1);
|
|||
namespace Domain\Models\Entities;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Domain\Enums\SellUnit;
|
||||
use Domain\Enums\TaxRate;
|
||||
use Domain\Enums\Units\InvoicingUnit;
|
||||
use Domain\Models\ValueObjects\DiscountRate;
|
||||
use Domain\Models\ValueObjects\DiscountValue;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Models\ValueObjects\ProductPricing;
|
||||
use Domain\Models\ValueObjects\ProductWeight;
|
||||
use Money\Money;
|
||||
use Shared\DDD\Entity;
|
||||
|
|
@ -20,10 +17,6 @@ use Shared\DDD\Entity;
|
|||
/** @implements \Shared\DDD\Entity<\Domain\Models\ValueObjects\Entities\ProductId> */
|
||||
final readonly class Product implements Entity
|
||||
{
|
||||
public const int ROUNDING = Money::ROUND_UP;
|
||||
|
||||
private const int GRAMS_PER_KG = 1000;
|
||||
|
||||
public function __construct(
|
||||
private ProductId $id,
|
||||
public Money $price,
|
||||
|
|
@ -44,145 +37,4 @@ final readonly class Product implements Entity
|
|||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function canBeOrderOn(?CarbonInterface $date = null): bool
|
||||
{
|
||||
$target = $date ?? CarbonImmutable::now();
|
||||
|
||||
if ($this->orderable_start && $target < $this->orderable_start) {
|
||||
return false;
|
||||
}
|
||||
if ($this->orderable_end && $target > $this->orderable_end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->status && $this->hasInco;
|
||||
}
|
||||
|
||||
public function isOrderable(?CarbonInterface $date = null): bool
|
||||
{
|
||||
return $this->canBeOrderOn($date);
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function hasInco(): bool
|
||||
{
|
||||
return $this->hasInco;
|
||||
}
|
||||
|
||||
public function hasDiscount(): bool
|
||||
{
|
||||
return $this->validDiscount !== null;
|
||||
}
|
||||
|
||||
public function basePriceHt(): Money
|
||||
{
|
||||
if ($this->invoicing_unit === InvoicingUnit::KGM && $this->sell_unit === SellUnit::PCE && $this->product_weight) {
|
||||
// * Price per kg => price of the piece: price x grams / 1000 (single rounding, no float)
|
||||
return $this->price
|
||||
->multiply($this->product_weight->toGrams())
|
||||
->divide(self::GRAMS_PER_KG, self::ROUNDING);
|
||||
}
|
||||
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
public function basePriceTtc(): Money
|
||||
{
|
||||
return $this->tax_rate->applyTo($this->basePriceHt());
|
||||
}
|
||||
|
||||
public function discountAmountHt(): Money
|
||||
{
|
||||
$basePriceHt = $this->basePriceHt();
|
||||
$discountAmount = $this->validDiscount?->calculateDiscountAmount($basePriceHt)
|
||||
?? new Money(0, $basePriceHt->getCurrency());
|
||||
|
||||
if ($discountAmount->greaterThan($basePriceHt)) {
|
||||
return $basePriceHt;
|
||||
}
|
||||
|
||||
return $discountAmount;
|
||||
}
|
||||
|
||||
public function discountedPriceHt(): Money
|
||||
{
|
||||
return $this->basePriceHt()->subtract($this->discountAmountHt());
|
||||
}
|
||||
|
||||
public function discountedPriceTtc(): Money
|
||||
{
|
||||
return $this->tax_rate->applyTo($this->discountedPriceHt());
|
||||
}
|
||||
|
||||
public function priceKgTtc(): Money
|
||||
{
|
||||
if ($this->invoicing_unit === InvoicingUnit::KGM) {
|
||||
// * The product price is already a price per kg: it is the reference for the kg price
|
||||
$discountAmountKgHt = $this->validDiscount?->calculateDiscountAmount($this->price)
|
||||
?? new Money(0, $this->price->getCurrency());
|
||||
|
||||
$grams = $this->product_weight?->toGrams() ?? 0;
|
||||
|
||||
if (
|
||||
$this->validDiscount !== null
|
||||
&& ! $this->validDiscount->isPercentage()
|
||||
&& $this->sell_unit === SellUnit::PCE
|
||||
&& $grams > 0
|
||||
) {
|
||||
// * A fixed amount applies to the piece: bring it back to a per-kg amount
|
||||
$discountAmountKgHt = $discountAmountKgHt
|
||||
->multiply(self::GRAMS_PER_KG)
|
||||
->divide($grams, self::ROUNDING);
|
||||
}
|
||||
|
||||
$discountedKgPriceHt = $this->price->subtract($discountAmountKgHt);
|
||||
|
||||
if ($discountedKgPriceHt->isNegative()) {
|
||||
$discountedKgPriceHt = new Money(0, $this->price->getCurrency());
|
||||
}
|
||||
|
||||
return $this->tax_rate->applyTo($discountedKgPriceHt);
|
||||
}
|
||||
|
||||
$priceTtc = $this->discountedPriceTtc();
|
||||
$grams = $this->product_weight?->toGrams() ?? 0;
|
||||
|
||||
// NOTE: If the product is not published or not sold by piece, we don't need to convert the price to per kg
|
||||
if ($grams > 0 && $this->status && $this->invoicing_unit === InvoicingUnit::PCE) {
|
||||
// * Price of the piece => price per kg: price x 1000 / grams (single rounding, no float)
|
||||
return $priceTtc
|
||||
->multiply(self::GRAMS_PER_KG)
|
||||
->divide($grams, self::ROUNDING);
|
||||
}
|
||||
|
||||
return $priceTtc;
|
||||
}
|
||||
|
||||
public function discountRate(): DiscountRate
|
||||
{
|
||||
if ($this->validDiscount?->isPercentage()) {
|
||||
return DiscountRate::fromPercentage($this->validDiscount->getPercentage());
|
||||
}
|
||||
|
||||
return DiscountRate::fromAmounts($this->discountAmountHt(), $this->basePriceHt());
|
||||
}
|
||||
|
||||
public function pricing(): ProductPricing
|
||||
{
|
||||
return new ProductPricing(
|
||||
basePriceHt: $this->basePriceHt(),
|
||||
basePriceTtc: $this->basePriceTtc(),
|
||||
discountAmountHt: $this->discountAmountHt(),
|
||||
discountedPriceHt: $this->discountedPriceHt(),
|
||||
discountedPriceTtc: $this->discountedPriceTtc(),
|
||||
priceKgTtc: $this->priceKgTtc(),
|
||||
discountRate: $this->discountRate(),
|
||||
hasDiscount: $this->hasDiscount()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ namespace Domain\Models\ValueObjects;
|
|||
use Domain\Enums\ProductBrand;
|
||||
use Domain\Enums\TaxRate;
|
||||
use Domain\Enums\Units\OrderUnit;
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Services\PricingService;
|
||||
use Money\Money;
|
||||
|
||||
final readonly class BasketProduct
|
||||
|
|
@ -58,29 +58,22 @@ final readonly class BasketProduct
|
|||
return $this->unitPriceHt;
|
||||
}
|
||||
|
||||
public function unitPriceDiscounted(): Money
|
||||
{
|
||||
return $this->unitPriceHt->subtract($this->unitDiscount);
|
||||
}
|
||||
|
||||
public function unitTax(): Money
|
||||
{
|
||||
return $this->unitTtc()->subtract($this->unitPriceDiscounted());
|
||||
}
|
||||
|
||||
public function unitTtc(): Money
|
||||
{
|
||||
return $this->taxRate->applyTo($this->unitPriceDiscounted());
|
||||
$unitPriceDiscounted = $this->unitPriceHt->subtract($this->unitDiscount);
|
||||
|
||||
// applyTo renvoie déjà le montant TTC (900 + 180 = 1080)
|
||||
return $this->taxRate->applyTo($unitPriceDiscounted);
|
||||
}
|
||||
|
||||
public function discountTotal(): Money
|
||||
{
|
||||
return $this->unitDiscount->multiply($this->qty, Product::ROUNDING);
|
||||
return $this->unitDiscount->multiply($this->qty, PricingService::ROUNDING);
|
||||
}
|
||||
|
||||
public function priceTotalHt(): Money
|
||||
{
|
||||
return $this->unitPriceHt->multiply($this->qty, Product::ROUNDING);
|
||||
return $this->unitPriceHt->multiply($this->qty, PricingService::ROUNDING);
|
||||
}
|
||||
|
||||
public function subtotalHt(): Money
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Models\Entities\Shop;
|
||||
|
||||
final readonly class DeliveryAddress
|
||||
{
|
||||
public function __construct(
|
||||
|
|
@ -20,19 +18,6 @@ final readonly class DeliveryAddress
|
|||
$this->validate();
|
||||
}
|
||||
|
||||
public static function fromShop(Shop $shop): self
|
||||
{
|
||||
return new self(
|
||||
name: $shop->name,
|
||||
address: $shop->address,
|
||||
postcode: $shop->postcode,
|
||||
city: $shop->city,
|
||||
phone: $shop->phone,
|
||||
latitude: $shop->latitude,
|
||||
longitude: $shop->longitude
|
||||
);
|
||||
}
|
||||
|
||||
private function validate(): void
|
||||
{
|
||||
if (trim($this->name) === '') {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,9 @@ declare(strict_types=1);
|
|||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Enums\DiscountType;
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Services\PricingService;
|
||||
use Money\Money;
|
||||
|
||||
/**
|
||||
* Represents a product discount (either a percentage or a fixed amount).
|
||||
*
|
||||
* Business rule: All discounts apply to the piece (sold unit), regardless of the product's invoicing unit.
|
||||
*/
|
||||
final readonly class DiscountValue
|
||||
{
|
||||
private function __construct(
|
||||
|
|
@ -118,6 +113,6 @@ final readonly class DiscountValue
|
|||
|
||||
$rate = $this->getPercentageRate();
|
||||
|
||||
return $basePrice->multiply((string) $rate, Product::ROUNDING);
|
||||
return $basePrice->multiply((string) $rate, PricingService::ROUNDING);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Models\Entities\Shopper;
|
||||
|
||||
final readonly class OrderEmployee
|
||||
{
|
||||
public function __construct(
|
||||
|
|
@ -20,18 +18,6 @@ final readonly class OrderEmployee
|
|||
$this->assertNotEmpty('lastname', $lastname);
|
||||
}
|
||||
|
||||
public static function fromShopper(Shopper $shopper): self
|
||||
{
|
||||
return new self(
|
||||
matricule: clone $shopper->matricule,
|
||||
firstname: $shopper->prenom,
|
||||
lastname: $shopper->nom,
|
||||
facility: $shopper->etablissement,
|
||||
site: $shopper->site,
|
||||
service: $shopper->service
|
||||
);
|
||||
}
|
||||
|
||||
private function assertNotEmpty(string $attribute, string $value): void
|
||||
{
|
||||
if (empty(trim($value))) {
|
||||
|
|
|
|||
|
|
@ -51,19 +51,4 @@ final readonly class OrderLine
|
|||
throw new \DomainException('Total TTC must be non-negative');
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromBasketProduct(BasketProduct $item): self
|
||||
{
|
||||
return new self(
|
||||
product: OrderLineProduct::fromBasketProduct($item),
|
||||
quantity: $item->qty,
|
||||
unitPriceHt: clone $item->unitPriceHt,
|
||||
unitDiscount: clone $item->unitDiscount,
|
||||
unitTax: clone $item->unitTax(),
|
||||
totalHt: clone $item->subtotalHt(),
|
||||
totalDiscount: clone $item->discountTotal(),
|
||||
totalTax: clone $item->taxTotal(),
|
||||
totalTtc: clone $item->totalTtc()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,21 +44,4 @@ final readonly class OrderLineProduct
|
|||
throw new \DomainException('Price per kg TTC has to be positive');
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromBasketProduct(BasketProduct $item): self
|
||||
{
|
||||
return new self(
|
||||
incoId: $item->incoId ?? throw new \DomainException('incoId cannot be null for an order'),
|
||||
originalId: $item->id,
|
||||
name: $item->name,
|
||||
weight: new ProductWeight($item->weightGrams, $item->weightVariable),
|
||||
brand: $item->brand ?? ProductBrand::none,
|
||||
orderUnit: $item->orderUnit,
|
||||
price: clone $item->unitPriceHt,
|
||||
discountedPrice: $item->unitPriceDiscounted(),
|
||||
priceKgTtc: clone $item->priceKgTtc,
|
||||
taxRate: $item->taxRate,
|
||||
picture: $item->picture
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Shared\Exceptions\InvalidUuidException;
|
||||
use Shared\Helpers\UuidHelper;
|
||||
|
||||
final readonly class OrderReference implements \Stringable
|
||||
|
|
@ -19,29 +17,9 @@ final readonly class OrderReference implements \Stringable
|
|||
}
|
||||
}
|
||||
|
||||
public static function fromString(string $value): self
|
||||
{
|
||||
if (UuidHelper::isValid($value)) {
|
||||
return new self(Uuid::fromString($value));
|
||||
}
|
||||
|
||||
return self::fromBase58($value);
|
||||
}
|
||||
|
||||
public static function fromBase58(string $base58): self
|
||||
{
|
||||
try {
|
||||
$uuid = UuidHelper::fromBase58($base58);
|
||||
} catch (InvalidUuidException) {
|
||||
throw new \DomainException('Order reference must be a valid base-58 string');
|
||||
}
|
||||
|
||||
return new self($uuid);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
return UuidHelper::toBase58($this->value);
|
||||
return $this->value->toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Domain\Models\Entities\PickupExceptionOnDate;
|
||||
|
||||
|
|
@ -21,21 +20,4 @@ final readonly class ShopPickupOverride
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function hasReplacement(): bool
|
||||
{
|
||||
return $this->replacementDate !== null;
|
||||
}
|
||||
|
||||
public function effectiveDeadline(CarbonInterface $defaultDeadline): CarbonImmutable
|
||||
{
|
||||
return $this->orderDeadlineAt !== null
|
||||
? CarbonImmutable::createFromInterface($this->orderDeadlineAt)
|
||||
: CarbonImmutable::createFromInterface($defaultDeadline);
|
||||
}
|
||||
|
||||
public function isDeadlinePassed(CarbonInterface $now, CarbonInterface $defaultDeadline): bool
|
||||
{
|
||||
return $now->greaterThan($this->effectiveDeadline($defaultDeadline));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Services;
|
||||
|
||||
use Domain\Enums\OrderStatus;
|
||||
use Domain\Enums\ProductBrand;
|
||||
use Domain\Models\Entities\Basket;
|
||||
use Domain\Models\Entities\Order;
|
||||
use Domain\Models\Entities\Shop;
|
||||
use Domain\Models\Entities\Shopper;
|
||||
use Domain\Models\ValueObjects\DeliveryAddress;
|
||||
use Domain\Models\ValueObjects\Entities\OrderId;
|
||||
use Domain\Models\ValueObjects\OrderEmployee;
|
||||
use Domain\Models\ValueObjects\OrderLine;
|
||||
use Domain\Models\ValueObjects\OrderLineProduct;
|
||||
use Domain\Models\ValueObjects\OrderReference;
|
||||
use Domain\Models\ValueObjects\PickupTimeSlot;
|
||||
use Domain\Models\ValueObjects\ProductWeight;
|
||||
use Domain\Ports\Repositories\OrderRepository;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
|
|
@ -27,13 +34,62 @@ class OrderService
|
|||
Shopper $shopper,
|
||||
Shop $shop
|
||||
): Order {
|
||||
return Order::fromBasket(
|
||||
$lines = [];
|
||||
foreach ($basket->items() as $item) {
|
||||
$orderLineProduct = new OrderLineProduct(
|
||||
incoId: $item->incoId ?? throw new \DomainException('incoId cannot be null for an order'),
|
||||
originalId: $item->id,
|
||||
name: $item->name,
|
||||
weight: new ProductWeight($item->weightGrams, $item->weightVariable),
|
||||
brand: $item->brand ?? ProductBrand::none,
|
||||
orderUnit: $item->orderUnit,
|
||||
price: clone $item->unitPriceHt,
|
||||
discountedPrice: $item->unitPriceHt->subtract($item->unitDiscount),
|
||||
priceKgTtc: clone $item->priceKgTtc,
|
||||
taxRate: $item->taxRate,
|
||||
picture: $item->picture
|
||||
);
|
||||
|
||||
$lines[] = new OrderLine(
|
||||
product: $orderLineProduct,
|
||||
quantity: $item->qty,
|
||||
unitPriceHt: clone $item->unitPriceHt,
|
||||
unitDiscount: clone $item->unitDiscount,
|
||||
unitTax: clone $item->taxRate
|
||||
->applyTo($item->unitPriceHt
|
||||
->subtract($item->unitDiscount))
|
||||
->subtract($item->unitPriceHt
|
||||
->subtract($item->unitDiscount)),
|
||||
totalHt: clone $item->subtotalHt(),
|
||||
totalDiscount: clone $item->discountTotal(),
|
||||
totalTax: clone $item->taxTotal(),
|
||||
totalTtc: clone $item->totalTtc()
|
||||
);
|
||||
}
|
||||
|
||||
return new Order(
|
||||
id: $this->newOrderId(),
|
||||
shopId: clone $shop->id(),
|
||||
reference: $this->newReference(),
|
||||
basket: $basket,
|
||||
pickupTimeSlot: $pickupTimeSlot,
|
||||
shopper: $shopper,
|
||||
shop: $shop
|
||||
employee: new OrderEmployee(
|
||||
matricule: clone $shopper->matricule,
|
||||
firstname: $shopper->prenom,
|
||||
lastname: $shopper->nom,
|
||||
facility: $shopper->etablissement,
|
||||
site: $shopper->site,
|
||||
service: $shopper->service
|
||||
),
|
||||
deliveryAddress: new DeliveryAddress(
|
||||
name: $shop->name,
|
||||
address: $shop->address,
|
||||
postcode: $shop->postcode,
|
||||
city: $shop->city,
|
||||
phone: $shop->phone
|
||||
),
|
||||
pickupTimeSlot: clone $pickupTimeSlot,
|
||||
status: OrderStatus::VALIDATED,
|
||||
total: clone $basket->totalTaxIncluded(),
|
||||
lines: $lines
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ use Domain\Builders\OrderBuilder;
|
|||
use Domain\Builders\OrderBuilderCompleted;
|
||||
use Domain\Builders\OrderBuilderWithBasket;
|
||||
use Domain\Enums\OrderTunnelStep;
|
||||
use Domain\Events\AnOrderHasBeenPlaced;
|
||||
use Domain\Events\Dto\OrderPlacedLine;
|
||||
use Domain\Events\Dto\OrderPlacedShop;
|
||||
use Domain\Exceptions\OrderTunnel\MissingOrderBuilderException;
|
||||
use Domain\Exceptions\OrderTunnel\OrderLimitExceededException;
|
||||
use Domain\Exceptions\OrderTunnel\WeeklyLimitExceededException;
|
||||
|
|
@ -18,6 +21,7 @@ use Domain\Models\Entities\Order;
|
|||
use Domain\Models\Entities\Shop;
|
||||
use Domain\Models\Entities\Shopper;
|
||||
use Domain\Models\ValueObjects\Entities\ShopperId;
|
||||
use Domain\Models\ValueObjects\OrderLine;
|
||||
use Domain\Models\ValueObjects\PickupTimeSlot;
|
||||
use Domain\Policies\CustomerOrderLimitPolicy;
|
||||
use Domain\Ports\Repositories\OrderRepository;
|
||||
|
|
@ -215,7 +219,8 @@ class OrderTunnelService
|
|||
|
||||
$this->orderRepository->persist($order);
|
||||
|
||||
$this->eventDispatcher->dispatch($order->toPlacedEvent());
|
||||
$event = $this->mapNewOrderEvent($order);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$this->basketService->empty();
|
||||
$this->orderBuilderSessionManager->clear();
|
||||
|
|
@ -228,4 +233,39 @@ class OrderTunnelService
|
|||
{
|
||||
return CarbonImmutable::now()->addDays(self::TIMESLOTS_DAY_TARGET);
|
||||
}
|
||||
|
||||
private function mapNewOrderEvent(Order $order): AnOrderHasBeenPlaced
|
||||
{
|
||||
return new AnOrderHasBeenPlaced(
|
||||
orderId: $order->id(),
|
||||
byMatricule: $order->employee->matricule,
|
||||
employee: clone $order->employee,
|
||||
reference: $order->reference,
|
||||
totalTtc: $order->getTotal(),
|
||||
shop: new OrderPlacedShop(
|
||||
name: $order->deliveryAddress->name,
|
||||
address: $order->deliveryAddress->address,
|
||||
postcode: $order->deliveryAddress->postcode,
|
||||
city: $order->deliveryAddress->city
|
||||
),
|
||||
pickupDay: CarbonImmutable::instance($order->pickupTimeSlot->getAt()),
|
||||
lines: array_map(
|
||||
[$this, 'mapToPlaceOrderLine'],
|
||||
$order->getLines()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function mapToPlaceOrderLine(OrderLine $line): OrderPlacedLine
|
||||
{
|
||||
return new OrderPlacedLine(
|
||||
productName: $line->product->name,
|
||||
productIncoId: clone $line->product->incoId,
|
||||
productWeightIsVariable: $line->product->weight->isVariable,
|
||||
productPriceKgTtc: clone $line->product->priceKgTtc,
|
||||
productOrderUnit: $line->product->orderUnit,
|
||||
quantity: $line->quantity,
|
||||
totalTtc: clone $line->totalTtc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ class PickupSchedulingService
|
|||
return CarbonPeriod::create($defaultDeadLine, $pickupDate->endOfDay());
|
||||
}
|
||||
|
||||
public function isPickupDay(PickupExceptionOnDate $exception): bool
|
||||
{
|
||||
return $exception->date->isDayOfWeek(self::PICKUP_DAY->value);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\EditablePickupExceptionForShop[] */
|
||||
public function getEditablePickupExceptionsFor(Shop $shop): array
|
||||
{
|
||||
|
|
@ -80,23 +85,24 @@ class PickupSchedulingService
|
|||
return array_map(function (PickupExceptionOnDate $pickupException) use ($shopExceptions) {
|
||||
$associated = null;
|
||||
foreach ($shopExceptions as $shopEx) {
|
||||
if ($shopEx->exception->id->equals($pickupException->id)) {
|
||||
if ($shopEx->exception->id->value === $pickupException->id->value) {
|
||||
$associated = $shopEx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$isChecked = $associated !== null;
|
||||
$isPickupDay = $pickupException->isPickupDay(self::PICKUP_DAY);
|
||||
$defaultDeadline = $this->getDefaultDeadline($pickupException->date);
|
||||
$isBlocked = $associated !== null && ! $associated->hasReplacement();
|
||||
$isPastDeadline = $associated !== null && $associated->isDeadlinePassed($this->getNow(), $defaultDeadline);
|
||||
$isChecked = $associated !== null;
|
||||
|
||||
$isPickupDay = $this->isPickupDay($pickupException);
|
||||
$deadline = $associated?->orderDeadlineAt
|
||||
? CarbonImmutable::createFromInterface($associated->orderDeadlineAt)
|
||||
: $this->getDefaultDeadline(CarbonImmutable::createFromInterface($pickupException->date));
|
||||
|
||||
return new EditablePickupExceptionForShop(
|
||||
$pickupException,
|
||||
$isPickupDay,
|
||||
$isChecked && $isPickupDay && $isBlocked,
|
||||
$isChecked && $isPickupDay && $isPastDeadline
|
||||
$isChecked && $isPickupDay && $associated->replacementDate === null,
|
||||
$isChecked && $isPickupDay && $this->getNow()->greaterThan($deadline)
|
||||
);
|
||||
}, $allExceptions);
|
||||
}
|
||||
|
|
@ -154,15 +160,23 @@ class PickupSchedulingService
|
|||
|
||||
protected function getTimeSlotForException(ShopPickupOverride $shopPickupException): ?PickupTimeSlot
|
||||
{
|
||||
if (! $shopPickupException->hasReplacement() || $shopPickupException->replacementDate === null) {
|
||||
$replacement = $shopPickupException->replacementDate;
|
||||
|
||||
// ? Is there a substitute date configured ?
|
||||
if ($replacement === null) {
|
||||
// * Blocked week: exception active but no replacement provided
|
||||
return null;
|
||||
}
|
||||
|
||||
$defaultDeadline = $this->getDefaultDeadline(CarbonImmutable::createFromInterface($shopPickupException->exception->date));
|
||||
$deadline = $shopPickupException->effectiveDeadline($defaultDeadline);
|
||||
// ? Is there an order deadline ?
|
||||
$deadline = $shopPickupException->orderDeadlineAt
|
||||
// * Use exception deadline.
|
||||
? CarbonImmutable::createFromInterface($shopPickupException->orderDeadlineAt)
|
||||
// * Use default deadline.
|
||||
: $this->getDefaultDeadline(CarbonImmutable::createFromInterface($shopPickupException->exception->date));
|
||||
|
||||
return $this->isBefore($deadline)
|
||||
? $this->createTimeSlot($shopPickupException->replacementDate->toImmutable())
|
||||
? $this->createTimeSlot($replacement->toImmutable())
|
||||
: null;
|
||||
}
|
||||
|
||||
|
|
@ -181,16 +195,12 @@ class PickupSchedulingService
|
|||
->addDays(self::CUTOFF_DAY->value - 1)
|
||||
->addHours(self::CUTOFF_HOUR);
|
||||
|
||||
return $this->getCutoffWeekOffset() !== 0
|
||||
? $deadline->addWeeks($this->getCutoffWeekOffset())
|
||||
// @phpstan-ignore notIdentical.alwaysFalse
|
||||
return self::CUTOFF_WEEK_OFFSET !== 0
|
||||
? $deadline->addWeeks(self::CUTOFF_WEEK_OFFSET)
|
||||
: $deadline;
|
||||
}
|
||||
|
||||
private function getCutoffWeekOffset(): int
|
||||
{
|
||||
return self::CUTOFF_WEEK_OFFSET;
|
||||
}
|
||||
|
||||
protected function isBefore(CarbonInterface $date): bool
|
||||
{
|
||||
return $this->getNow()->lessThanOrEqualTo($date);
|
||||
|
|
|
|||
90
domain/Services/PricingService.php
Normal file
90
domain/Services/PricingService.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Services;
|
||||
|
||||
use Domain\Enums\SellUnit;
|
||||
use Domain\Enums\TaxRate;
|
||||
use Domain\Enums\Units\InvoicingUnit;
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\DiscountRate;
|
||||
use Domain\Models\ValueObjects\ProductPricing;
|
||||
use Money\Money;
|
||||
|
||||
class PricingService
|
||||
{
|
||||
public const ROUNDING = Money::ROUND_UP;
|
||||
|
||||
private const GRAMS_PER_KG = 1000;
|
||||
|
||||
public function calculateForProduct(Product $product): ProductPricing
|
||||
{
|
||||
$basePriceHt = $this->priceHt($product);
|
||||
$basePriceTtc = $this->applyTax($basePriceHt, $product->tax_rate);
|
||||
|
||||
$discountAmountHt = $product->validDiscount?->calculateDiscountAmount($basePriceHt)
|
||||
?? new Money(0, $basePriceHt->getCurrency());
|
||||
|
||||
$discountedPriceHt = $basePriceHt->subtract($discountAmountHt);
|
||||
$discountedPriceTtc = $this->applyTax($discountedPriceHt, $product->tax_rate);
|
||||
|
||||
$priceKgTtc = $this->calculatePriceKgTtc($discountedPriceHt, $product);
|
||||
$discountRate = $this->calculateDiscountRate($basePriceHt, $discountAmountHt, $product);
|
||||
|
||||
return new ProductPricing(
|
||||
basePriceHt: $basePriceHt,
|
||||
basePriceTtc: $basePriceTtc,
|
||||
discountAmountHt: $discountAmountHt,
|
||||
discountedPriceHt: $discountedPriceHt,
|
||||
discountedPriceTtc: $discountedPriceTtc,
|
||||
priceKgTtc: $priceKgTtc,
|
||||
discountRate: $discountRate,
|
||||
hasDiscount: $product->validDiscount !== null
|
||||
);
|
||||
}
|
||||
|
||||
private function priceHt(Product $product): Money
|
||||
{
|
||||
if ($product->invoicing_unit === InvoicingUnit::KGM && $product->sell_unit === SellUnit::PCE && $product->product_weight) {
|
||||
// * Price per kg => price of the piece: price × grams ÷ 1000 (single rounding, no float)
|
||||
return $product->price
|
||||
->multiply($product->product_weight->toGrams())
|
||||
->divide(self::GRAMS_PER_KG, PricingService::ROUNDING);
|
||||
}
|
||||
|
||||
return $product->price;
|
||||
}
|
||||
|
||||
private function calculatePriceKgTtc(Money $discountedPriceHt, Product $product): Money
|
||||
{
|
||||
$priceTtc = $this->applyTax($discountedPriceHt, $product->tax_rate);
|
||||
|
||||
$grams = $product->product_weight?->toGrams() ?? 0;
|
||||
|
||||
if ($grams > 0 && $product->status && $product->invoicing_unit === InvoicingUnit::PCE) {
|
||||
// * Price of the piece => price per kg: price × 1000 ÷ grams (single rounding, no float)
|
||||
return $priceTtc
|
||||
->multiply(self::GRAMS_PER_KG)
|
||||
->divide($grams, self::ROUNDING);
|
||||
}
|
||||
|
||||
return $priceTtc;
|
||||
}
|
||||
|
||||
private function applyTax(Money $price, TaxRate $taxRate): Money
|
||||
{
|
||||
return $taxRate->applyTo($price);
|
||||
}
|
||||
|
||||
private function calculateDiscountRate(Money $priceHt, Money $discountAmount, Product $product): DiscountRate
|
||||
{
|
||||
$discount = $product->validDiscount;
|
||||
|
||||
if ($discount?->isPercentage()) {
|
||||
return DiscountRate::fromPercentage($discount->getPercentage());
|
||||
}
|
||||
|
||||
return DiscountRate::fromAmounts($discountAmount, $priceHt);
|
||||
}
|
||||
}
|
||||
50
domain/Services/ProductAvailabilityService.php
Normal file
50
domain/Services/ProductAvailabilityService.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Services;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\BasketProduct;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
|
||||
class ProductAvailabilityService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductRepository $productRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function isOrderable(Product $product): bool
|
||||
{
|
||||
$now = CarbonImmutable::now();
|
||||
$start = ! $product->orderable_start || $now->gt($product->orderable_start);
|
||||
$end = ! $product->orderable_end || $now->lt($product->orderable_end);
|
||||
$isOrderable = $start && $end;
|
||||
|
||||
return $isOrderable && $product->status && $this->isAssociatedWithInco($product);
|
||||
}
|
||||
|
||||
public function isPublished(Product $product): bool
|
||||
{
|
||||
return $product->status;
|
||||
}
|
||||
|
||||
public function isBasketProductPublished(BasketProduct $basketProduct): bool
|
||||
{
|
||||
$product = $this->productRepository->find($basketProduct->id);
|
||||
|
||||
if (! $product) {
|
||||
// NOTE: Don't throw an exception here, as the product may have been deleted after being added to the basket
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isPublished($product);
|
||||
}
|
||||
|
||||
public function isAssociatedWithInco(Product $product): bool
|
||||
{
|
||||
return $product->hasInco;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Domain\Services;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Domain\Enums\Units\OrderUnit as UnitsOrderUnit;
|
||||
use Domain\Events\Dto\RegateOrderCsvGenerated;
|
||||
use Domain\Models\ValueObjects\Matricule;
|
||||
use Domain\Models\ValueObjects\OrderReference;
|
||||
|
|
@ -53,7 +54,7 @@ class RegateService
|
|||
(string) $line->productIncoId->value,
|
||||
'commande ferme',
|
||||
(string) $line->productOrderUnit->value,
|
||||
$line->productOrderUnit->toRegateCode(),
|
||||
$this->maptoRegateUnit($line->productOrderUnit),
|
||||
'',
|
||||
$pickupDay->format('d/m/Y'),
|
||||
(string) $line->quantity,
|
||||
|
|
@ -76,4 +77,13 @@ class RegateService
|
|||
new RegateOrderCsvGenerated($csvContent, $fileName)
|
||||
);
|
||||
}
|
||||
|
||||
private function maptoRegateUnit(UnitsOrderUnit $unit): string
|
||||
{
|
||||
return match ($unit) {
|
||||
UnitsOrderUnit::COLIS => 'UEA',
|
||||
UnitsOrderUnit::KGM => 'KGM',
|
||||
UnitsOrderUnit::PCE => 'UB',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ class ShopperService
|
|||
{
|
||||
$shopper = $this->getShopperFor($shopperId);
|
||||
|
||||
return $shopper !== null ? $this->resolveShopForEmployee($shopper) : null;
|
||||
if ($shopper === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->resolveShopForEmployee($shopper);
|
||||
}
|
||||
|
||||
public function resolveShopForEmployee(Shopper $shopper): Shop
|
||||
|
|
@ -68,6 +72,12 @@ class ShopperService
|
|||
|
||||
public function canCustomerPassOrdersApp(ShopperId $shopperId): bool
|
||||
{
|
||||
return $this->getShopperFor($shopperId)?->isActive() ?? false;
|
||||
$shopper = $this->getShopperFor($shopperId);
|
||||
|
||||
if ($shopper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $shopper->isActive();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class EloquentPickupExceptionRepository implements PickupExceptionRepository
|
|||
->where('date', '>', $period->getStartDate()->toImmutable())
|
||||
->where('date', '<=', $period->getEndDate()?->toImmutable())
|
||||
->get()
|
||||
->map(fn (PickupException $ex) => $this->toShopDomain($ex, $ex->pivot))
|
||||
->map(fn (PickupException $ex) => $this->toShopDomain($ex))
|
||||
->all();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ use Domain\Models\Entities\Product as DomainProduct;
|
|||
use Domain\Models\ValueObjects\BasketProduct;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Ports\Repositories\ProductRepository;
|
||||
use Domain\Services\PricingService;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class EloquentProductRepository implements ProductRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PricingService $pricingService
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function find(ProductId $id): ?DomainProduct
|
||||
{
|
||||
|
|
@ -66,12 +72,11 @@ class EloquentProductRepository implements ProductRepository
|
|||
public function getBasketProduct(ProductId $id, int $qty): BasketProduct
|
||||
{
|
||||
$product = Product::query()
|
||||
->with(['inco', 'validDiscount'])
|
||||
->where('id', $id->value)
|
||||
->firstOrFail();
|
||||
|
||||
$domainProduct = $this->toEntity($product);
|
||||
$pricing = $domainProduct->pricing();
|
||||
$pricing = $this->pricingService->calculateForProduct($domainProduct);
|
||||
|
||||
return new BasketProduct(
|
||||
id: $id,
|
||||
|
|
|
|||
16
lang/en.json
16
lang/en.json
|
|
@ -46,7 +46,6 @@
|
|||
"Agree": "Agree",
|
||||
"Aisles": "Aisles",
|
||||
"All aisles": "All aisles",
|
||||
"All discounts": "All discounts",
|
||||
"All products have been selected.": "All products have been selected.",
|
||||
"All rights reserved.": "All rights reserved.",
|
||||
"Allergens": "Allergens",
|
||||
|
|
@ -97,7 +96,7 @@
|
|||
"Basket has been cleared": "Basket has been cleared",
|
||||
"Basket Summary": "Basket Summary",
|
||||
"Before proceeding, please check your email for a verification link.": "Before proceeding, please check your email for a verification link.",
|
||||
"Blocked week": "Blocked week",
|
||||
"Bloqued week": "Bloqued week",
|
||||
"Booking": "Booking",
|
||||
"Brand": "Brand",
|
||||
"Breadcrumb": "Breadcrumb",
|
||||
|
|
@ -286,7 +285,6 @@
|
|||
"Frequency": "Frequency",
|
||||
"Frequently asked questions.": "Frequently asked questions.",
|
||||
"from": "from",
|
||||
"from :date": "from :date",
|
||||
"From date": "From date",
|
||||
"Gateway Timeout": "Gateway Timeout",
|
||||
"general conditions": "general conditions",
|
||||
|
|
@ -732,7 +730,6 @@
|
|||
"The answer": "The answer",
|
||||
"The brand associated with the product": "The brand associated with the product",
|
||||
"The customer account was not found.": "The customer account was not found.",
|
||||
"The discount applies to the piece (sold unit).": "The discount applies to the piece (sold unit).",
|
||||
"The employee sync job has been dispatched": "The employee sync job has been dispatched",
|
||||
"The existing encrypted environment file is not in readable format. Use --force to overwrite it.": "The existing encrypted environment file is not in readable format. Use --force to overwrite it.",
|
||||
"The existing encryption key is required to update the encrypted environment file.": "The existing encryption key is required to update the encrypted environment file.",
|
||||
|
|
@ -798,7 +795,6 @@
|
|||
"Unprocessable Entity": "Unprocessable Entity",
|
||||
"Unsubscribe": "Unsubscribe",
|
||||
"Unsupported Media Type": "Unsupported Media Type",
|
||||
"until :date": "until :date",
|
||||
"Up": "Up",
|
||||
"Update": "Update",
|
||||
"Update :name": "Update :name",
|
||||
|
|
@ -871,11 +867,5 @@
|
|||
"Your password has been changed on :appname": "Your password has been changed on :appname",
|
||||
"Your password has been changed. If you are not at the origin of this change, please contact us as soon as possible using the contact form or by contacting your manager.": "Your password has been changed. If you are not at the origin of this change, please contact us as soon as possible using the contact form or by contacting your manager.",
|
||||
"your_message": "your_message",
|
||||
"Zip code": "Zip code",
|
||||
"Your search": "Your search",
|
||||
"Choose position": "Choose position",
|
||||
"Move": "Move",
|
||||
"Move down": "Move down",
|
||||
"Move up": "Move up",
|
||||
"New position": "New position"
|
||||
}
|
||||
"Zip code": "Zip code"
|
||||
}
|
||||
|
|
@ -26,8 +26,7 @@ return [
|
|||
'delete_model' => 'Delete :model',
|
||||
'save_model' => 'Save :model',
|
||||
'edit_model' => 'Update :model',
|
||||
'no_results' => 'No results',
|
||||
'no_results_for_model' => 'No results for :model',
|
||||
'no_results' => 'No results for :model',
|
||||
'publish' => 'Publish',
|
||||
'unpublish' => 'Unpublish',
|
||||
'starred' => 'Highlighted',
|
||||
|
|
|
|||
20
lang/fr.json
20
lang/fr.json
|
|
@ -17,7 +17,7 @@
|
|||
":title Pick up your order :/title :desc :/desc": ":title Retirez :/title :desc votre commande :/desc",
|
||||
":title Save money :/title :desc Get even more out of our gourmet products by buying directly :/desc": ":title Des économies :/title :desc Profitez encore plus de nos produits gourmands en achetant en direct :/desc",
|
||||
":title Select :/title :desc your products online :/desc": ":title Sélectionnez :/title :desc vos produits en ligne :/desc",
|
||||
":title Select :/title :desc your pickup day :/desc": ":title Choisissez :/title :desc votre jour de retrait :/desc",
|
||||
":title Select :/title :desc your time slot :/desc": ":title Choisissez :/title :desc votre créneau horaire :/desc",
|
||||
"A confirmation has just been sent to your email address": "Une confirmation vient de vous être envoyée sur votre adresse e-mail",
|
||||
"A decryption key is required.": "Une clé de déchiffrement est requise.",
|
||||
"A fresh verification link has been sent to your email address.": "Un nouveau lien de vérification a été envoyé à votre adresse e-mail.",
|
||||
|
|
@ -45,7 +45,6 @@
|
|||
"Agree": "Accepter",
|
||||
"Aisles": "Rayons",
|
||||
"All aisles": "Tous les rayons",
|
||||
"All discounts": "Toutes les promotions",
|
||||
"All products have been selected.": "Tous les produits ont été sélectionnés.",
|
||||
"All rights reserved.": "Tous droits réservés.",
|
||||
"Allergens": "Allergènes",
|
||||
|
|
@ -96,7 +95,7 @@
|
|||
"Basket has been cleared": "Le panier a été vidé",
|
||||
"Basket Summary": "Récapitulatif de votre panier",
|
||||
"Before proceeding, please check your email for a verification link.": "Avant de continuer, merci de vérifier votre e-mail contenant un lien de vérification.",
|
||||
"Blocked week": "Semaine bloquée",
|
||||
"Bloqued week": "Semaine bloquée",
|
||||
"Booking": "Réservation",
|
||||
"Brand": "Marque",
|
||||
"Breadcrumb": "Fil d'Ariane",
|
||||
|
|
@ -283,7 +282,6 @@
|
|||
"Frequency": "Fréquence",
|
||||
"Frequently asked questions.": "Foire aux questions fréquemment posées.",
|
||||
"from": "de",
|
||||
"from :date": "du :date",
|
||||
"From date": "Du",
|
||||
"Gateway Timeout": "Temps d'attente de la passerelle dépassé",
|
||||
"general conditions": "conditions générales",
|
||||
|
|
@ -447,7 +445,7 @@
|
|||
"No pickup exception has been created for now.": "Aucune exception de retrait n'a été créée pour le moment.",
|
||||
"No picture available": "Aucune image disponible",
|
||||
"No product matches your search.": "Aucun produit ne correspond à votre recherche.",
|
||||
"No replacement date configured": "Pas de jour de remplacement configuré",
|
||||
"No replacement date configured": "Pas de jour de remplacemenet configuré",
|
||||
"No results found.": "Aucun résultat trouvé.",
|
||||
"No site configured. Click the button below to add a site.": "Aucun site configuré. Cliquez sur le bouton ci-dessous pour ajouter un site.",
|
||||
"No substitute days have been set up. No time slots will be generated for this week.": "Aucun jour de remplacement configuré. Aucun créneau ne sera généré pour cette semaine.",
|
||||
|
|
@ -721,7 +719,6 @@
|
|||
"The answer": "La réponse",
|
||||
"The brand associated with the product": "La marque associée au produit",
|
||||
"The customer account was not found.": "Le compte client n'est pas trouvé.",
|
||||
"The discount applies to the piece (sold unit).": "La remise s'applique à la pièce (unité de vente).",
|
||||
"The employee sync job has been dispatched": "Le job de synchronisation des salariés a été lancé",
|
||||
"The existing encrypted environment file is not in readable format. Use --force to overwrite it.": "The existing encrypted environment file is not in readable format. Use --force to overwrite it.",
|
||||
"The existing encryption key is required to update the encrypted environment file.": "The existing encryption key is required to update the encrypted environment file.",
|
||||
|
|
@ -785,7 +782,6 @@
|
|||
"Unprocessable Entity": "Entité non traitable",
|
||||
"Unsubscribe": "Se désabonner",
|
||||
"Unsupported Media Type": "Type de média non supporté",
|
||||
"until :date": "au :date",
|
||||
"Up": "Monter",
|
||||
"Update": "Mettre à jour",
|
||||
"Update :name": "Mettre à jour :name",
|
||||
|
|
@ -858,11 +854,5 @@
|
|||
"Your password has been changed on :appname": "Votre mot de passe a été changé sur :appname",
|
||||
"Your password has been changed. If you are not at the origin of this change, please contact us as soon as possible using the contact form or by contacting your manager.": "Votre mot de passe a été changé. Si vous n'êtes pas à l'origine de ceci, veuillez nous contacter au plus vite en utilisant le formulaire de contact ou en vous rapprochant de votre responsable.",
|
||||
"your_message": "Votre message",
|
||||
"Zip code": "Code postal",
|
||||
"Your search": "Votre recherche",
|
||||
"Choose position": "Choisir une position",
|
||||
"Move": "Déplacer",
|
||||
"Move down": "Descendre d'un cran",
|
||||
"Move up": "Monter d'un cran",
|
||||
"New position": "Nouvelle position"
|
||||
}
|
||||
"Zip code": "Code postal"
|
||||
}
|
||||
|
|
@ -15,26 +15,25 @@ return [
|
|||
],
|
||||
|
||||
'helpers' => [
|
||||
'list' => 'Liste',
|
||||
'view_associated' => 'Voir les :model liés',
|
||||
'list_all' => 'Lister tous les :model',
|
||||
'create_model' => 'Créer :model',
|
||||
'view_model' => 'Voir :model',
|
||||
'associate_model' => 'Associer :model',
|
||||
'update_model' => 'Éditer :model',
|
||||
'duplicate_model' => 'Dupliquer :model',
|
||||
'delete_model' => 'Supprimer :model',
|
||||
'save_model' => 'Sauvegarder :model',
|
||||
'edit_model' => 'Mettre à jour :model',
|
||||
'no_results' => 'Aucun résultat',
|
||||
'no_results_for_model' => 'Aucun résultat pour :model',
|
||||
'publish' => 'Publier',
|
||||
'unpublish' => 'Dépublier',
|
||||
'starred' => 'Mis en avant',
|
||||
'discounted' => 'En promotion',
|
||||
'star' => 'Mettre en avant',
|
||||
'unstar' => 'Ne pas mettre en avant',
|
||||
'seo_title' => 'Doit décrire précisément la page, recommandation pour un positionnement
|
||||
'list' => 'Liste',
|
||||
'view_associated' => 'Voir les :model liés',
|
||||
'list_all' => 'Lister tous les :model',
|
||||
'create_model' => 'Créer :model',
|
||||
'view_model' => 'Voir :model',
|
||||
'associate_model' => 'Associer :model',
|
||||
'update_model' => 'Éditer :model',
|
||||
'duplicate_model' => 'Dupliquer :model',
|
||||
'delete_model' => 'Supprimer :model',
|
||||
'save_model' => 'Sauvegarder :model',
|
||||
'edit_model' => 'Mettre à jour :model',
|
||||
'no_results' => 'Aucun résultat pour :model',
|
||||
'publish' => 'Publier',
|
||||
'unpublish' => 'Dépublier',
|
||||
'starred' => 'Mis en avant',
|
||||
'discounted' => 'En promotion',
|
||||
'star' => 'Mettre en avant',
|
||||
'unstar' => 'Ne pas mettre en avant',
|
||||
'seo_title' => 'Doit décrire précisément la page, recommandation pour un positionnement
|
||||
optimal dans les moteurs de recherche (balise <title>).',
|
||||
'seo_description' => 'Doit décrire précisément le contenu de la page (balise <meta> description).',
|
||||
'required_fields' => 'Champs obligatoires',
|
||||
|
|
|
|||
|
|
@ -350,7 +350,6 @@ return [
|
|||
'nv_serving_size_g' => 'Taille d\'une portion (g)',
|
||||
'nv_serving_sugar_g' => 'Sucres par portion (g)',
|
||||
'nv_sugar_g' => 'Sucres (g)',
|
||||
'object' => 'objet',
|
||||
'ok_cgv' => 'conditions générales de vente',
|
||||
'optin' => 'inscription à la newsletter',
|
||||
'order' => 'ordre',
|
||||
|
|
@ -371,7 +370,6 @@ return [
|
|||
'picture_large_title' => 'titre de l\'image large',
|
||||
'picture_title' => 'titre de l\'image',
|
||||
'preparation' => 'Préparation',
|
||||
'postal_address' => 'adresse postale',
|
||||
'question' => 'question',
|
||||
'reference' => 'référence',
|
||||
'repeat_frequency' => 'fréquence de répétition',
|
||||
|
|
|
|||
|
|
@ -72,9 +72,6 @@ spec:
|
|||
containers:
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-container
|
||||
image: $IMAGE_TAG
|
||||
env:
|
||||
- name: XDG_CONFIG_HOME # For tinker
|
||||
value: /tmp
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
|
|
@ -114,9 +111,9 @@ spec:
|
|||
imagePullSecrets:
|
||||
- name: docker-registry-credentials
|
||||
securityContext:
|
||||
runAsUser: 82
|
||||
runAsGroup: 82
|
||||
fsGroup: 82
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
fsGroup: 33
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
volumes:
|
||||
- name: storage
|
||||
|
|
@ -200,8 +197,8 @@ spec:
|
|||
persistentVolumeClaim:
|
||||
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
|
||||
securityContext:
|
||||
runAsUser: 82
|
||||
runAsGroup: 82
|
||||
fsGroup: 82
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
fsGroup: 33
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
restartPolicy: OnFailure
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="mysql"/>
|
||||
<!-- <env name="DB_DATABASE" value="testing_db"/> -->
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 19 KiB |
|
|
@ -21,16 +21,3 @@
|
|||
.stretched-link::after {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
border-color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
.focus-ring-dark {
|
||||
--bs-focus-ring-color: rgb(var(--bs-dark-rgb), 0.25) !important;
|
||||
}
|
||||
|
||||
|
||||
.btn-tertiary:hover {
|
||||
background-color: rgb(var(--bs-light-rgb), 0.85) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
$primary: $color-primary;
|
||||
$secondary: $color-secondary;
|
||||
$tertiary: $color-tertiary;
|
||||
$success: $color-success;
|
||||
$info: $color-info;
|
||||
$warning: $color-warning;
|
||||
|
|
@ -12,7 +11,6 @@ $dark: $color-dark;
|
|||
$custom-theme-colors: (
|
||||
primary: $primary,
|
||||
secondary: $secondary,
|
||||
tertiary: $tertiary,
|
||||
success: $success,
|
||||
info: $info,
|
||||
warning: $warning,
|
||||
|
|
@ -65,8 +63,3 @@ $form-file-button-bg: var(--bs-secondary-bg, #e9ecef);
|
|||
$form-file-button-color: var(--bs-body-color, #212529);
|
||||
$form-file-button-hover-bg: var(--bs-secondary-bg, #e9ecef);
|
||||
$form-file-button-hover-color: var(--bs-body-color, #212529);
|
||||
|
||||
// * ACCORDION
|
||||
$accordion-color: var(--bs-body-color);
|
||||
$accordion-button-active-bg: var(--bs-primary);
|
||||
$accordion-button-active-color: var(--bs-light-text);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ $color-secondary: #5c636a;
|
|||
$color-info: #045487;
|
||||
$color-warning: #ffc454;
|
||||
$color-primary: $color-cocotte;
|
||||
$color-tertiary: #f8f6ed;
|
||||
|
||||
// * BODY
|
||||
$body-bg: #f8f9fa;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue