Class: CanadianTire::StoreAddressChangesImporter
- Inherits:
-
Object
- Object
- CanadianTire::StoreAddressChangesImporter
- 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
- #call ⇒ Object
-
#initialize(path:, dry_run: false, logger: Rails.logger) ⇒ StoreAddressChangesImporter
constructor
A new instance of StoreAddressChangesImporter.
Constructor Details
#initialize(path:, dry_run: false, logger: Rails.logger) ⇒ StoreAddressChangesImporter
Returns a new instance of StoreAddressChangesImporter.
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
.call ⇒ Result
84 |
# File 'app/services/canadian_tire/store_address_changes_importer.rb', line 84 def self.call(...) = new(...).call |
Instance Method Details
#call ⇒ Object
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 |