rate-card 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 160b23b771a6920c394ff1d8e33b625499f0bd384c8aab8453471c18d1244a23
4
- data.tar.gz: c03c94b913253b5037be1f5f4d2b9b11304943712215b81bcbae092148675b84
3
+ metadata.gz: 21c45d1eea2e85a47a9f268250611d10548eb3b812d4d5dd27716506c9802e3e
4
+ data.tar.gz: c209d3d667be368253081ded2e7aab44f9138cefebeb086d27257f137b92538d
5
5
  SHA512:
6
- metadata.gz: d11060dc015d5eaa28ab8ec883bc38b865383f22e85cccce7ea455945b304fabefa0f22c9463e0ed8bdf986fa3b3cc2d43975154d52296ed29999b518343e34a
7
- data.tar.gz: 6f1649f1183ab8b1b7627b43494d9277999e22939e4492852be6827ff9694edd768bbd4ca7202bcf714b684ecc7deed9ace3d50661202a26070fdfcb0caf02a6
6
+ metadata.gz: 0ae5c80051382299425a11519bf0f8fa8132906f58cbf34c971480cb28989857fb12aff2f7099a1709187109a1e6bb530c778a2b5da3e6c1309bbede94cee8d6
7
+ data.tar.gz: b90f381a9eb2f54c2b69a5653ca1c71e909210787040fe7e67e5872fd759efda0eca6a7a5b66e8df58fb9d9b615d718bac6aaeb062befad1e61d62cebb643463
data/exe/rate-card CHANGED
@@ -102,7 +102,13 @@ else
102
102
  exit 1
103
103
  end
104
104
 
105
- spec = app.spec
106
- spec.show_table = options[:show_table]
105
+ # Usually one pass. UPS rural mode with more than one surcharge type checked
106
+ # produces several - each is its own card with its own files, run and
107
+ # reported independently so one failing does not stop the others from
108
+ # writing. The process exit code is the worst of all of them.
109
+ exit_codes = app.results.map do |result|
110
+ result[:spec].show_table = options[:show_table]
111
+ RateCard::Runner.new(spec: result[:spec], grid: result[:grid], ui: ui).run
112
+ end
107
113
 
108
- exit RateCard::Runner.new(spec: spec, grid: app.grid, ui: ui).run
114
+ exit exit_codes.max
@@ -162,13 +162,16 @@ module RateCard
162
162
  Pathname.new(output_base).join("#{slug}_#{customer_id}_#{timestamp}#{rural_suffix}")
163
163
  end
164
164
 
165
+ # UPS rural mode runs one pass per selected surcharge type (see
166
+ # TUI::App#build_specs), so a single spec's zones is exactly one symbol -
167
+ # the suffix names it, so two same-second passes don't collide on
168
+ # run_dir. USPS rural mode sweeps real zones as usual, so it just gets
169
+ # the generic suffix.
165
170
  def rural_suffix
166
- case rural
167
- when true then '_rural'
168
- when :edas, :rdas then "_#{rural}"
169
- when :both then '_edas_rdas'
170
- else ''
171
- end
171
+ return '' unless rural?
172
+
173
+ surcharge = zones.find { |zone| zone.is_a?(Symbol) }
174
+ surcharge ? "_#{surcharge}" : '_rural'
172
175
  end
173
176
 
174
177
  def validate!
@@ -24,11 +24,14 @@ module RateCard
24
24
 
25
25
  # No :token stage — see TokenPrompt for why it cannot live in the loop.
26
26
  STAGES = %i[
27
- loading rate_mode carrier rural services zones unit weights package_type
28
- rate_keys confirm fetching
27
+ loading rate_mode carrier rural rural_surcharges services zones unit weights
28
+ package_type rate_keys confirm fetching
29
29
  ].freeze
30
30
 
31
- attr_reader :spec, :grid, :error, :notifier
31
+ # #spec/#grid hold the most recently finished pass, for every caller that
32
+ # only ever produced one (every carrier except UPS rural mode with more
33
+ # than one surcharge type checked). #results holds every pass.
34
+ attr_reader :spec, :grid, :error, :notifier, :results
32
35
 
33
36
  # notifier is set by the caller to the Bubbletea::Runner, whose #send is
34
37
  # the only way a worker thread can get a message onto the event loop.
@@ -54,6 +57,7 @@ module RateCard
54
57
  @failure_sparkline = Ntcharts::Sparkline.new(32, 1)
55
58
  @failure_sparkline.style = Lipgloss::Style.new.foreground(Theme::WARNING)
56
59
  @log = []
