897 lines
30 KiB
PHP
897 lines
30 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Enums\DefaultProductType;
|
|
use App\Enums\GeneralConfigType;
|
|
use App\Enums\InvoicingUnit;
|
|
use App\Enums\SellUnit;
|
|
use App\Enums\TaxRate;
|
|
use App\Enums\WeightUnit;
|
|
use App\Lib\Helpers\FileStorageHelper;
|
|
use App\Lib\Helpers\ToolboxHelper;
|
|
use App\Traits\Models\MiniCrudModel;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Gloudemans\Shoppingcart\Contracts\Buyable;
|
|
use Gloudemans\Shoppingcart\CanBeBought;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
use Kwaadpepper\Enum\Traits\CastsEnums;
|
|
|
|
/**
|
|
* Product
|
|
*
|
|
* By default engine supports product base and inco products
|
|
*
|
|
* @property float priceHt Base Price
|
|
* @property float discountedPriceHt Base Price after discount
|
|
* @property float priceTtc Price VAT included
|
|
* @property float discountedPriceTtc|string Price after discount VAT included
|
|
* @property float discountAmount Discount amount
|
|
* @property float discountRate Discount rate
|
|
* @property array data The overlapped data like defined in template.
|
|
* @property array dataObject The overlapped data like defined in template as object.
|
|
*
|
|
* @package App\Models
|
|
*/
|
|
class Product extends Model implements Buyable
|
|
{
|
|
use CanBeBought;
|
|
use CastsEnums;
|
|
use HasFactory;
|
|
use MiniCrudModel;
|
|
use SoftDeletes;
|
|
|
|
/**
|
|
* The attributes that should be cast to native types.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $casts = [
|
|
'star' => 'bool',
|
|
'status' => 'bool',
|
|
'orderable' => 'bool',
|
|
'publish_at' => 'datetime',
|
|
'unpublish_at' => 'datetime',
|
|
'orderable_start' => 'datetime',
|
|
'orderable_end' => 'datetime',
|
|
'data' => 'array'
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast to Enums.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $enumCasts = [
|
|
'tax_rate' => TaxRate::class,
|
|
];
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $fillable = [
|
|
'reference', 'type',
|
|
'name', 'description',
|
|
'picture', 'price',
|
|
'sell_unit', 'weight',
|
|
'tax_rate', 'star',
|
|
'status', 'orderable',
|
|
'orderable_at', 'unorderable_at',
|
|
'publish_at', 'unpublish_at',
|
|
'data', 'range_id',
|
|
];
|
|
|
|
/**
|
|
* Perform any actions required after the model boots.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function booted()
|
|
{
|
|
// * EVENT CLOSURES.
|
|
static::creating(function (self $model) {
|
|
// * ASSERTIONS
|
|
static::assertNameIsUnique($model->name);
|
|
ToolboxHelper::assertNoInternalBoUrls('description', $model->description);
|
|
self::assertIsAboveZero('price', trans('Prix'), $model->price);
|
|
self::assertIsAboveZero('weight', trans('Poids'), $model->weight);
|
|
|
|
// * TRANSFORMATIONS
|
|
$model->reference = $model->reference ?: ToolboxHelper::getUniqueProductReference();
|
|
$model->slug = Str::slug($model->name);
|
|
OldSlugable::removeSameSlugs($model, $model->slug);
|
|
$model->description = clean($model->description);
|
|
$model->description = FileStorageHelper::storeModelFilesFromHtml($model, $model->description);
|
|
if ($model->picture) {
|
|
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
|
}
|
|
$model->makeStatusSense();
|
|
$model->makeOrderableSense();
|
|
$model->minicrudCreating();
|
|
});
|
|
static::created(function (self $model) {
|
|
$model->flashMessages();
|
|
});
|
|
static::updating(function (self $model) {
|
|
// * ASSERTIONS
|
|
static::assertNameIsUnique($model->name, $model->id);
|
|
ToolboxHelper::assertNoInternalBoUrls('description', $model->description);
|
|
self::assertIsAboveZero('price', trans('Prix'), $model->price);
|
|
self::assertIsAboveZero('weight', trans('Poids'), $model->weight);
|
|
|
|
// * TRANSFORMATIONS
|
|
$model->reference = $model->reference ?: ToolboxHelper::getUniqueProductReference();
|
|
$model->slug = Str::slug($model->name);
|
|
if ($model->slug != $model->getOriginal('slug')) {
|
|
OldSlugable::removeSameSlugs($model, $model->getOriginal('slug'));
|
|
}
|
|
OldSlugable::insertSlug($model);
|
|
$model->description = clean($model->description);
|
|
$model->description = FileStorageHelper::storeModelFilesFromHtml($model, $model->description);
|
|
if ($model->picture) {
|
|
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
|
}
|
|
$model->makeStatusSense();
|
|
$model->makeOrderableSense();
|
|
$model->minicrudUpdating();
|
|
});
|
|
static::updated(function (self $model) {
|
|
FileStorageHelper::removeOldFile($model, 'picture');
|
|
FileStorageHelper::removeOldModelFilesFromHtml($model->description, $model->getOriginal('description'));
|
|
$model->flashMessages();
|
|
$model->minicrudUpdated();
|
|
});
|
|
static::deleted(function (self $model) {
|
|
$model->oldSlugs()->delete();
|
|
FileStorageHelper::removeFile($model, 'picture');
|
|
FileStorageHelper::removeModelFilesOfHtml($model->description);
|
|
$model->minicrudDeleted();
|
|
});
|
|
}
|
|
|
|
// * RELATIONSHIPS
|
|
|
|
/**
|
|
* BelongsToMany products relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function associatedProducts()
|
|
{
|
|
return $this->belongsToMany(self::class, 'product_product', 'associated_id', 'product_id');
|
|
}
|
|
|
|
/**
|
|
* BelongsTo range relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function range()
|
|
{
|
|
return $this->belongsTo(Range::class);
|
|
}
|
|
|
|
/**
|
|
* MorphMany old_slugable relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
|
*/
|
|
public function oldSlugs()
|
|
{
|
|
return $this->morphMany(OldSlugable::class, 'old_slugable');
|
|
}
|
|
|
|
/**
|
|
* belongsToMany discounts relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function discounts()
|
|
{
|
|
return $this->belongsToMany(Discount::class, 'product_discount');
|
|
}
|
|
|
|
/**
|
|
* belongsToMany tags relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function tags()
|
|
{
|
|
return $this->belongsToMany(Tag::class);
|
|
}
|
|
|
|
/**
|
|
* belongsTo xml_inco relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function xmlInco()
|
|
{
|
|
return $this->belongsTo(XmlInco::class);
|
|
}
|
|
|
|
// * METHODS
|
|
|
|
/**
|
|
* Flash boolean attributes modification messages
|
|
*
|
|
* @param boolean $old Ignore old attribute comparison.
|
|
* @return void
|
|
*/
|
|
public function flashMessages(bool $old = true)
|
|
{
|
|
$booleanChecks = [
|
|
'status' => [
|
|
'info' => trans(':model est maintenant publié', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
]),
|
|
'warning' => trans(':model est maintenant dépublié', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
])
|
|
],
|
|
'star' => [
|
|
'info' => trans(':model est maintenant mis en avant', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
]),
|
|
'warning' => trans(':model n\'est maintenant plus mis en avant', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
])
|
|
],
|
|
'orderable' => [
|
|
'warning' => trans(':model est maintenant disponible à la vente', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
]),
|
|
'error' => trans(':model n\'est maintenant plus disponible à la vente', [
|
|
'model' => Str::singular(trans('models.classes.products'))
|
|
])
|
|
]
|
|
];
|
|
$sessionMessages = [];
|
|
|
|
// * Merge messages
|
|
foreach ($booleanChecks as $attribute => $booleanCheck) {
|
|
if ($old and $this->getOriginal($attribute) == $this->{$attribute}) {
|
|
continue;
|
|
}
|
|
/** @var string $flashType */
|
|
$flashType = \array_keys($booleanCheck)[$this->{$attribute} ? 0 : 1];
|
|
if (!\array_key_exists($flashType, $sessionMessages)) {
|
|
$sessionMessages[$flashType] = [];
|
|
}
|
|
$sessionMessages[$flashType][] = $booleanCheck[$flashType];
|
|
}
|
|
|
|
foreach ($sessionMessages as $flashType => $messages) {
|
|
session()->flash($flashType, collect($messages)->implode("\n"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check this product an inco sheet associated and its type is inco
|
|
* @return boolean
|
|
*/
|
|
public function hasAndIsInco(): bool
|
|
{
|
|
return $this->type->value === DefaultProductType::inco()->value and
|
|
$this->xmlInco;
|
|
}
|
|
|
|
/**
|
|
* Getter data_object, the object overloaded attributes from theme
|
|
*
|
|
* @return object
|
|
*/
|
|
public function getDataObjectAttribute(): object
|
|
{
|
|
return ToolboxHelper::arrayToObject($this->data);
|
|
}
|
|
|
|
/**
|
|
* Get inco nutritionnal values, if product is inco.
|
|
*
|
|
* @return object|null
|
|
*/
|
|
public function getIncoNutritionalValues(): ?object
|
|
{
|
|
if (!$this->hasAndIsInco()) {
|
|
return null;
|
|
}
|
|
$nutri_values = $this->xmlInco->nutritional_values;
|
|
|
|
if (empty($nutri_values)) {
|
|
return null;
|
|
}
|
|
|
|
return (object)[
|
|
// Pour la ref.
|
|
'nv_ref_weight_g' => $nutri_values['taille_ref'],
|
|
// Energie Kj.
|
|
'nv_kj' => $nutri_values['val_energ_kj_r'],
|
|
// Energie Kcal.
|
|
'nv_kcal' => $nutri_values['val_energ_kcal_r'],
|
|
// Graisses.
|
|
'nv_fat_g' => $nutri_values['mat_grass_r'],
|
|
// Dont acides gras saturés.
|
|
'nv_sat_fat_g' => $nutri_values['ags_r'],
|
|
// Glucides.
|
|
'nv_carb_g' => $nutri_values['glucides_r'],
|
|
// Dont sucres.
|
|
'nv_sugar_g' => $nutri_values['sucre_r'],
|
|
// Fibres alimentaires.
|
|
'nv_fiber_g' => $nutri_values['fib_alim_r'],
|
|
// Protéines.
|
|
'nv_protein_g' => $nutri_values['proteine_r'],
|
|
// Sel.
|
|
'nv_salt_g' => $nutri_values['sel_r'],
|
|
|
|
/** Autres valeurs dispos
|
|
* $nutri_values["sodium_r"];
|
|
* $nutri_values["ener_e14_r"];
|
|
* $nutri_values["fruits_r"];
|
|
* $nutri_values["fruits_coque_sech_r"];
|
|
* $nutri_values["ener_kjo_r"];
|
|
* $nutri_values["fatsat_r"];
|
|
* $nutri_values["choval_r"];
|
|
* $nutri_values["sugar_r"];
|
|
* $nutri_values["fibtg_r"];
|
|
* $nutri_values["pro_r"];
|
|
* $nutri_values["salteq_r"];
|
|
*/
|
|
|
|
// * Valeurs nutritionnelles par portion.
|
|
// Taille portion en grammes.
|
|
'nv_serving_size_g' => $nutri_values['taille_port'],
|
|
// Energie Kj.
|
|
'nv_serving_kj' => $nutri_values['val_energ_kj_p'],
|
|
// Energie Kcal.
|
|
'nv_serving_kcal' => $nutri_values['val_energ_kcal_p'],
|
|
// Graisses.
|
|
'nv_serving_fat_g' => $nutri_values['mat_grass_p'],
|
|
// Dont acides gras saturés.
|
|
'nv_serving_sat_fat_g' => $nutri_values['ags_p'],
|
|
// Glucides.
|
|
'nv_serving_carb_g' => $nutri_values['glucides_p'],
|
|
// Dont sucres.
|
|
'nv_serving_sugar_g' => $nutri_values['sucre_p'],
|
|
// Fibres alimentaires.
|
|
'nv_serving_fiber_g' => $nutri_values['fib_alim_p'],
|
|
// Protéines.
|
|
'nv_serving_protein_g' => $nutri_values['proteine_p'],
|
|
// Sel.
|
|
'nv_serving_salt_g' => $nutri_values['sel_p'],
|
|
|
|
/**
|
|
* autres valeurs disponibles
|
|
* $nutri_values["sodium_p"];
|
|
* $nutri_values["fruits_p"];
|
|
*/
|
|
|
|
// * Apports journaliers.
|
|
// Energie % recommandé.
|
|
'nv_daily_energy' => $nutri_values['pourcent_apport_ref_kjo_r'],
|
|
// Graisses.
|
|
'nv_daily_fat' => $nutri_values['pourcent_apport_ref_fat_r'],
|
|
// Dont acides gras saturés.
|
|
'nv_daily_sat_fat' => $nutri_values['pourcent_apport_ref_fatsat_r'],
|
|
// Glucides.
|
|
'nv_daily_carb' => $nutri_values['pourcent_apport_ref_choval_r'],
|
|
// Dont sucres.
|
|
'nv_daily_sugar' => $nutri_values['pourcent_apport_ref_sugar_r'],
|
|
// Fibres alimentaires.
|
|
'nv_daily_fiber' => $nutri_values['pourcent_apport_ref_fibtg_r'],
|
|
// Protéines.
|
|
'nv_daily_protein' => $nutri_values['pourcent_apport_ref_pro_r'],
|
|
// Sel.
|
|
'nv_daily_salt' => $nutri_values['pourcent_apport_ref_salteq_r'],
|
|
|
|
/**
|
|
* Autre valeur dispo
|
|
* $nutri_values["pourcent_apport_ref_e14_r"]
|
|
*/
|
|
|
|
// * Nutriscore.
|
|
'nutriscore' => $nutri_values['classement'],
|
|
'nutriscore_args' => '',
|
|
|
|
/**
|
|
* Infos n'ayant pas de mapping
|
|
*
|
|
* $nutri_values["val_energ_kj_nut"];
|
|
* $nutri_values["val_energ_kcal_nut"];
|
|
* $nutri_values["mat_grass_nut"];
|
|
* $nutri_values["ags_nut"];
|
|
* $nutri_values["glucides_nut"];
|
|
* $nutri_values["sucre_nut"];
|
|
* $nutri_values["sucre_r"];
|
|
* $nutri_values["fib_alim_nut"];
|
|
* $nutri_values["proteine_nut"];
|
|
* $nutri_values["sel_nut"];
|
|
* $nutri_values["apport_ref"];
|
|
* $nutri_values["fruits_coque_sech_p"];
|
|
* $nutri_values["ener_e14_p"];
|
|
* $nutri_values["pourcent_apport_ref_e14_p"];
|
|
* $nutri_values["ener_kjo_p"];
|
|
* $nutri_values["pourcent_apport_ref_kjo_p"];
|
|
* $nutri_values["fat_p"];
|
|
* $nutri_values["pourcent_apport_ref_fat_p"];
|
|
* $nutri_values["fatsat_p"];
|
|
* $nutri_values["pourcent_apport_ref_fatsat_p"];
|
|
* $nutri_values["choval_p"];
|
|
* $nutri_values["pourcent_apport_ref_choval_p"];
|
|
* $nutri_values["sugar_p"];
|
|
* $nutri_values["pourcent_apport_ref_sugar_p"];
|
|
* $nutri_values["fibtg_p"];
|
|
* $nutri_values["pourcent_apport_ref_fibtg_p"];
|
|
* $nutri_values["pro_p"];
|
|
* $nutri_values["pourcent_apport_ref_pro_p"];
|
|
* $nutri_values["salteq_p"];
|
|
* $nutri_values["pourcent_apport_ref_salteq_p"];
|
|
* $nutri_values["apport_port"];
|
|
* $nutri_values["part_p"];
|
|
* $nutri_values["part_g"];
|
|
* $nutri_values["part_l"];
|
|
* $nutri_values["p_sur_l"];
|
|
* $nutri_values["pourcent_ags"];
|
|
* $nutri_values["pourcent_agi"];
|
|
* $nutri_values["pourcent_ss"];
|
|
* $nutri_values["pourcent_amidon"];
|
|
* $nutri_values["composante_n"];
|
|
* $nutri_values["composante_p"];
|
|
* $nutri_values["pourcent_fruits"];
|
|
* $nutri_values["mat_grass_r_prec"];
|
|
* $nutri_values["ags_r_prec"];
|
|
* $nutri_values["glucides_r_prec"];
|
|
* $nutri_values["sucre_r_prec"];
|
|
* $nutri_values["fib_alim_r_prec"];
|
|
* $nutri_values["proteine_r_prec"];
|
|
* $nutri_values["sel_r_prec"];
|
|
* $nutri_values["sodium_r_prec"];
|
|
* $nutri_values["fruits_r_prec"];
|
|
* $nutri_values["fruits_coque_sech_r_prec"];
|
|
* $nutri_values["score_nutri"];
|
|
*/
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Asserts using validation that the name is unique
|
|
*
|
|
* @param mixed $name
|
|
* @param integer|null $id
|
|
* @return void
|
|
* @throws \Illuminate\Validation\ValidationException If an actu name already exists.
|
|
*/
|
|
private static function assertNameIsUnique($name, ?int $id = null)
|
|
{
|
|
$table = (new self())->getTable();
|
|
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
|
|
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name), $id);
|
|
}
|
|
|
|
/**
|
|
* Adjusts model status using current status and publish dates
|
|
*
|
|
* @return void
|
|
*/
|
|
public function makeStatusSense(): void
|
|
{
|
|
/** @var \Carbon\Carbon|null */
|
|
$publish_at = $this->publish_at;
|
|
/** @var \Carbon\Carbon|null */
|
|
$unpublish_at = $this->unpublish_at;
|
|
// * Ignore unpublish_at in the past
|
|
if ($unpublish_at and $unpublish_at->lessThan(now())) {
|
|
return;
|
|
}
|
|
// * Ignore publish_at in the futur
|
|
if ($publish_at and $publish_at->greaterThan(now())) {
|
|
return;
|
|
}
|
|
if ($publish_at or $unpublish_at) {
|
|
$this->status = $this->planificated_status;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Getter the current status following planification dates
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function getPlanificatedStatusAttribute(): bool
|
|
{
|
|
/** @var \Carbon\Carbon|null */
|
|
$publish_at = $this->publish_at;
|
|
/** @var \Carbon\Carbon|null */
|
|
$unpublish_at = $this->unpublish_at;
|
|
if ($publish_at and $unpublish_at) {
|
|
return $unpublish_at->greaterThanOrEqualTo(now()) and $publish_at->lessThan(now());
|
|
}
|
|
if ($publish_at) {
|
|
return $publish_at->lessThan(now());
|
|
}
|
|
if ($unpublish_at) {
|
|
return $unpublish_at->greaterThanOrEqualTo(now());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adjusts model status using current orderable status and orderable dates
|
|
*
|
|
* @return void
|
|
*/
|
|
public function makeOrderableSense(): void
|
|
{
|
|
/** @var \Carbon\Carbon|null */
|
|
$orderable_at = $this->orderable_at;
|
|
/** @var \Carbon\Carbon|null */
|
|
$unorderable_at = $this->unorderable_at;
|
|
// * Ignore unorderable_at in the past
|
|
if ($unorderable_at and $orderable_at->lessThan(now())) {
|
|
return;
|
|
}
|
|
// * Ignore orderable_at in the futur
|
|
if ($orderable_at and $orderable_at->greaterThan(now())) {
|
|
return;
|
|
}
|
|
if ($orderable_at or $unorderable_at) {
|
|
$this->status = $this->order_planificated_status;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Getter the current orderable status following planification dates
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function getOrderPlanificatedStatusAttribute(): bool
|
|
{
|
|
/** @var \Carbon\Carbon|null */
|
|
$orderable_at = $this->orderable_at;
|
|
/** @var \Carbon\Carbon|null */
|
|
$unorderable_at = $this->unorderable_at;
|
|
if ($orderable_at and $unorderable_at) {
|
|
return $unorderable_at->greaterThanOrEqualTo(now()) and $orderable_at->lessThan(now());
|
|
}
|
|
if ($orderable_at) {
|
|
return $orderable_at->lessThan(now());
|
|
}
|
|
if ($unorderable_at) {
|
|
return $unorderable_at->greaterThanOrEqualTo(now());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Assert Is Above Zero
|
|
*
|
|
* @param string $attribute
|
|
* @param string $name
|
|
* @param integer $value
|
|
* @return void
|
|
* @throws \Illuminate\Validation\ValidationException If the value is less or equals to 0.
|
|
*/
|
|
private static function assertIsAboveZero(string $attribute, string $name, int $value): void
|
|
{
|
|
Validator::make([
|
|
$attribute => $value
|
|
], [
|
|
$attribute => 'numeric|min:0'
|
|
], [], [
|
|
$attribute => $name
|
|
])->validate();
|
|
}
|
|
|
|
// * MUTATORS
|
|
|
|
/**
|
|
* Setter price
|
|
*
|
|
* ! Stored as integer (cents)
|
|
*
|
|
* @param float $value
|
|
* @return void
|
|
*/
|
|
public function setPriceAttribute(float $value)
|
|
{
|
|
$this->attributes['price'] = \intval($value * 100);
|
|
}
|
|
|
|
/**
|
|
* Getter price
|
|
*
|
|
* ! Stored as integer (cents)
|
|
*
|
|
* @return float
|
|
*/
|
|
public function getPriceAttribute(): float
|
|
{
|
|
return \intval($this->attributes['price']) / 100;
|
|
}
|
|
|
|
// * Dynamic Minicrud inputs
|
|
|
|
/**
|
|
* Fetch Product Types
|
|
* @return array<string,string>
|
|
* @throws \RuntimeException If the theme does not have a valid 'types' definition.
|
|
*/
|
|
public static function fetchTypes(): array
|
|
{
|
|
return collect(\array_replace_recursive(
|
|
\collect(DefaultProductType::toArray())->mapWithKeys(function (DefaultProductType $type) {
|
|
return [$type->value => (object)$type->jsonSerialize()];
|
|
})->all(),
|
|
\collect(config('theme.types'))->mapWithKeys(function (array $typeDefinition) {
|
|
if (!array_key_exists('type', $typeDefinition)) {
|
|
throw new \RuntimeException('Theme type definition must have a type defined');
|
|
}
|
|
return [$typeDefinition['type']['value'] => (object)$typeDefinition['type']];
|
|
})->all()
|
|
))->unique()->all();
|
|
}
|
|
|
|
/**
|
|
* Fetch Product Types Inputs
|
|
* @return array<string,array>
|
|
* @throws \RuntimeException If the theme does not have a valid 'types' definition.
|
|
*/
|
|
public static function fetchTypesInputs(): array
|
|
{
|
|
$themeTypes = \collect(config('theme.types'))->keyBy('type.value');
|
|
return \collect(self::fetchTypes())->mapWithKeys(function (object $type) use ($themeTypes) {
|
|
switch (true) {
|
|
case $type->value === DefaultProductType::default()->value:
|
|
return [$type->value => []];
|
|
case $type->value === DefaultProductType::inco()->value:
|
|
return [$type->value => DefaultProductType::incoInputs()];
|
|
case $themeTypes->has($type->value):
|
|
$typeDefinition = $themeTypes->get($type->value);
|
|
if (!\is_array($typeDefinition)) {
|
|
throw new \RuntimeException('Theme type definition must be an array');
|
|
}
|
|
if (!array_key_exists('type', $typeDefinition)) {
|
|
throw new \RuntimeException('Theme type definition must have a type defined');
|
|
}
|
|
if (!array_key_exists('data', $typeDefinition)) {
|
|
throw new \RuntimeException('Theme type definition must have data defined');
|
|
}
|
|
return (object)[$typeDefinition['type']['value'] => $typeDefinition['data']];
|
|
default:
|
|
throw new \RuntimeException("Unhandled default type {$type->value}");
|
|
}
|
|
})->all();
|
|
}
|
|
|
|
/**
|
|
* Get Minicrud data definition for a product type
|
|
*
|
|
* @param string $typeValue
|
|
* @return mixed
|
|
* @throws \RuntimeException In the $typeValue does not have a minicrud data definition.
|
|
*/
|
|
public static function getMinicrudInputsFromType(string $typeValue)
|
|
{
|
|
if (!($minicrudData = \collect(self::fetchTypesInputs())->get($typeValue))) {
|
|
throw new \RuntimeException("Undefined minicrud data definition for type {$typeValue}");
|
|
}
|
|
return $minicrudData;
|
|
}
|
|
|
|
/**
|
|
* Getter discount rate
|
|
*
|
|
* @return object
|
|
* @throws \RuntimeException If the product type is undefined in config.
|
|
*/
|
|
public function getTypeAttribute(): object
|
|
{
|
|
$types = self::fetchTypes();
|
|
$type = $this->attributes['type'];
|
|
if (!\array_key_exists($type, $types)) {
|
|
throw new \RuntimeException("Undefined product type '$type'");
|
|
}
|
|
return $types[$type];
|
|
}
|
|
|
|
/**
|
|
* Price calculations
|
|
* We need : gross price (prix avant taxe et remises)
|
|
* discounted price (prix avant taxe avec remises)
|
|
* net price (prix avec taxe)
|
|
* net discounted proice (prix avec taxe et remises)
|
|
*/
|
|
|
|
/**
|
|
* Getter Utility to bring prices as a pseudo relation
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCalcPrices()
|
|
{
|
|
return [
|
|
'priceHt' => $this->priceHt,
|
|
'discountAmount' => $this->discountAmount,
|
|
'discountRate' => $this->discountRate,
|
|
'discountedPriceHt' => $this->discountedPriceHt,
|
|
'priceTtc' => $this->priceTtc,
|
|
'discountedPriceTtc' => $this->discountedPriceTtc
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Getter the gross price
|
|
* Accessor method for priceHt
|
|
*
|
|
* @author Olivier Legat <olivier.legat@ldc.fr>
|
|
*
|
|
* @return float
|
|
*/
|
|
public function getPriceHtAttribute()
|
|
{
|
|
/** Tenir compte de l'unité de facturation et de l'unité de vente (qui doit être stricte)
|
|
* si facturation KG et vente piece
|
|
* prendre le poids, convertir en KG => pre-requis l'unité de poids doit être un enum
|
|
* multiplier le $this->attributes['price'] par les KG
|
|
* sinon
|
|
* value => value
|
|
*/
|
|
// FIXME: plus de sell unit management.
|
|
if (config('bosettings.sell_unit_management') == "strict") {
|
|
// FIXME: invoicing_unit No longer available.
|
|
$calculate = ($this->invoicing_unit == InvoicingUnit::kgm()->value
|
|
&& $this->sell_unit == SellUnit::pce()->value
|
|
);
|
|
} else {
|
|
$calculate = false;
|
|
}
|
|
|
|
if ($calculate) {
|
|
// Weight converter.
|
|
$weighted_price = \floatval($this->attributes['price'])
|
|
* (\floatval($this->weight) * WeightUnit::convert()[$this->weight_unit]);
|
|
return \floatval(round($weighted_price * 1000) / 1000);
|
|
} else {
|
|
// No calculations, alias of db property.
|
|
return \floatval($this->attributes['price']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Getter for discounted price
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function getDiscountedPriceHtAttribute(): int
|
|
{
|
|
return $this->priceHt - $this->discountAmount;
|
|
}
|
|
|
|
/**
|
|
* Getter for net price
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function getPriceTtcAttribute(): int
|
|
{
|
|
$priceTtc = $this->priceHt * (1 + ($this->tax_rate->value / 100));
|
|
/**
|
|
* On travaille avec des centimes puis on redivise par 100 pour avoir des unités monetaires
|
|
* on augmente la précision à 3 digits, l'affichage formate les prix sur 2 décimales
|
|
*/
|
|
return round($priceTtc);
|
|
}
|
|
|
|
/**
|
|
* Getter for net discounted price
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function getDiscountedPriceTtcAttribute()
|
|
{
|
|
$discountedPriceTtc = $this->discountedPriceHt * (1 + ($this->tax_rate->value / 100));
|
|
|
|
// On travaille avec des centimes puis on redivise par 100 pour avoir des unités monetaires.
|
|
$roundingDiscountedPriceTTC = \floatval(round($discountedPriceTtc * 1000) / 1000);
|
|
|
|
// * Show prices for anyone.
|
|
return $roundingDiscountedPriceTTC;
|
|
}
|
|
|
|
/**
|
|
* Discount calculators
|
|
*/
|
|
|
|
/**
|
|
* Getter for discount amount
|
|
*
|
|
* @return float
|
|
*/
|
|
public function getDiscountAmountAttribute()
|
|
{
|
|
/** @var \App\Models\Discount|null */
|
|
$applicableDiscount = $this->discounts->first();
|
|
if (\is_null($applicableDiscount)) {
|
|
return 0;
|
|
}
|
|
if ($applicableDiscount->type->value == 1) {
|
|
$discountAmount = $applicableDiscount->discount_value;
|
|
} else {
|
|
// On augmente la précision à 3 digits, l'affichage formate les prix sur 2 décimales.
|
|
$discountAmount = $this->priceHt * ($applicableDiscount->discount_value / 100);
|
|
$discountAmount = round($discountAmount * 1000) / 1000;
|
|
}
|
|
return \floatval($discountAmount);
|
|
}
|
|
|
|
/**
|
|
* Getter discount rate
|
|
*
|
|
* @return float
|
|
*/
|
|
public function getDiscountRateAttribute()
|
|
{
|
|
/** @var \App\Models\Discount|null */
|
|
$applicableDiscount = $this->discounts->first();
|
|
if (\is_null($applicableDiscount)) {
|
|
return 0;
|
|
}
|
|
if ($applicableDiscount->type->value == 1) {
|
|
// On veut un pourcentage.
|
|
$discountRate = ($applicableDiscount->discount_value / $this->priceHt) * 100;
|
|
} else {
|
|
$discountRate = $applicableDiscount->discount_value;
|
|
}
|
|
return \floatval($discountRate);
|
|
}
|
|
|
|
/**
|
|
* buyablePrice attribute alias
|
|
*
|
|
* ! Overload Gloudemans\Shoppingcart\Contracts\Buyable::getBuyablePrice()
|
|
*
|
|
* @param mixed $options
|
|
* @return float
|
|
*
|
|
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
|
*/
|
|
public function getBuyablePrice($options = null)
|
|
{
|
|
// @phpcs:enable
|
|
return \floatval($this->priceHt);
|
|
}
|
|
|
|
/**
|
|
* Config says to display prices ?
|
|
* @return boolean
|
|
*/
|
|
public static function displayPrices(): bool
|
|
{
|
|
/** @var \App\Models\GeneralConfig */
|
|
$config = Cache::get('GeneralConfigGlobal')->where('type', GeneralConfigType::global())->first();
|
|
if ($config->data['orders_display_prices_for_auth']['value']) {
|
|
return Auth::guard('frontend')->check();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get displayable price value
|
|
*
|
|
* @param mixed $value
|
|
* @return string
|
|
*/
|
|
public static function fmtCurrency($value): string
|
|
{
|
|
return self::displayPrices() ?
|
|
sprintf('%s € TTC', number_format(\intval($value) / 100, 2, ',', ' ')) : '-.-- €';
|
|
}
|
|
}
|