Align commerce module with the monorepo module structure by updating all namespaces to use the Core\Mod\Commerce convention. This change supports the recent monorepo separation and ensures consistency with other modules. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
221 lines
5.7 KiB
PHP
221 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Mod\Commerce\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
/**
|
|
* Product price in a specific currency.
|
|
*
|
|
* Allows products to have explicit prices in multiple currencies,
|
|
* with fallback to auto-conversion from the base price.
|
|
*
|
|
* @property int $id
|
|
* @property int $product_id
|
|
* @property string $currency
|
|
* @property int $amount Price in smallest unit (cents/pence)
|
|
* @property bool $is_manual Whether this is a manual override
|
|
* @property float|null $exchange_rate_used Rate used for auto-conversion
|
|
*/
|
|
class ProductPrice extends Model
|
|
{
|
|
protected $table = 'commerce_product_prices';
|
|
|
|
protected $fillable = [
|
|
'product_id',
|
|
'currency',
|
|
'amount',
|
|
'is_manual',
|
|
'exchange_rate_used',
|
|
];
|
|
|
|
protected $casts = [
|
|
'amount' => 'integer',
|
|
'is_manual' => 'boolean',
|
|
'exchange_rate_used' => 'decimal:8',
|
|
];
|
|
|
|
/**
|
|
* Get the product.
|
|
*/
|
|
public function product(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Product::class);
|
|
}
|
|
|
|
/**
|
|
* Get the formatted price.
|
|
*/
|
|
public function getFormattedAttribute(): string
|
|
{
|
|
return $this->format();
|
|
}
|
|
|
|
/**
|
|
* Format the price for display.
|
|
*/
|
|
public function format(): string
|
|
{
|
|
$config = config("commerce.currencies.supported.{$this->currency}", []);
|
|
$symbol = $config['symbol'] ?? $this->currency;
|
|
$position = $config['symbol_position'] ?? 'before';
|
|
$decimals = $config['decimal_places'] ?? 2;
|
|
$thousandsSep = $config['thousands_separator'] ?? ',';
|
|
$decimalSep = $config['decimal_separator'] ?? '.';
|
|
|
|
$value = number_format(
|
|
$this->amount / 100,
|
|
$decimals,
|
|
$decimalSep,
|
|
$thousandsSep
|
|
);
|
|
|
|
return $position === 'before'
|
|
? "{$symbol}{$value}"
|
|
: "{$value}{$symbol}";
|
|
}
|
|
|
|
/**
|
|
* Get price as decimal (not cents).
|
|
*/
|
|
public function getDecimalAmount(): float
|
|
{
|
|
return $this->amount / 100;
|
|
}
|
|
|
|
/**
|
|
* Set price from decimal amount.
|
|
*/
|
|
public function setDecimalAmount(float $amount): self
|
|
{
|
|
$this->amount = (int) round($amount * 100);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get or create a price for a product in a currency.
|
|
*
|
|
* If no explicit price exists and auto-convert is enabled,
|
|
* creates an auto-converted price.
|
|
*/
|
|
public static function getOrCreate(Product $product, string $currency): ?self
|
|
{
|
|
$currency = strtoupper($currency);
|
|
|
|
// Check for existing price
|
|
$price = static::where('product_id', $product->id)
|
|
->where('currency', $currency)
|
|
->first();
|
|
|
|
if ($price) {
|
|
return $price;
|
|
}
|
|
|
|
// Check if auto-conversion is enabled
|
|
if (! config('commerce.currencies.auto_convert', true)) {
|
|
return null;
|
|
}
|
|
|
|
// Get base price and convert
|
|
$baseCurrency = $product->currency ?? config('commerce.currencies.base', 'GBP');
|
|
|
|
if ($baseCurrency === $currency) {
|
|
// Create with base price
|
|
return static::create([
|
|
'product_id' => $product->id,
|
|
'currency' => $currency,
|
|
'amount' => $product->price,
|
|
'is_manual' => false,
|
|
'exchange_rate_used' => 1.0,
|
|
]);
|
|
}
|
|
|
|
$rate = ExchangeRate::getRate($baseCurrency, $currency);
|
|
|
|
if ($rate === null) {
|
|
return null;
|
|
}
|
|
|
|
$convertedAmount = (int) round($product->price * $rate);
|
|
|
|
return static::create([
|
|
'product_id' => $product->id,
|
|
'currency' => $currency,
|
|
'amount' => $convertedAmount,
|
|
'is_manual' => false,
|
|
'exchange_rate_used' => $rate,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update all auto-converted prices for a product.
|
|
*/
|
|
public static function refreshAutoConverted(Product $product): void
|
|
{
|
|
$baseCurrency = $product->currency ?? config('commerce.currencies.base', 'GBP');
|
|
$supportedCurrencies = array_keys(config('commerce.currencies.supported', []));
|
|
|
|
foreach ($supportedCurrencies as $currency) {
|
|
if ($currency === $baseCurrency) {
|
|
continue;
|
|
}
|
|
|
|
$existing = static::where('product_id', $product->id)
|
|
->where('currency', $currency)
|
|
->first();
|
|
|
|
// Skip manual prices
|
|
if ($existing && $existing->is_manual) {
|
|
continue;
|
|
}
|
|
|
|
$rate = ExchangeRate::getRate($baseCurrency, $currency);
|
|
|
|
if ($rate === null) {
|
|
continue;
|
|
}
|
|
|
|
$convertedAmount = (int) round($product->price * $rate);
|
|
|
|
static::updateOrCreate(
|
|
[
|
|
'product_id' => $product->id,
|
|
'currency' => $currency,
|
|
],
|
|
[
|
|
'amount' => $convertedAmount,
|
|
'is_manual' => false,
|
|
'exchange_rate_used' => $rate,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scope for manual prices only.
|
|
*/
|
|
public function scopeManual($query)
|
|
{
|
|
return $query->where('is_manual', true);
|
|
}
|
|
|
|
/**
|
|
* Scope for auto-converted prices.
|
|
*/
|
|
public function scopeAutoConverted($query)
|
|
{
|
|
return $query->where('is_manual', false);
|
|
}
|
|
|
|
/**
|
|
* Scope for a specific currency.
|
|
*/
|
|
public function scopeForCurrency($query, string $currency)
|
|
{
|
|
return $query->where('currency', strtoupper($currency));
|
|
}
|
|
}
|