nbg and auto fill GEL exchange rate
All checks were successful
Build and Deploy to Production / build (push) Successful in 47s
Build and Deploy to Production / deploy (push) Successful in 27s

This commit is contained in:
Sergei Poljanski 2026-06-08 19:29:25 +03:00
commit 7dbeb0dd49
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
7 changed files with 161 additions and 5 deletions

61
lib/gel_rate.rb Normal file
View file

@ -0,0 +1,61 @@
require 'net/http'
require 'json'
require 'uri'
require 'date'
require 'bigdecimal'
# Fetches the official GEL exchange rate from the National Bank of Georgia.
# https://nbg.gov.ge/en/monetary-policy/currency
#
# API: GET .../currencies/en/json/?currencies=USD&date=YYYY-MM-DD
# Response is a top-level array; rate is quoted per `quantity` units, so the
# per-unit rate is rate / quantity. NBG returns the rate valid on or before the
# requested date (weekends/holidays roll back to the last published rate).
module GelRate
ENDPOINT = 'https://nbg.gov.ge/gw/api/ct/monetarypolicy/currencies/en/json/'.freeze
TIMEOUT = 6 # seconds
class Error < StandardError; end
module_function
# Returns a BigDecimal: how many GEL per 1 unit of `currency` on `date`.
# `date` may be a Date or YYYY-MM-DD string; omitted ⇒ latest published rate.
def fetch(currency = 'USD', date: nil)
currency = currency.to_s.upcase
uri = URI(ENDPOINT)
query = { currencies: currency }
query[:date] = normalize_date(date) if date
uri.query = URI.encode_www_form(query)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
http.get(uri.request_uri, 'Accept' => 'application/json')
end
raise Error, "NBG HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
data = JSON.parse(res.body)
entry = Array(data).first or raise Error, 'Empty NBG response'
cur = Array(entry['currencies']).find { |c| c['code'] == currency } \
or raise Error, "No #{currency} in NBG response"
rate = BigDecimal(cur.fetch('rate').to_s)
qty = BigDecimal(cur.fetch('quantity', 1).to_s)
raise Error, 'NBG quantity is zero' if qty.zero?
(rate / qty).round(8)
rescue JSON::ParserError => e
raise Error, "Bad JSON from NBG: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout
raise Error, 'NBG timed out'
rescue KeyError => e
raise Error, "Missing field in NBG response: #{e.message}"
end
def normalize_date(date)
d = date.is_a?(Date) ? date : Date.parse(date.to_s)
d.strftime('%Y-%m-%d')
rescue ArgumentError
raise Error, "Invalid date: #{date}"
end
end

View file

@ -210,7 +210,7 @@ class InvoicePdf
['Total', "#{@invoice.currency} #{fmt_money(total)}"]
]
if @invoice.currency != 'GEL'
rows << ["In GEL (rate #{fmt_money(@invoice.gel_rate)})", "GEL #{fmt_money(@invoice.total_gel)}"]
rows << ["In GEL (rate #{fmt_rate(@invoice.gel_rate)})", "GEL #{fmt_money(@invoice.total_gel)}"]
end
pdf.bounding_box([pdf.bounds.right - 270, pdf.cursor], width: 270) do
@ -277,7 +277,7 @@ class InvoicePdf
pdf.text "Amount: #{fmt_ltc(amount)} LTC", style: :bold
if @invoice.ltc_rate
pdf.fill_color COLOR_MUTED
pdf.text "(rate 1 LTC = #{fmt_money(@invoice.ltc_rate)} #{@invoice.currency} at issue)", size: 8
pdf.text "(rate 1 LTC = #{fmt_rate(@invoice.ltc_rate)} #{@invoice.currency} at issue)", size: 8
pdf.fill_color COLOR_TEXT
end
pdf.move_down 3
@ -329,6 +329,17 @@ class InvoicePdf
BigDecimal(value.to_s).round(8).to_s('F').sub(/(\.\d*?)0+$/, '\\1').sub(/\.$/, '')
end
# Exchange rate: keep full captured precision (up to 8 dp), trim trailing
# zeros, but pad to at least `min_dp` so it reads as a rate. GEL/USD rates are
# quoted to 4 dp by the NBG, so the default min is 4.
def fmt_rate(value, min_dp: 4)
s = BigDecimal(value.to_s).round(8).to_s('F')
int, frac = s.split('.')
frac = (frac || '').sub(/0+$/, '')
frac = frac.ljust(min_dp, '0')
"#{with_thousands("#{int}.00").split('.').first}.#{frac}"
end
def fmt_qty(value)
bd = BigDecimal(value.to_s)
bd.frac.zero? ? bd.to_i.to_s : bd.to_s('F')