Class: CanadianTire::StoreAddressesImporter

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

.callResult

Parameters:

  • path (String, Pathname)

    path to a CTC .xlsx workbook.

  • dry_run (Boolean)

    when true, the transaction is rolled back
    at the end so no DB changes persist.

Returns:



78
# File 'app/services/canadian_tire/store_addresses_importer.rb', line 78

def self.call(...) = new(...).call

Instance Method Details

#callObject



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