2025-07-29 13:01:39 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:40 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:41 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:42 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:44 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:45 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:46 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:47 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:47 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:01:48 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:01 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: , price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: 1,000.22, price: ; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] parseTotal received: 1000.22; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] parseTotal returning cents: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] Parsed cents: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] BeforeValidate priceDisplay: 1,000.22, price: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] parseTotal received: 1000.22; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] parseTotal returning cents: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:13 [::1][1][-][error][debug.mode] Parsed cents: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] AfterFind price: 100022; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] getMoneyValue received: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] AfterFind priceDisplay set to: 10002200.00; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] AfterFind price: 100022; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] getMoneyValue received: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:14 [::1][1][-][error][debug.mode] AfterFind priceDisplay set to: 10002200.00; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}
2025-07-29 13:02:19 [::1][1][-][error][debug.mode] AfterFind price: 100022; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}
2025-07-29 13:02:19 [::1][1][-][error][debug.mode] getMoneyValue received: 100022; Context: {
    "category": "debug.mode"
}
2025-07-29 13:02:19 [::1][1][-][error][debug.mode] AfterFind priceDisplay set to: 10002200.00; Context: {
    "method": "app\\models\\Content::afterFind",
    "category": "debug.mode"
}

```
class Content extends ActiveRecord
{
	public string $priceDisplay = '';
	
	public static function tableName(): string
	{
		return '{{%content}}';
	}
	
	public function rules(): array
	{
		return [
			[['priceDisplay'], 'safe'],
			['priceDisplay', 'validatePriceDisplay'],
			// Internal price integer cents for DB storage
			['price', 'integer'],
			['price', 'compare', 'compareValue' => 200, 'operator' => '>='],// min 200 cents ($2.00)
			['price', 'compare', 'compareValue' => 500000000, 'operator' => '<='],// max 5,000,000.00 dollars in cents
			// quantity
			['quantity', 'integer', 'min' => 0, 'skipOnEmpty' => true, 'on' => ['product_artwork', 'product_item'],],
		];
	}
	
	/**
	* Validate Price Display
	*
	* Note: Do NOT add parameter(s) type hints. This method overrides an interface 
	* without type hints on parameters, adding types will cause PHP compatibility errors. 
	* Only docblock types to indicate expected input types.
	*/
	public function validatePriceDisplay($attribute, $params = []): void
	{
		$value = $this->$attribute;
		
		// Empty check
		if ($value === null || $value === '')
		{
			$this->addError($attribute, \Yii::t('app', 'Price cannot be blank.'));
			
			return;
		}
		
		// Validate format: digits possibly separated by thousands commas, optional 0-2 decimals with dot separator
		if (!preg_match('/^\d{1,3}(,\d{3})*(\.\d{1,2})?$/', $value))
		{
			$this->addError($attribute, \Yii::t('app', 'Invalid price format. Use comma for thousands and dot for decimals.'));
			
			return;
		}
		
		// Remove commas for numeric check
		$normalized = str_replace(',', '', $value);
		
		if (!is_numeric($normalized))
		{
			$this->addError($attribute, \Yii::t('app', 'Price must be a valid number.'));
			
			return;
		}
		
		$floatVal = (float) $normalized;
		
		if ($floatVal < 2.00 || $floatVal > 5000000.00)
		{
			$this->addError($attribute, \Yii::t('app', 'Price must be between $2.00 and $5,000,000.00.'));
			
			return;
		}
		
		// Optional: normalize attribute with commas removed (if you want)
		$this->$attribute = $normalized;
	}
	
	public function afterFind(): void
	{
		parent::afterFind();
		
		// ...custom code here...
		
		/**
		* Display Price handled with MoneyHelper
		* @see app\helpers\MoneyHelper.php
		*
		* Convert cents integer -> decimal string for form input
		*/
		LogHelper::debug("AfterFind price: {$this->price}", ['method' => __METHOD__]);// TODO
		
		if ($this->price !== null)
		{
			// e.g. 400000 -> "4000.00"
			$this->priceDisplay = MoneyHelper::getMoneyValue($this->price);
			
			LogHelper::debug("AfterFind priceDisplay set to: {$this->priceDisplay}", ['method' => __METHOD__]);// TODO
		}
		else
		{
			$this->priceDisplay = '';
		}
	}
	
	public function beforeValidate(): bool
	{
		if (!parent::beforeValidate())
		{
			return false;
		}
		
		// ...custom code here...
		
		/**
		* Display Price handled with MoneyHelper
		* @see app\helpers\MoneyHelper.php
		*
		* Note: When parsing user input in beforeValidate(), use the normalized 
		* priceDisplay (commas removed) to parse to cents.
		*/
		LogHelper::debug("BeforeValidate priceDisplay: {$this->priceDisplay}, price: {$this->price}");
		
		if ($this->priceDisplay !== '')
		{
			// Remove thousand separators before parsing if needed
			$normalized = str_replace(',', '', $this->priceDisplay);
			
			try
			{
				$cents = MoneyHelper::parseTotal($normalized);
				
				LogHelper::debug("Parsed cents: {$cents}");// TODO
				
				$this->price = $cents;
			}
			catch (Throwable $ex)
			{
				$this->addError('priceDisplay', \Yii::t('app', 'Invalid price format.'));
				
				return false;
			}
			/*$cents = MoneyHelper::parseTotal($this->priceDisplay);
			LogHelper::debug("Parsed cents: {$cents}");// TODO
			if (!is_int($cents))
			{
				$this->addError('priceDisplay', \Yii::t('app', 'Invalid price format.'));
				return false;
			}
			$this->price = $cents;//*/
		}
		
		return true;
	}
	
	/**
	* Get Price Formatted
	* @return string
	*
	* Returns the price formatted as a decimal string for display purposes.
	* Use price elsewhere in your PHP code (business logic, DB), and use:
	* 
	* 	echo $model->getPriceFormatted();
	*
	* if you want a formatted display string, like "15.99"
	*/
	public function getPriceFormatted(): ?string
	{
		if ($this->price === null)
		{
			return null;
		}
		
		return MoneyHelper::getMoneyValue($this->price);
	}
}
```
content view
```
<div class="col-md-9">
<?= $form->field($model, 'priceDisplay', [
	//'options' => ['class' => 'field-type-product field-type-template-product-artwork field-type-template-product-item']
])->TextInput([
            'class' => 'form-control square',
            'type' => 'text',
            'placeholder' => \Yii::t('app', 'Enter price, e.g. 1234.56'),
            'aria-label' => \Yii::t('app', 'Amount in dollars (2.00)'),
]); ?>
</div><!-- .col-md-9 -->
```
moneyhelper
```
declare(strict_types=1);

namespace app\helpers;

use Throwable;

use Money\Money;
use Money\Currency;
use Money\Number;
use Money\Teller;
use Money\Currencies\ISOCurrencies;
use Money\Parser\IntlLocalizedDecimalParser;
use Money\Formatter\IntlMoneyFormatter;
use Money\Formatter\DecimalMoneyFormatter;

use Yii;
use yii\base\InvalidConfigException as YiiBaseInvalidConfigException;

use app\helpers\ExceptionLogger;
use app\helpers\FormatterHelper;
use app\helpers\LogHelper;

class MoneyHelper
{
	private static ?FormatterHelper $_formatter = null;
	
	private static function getFormatter(): FormatterHelper
	{
		if (self::$_formatter === null)
		{
			self::$_formatter = FormatterHelper::create(\Yii::$app->formatter);
		}
		
		return self::$_formatter;
	}
	
	private static function validateNumeric($value, string $context): void
	{
		if (!is_numeric($value))
		{
			$error = is_object($value) ? get_class($value) : gettype($value);
			$msg = \Yii::t('app', 'Money value has to be a numeric value in {context}, `{error}`', [
				'context' => $context,
				'error' => $error,
			]);
			
			\Yii::error($msg);
			
			throw new YiiBaseInvalidConfigException($msg);
		}
	}
	
	public static function getDiscount(string $price, string $discount): string
	{
		try
		{
			self::validateNumeric($price, 'getDiscount');
			self::validateNumeric($discount, 'getDiscount');
			
			$teller = Teller::USD();
			$discountAmount = $teller->multiply($price, $discount);
			
			return (string) $discountAmount;
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_getDiscount_error_');
			
			return $e->getMessage();
		}	
	}
	
	public static function totalAmount(int|string $quantity, int|string $amount): int|string
	{
		try
		{
			self::validateNumeric($quantity, 'totalAmount');
			self::validateNumeric($amount, 'totalAmount');
			
			$teller = Teller::CAD();
			
			return (int) $teller->multiply($quantity, $amount);
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_totalAmount_error_');
			
			return $e->getMessage();
		}
	}
	
	public static function getStripeFee(int|string $amount): string
	{
		try
		{
			self::validateNumeric($amount, 'getStripeFee');
			
			$formatter = self::getFormatter();
			
			$moneyFormatter = new Money($amount, new Currency($formatter->currencyCode()));
			$fees = $moneyFormatter->multiply('1.062', Money::ROUND_UP);
			
			return (string) $fees->getAmount();
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_getStripeFee_error_');
			
			return $e->getMessage();
		}
	}
	
	public static function getTax(int|string $amount): string
	{
		try
		{
			self::validateNumeric($amount, 'getTax');
			
			$formatter = self::getFormatter();
			
			$moneyFormatter = new Money($amount, new Currency($formatter->currencyCode()));
			$gross = $moneyFormatter->multiply('1.13', Money::ROUND_UP);
			
			return (string) $gross->getAmount();
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_getTax_error_');
			
			return $e->getMessage();
		}
	}
	
	public static function getIntl(int|string $amount): string
	{
		try
		{
			self::validateNumeric($amount, 'getIntl');
			
			$formatter = self::getFormatter();
			
			$money = new Money($amount, new Currency($formatter->currencyCode()));
			$currencies = new ISOCurrencies();
			$numberFormatter = new \NumberFormatter($formatter->locale(), \NumberFormatter::CURRENCY);
			$moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies);
			
			return (string) $moneyFormatter->format($money);
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_getIntl_error_');
			
			return $e->getMessage();
		}
	}
	
	public static function getMoneyValue(int $value): string
	{
		LogHelper::debug("getMoneyValue received: $value");
	
		try
		{
			self::validateNumeric($value, 'getMoneyValue');
			
			// Scale by 2 decimals = divide by 100 to convert cents to dollars
			$number = Number::fromNumber($value)->base10(-2);
			
			return number_format((float)(string)$number, 2, '.', '');
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_getMoneyValue_error_');
			
			return $e->getMessage();
		}
	}
	
	public static function parseTotal(string $total): int
	{
		try
		{
			LogHelper::debug("parseTotal received: {$total}");
			
			self::validateNumeric($total, 'parseTotal');
			
			$formatter = self::getFormatter();
			
			$currencies = new ISOCurrencies();
			$numberFormatter = new \NumberFormatter($formatter->locale(), \NumberFormatter::DECIMAL);
			$moneyParser = new IntlLocalizedDecimalParser($numberFormatter, $currencies);
			$money = $moneyParser->parse($total, new Currency($formatter->currencyCode()));
			
			if ($money === null)
			{
				throw new YiiBaseInvalidArgumentException("Failed to parse money amount from '{$total}'");
			}
			
			LogHelper::debug("parseTotal returning cents: " . $money->getAmount());
			
			return (int) $money->getAmount();
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception
			ExceptionLogger::log($e, 'money_helper_parseTotal_error_');
			
			// Let caller handle exception, don't return string error
			throw $e;
		}
	}
}

```
web.php (config
```
	'components' => [
		'formatter' => [
			// @url https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes
			'locale' => 'en_CA',
			'defaultTimeZone' => 'UTC',
			'timeZone' => 'America/Toronto',
			/**
			* Important Note: 
			*	Remember that the global formatter configuration affects how date inputs are rendered.
			*	For example, when using jquery ui datepicker, the 'dateFormat' option (e.g., 'yy-mm-dd') 
			*	may conflict with this global setting if not properly aligned.
			*
			*	Ensure to check formatter settings when debugging date format issues in input fields.
			*/
			'dateFormat' => 'php:Y-m-d',
			'datetimeFormat' => 'php:Y-m-d H:i:s',
			'timeFormat' => 'H:i:s',
			'decimalSeparator' => '.',
			'thousandSeparator' => ',',
			// @url https://stripe.com/docs/currencies
			'currencyCode' => 'CAD',
		],// formatter */
	],
```
help