60
+ @results = []
57
61
  end
58
62
 
59
63
  def cancelled? = @cancelled
@@ -69,7 +73,7 @@ module RateCard
69
73
  when ServicesLoaded then services_loaded(message.services)
70
74
  when LoadFailed then return fail_with(message.error)
71
75
  when ProgressAdvanced then progress_advanced(message)
72
- when FetchFinished then return finish(message.grid)
76
+ when FetchFinished then return fetch_finished(message.grid)
73
77
  when FetchFailed then return fail_with(message.error)
74
78
  else
75
79
  return [self, spinner_or_field(message)]
@@ -175,10 +179,11 @@ module RateCard
175
179
 
176
180
  def field_for(stage)
177
181
  case stage
178
- when :rate_mode then rate_mode_field
179
- when :carrier then carrier_field
180
- when :rural then rural_field
181
- when :services then services_field
182
+ when :rate_mode then rate_mode_field
183
+ when :carrier then carrier_field
184
+ when :rural then rural_field
185
+ when :rural_surcharges then rural_surcharges_field
186
+ when :services then services_field
182
187
  when :zones then zones_field
183
188
  when :unit then unit_field
184
189
  when :weights then weights_field
@@ -241,39 +246,36 @@ module RateCard
241
246
 
242
247
  # Only USPS and UPS have a rural/DAS chart (Addresses::USPS_RURAL_DAS,
243
248
  # Addresses::UPS_RURAL); every other carrier is decided :normal (nil)
244
- # and skips this stage, same as any other single-answer stage.
249
+ # and skips this stage, same as any other single-answer stage. Same
250
+ # Normal/Rural toggle for both carriers - which surcharge type(s) UPS
251
+ # means by "rural" is a separate question, :rural_surcharges below.
245
252
  def rural_field
246
253
  return nil unless Constants::Carriers.rural_aware?(carrier)
247
254
 
