Class: TypedEAV::Field::Currency
- Inherits:
-
Base
- Object
- ActiveRecord::Base
- ApplicationRecord
- Base
- TypedEAV::Field::Currency
- Defined in:
- app/models/typed_eav/field/currency.rb
Overview
Two-cell field type: stores ‘BigDecimal, currency: String` across decimal_value (amount) + string_value (currency ISO 4217 code).
Phase 05 contract:
-
value_columns: [:decimal_value, :string_value] — propagates through versioning’s snapshot loop (Phase 04) and the Value _dispatch_value_change_update filter so a change to either cell correctly fires the :update event.
-
operator_column: :currency_eq → :string_value; everything else →:decimal_value. Routed via QueryBuilder.filter (plan 05-01).
-
read_value / write_value / apply_default_to: paired overrides that compose / unpack the currency hash across the two physical columns. Without all three, single-cell defaults would write a Hash to decimal_value and raise TypeMismatch.
-
cast: Hash input only. Bare Numeric/String is invalid — explicit currency dimension is required at write time. Silently defaulting to default_currency would invite bugs where users forget the currency dimension entirely.
Operators (explicit narrowing — does NOT inherit string-search ops like :contains/:starts_with from decimal_value’s default since those don’t apply to amount-numeric or currency-code searches):
-
:eq, :gt, :lt, :gteq, :lteq, :between target the amount.
-
:currency_eq targets the currency code (registered ONLY on this class — QueryBuilder’s operator-validation gate rejects it on any non-Currency field).
-
:is_null / :is_not_null target the amount column (a Currency value is considered null when its amount is null).
Options:
-
default_currency: String ISO 4217 code (e.g., “USD”). Used as the currency fallback when cast input has amount but no currency. Never applies as a global silent default — only when cast input already has an amount and no explicit currency.
-
allowed_currencies: Array<String> of ISO codes. When set, validate_typed_value enforces inclusion.
Constant Summary
Constants inherited from Base
Constants included from ColumnMapping
ColumnMapping::DEFAULT_OPERATORS_BY_COLUMN, ColumnMapping::FALLBACK_OPERATORS
Class Method Summary collapse
Instance Method Summary collapse
-
#cast(raw) ⇒ Object
Cast Hash input → [BigDecimal, currency: String, false] or [nil, false] for nil/blank, or [nil, true] for unparseable input.
-
#validate_typed_value(record, val) ⇒ Object
Co-population validation + allowed_currencies inclusion.
Methods inherited from Base
#allowed_option_values, #apply_default_to, #array_field?, #backfill_default!, #clear_option_cache!, #default_value, #default_value=, export_schema, #field_type_name, import_schema, #insert_at, #move_higher, #move_lower, #move_to_bottom, #move_to_top, #optionable?, #read_value, #storage_contract, storage_contract_class, #write_value
Class Method Details
.operator_column(operator) ⇒ Object
56 57 58 |
# File 'app/models/typed_eav/field/currency.rb', line 56 def self.operator_column(operator) storage_contract_class.query_column(operator) end |
.value_columns ⇒ Object
52 53 54 |
# File 'app/models/typed_eav/field/currency.rb', line 52 def self.value_columns storage_contract_class.value_columns end |
Instance Method Details
#cast(raw) ⇒ Object
Cast Hash input → [BigDecimal, currency: String, false] or [nil, false] for nil/blank, or [nil, true] for unparseable input. Bare Numeric or String input is [nil, true] — users MUST pass a hash to make the currency dimension explicit (locked plan-time decision; preventing silent default_currency reliance in the ergonomic-but-error-prone scalar-cast case). rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity – one cast method with linear branches for nil/non-hash/amount-parse/currency-coercion/currency-shape; splitting hides the cast contract from a single read.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'app/models/typed_eav/field/currency.rb', line 67 def cast(raw) return [nil, false] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?) return [nil, true] unless raw.is_a?(Hash) amount_raw = raw[:amount] || raw["amount"] currency_raw = raw[:currency] || raw["currency"] amount_bd = nil if amount_raw.present? amount_bd = BigDecimal(amount_raw.to_s, exception: false) return [nil, true] if amount_bd.nil? end currency_str = currency_raw.is_a?(String) ? currency_raw.upcase : nil # default_currency fallback applies ONLY when the hash has an # amount but no currency. When the hash has neither, the result is # {amount: nil, currency: nil} — falsy enough that read_value # returns nil. When the hash has only a currency, the fallback # does NOT trigger (amount stays nil); validation will catch the # co-population requirement at save time. currency_str ||= default_currency if amount_bd && default_currency.present? # Reject non-3-letter currency codes (validation also catches this # on save; cast catches it earlier so :invalid is set on the cast # result and Value#validate_value surfaces :invalid promptly). return [nil, true] if currency_str && currency_str !~ /\A[A-Z]{3}\z/ [{ amount: amount_bd, currency: currency_str }, false] end |
#validate_typed_value(record, val) ⇒ Object
Co-population validation + allowed_currencies inclusion. When val is a Hash, requires both :amount and :currency populated. When allowed_currencies is set, val must be in the list. Without the co-population check, a half-populated row (amount-only or currency-only) would silently round-trip.
102 103 104 105 106 107 108 109 110 111 112 |
# File 'app/models/typed_eav/field/currency.rb', line 102 def validate_typed_value(record, val) return if val.nil? unless val.is_a?(Hash) && val[:amount].present? && val[:currency].present? record.errors.add(:value, "must have both amount and currency") return end allowed = [:allowed_currencies] record.errors.add(:value, :inclusion) if allowed.present? && Array(allowed).exclude?(val[:currency]) end |