Class: CanadianTire::StoreAddressChangesImporter

Inherits:
Object
  • Object
show all
Defined in:
app/services/canadian_tire/store_address_changes_importer.rb

Overview

Applies the +StoreAddressChanges+ sheet of a Canadian Tire "Store listings"
.xlsx workbook as a delta against +canadian_tire_store_addresses+.

Replaces the previous full drop/recreate migration pattern (see
+db/migrate/refresh_canadian_tire_store_addresses_from_csv.rb+) with a
per-row upsert driven by the sheet's +CHANGE TYPE+ column.

== Change-type action mapping

[+ADDITION+] insert a new row; +new_store_indicator: true+,
+active_indicator: true+.
[+DELETION+] hard-delete the row by +store_number+ (history is
preserved on referencing records elsewhere).
[+ADDRESS CHANGES+,
+STREET ADDRESS CHANGE+,
+CITY, PROV CHANGE+] update the seven non-key fields with the row's
values (CTC labeling drift — all reduce to UPDATE).
[anything else] recorded as an error so a new CTC label surfaces
on first occurrence instead of silently no-oping.

== Defensive cleanups

Every CTC sheet ships with whitespace padding on every header and every
cell, and a couple of recent workbooks carry junk trailing header columns
(+#N/A+, +RESS+, +ESS 1+) from clipboard accidents. This importer strips
every value, ignores any column past the canonical 11, and 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::StoreAddressChangesImporter.call(path: '/tmp/ctc.xlsx')
result.success? # => true
result.summary # => "added=11 deleted=27 updated=1 skipped=0 errors=0"

Pass +dry_run: true+ to validate without persisting (transaction is rolled
back at the end of the run).

Defined Under Namespace

Classes: Result

Constant Summary collapse

SHEET_NAME =
'StoreAddressChanges'
HEADER_TO_ATTR =

Canonical header → DB column mapping. The two trailing entries are
sheet-only and don't get written to the DB (they drive routing and are
logged for auditability).

{
  'STORE #'            => :store_number,
  'N=NEW STORE'        => :new_store_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,
  'CHANGE TYPE'        => :_change_type,
  'DATE OF CHANGE'     => :_date_of_change,
}.freeze
UPDATE_TYPES =
['ADDRESS CHANGES', 'STREET ADDRESS CHANGE', 'CITY, PROV CHANGE'].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:, dry_run: false, logger: Rails.logger) ⇒ StoreAddressChangesImporter

Returns a new instance of StoreAddressChangesImporter.

Parameters:

  • path (String, Pathname)

    path to a CTC .xlsx workbook.

  • dry_run (Boolean) (defaults to: false)

    when true, the transaction is rolled back at
    the end so no DB changes persist (counts in +Result+ still reflect
    what would have changed).



90
91
92
93
94
95
96
97
98
99
# File 'app/services/canadian_tire/store_address_changes_importer.rb', line 90

def initialize(path:, dry_run: false, logger: Rails.logger)
  @path = path
  @dry_run = dry_run
  @logger = logger
  @added = 0
  @deleted = 0
  @updated = 0
  @skipped = []
  @errors = []
end

Class Method Details

.callResult

Returns:



84
# File 'app/services/canadian_tire/store_address_changes_importer.rb', line 84

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

Instance Method Details

#callObject



101
102
103
104
105
106
107
108
109
# File 'app/services/canadian_tire/store_address_changes_importer.rb', line 101

def call
  rows = read_rows
  ApplicationRecord.transaction do
    rows.each { |row| apply_row(row) }
    raise ActiveRecord::Rollback if @dry_run
  end
  Result.new(added: @added, deleted: @deleted, updated: @updated,
             skipped: @skipped, errors: @errors)
end