248
- choices = carrier == 'USPS' ? usps_rural_choices : ups_rural_choices
249
- Fields::Select.new(label: 'Rural / DAS test mode', choices: choices,
255
+ choices = [['Normal', false], ['Rural (DAS)', true]]
256
+ Fields::Select.new(label: 'Rural / DAS', choices: choices,
250
257
  selected: choices.index { |_, v| v == @answers[:rural] } || 0)
251
258
  end
252
259
 
253
- def usps_rural_choices
254
- [['Normal', false], ['Rural (DAS)', true]]
255
- end
256
-
257
- def ups_rural_choices
258
- [['Normal', nil], ['EDAS', :edas], ['RDAS', :rdas], ['Both', :both]]
259
- end
260
+ # UPS has no per-zone rural chart - each surcharge type is one fixed
261
+ # address, not a zone sweep - so this replaces the zones question for
262
+ # UPS rural mode. Checking both produces two separate rate cards (see
263
+ # #build_specs), one per surcharge type, rather than one card that
264
+ # conflates two different addresses under one "zone" axis.
265
+ def rural_surcharges_field
266
+ return nil unless ups_rural?
260
267
 
261
- # UPS rural mode has no real zone choice - each surcharge type is a
262
- # single fixed address - so it's decided here as the chosen surcharge
263
- # symbol(s) and the zones stage is skipped rather than asked.
264
- def ups_rural_zones
265
- case @answers[:rural]
266
- when :edas then [:edas]
267
- when :rdas then [:rdas]
268
- when :both then %i[edas rdas]
269
- end
268
+ choices = [['EDAS', :edas], ['RDAS', :rdas]]
269
+ answered = @answers[:rural_surcharges]
270
+ Fields::MultiSelect.new(
271
+ label: 'Surcharge types',
272
+ choices: choices,
273
+ checked: answered ? checked_indexes(choices.map(&:last), answered) : (0...choices.length)
274
+ )
270
275
  end
271
276
 
272
277
  def zones_field
273
- if carrier == 'UPS' && (decided = ups_rural_zones)
274
- @answers[:zones] = decided
275
- return nil
276
- end
278
+ return nil if ups_rural?
277
279
 
278
280
  available = usps_rural? ? Constants::Addresses::USPS_RURAL_DAS.keys.sort : Constants::Addresses.available_zones(carrier)
279
281
  full = "#{available.first}-#{available.last}"
@@ -295,6 +297,10 @@ module RateCard
295
297
  carrier == 'USPS' && @answers[:rural] == true
296
298
  end
297
299
 
300
+ def ups_rural?
301
+ carrier == 'UPS' && @answers[:rural] == true
302
+ end
303
+
298
304
  # Weight is fixed per cubic tier, so asking for a display unit is
299
305
  # meaningless in cubic mode — decided as :oz (unused) and skipped.
300
306
  def unit_field
@@ -413,18 +419,45 @@ module RateCard
413
419
 
414
420
  # ---------------------------------------------------------------- fetch
415
421
 
422
+ # UPS rural mode with more than one surcharge type checked runs as
423
+ # several independent passes (see #build_specs) - each one calls
424
+ # Grid.build, feeds the same progress bar, and lands its own {spec:,
425
+ # grid:} in #results before the next pass starts. Every other carrier
426
+ # and mode has exactly one pass, same as before this existed.
416
427
  def start_fetch
417
- @spec = build_spec
428
+ @pending_specs = build_specs
418
429
  # Before the first call, so a bad path is not discovered after 128 of them.
419
430
  begin
420
- CsvWriter.ensure_writable!(@spec.output_base)
431
+ @pending_specs.each { |spec| CsvWriter.ensure_writable!(spec.output_base) }
421
432
  rescue OutputNotWritable => e
422
433
  return fail_with(e).last
423
434
  end
424
435
 
436
+ @results = []
437
+ @total_passes = @pending_specs.length
438
+ @pass_index = 0
439
+ next_pass
440
+ end
441
+
442
+ # Does not clear @spec when there is no next pass: Bubbletea renders once
443
+ # more before a returned Bubbletea.quit actually stops the loop, and
444
+ # that render still hits :fetching's view, which reads @spec. Leaving it
445
+ # pointed at the just-finished pass keeps that last render honest rather
446
+ # than crashing on a nil spec.
447
+ def next_pass
448
+ next_spec = @pending_specs.shift
449
+ return Bubbletea.quit if next_spec.nil?
450
+
451
+ @spec = next_spec
452
+ @pass_index += 1
453
+ @completed = 0
454
+ @failed = 0
455
+ fetch_command(@spec)
456
+ end
457
+
458
+ def fetch_command(spec)
425
459
  client = @client_factory.call(@token)
426
460
  notifier = @notifier
427
- spec = @spec
428
461
  provider = @provider
429
462
 
430
463
  lambda do
@@ -448,9 +481,10 @@ module RateCard
448
481
  @failure_sparkline.push(message.failed)
449
482
  end
450
483
 
451
- def finish(grid)
484
+ def fetch_finished(grid)
452
485
  @grid = grid
453
- [self, Bubbletea.quit]
486
+ @results << { spec: @spec, grid: grid }
487
+ [self, next_pass]
454
488
  end
455
489
 
456
490
  def fail_with(error)
@@ -458,7 +492,23 @@ module RateCard
458
492
  [self, Bubbletea.quit]
459
493
  end
460
494
 
495
+ # One RunSpec per pass. UPS rural mode runs one pass per checked
496
+ # surcharge type (each is a fixed single address, not a zone sweep -
497
+ # see Addresses::UPS_RURAL) rather than folding both into one card's
498
+ # zone axis. Every other carrier/mode is the single pass it always was.
499
+ def build_specs
500
+ if ups_rural?
501
+ (@answers[:rural_surcharges] || []).map { |surcharge| run_spec(zones: [surcharge]) }
502
+ else
503
+ [run_spec(zones: @answers[:zones])]
504
+ end
505
+ end
506
+
461
507
  def build_spec
508
+ build_specs.first
509
+ end
510
+
511
+ def run_spec(zones:)
462
512
  cubic = @answers[:rate_mode] == :cubic
463
513
  RunSpec.new(
464
514
  token: @token,
@@ -466,7 +516,7 @@ module RateCard
466
516
  customer_id: identity[:customer_id],
467
517
  carrier: carrier,
468
518
  services: @answers[:services],
469
- zones: @answers[:zones],
519
+ zones: zones,
470
520
  weight_unit: @answers[:unit] || :oz,
471
521
  weights: cubic ? [] : @answers[:weights],
472
522
  cubic_tiers: cubic ? @answers[:weights] : [],
@@ -498,18 +548,24 @@ module RateCard
498
548
  # Everything the run will do, gathered in one block. The answers are also
499
549
  # in the transcript above, but they arrived one at a time over eight
500
550
  # screens; this is the only place they can be read against each other.
551
+ #
552
+ # Built from real RunSpecs (build_specs) rather than raw @answers, so a
553
+ # UPS rural run with both surcharge types checked recaps as the two
554
+ # separate cards it will actually produce, one line each.
501
555
  def recap_view
556
+ specs = build_specs
502
557
  carrier_line = " #{Theme.bold(carrier)}#{rural_recap} · #{@answers[:services].map(&:name).join(', ')}"
503
558
  [
504
559
  carrier_line,
505
- " zones #{RunSpec.compact_range(@answers[:zones])} · #{rows_recap} · #{@answers[:package_type]}",
560
+ *specs.map { |spec| " zones #{spec.zone_summary} · #{rows_recap} · #{@answers[:package_type]}" },
506
561
  " columns: #{@answers[:rate_keys].map { |k| RunSpec::RATE_KEY_LABELS.fetch(k) }.join(', ')}",
507
- " #{Theme.bold(call_count.to_s)} rate calls against #{Theme.danger('production')}"
562
+ " #{Theme.bold(specs.sum(&:call_count).to_s)} rate calls against #{Theme.danger('production')}" \
563
+ "#{specs.length > 1 ? " (#{specs.length} separate cards)" : ''}"
508
564
  ].join("\n")
509
565
  end
510
566
 
511
567
  def rural_recap
512
- return '' if !@answers[:rural] || @answers[:rural].nil?
568
+ return '' unless @answers[:rural]
513
569
 
514
570
  " (#{rural_label(@answers[:rural])})"
515
571
  end
@@ -522,16 +578,13 @@ module RateCard
522
578
  end
523
579
  end
524
580
 
525
- def call_count
526
- @answers[:weights].length * @answers[:zones].length
527
- end
528
-
529
581
  def fetch_view
530
582
  total = @spec.call_count
531
583
  percent = total.zero? ? 0.0 : @completed.to_f / total
584
+ title = @total_passes > 1 ? "fetching rates (pass #{@pass_index}/#{@total_passes})" : 'fetching rates'
532
585
  line = " #{@progress.view_as(percent)} #{@completed}/#{total}"
533
586
  line += " #{Theme.warning("#{Theme::ALERT} #{@failed} failed")}" if @failed.positive?
534
- view = "#{Theme.bold(' fetching rates')}\n#{line}"
587
+ view = "#{Theme.bold(" #{title}")}\n#{line}"
535
588
  view += "\n#{failure_sparkline_view}" if @failed.positive?
536
589
  view
537
590
  end
@@ -548,8 +601,9 @@ module RateCard
548
601
  # transcript of the run stays on screen — the recap is then a summary of
549
602
  # what is already visible rather than the first chance to check it.
550
603
  def answered_line(stage, value)
551
- label = { rate_mode: 'rate by', carrier: 'carrier', rural: 'rural / DAS', services: 'services',
552
- zones: 'zones', unit: 'unit', weights: 'weights', package_type: 'package',
604
+ label = { rate_mode: 'rate by', carrier: 'carrier', rural: 'rural / DAS',
605
+ rural_surcharges: 'surcharge types', services: 'services', zones: 'zones',
606
+ unit: 'unit', weights: 'weights', package_type: 'package',
553
607
  rate_keys: 'columns' }[stage]
554
608
  label = 'cubic tiers' if stage == :weights && @answers[:rate_mode] == :cubic
555
609
  return nil if label.nil?
@@ -559,22 +613,18 @@ module RateCard
559
613
 
560
614
  def describe(stage, value)
561
615
  case stage
562
- when :services then value.map(&:name).join(', ')
563
- when :zones then RunSpec.compact_range(value)
564
- when :weights then RunSpec.compact_range(value)
565
- when :rural then rural_label(value)
566
- when :rate_keys then value.map { |k| RunSpec::RATE_KEY_LABELS.fetch(k) }.join(', ')
616
+ when :services then value.map(&:name).join(', ')
617
+ when :zones then RunSpec.compact_range(value)
618
+ when :weights then RunSpec.compact_range(value)
619
+ when :rural then rural_label(value)
620
+ when :rural_surcharges then value.map { |v| v.to_s.upcase }.join(', ')
621
+ when :rate_keys then value.map { |k| RunSpec::RATE_KEY_LABELS.fetch(k) }.join(', ')
567
622
  else value.to_s
568
623
  end
569
624
  end
570
625
 
571
626
  def rural_label(value)
572
- case value
573
- when false, nil then 'normal'
574
- when true then 'rural (DAS)'
575
- when :edas, :rdas then value.to_s.upcase
576
- when :both then 'EDAS + RDAS'
577
- end
627
+ value ? 'rural (DAS)' : 'normal'
578
628
  end
579
629
  end
580
630
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RateCard
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rate-card
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - eHub