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 +4 -4
- data/exe/rate-card +9 -3
- data/lib/rate_card/run_spec.rb +9 -6
- data/lib/rate_card/tui/app.rb +108 -58
- data/lib/rate_card/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21c45d1eea2e85a47a9f268250611d10548eb3b812d4d5dd27716506c9802e3e
|
|
4
|
+
data.tar.gz: c209d3d667be368253081ded2e7aab44f9138cefebeb086d27257f137b92538d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
114
|
+
exit exit_codes.max
|
data/lib/rate_card/run_spec.rb
CHANGED
|
@@ -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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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!
|
data/lib/rate_card/tui/app.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
179
|
-
when :carrier
|
|
180
|
-
when :rural
|
|
181
|
-
when :
|
|
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 =
|
|
249
|
-
Fields::Select.new(label: 'Rural / DAS
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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!(
|
|
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
|
|
484
|
+
def fetch_finished(grid)
|
|
452
485
|
@grid = grid
|
|
453
|
-
|
|
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:
|
|
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 #{
|
|
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 ''
|
|
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(
|
|
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',
|
|
552
|
-
|
|
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
|
|
563
|
-
when :zones
|
|
564
|
-
when :weights
|
|
565
|
-
when :rural
|
|
566
|
-
when :
|
|
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
|
-
|
|
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
|
data/lib/rate_card/version.rb
CHANGED