asxpio/test/invoice_stats_test.rb
Sergei Poljanski 9128867a33
invoices: statistics page
Add /admin/invoices/stats with a year filter: invoiced, received and
outstanding totals in GEL at each invoice's captured rate (the one
currency every invoice has), overdue count, average days to pay, money
received per month by payment date as a bar chart with the same figures
in a table, per-currency totals, top clients and crypto option usage.
Cancelled invoices are left out of every amount; hidden ones follow the
usual superuser-only visibility.

Assisted-by: Claude:opus-5
2026-10-10 09:12:21 +04:00

82 lines
3.3 KiB
Ruby

require_relative 'test_helper'
class InvoiceStatsTest < Minitest::Test
include Rack::Test::Methods
include AdminLogin
def app
AsxpioWeb
end
def setup
skip 'TEST_DATABASE_URL not set' unless TestDb.available?
TestDb.clean!
TestDb.clean_users!
end
def make(client:, price:, issued:, paid: nil, cancelled: false, currency: 'EUR', rate: '3')
i = Invoice.build(client_name: client, client_email: 'c@example.com', currency: currency, gel_rate: rate,
issued_on: issued.to_s, due_on: (issued + 14).to_s,
items: [{ 'description' => 'work', 'qty' => '1', 'unit_price' => price.to_s }])
i.pdf_key = "invoices/#{i.number}-#{i.uuid}.pdf"
i.paid_at = Time.utc(paid.year, paid.month, paid.day, 12) if paid
i.cancelled_at = Time.now.utc if cancelled
i.save_changes
i
end
def test_figures
make(client: 'ACME', price: 100, issued: Date.new(2026, 1, 10), paid: Date.new(2026, 2, 5)) # 300 GEL, 26 days
make(client: 'ACME', price: 50, issued: Date.new(2026, 2, 1)) # 150 GEL, unpaid
make(client: 'Beta', price: 200, issued: Date.new(2026, 2, 3), cancelled: true) # ignored
make(client: 'Old', price: 10, issued: Date.new(2025, 12, 1), paid: Date.new(2026, 1, 2), currency: 'USD', rate: '2.7')
s = InvoiceStats.new(Invoice.all, year: 2026)
assert_equal 2, s.live.size
assert_equal 1, s.cancelled.size
assert_equal BigDecimal('450'), s.gel(s.live)
assert_equal BigDecimal('150'), s.gel(s.unpaid)
assert_equal 26, s.avg_days_to_pay
jan = s.monthly[0]
feb = s.monthly[1]
assert_equal [1, BigDecimal('300')], [jan[1], jan[2]]
assert_equal BigDecimal('27'), jan[3] # 2025 invoice paid in Jan 2026 counts as received then
assert_equal BigDecimal('300'), feb[3] # paid by payment date
assert_equal 12, s.monthly.size
assert_equal [['ACME', 2, BigDecimal('450')]], s.top_clients
assert_equal [2026, 2025], s.years
all = InvoiceStats.new(Invoice.all)
assert_equal 3, all.live.size
assert_equal({ 'EUR' => { invoiced: BigDecimal('150'), paid: BigDecimal('100'), outstanding: BigDecimal('50') },
'USD' => { invoiced: BigDecimal('10'), paid: BigDecimal('10'), outstanding: BigDecimal('0') } }, all.by_currency)
end
def test_page_renders_and_respects_hidden
make(client: 'ACME', price: 100, issued: Date.today - 20, paid: Date.today - 5)
h = make(client: 'Secret Ltd', price: 999, issued: Date.today - 3)
h.update(hidden: true)
admin_login!
get '/admin/invoices/stats'
assert last_response.ok?, last_response.body[0, 300]
assert_includes last_response.body, 'Secret Ltd' # superuser sees hidden
assert_includes last_response.body, 'class="bar-chart"'
get "/admin/invoices/stats?year=#{Date.today.year}"
assert last_response.ok?
clear_cookies
@admin_logged_in = false
u = User.create(username: 'clerk', password: 'clerk-password-1')
UserPermission.create(user_id: u.id, area: 'invoices', can_read: true)
admin_login!(username: 'clerk', password: 'clerk-password-1')
get '/admin/invoices/stats'
assert last_response.ok?
refute_includes last_response.body, 'Secret Ltd'
end
def test_empty_state
admin_login!
get '/admin/invoices/stats'
assert last_response.ok?
end
end