mirror of
https://github.com/System-End/theseus.git
synced 2026-04-20 00:35:18 +00:00
272 lines
9 KiB
Ruby
272 lines
9 KiB
Ruby
namespace :usps do
|
|
desc "Fetch latest USPS postage rates and regenerate pricing_engine.rb"
|
|
task update_rates: :environment do
|
|
puts "Fetching USPS rates..."
|
|
|
|
# Weight breakpoints for each category
|
|
letter_weights = [1.0, 2.0, 3.0, 3.5]
|
|
flat_weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0]
|
|
fcmi_letter_weights = [1.0, 2.0, 3.0, 3.5]
|
|
fcmi_flat_weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 12.0, 15.994]
|
|
|
|
# Countries for FCMI rate groups
|
|
fcmi_countries = { ca: "CA", mx: "MX", other: "GB" }
|
|
|
|
# Fetch domestic rates (the letter-rates API returns both metered and stamp prices)
|
|
puts " Fetching domestic letter rates..."
|
|
us_letter_rates = fetch_domestic_rates("LETTERS", letter_weights)
|
|
|
|
puts " Fetching domestic flat rates..."
|
|
us_flat_rates = fetch_domestic_rates("FLATS", flat_weights)
|
|
|
|
# For stamp rates, we use the same endpoint - USPS letter-rates returns retail prices
|
|
# which are the same as stamp prices
|
|
us_stamp_letter_rates = us_letter_rates.dup
|
|
us_stamp_flat_rates = us_flat_rates.dup
|
|
|
|
# Fetch FCMI rates
|
|
puts " Fetching FCMI letter rates..."
|
|
fcmi_letter_rates = fetch_fcmi_rates("LETTERS", fcmi_letter_weights, fcmi_countries)
|
|
|
|
puts " Fetching FCMI flat rates..."
|
|
fcmi_flat_rates = fetch_fcmi_rates("FLATS", fcmi_flat_weights, fcmi_countries)
|
|
|
|
# Fetch non-machinable surcharges
|
|
puts " Fetching domestic non-machinable surcharge..."
|
|
domestic_nm_surcharge = fetch_domestic_non_machinable_surcharge
|
|
puts " Fetching international non-machinable surcharge..."
|
|
intl_nm_surcharge = fetch_international_non_machinable_surcharge
|
|
|
|
# Use the higher of the two (they should be the same, but just in case)
|
|
nm_surcharge = [domestic_nm_surcharge, intl_nm_surcharge].max
|
|
puts " Using non-machinable surcharge: $#{nm_surcharge}"
|
|
|
|
puts "Generating pricing_engine.rb..."
|
|
generate_pricing_engine(
|
|
fcmi_letter_rates: fcmi_letter_rates,
|
|
fcmi_flat_rates: fcmi_flat_rates,
|
|
fcmi_nm_surcharge: nm_surcharge,
|
|
us_letter_rates: us_letter_rates,
|
|
us_flat_rates: us_flat_rates,
|
|
us_stamp_letter_rates: us_stamp_letter_rates,
|
|
us_stamp_flat_rates: us_stamp_flat_rates
|
|
)
|
|
|
|
puts "Done! Updated app/services/usps/pricing_engine.rb"
|
|
end
|
|
|
|
def fetch_domestic_rates(processing_category, weights, non_machinable: false)
|
|
rates = {}
|
|
nm_indicators = non_machinable ? { isRigid: true } : {}
|
|
weights.each do |weight|
|
|
response = USPS::APIService.letter_price(
|
|
processing_category: processing_category,
|
|
weight: weight,
|
|
non_machinable_indicators: nm_indicators
|
|
)
|
|
price = response.dig(:rates, 0, :price) || response.dig(:totalBasePrice)
|
|
rates[weight] = price.to_f
|
|
print "."
|
|
end
|
|
puts
|
|
rates
|
|
end
|
|
|
|
def fetch_fcmi_rates(processing_category, weights, countries)
|
|
rates = {}
|
|
weights.each do |weight|
|
|
rates[weight] = {}
|
|
countries.each do |key, country_code|
|
|
response = USPS::APIService.international_letter_price(
|
|
processing_category: processing_category,
|
|
weight: weight,
|
|
destination_country_code: country_code
|
|
)
|
|
price = response.dig(:rates, 0, :price) || response.dig(:totalBasePrice)
|
|
rates[weight][key] = price.to_f
|
|
print "."
|
|
end
|
|
end
|
|
puts
|
|
rates
|
|
end
|
|
|
|
def fetch_domestic_non_machinable_surcharge
|
|
response = USPS::APIService.letter_price(
|
|
processing_category: "LETTERS",
|
|
weight: 1.0,
|
|
non_machinable_indicators: { isRigid: true }
|
|
)
|
|
extract_nm_surcharge(response, "domestic")
|
|
end
|
|
|
|
def fetch_international_non_machinable_surcharge
|
|
response = USPS::APIService.international_letter_price(
|
|
processing_category: "LETTERS",
|
|
weight: 1.0,
|
|
destination_country_code: "GB"
|
|
)
|
|
# International API may not have NM indicators, so we compare with a machinable request
|
|
# or look for fees in response
|
|
extract_nm_surcharge(response, "international")
|
|
end
|
|
|
|
def extract_nm_surcharge(response, label)
|
|
fees = response.dig(:rates, 0, :fees) || []
|
|
nm_fee = fees.find { |f| f[:name]&.downcase&.include?("nonmachinable") }
|
|
|
|
if nm_fee
|
|
puts " (#{label}: from API fees: $#{nm_fee[:price]})"
|
|
nm_fee[:price].to_f
|
|
else
|
|
total = response[:totalBasePrice].to_f
|
|
base = response.dig(:rates, 0, :price).to_f
|
|
diff = (total - base).round(2)
|
|
if diff.positive?
|
|
puts " (#{label}: calculated from price difference: $#{diff})"
|
|
diff
|
|
else
|
|
puts " (#{label}: using fallback value: $0.46)"
|
|
0.46
|
|
end
|
|
end
|
|
end
|
|
|
|
def generate_pricing_engine(
|
|
fcmi_letter_rates:,
|
|
fcmi_flat_rates:,
|
|
fcmi_nm_surcharge:,
|
|
us_letter_rates:,
|
|
us_flat_rates:,
|
|
us_stamp_letter_rates:,
|
|
us_stamp_flat_rates:
|
|
)
|
|
content = <<~RUBY
|
|
module USPS
|
|
class PricingEngine
|
|
class << self
|
|
# this will have to be updated when they come out with a new notice 123!
|
|
FCMI_RATE_TABLE = {
|
|
letter: {
|
|
#{format_fcmi_rates(fcmi_letter_rates, 10)}
|
|
},
|
|
flat: {
|
|
#{format_fcmi_rates(fcmi_flat_rates, 10)}
|
|
}
|
|
}
|
|
FCMI_NON_MACHINABLE_SURCHARGE = #{fcmi_nm_surcharge}
|
|
|
|
US_LETTER_RATES = {
|
|
#{format_simple_rates(us_letter_rates, 8)}
|
|
}
|
|
|
|
US_FLAT_RATES = {
|
|
#{format_simple_rates(us_flat_rates, 8)}
|
|
}
|
|
|
|
US_STAMP_LETTER_RATES = {
|
|
#{format_simple_rates(us_stamp_letter_rates, 8)}
|
|
}
|
|
|
|
US_STAMP_FLAT_RATES = {
|
|
#{format_simple_rates(us_stamp_flat_rates, 8)}
|
|
}
|
|
|
|
def metered_price(processing_category, weight, non_machinable = false)
|
|
type = processing_category.to_sym
|
|
rates = case type
|
|
when :letter
|
|
US_LETTER_RATES
|
|
when :flat
|
|
US_FLAT_RATES
|
|
else
|
|
raise ArgumentError, "type must be :letter or :flat"
|
|
end
|
|
|
|
rate = rates.find { |k, v| weight <= k }&.last
|
|
raise ArgumentError, "\#{weight} oz is too heavy for a \#{type}" unless rate
|
|
|
|
if non_machinable
|
|
raise ArgumentError, "only letters can be non-machinable!" unless type == :letter
|
|
rate += FCMI_NON_MACHINABLE_SURCHARGE
|
|
end
|
|
|
|
rate
|
|
end
|
|
|
|
def domestic_stamp_price(processing_category, weight, non_machinable = false)
|
|
type = processing_category.to_sym
|
|
rates = case type
|
|
when :letter
|
|
US_STAMP_LETTER_RATES
|
|
when :flat
|
|
US_STAMP_FLAT_RATES
|
|
else
|
|
raise ArgumentError, "type must be :letter or :flat"
|
|
end
|
|
|
|
rate = rates.find { |k, v| weight <= k }&.last
|
|
raise ArgumentError, "\#{weight} oz is too heavy for a \#{type}" unless rate
|
|
|
|
if non_machinable
|
|
raise ArgumentError, "only letters can be non-machinable!" unless type == :letter
|
|
rate += FCMI_NON_MACHINABLE_SURCHARGE
|
|
end
|
|
|
|
rate
|
|
end
|
|
|
|
def fcmi_price(processing_category, weight, country, non_machinable = false)
|
|
type = processing_category.to_sym
|
|
rates = FCMI_RATE_TABLE[type]
|
|
|
|
raise ArgumentError, "idk the rates for \#{type}..." unless rates
|
|
country = case country
|
|
when "CA"
|
|
:ca
|
|
when "MX"
|
|
:mx
|
|
else
|
|
:other
|
|
end
|
|
|
|
rate = rates.find { |k, v| weight <= k }&.dig(1)
|
|
raise "\#{weight} oz is too heavy for an FCMI \#{type}" unless rate
|
|
price = rate[country]
|
|
if non_machinable
|
|
raise ArgumentError, "only letters can be nonmachinable!" unless type == :letter
|
|
price += FCMI_NON_MACHINABLE_SURCHARGE
|
|
end
|
|
price
|
|
end
|
|
|
|
def stamp_price(processing_category, weight, country, non_machinable = false)
|
|
if country == "US"
|
|
domestic_stamp_price(processing_category, weight, non_machinable)
|
|
else
|
|
fcmi_price(processing_category, weight, country, non_machinable)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
RUBY
|
|
|
|
File.write(Rails.root.join("app/services/usps/pricing_engine.rb"), content)
|
|
end
|
|
|
|
def format_fcmi_rates(rates, indent)
|
|
lines = []
|
|
rates.each do |weight, countries|
|
|
lines << "#{' ' * indent}#{weight} => {"
|
|
country_lines = countries.map { |k, v| "#{' ' * (indent + 2)}#{k}: #{format("%.2f", v)}" }
|
|
lines << country_lines.join(",\n")
|
|
lines << "#{' ' * indent}},"
|
|
end
|
|
lines.join("\n")
|
|
end
|
|
|
|
def format_simple_rates(rates, indent)
|
|
rates.map { |weight, price| "#{' ' * indent}#{weight} => #{format("%.2f", price)}" }.join(",\n")
|
|
end
|
|
end
|