Payments often arrive before they are recorded, so stamping paid_at with the click time skews received-per-month and days-to-pay stats. Mark paid now takes an optional date, and a paid invoice gets a form to correct it. Past days are stored at 12:00 UTC so the date holds in any timezone; future and malformed dates are refused. The PDF does not print the date, so nothing is re-rendered. Assisted-by: Claude:opus-5-5 Claude-Session: https://claude.ai/code/session_01TCc5TvWnAs5xPvGMQXTJw3
126 lines
4.8 KiB
Ruby
126 lines
4.8 KiB
Ruby
require_relative 'test_helper'
|
|
|
|
class InvoiceCancelTest < 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!
|
|
@inv = Invoice.build(client_name: 'ACME', client_email: 'b@example.com', currency: 'EUR', gel_rate: '3.05',
|
|
items: [{ 'description' => 'work', 'qty' => '1', 'unit_price' => '100' }])
|
|
@inv.pdf_key = "invoices/#{@inv.number}-#{@inv.uuid}.pdf"
|
|
@inv.save_changes
|
|
end
|
|
|
|
def token
|
|
get '/admin'
|
|
last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
end
|
|
|
|
def test_cancel_marks_status_and_renders_cancelled_pdf
|
|
admin_login!
|
|
post "/admin/invoices/#{@inv.uuid}/cancel", { authenticity_token: token, reason: 'Duplicate of INV-1' }
|
|
assert_equal 302, last_response.status
|
|
@inv.refresh
|
|
assert @inv.cancelled?
|
|
assert_equal 'cancelled', @inv.status
|
|
assert_equal 'Duplicate of INV-1', @inv.cancel_reason
|
|
assert_match(/-cancelled\.pdf\z/, @inv.current_pdf_key)
|
|
get "/i/#{@inv.uuid}"
|
|
assert_includes last_response.body, 'badge-cancelled'
|
|
assert_includes last_response.body, 'not payable'
|
|
|
|
post "/admin/invoices/#{@inv.uuid}/paid", { authenticity_token: token }
|
|
assert_equal 422, last_response.status
|
|
|
|
post "/admin/invoices/#{@inv.uuid}/reinstate", { authenticity_token: token }
|
|
refute @inv.refresh.cancelled?
|
|
assert_equal 'pending', @inv.status
|
|
end
|
|
|
|
def test_paid_date_can_be_backdated_and_changed
|
|
admin_login!
|
|
post "/admin/invoices/#{@inv.uuid}/paid", { authenticity_token: token, paid_on: '2026-01-15' }
|
|
assert_equal 302, last_response.status
|
|
assert_equal Time.utc(2026, 1, 15, 12), @inv.refresh.paid_at
|
|
|
|
post "/admin/invoices/#{@inv.uuid}/paid-date", { authenticity_token: token, paid_on: '2026-02-01' }
|
|
assert_equal 302, last_response.status
|
|
assert_equal Date.new(2026, 2, 1), @inv.refresh.paid_at.utc.to_date
|
|
|
|
future = (Date.today + 3).iso8601
|
|
post "/admin/invoices/#{@inv.uuid}/paid-date", { authenticity_token: token, paid_on: future }
|
|
assert_equal 422, last_response.status
|
|
post "/admin/invoices/#{@inv.uuid}/paid-date", { authenticity_token: token, paid_on: 'nope' }
|
|
assert_equal 422, last_response.status
|
|
assert_equal Date.new(2026, 2, 1), @inv.refresh.paid_at.utc.to_date
|
|
|
|
get "/admin/invoices/#{@inv.uuid}"
|
|
assert_includes last_response.body, 'value="2026-02-01"'
|
|
|
|
post "/admin/invoices/#{@inv.uuid}/paid", { authenticity_token: token }
|
|
refute @inv.refresh.paid?
|
|
post "/admin/invoices/#{@inv.uuid}/paid", { authenticity_token: token, paid_on: '' }
|
|
assert_in_delta Time.now.utc, @inv.refresh.paid_at, 60
|
|
end
|
|
|
|
# Every state of the admin invoice page renders (pending, paid,
|
|
# cancelled, hidden), for a superuser and for a read-only user.
|
|
def test_admin_invoice_page_renders_in_every_state
|
|
admin_login!
|
|
get "/admin/invoices/#{@inv.uuid}"
|
|
assert last_response.ok?, last_response.body[0, 300]
|
|
assert_includes last_response.body, 'Cancel invoice'
|
|
@inv.update(paid_at: Time.now.utc)
|
|
get "/admin/invoices/#{@inv.uuid}"
|
|
assert last_response.ok?, last_response.body[0, 300]
|
|
@inv.update(cancelled_at: Time.now.utc, cancel_reason: 'dup', hidden: true)
|
|
get "/admin/invoices/#{@inv.uuid}"
|
|
assert last_response.ok?, last_response.body[0, 300]
|
|
assert_includes last_response.body, 'Reinstate'
|
|
end
|
|
|
|
def test_cancelled_pdf_renders
|
|
@inv.cancelled_at = Time.now.utc
|
|
@inv.cancel_reason = 'wrong client'
|
|
pdf = InvoicePdf.render(@inv, status: 'cancelled')
|
|
assert pdf.start_with?('%PDF')
|
|
end
|
|
|
|
def test_hidden_invoice_only_for_superusers
|
|
admin_login!
|
|
post "/admin/invoices/#{@inv.uuid}/hide", { authenticity_token: token }
|
|
assert @inv.refresh.hidden
|
|
get '/admin/invoices'
|
|
assert_includes last_response.body, 'badge-hidden'
|
|
|
|
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, can_write: true)
|
|
admin_login!(username: 'clerk', password: 'clerk-password-1')
|
|
get '/admin/invoices'
|
|
refute_includes last_response.body, @inv.number
|
|
get "/admin/invoices/#{@inv.uuid}"
|
|
assert_equal 404, last_response.status
|
|
post "/admin/invoices/#{@inv.uuid}/hide", { authenticity_token: token }
|
|
assert_equal 403, last_response.status
|
|
get '/admin'
|
|
refute_includes last_response.body, @inv.number
|
|
get "/i/#{@inv.uuid}"
|
|
assert last_response.ok? # client link unaffected
|
|
end
|
|
|
|
def test_dashboard_ignores_cancelled_for_outstanding
|
|
@inv.update(cancelled_at: Time.now.utc)
|
|
admin_login!
|
|
get '/admin'
|
|
assert_match %r{<b>0</b><span>unpaid</span>}, last_response.body
|
|
end
|
|
end
|