Files
boursecu_server/app/Models/Product.php
T

584 lines
20 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Rooyesh\Wallet\models\Transaction;
use Rooyesh\Wallet\models\Wallet;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class Product extends Model
{
use HasFactory,Selector,SoftDeletes;
private static ?bool $priceOffsetColumnExists = null;
private static ?bool $priceColumnExists = null;
private static ?bool $orderLimitColumnsExist = null;
private static ?bool $showInAssetsColumnExists = null;
private static ?bool $showOnTvColumnExists = null;
private static ?bool $useLastPriceIfZeroColumnExists = null;
private static ?bool $directPriceColumnExists = null;
private static ?bool $appPriceColumnExists = null;
private static ?bool $tradeByQuantityColumnExists = null;
private static ?bool $inventoryColumnExists = null;
private static ?bool $overrideTradingHoursColumnExists = null;
private static ?bool $cashFeePercentModeColumnsExist = null;
private static ?bool $physicalDeliveryColumnsExist = null;
private static ?bool $descriptionColumnExists = null;
private static ?bool $priceFormulaColumnsExist = null;
protected static function booted(): void
{
static::saving(function (Product $product) {
if (!static::hasPriceOffsetColumn()) {
unset($product->attributes['price_offset_percent']);
}
if (!static::hasOrderLimitColumns()) {
unset(
$product->attributes['order_limit_enabled'],
$product->attributes['min_order_cash'],
$product->attributes['max_order_cash']
);
}
if (!static::hasShowInAssetsColumn()) {
unset($product->attributes['show_in_assets']);
}
if (!static::hasShowOnTvColumn()) {
unset($product->attributes['show_on_tv']);
}
if (!static::hasUseLastPriceIfZeroColumn()) {
unset($product->attributes['use_last_price_if_zero']);
}
if (!static::hasDirectPriceColumn()) {
unset($product->attributes['direct_price']);
}
if (!static::hasAppPriceColumn()) {
unset($product->attributes['app_price']);
}
if (!static::hasTradeByQuantityColumn()) {
unset($product->attributes['trade_by_quantity']);
}
if (!static::hasInventoryColumn()) {
unset($product->attributes['inventory']);
}
if (!static::hasDescriptionColumn()) {
unset($product->attributes['description']);
}
if (!static::hasOverrideTradingHoursColumn()) {
unset($product->attributes['override_trading_hours']);
}
if (!static::hasCashFeePercentModeColumns()) {
unset(
$product->attributes['cash_fee_buy_is_percent'],
$product->attributes['cash_fee_sell_is_percent']
);
}
if (!static::hasPhysicalDeliveryColumns()) {
unset(
$product->attributes['has_physical_delivery'],
$product->attributes['min_physical_delivery_quantity']
);
}
if (!static::hasPriceFormulaColumns()) {
unset(
$product->attributes['price_formula_enabled'],
$product->attributes['price_formula']
);
}
});
}
public static function hasPriceFormulaColumns(): bool
{
if (static::$priceFormulaColumnsExist !== null) {
return static::$priceFormulaColumnsExist;
}
try {
$table = (new static)->getTable();
static::$priceFormulaColumnsExist = Schema::hasColumn($table, 'price_formula_enabled')
&& Schema::hasColumn($table, 'price_formula');
} catch (\Throwable) {
static::$priceFormulaColumnsExist = false;
}
return static::$priceFormulaColumnsExist;
}
public static function hasPhysicalDeliveryColumns(): bool
{
if (static::$physicalDeliveryColumnsExist !== null) {
return static::$physicalDeliveryColumnsExist;
}
try {
$table = (new static)->getTable();
static::$physicalDeliveryColumnsExist = Schema::hasColumn($table, 'has_physical_delivery')
&& Schema::hasColumn($table, 'min_physical_delivery_quantity');
} catch (\Throwable) {
static::$physicalDeliveryColumnsExist = false;
}
return static::$physicalDeliveryColumnsExist;
}
public static function hasOverrideTradingHoursColumn(): bool
{
if (static::$overrideTradingHoursColumnExists !== null) {
return static::$overrideTradingHoursColumnExists;
}
try {
static::$overrideTradingHoursColumnExists = Schema::hasColumn(
(new static)->getTable(),
'override_trading_hours'
);
} catch (\Throwable) {
static::$overrideTradingHoursColumnExists = false;
}
return static::$overrideTradingHoursColumnExists;
}
public static function hasCashFeePercentModeColumns(): bool
{
if (static::$cashFeePercentModeColumnsExist !== null) {
return static::$cashFeePercentModeColumnsExist;
}
try {
static::$cashFeePercentModeColumnsExist = Schema::hasColumn(
(new static)->getTable(),
'cash_fee_buy_is_percent'
) && Schema::hasColumn(
(new static)->getTable(),
'cash_fee_sell_is_percent'
);
} catch (\Throwable) {
static::$cashFeePercentModeColumnsExist = false;
}
return static::$cashFeePercentModeColumnsExist;
}
public static function hasPriceOffsetColumn(): bool
{
if (static::$priceOffsetColumnExists !== null) {
return static::$priceOffsetColumnExists;
}
try {
static::$priceOffsetColumnExists = Schema::hasColumn(
(new static)->getTable(),
'price_offset_percent'
);
} catch (\Throwable) {
static::$priceOffsetColumnExists = false;
}
return static::$priceOffsetColumnExists;
}
/** گلد علی: جدول products ممکن است ستون price نداشته باشد */
public static function hasStoredPriceColumns(): bool
{
if (static::$priceColumnExists !== null) {
return static::$priceColumnExists;
}
try {
static::$priceColumnExists = Schema::hasColumn((new static)->getTable(), 'price');
} catch (\Throwable) {
static::$priceColumnExists = false;
}
return static::$priceColumnExists;
}
public static function hasOrderLimitColumns(): bool
{
if (static::$orderLimitColumnsExist !== null) {
return static::$orderLimitColumnsExist;
}
try {
$table = (new static)->getTable();
static::$orderLimitColumnsExist = Schema::hasColumn($table, 'order_limit_enabled')
&& Schema::hasColumn($table, 'min_order_cash')
&& Schema::hasColumn($table, 'max_order_cash');
} catch (\Throwable) {
static::$orderLimitColumnsExist = false;
}
return static::$orderLimitColumnsExist;
}
public static function hasShowInAssetsColumn(): bool
{
if (static::$showInAssetsColumnExists !== null) {
return static::$showInAssetsColumnExists;
}
try {
static::$showInAssetsColumnExists = Schema::hasColumn(
(new static)->getTable(),
'show_in_assets'
);
} catch (\Throwable) {
static::$showInAssetsColumnExists = false;
}
return static::$showInAssetsColumnExists;
}
public static function hasShowOnTvColumn(): bool
{
if (static::$showOnTvColumnExists !== null) {
return static::$showOnTvColumnExists;
}
try {
static::$showOnTvColumnExists = Schema::hasColumn(
(new static)->getTable(),
'show_on_tv'
);
} catch (\Throwable) {
static::$showOnTvColumnExists = false;
}
return static::$showOnTvColumnExists;
}
public static function hasUseLastPriceIfZeroColumn(): bool
{
if (static::$useLastPriceIfZeroColumnExists !== null) {
return static::$useLastPriceIfZeroColumnExists;
}
try {
static::$useLastPriceIfZeroColumnExists = Schema::hasColumn(
(new static)->getTable(),
'use_last_price_if_zero'
);
} catch (\Throwable) {
static::$useLastPriceIfZeroColumnExists = false;
}
return static::$useLastPriceIfZeroColumnExists;
}
public static function hasDirectPriceColumn(): bool
{
if (static::$directPriceColumnExists !== null) {
return static::$directPriceColumnExists;
}
try {
static::$directPriceColumnExists = Schema::hasColumn(
(new static)->getTable(),
'direct_price'
);
} catch (\Throwable) {
static::$directPriceColumnExists = false;
}
return static::$directPriceColumnExists;
}
public static function hasAppPriceColumn(): bool
{
if (static::$appPriceColumnExists !== null) {
return static::$appPriceColumnExists;
}
try {
static::$appPriceColumnExists = Schema::hasColumn(
(new static)->getTable(),
'app_price'
);
} catch (\Throwable) {
static::$appPriceColumnExists = false;
}
return static::$appPriceColumnExists;
}
public static function hasTradeByQuantityColumn(): bool
{
if (static::$tradeByQuantityColumnExists !== null) {
return static::$tradeByQuantityColumnExists;
}
try {
static::$tradeByQuantityColumnExists = Schema::hasColumn(
(new static)->getTable(),
'trade_by_quantity'
);
} catch (\Throwable) {
static::$tradeByQuantityColumnExists = false;
}
return static::$tradeByQuantityColumnExists;
}
public static function hasInventoryColumn(): bool
{
if (static::$inventoryColumnExists !== null) {
return static::$inventoryColumnExists;
}
try {
static::$inventoryColumnExists = Schema::hasColumn(
(new static)->getTable(),
'inventory'
);
} catch (\Throwable) {
static::$inventoryColumnExists = false;
}
return static::$inventoryColumnExists;
}
public static function hasDescriptionColumn(): bool
{
if (static::$descriptionColumnExists !== null) {
return static::$descriptionColumnExists;
}
try {
static::$descriptionColumnExists = Schema::hasColumn(
(new static)->getTable(),
'description'
);
} catch (\Throwable) {
static::$descriptionColumnExists = false;
}
return static::$descriptionColumnExists;
}
public function orderLimitsEnabled(): bool
{
if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) {
return false;
}
return $this->min_order_cash !== null || $this->max_order_cash !== null;
}
public function validateOrderCashLimit(float $totalPriceRial): void
{
if (!$this->orderLimitsEnabled()) {
return;
}
$min = $this->min_order_cash;
$max = $this->max_order_cash;
// یک ریال تلرانس — وقتی target_cash به گرم تبدیل و دوباره ضرب می‌شود ممکن است
// total_price موقت چند ریال از حداقل کمتر شود (مثلاً دقیقاً ۲۰۰,۰۰۰,۰۰۰).
$toleranceRial = 1.0;
if ($min !== null && $totalPriceRial + $toleranceRial < (float) $min) {
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
'حداقل مبلغ این محصول ' . number_format((float) $min) . ' ریال می‌باشد'
);
}
if ($max !== null && $totalPriceRial > (float) $max + $toleranceRial) {
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
'حداکثر مبلغ این محصول ' . number_format((float) $max) . ' ریال می‌باشد'
);
}
}
public function physicalDeliveryEnabled(): bool
{
if (static::hasPhysicalDeliveryColumns()) {
return (bool) ($this->has_physical_delivery ?? false);
}
return (bool) ($this->attributes['has_physical_delivery'] ?? false);
}
public function minPhysicalDeliveryQuantity(): float
{
$value = static::hasPhysicalDeliveryColumns()
? ($this->min_physical_delivery_quantity ?? 0)
: ($this->attributes['min_physical_delivery_quantity'] ?? 0);
return max(0.0, (float) $value);
}
public function getPriceOffsetPercentAttribute($value): float
{
if (!static::hasPriceOffsetColumn()) {
return 0.0;
}
return (float) ($value ?? 0);
}
protected $fillable = [
'title', // The title of the record.
'price', // The amount of the record.
'amount', // The amount of the record.
'unit', // The unit of the item.
'group_type', // The unit of the item.
'finance_p_name', // کد کار زرین (idKar)
'file_id', // The ID of the associated file.
'can_canceled', // Whether the record can be canceled (boolean).
'active', // The active status of the record (boolean).
'for_barayand', // Whether it is for Barayand (boolean).
'show', // Whether to show the record (boolean).
'show_in_assets', // Show balance on customer assets page cards
'show_on_tv', // Show this product on customer /tv board
'trade_by_quantity', // UI معامله مقداری: ارزش کل + مقدار
'use_last_price_if_zero', // Keep last DB price when automatic provider returns zero
'open_sell', // Whether selling is open (boolean).
'auto_open_at', // Whether selling is open (boolean).
'auto_close_at', // Whether selling is open (boolean).
'override_trading_hours', // روشن: بازه ساعت خود محصول جایگزین بازه سراسری می‌شود
'kalhor_name', // The name of the Kalhor entity.
'default_credit', // Default credit amount.
'reading_method', // The method of reading.
'open_buy', // Whether buying is open (boolean).
'percent_fee_sell', // Add percent_fee_sell to fillable
'cash_fee_buy', // Add cash_fee_buy to fillable
'cash_fee_sell', // Add cash_fee_sell to fillable
'cash_fee_buy_is_percent',
'cash_fee_sell_is_percent',
'percent_fee_buy', // Add percent_fee_buy to fillable
'order_limit_enabled',
'min_order_cash',
'max_order_cash',
'has_physical_delivery', // روشن: این محصول امکان تحویل فیزیکی دارد
'min_physical_delivery_quantity', // حداقل موجودی برای فعال شدن تحویل (عدد برای تعدادی، گرم/کیلوگرم برای وزنی)
'metal_id', //the material of metal (#gold or #silver)
'price_offset_percent', // admin only: +/- % vs automatic reference (baked into price on sync)
'price_profit',
'bubble',
'inventory',
'description',
'direct_price',
'app_price', // روشن: قیمت واحد از قیمت زندهٔ اپ هر گرم × وزن محاسبه می‌شود
'price_formula_enabled',
'price_formula',
];
protected $attributes = [
'show_in_assets' => false,
'show_on_tv' => false,
'trade_by_quantity' => false,
'use_last_price_if_zero' => false,
'cash_fee_buy_is_percent' => false,
'cash_fee_sell_is_percent' => false,
'app_price' => false,
'price_formula_enabled' => false,
];
protected $casts = [
'can_canceled' => 'boolean', // Cast to boolean.
'active' => 'boolean', // Cast to boolean.
'show' => 'boolean', // Cast to boolean.
'show_in_assets' => 'boolean',
'show_on_tv' => 'boolean',
'trade_by_quantity' => 'boolean',
'use_last_price_if_zero' => 'boolean',
'override_trading_hours' => 'boolean',
'for_barayand' => 'boolean', // Cast to boolean.
'open_sell' => 'boolean', // Cast to boolean.
'auto_sell' => 'boolean', // Cast to boolean (you mentioned this in your casts but it's missing in `$fillable`).
'open_buy' => 'boolean', // Cast to boolean.
'auto_buy' => 'boolean', // Cast to boolean.
'price' => 'int', // Cast to float.
'percent_fee_sell' => 'float', // Cast to float.
'percent_fee_buy' => 'float', // Cast to float.
'order_limit_enabled' => 'boolean',
'min_order_cash' => 'int',
'max_order_cash' => 'int',
'has_physical_delivery' => 'boolean',
'min_physical_delivery_quantity' => 'float',
'cash_fee_buy' => 'float',
'cash_fee_sell' => 'float',
'cash_fee_buy_is_percent' => 'boolean',
'cash_fee_sell_is_percent' => 'boolean',
'price_offset_percent' => 'float',
'price_profit' => 'int',
'app_price' => 'boolean',
'price_formula_enabled' => 'boolean',
];
static $METAL_ID = [
1=>'طلا',
2=>'نقره',
];
public function wallet()
{
return $this->hasOne(Wallet::class);
}
public function walletMy()
{
return $this->hasOne(Wallet::class)->where('owner',auth()->user()->id);
}
public function file(){
return $this->belongsTo(File::class);
}
public function price(){
return $this->hasOne(Price::class)->orderBy('id','desc');
}
public function getPrices()
{
$now = Carbon::now();
$lastMonth = Carbon::now()->subMonth();
$subQuery = DB::table('prices')
->select('product_id', 'date_at', 'price')
->whereBetween('date_at', [$lastMonth, $now])
->where('product_id', $this->id)
->orderBy('date_at', 'desc')
->distinct('date_at')
->groupBy('date_at', 'product_id', 'price');
return DB::table(DB::raw("({$subQuery->toSql()}) as sub"))
->mergeBindings($subQuery->getQuery())
->get(['product_id', 'date_at', 'price']);
}
public function prices(){
$now = Carbon::now();
$lastMonth = Carbon::now()->subMonth();
$data = [];
$datas= $this->hasMany(Price::class, 'product_id')
->whereBetween('date_at', [$lastMonth, $now])
->distinct('date_at')
->select(['product_id', 'date_at', 'difference', 'price']);
return $datas;
}
public function default(){
return $this->hasMany(ProductCredit::class);
}
public function sub(){
return $this->hasOne(PackageProduct::class)->where('is_base',true);
}
// public function wallet(){
// return $this->hasOne(Wallet::class);
// }
public function transaction(){
return $this->hasMany(Transaction::class);
}
public function limitedTransactions()
{
return $this->hasMany(Transaction::class)->select('id', 'gold', 'title', 'created_at', 'product_id');
}
public function transactionAgentPanel(){
return $this->hasMany(Transaction::class)->where('note','برداشت بابت فروش')->orwhere('note','واریز بابت خرید')->orwhere('note','سند ورود طلا');
}
}