Class: CanadianTire::StoreAddressesImporter
- Inherits:
-
Object
- Object
- CanadianTire::StoreAddressesImporter
- Defined in:
- app/services/canadian_tire/store_addresses_importer.rb
Overview
Full refresh of +canadian_tire_store_addresses+ from the +StoreAddresses+
sheet of a Canadian Tire "Store listings" .xlsx workbook.
Replaces the +db/migrate/refresh_canadian_tire_store_addresses_from_csv.rb+
drop/recreate migration pattern with an idempotent service that:
- wraps +delete_all+ + +insert_all+ in a single transaction (atomic — no
half-emptied table is ever visible to readers) - preserves the table schema, indexes, and FK constraints (the legacy
migrations rebuilt them from scratch every refresh, which is slower and
loses any indexes added later but not back-ported to the migration) - applies the same +postal_code.gsub(' ', '')+ normalization the legacy
CSV importer does at +db/migrate/20260407180000_refresh_canadian_tire_store_addresses_from_csv_040726.rb:41+
== Usage
result = CanadianTire::StoreAddressesImporter.call(path: '/tmp/ctc.xlsx')
result.success? # => true
result.summary # => "before=1126 after=1125 inserted=1125 errors=0"
Pass +dry_run: true+ to validate without persisting (transaction is
rolled back at the end).
== When to use this vs +StoreAddressChangesImporter+
Prefer +StoreAddressChangesImporter+ for routine ongoing updates — it
applies only the rows CTC marked as changed, leaves history alone, and
costs O(changes) instead of O(stores). Use this full importer when:
- the changes sheet has drifted from reality (drift detection or
recovery) - you're initializing the table for the first time on a new environment
- CTC sends a one-off sheet with no +StoreAddressChanges+ delta
Defined Under Namespace
Classes: Result
Constant Summary collapse
- SHEET_NAME =
'StoreAddresses'- HEADER_TO_ATTR =
Header → DB column. The two boolean source columns are kept under
their sheet labels here; +attrs_for_insert+ converts them. { 'STORE #' => :store_number, 'N=NEW STORE' => :new_store_indicator, 'ACTIVE' => :active_indicator, 'LOCATION NAME' => :location_name, 'LOCATION ADDRESS 1' => :location_address_name, 'LOCATION ADDRESS 2' => :location_address_street, 'CITY' => :city, 'PROV' => :state_code, 'COUNTRY' => :country_iso, 'POSTAL CODE' => :postal_code, }.freeze
Instance Attribute Summary collapse
-
#after_count [Integer] rows in the table after the run([Integer]) ⇒ Object
readonly
(equal to +before_count+ on +dry_run+).
-
#before_count [Integer] rows in the table before the run.([Integer]) ⇒ Object
readonly
Outcome of a full-refresh run.
-
#errors [Array<Hash>] read-time failures —([Array<Hash>]) ⇒ Object
readonly
missing headers, etc.
-
#inserted [Integer] rows the sheet contained and([Integer]) ⇒ Object
readonly
that would have been inserted (equal to +after_count+ on a live run).
Class Method Summary collapse
Instance Method Summary collapse
- #call ⇒ Object
-
#initialize(path:, dry_run: false, logger: Rails.logger) ⇒ StoreAddressesImporter
constructor
A new instance of StoreAddressesImporter.
Constructor Details
#initialize(path:, dry_run: false, logger: Rails.logger) ⇒ StoreAddressesImporter
Returns a new instance of StoreAddressesImporter.
80 81 82 83 84 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 80 def initialize(path:, dry_run: false, logger: Rails.logger) @path = path @dry_run = dry_run @logger = logger end |
Instance Attribute Details
#after_count [Integer] rows in the table after the run([Integer]) ⇒ Object (readonly)
(equal to +before_count+ on +dry_run+).
66 67 68 69 70 71 72 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 66 Result = Data.define(:before_count, :after_count, :inserted, :errors) do def initialize(before_count: 0, after_count: 0, inserted: 0, errors: []) = super def success? = errors.empty? def summary "before=#{before_count} after=#{after_count} inserted=#{inserted} errors=#{errors.length}" end end |
#before_count [Integer] rows in the table before the run.([Integer]) ⇒ Object (readonly)
Outcome of a full-refresh run.
66 67 68 69 70 71 72 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 66 Result = Data.define(:before_count, :after_count, :inserted, :errors) do def initialize(before_count: 0, after_count: 0, inserted: 0, errors: []) = super def success? = errors.empty? def summary "before=#{before_count} after=#{after_count} inserted=#{inserted} errors=#{errors.length}" end end |
#errors [Array<Hash>] read-time failures —([Array<Hash>]) ⇒ Object (readonly)
missing headers, etc. Per-row insert failures abort the whole
transaction (we want all-or-nothing for a full refresh).
66 67 68 69 70 71 72 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 66 Result = Data.define(:before_count, :after_count, :inserted, :errors) do def initialize(before_count: 0, after_count: 0, inserted: 0, errors: []) = super def success? = errors.empty? def summary "before=#{before_count} after=#{after_count} inserted=#{inserted} errors=#{errors.length}" end end |
#inserted [Integer] rows the sheet contained and([Integer]) ⇒ Object (readonly)
that would have been inserted (equal to +after_count+ on a live run).
66 67 68 69 70 71 72 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 66 Result = Data.define(:before_count, :after_count, :inserted, :errors) do def initialize(before_count: 0, after_count: 0, inserted: 0, errors: []) = super def success? = errors.empty? def summary "before=#{before_count} after=#{after_count} inserted=#{inserted} errors=#{errors.length}" end end |
Class Method Details
.call ⇒ Result
78 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 78 def self.call(...) = new(...).call |
Instance Method Details
#call ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 86 def call rows = read_rows before_count = CanadianTireStoreAddress.count inserted = 0 CanadianTireStoreAddress.transaction do CanadianTireStoreAddress.delete_all now = Time.current attrs = rows.map { |row| attrs_for_insert(row).merge(created_at: now, updated_at: now) } CanadianTireStoreAddress.insert_all(attrs) if attrs.any? inserted = attrs.length raise ActiveRecord::Rollback if @dry_run end after_count = @dry_run ? before_count : CanadianTireStoreAddress.count Result.new(before_count: before_count, after_count: after_count, inserted: inserted, errors: []) end |