Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New UI 2023 - Added partial payments to invoices #1258

Merged
merged 4 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ app/assets/builds/*
/app/assets/builds/*
!/app/assets/builds/.keep1
!/app/assets/builds/.keep

Dockerfile.dev
11 changes: 10 additions & 1 deletion app/components/modals/pay_invoice/component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@

<div class="c-modal__footer u-flex u-align-center u-content-sp">
<%= link_to t('invoices.download'), download_invoice_path(@invoice.uuid), { class: 'c-btn c-btn--ghost', download: true } %>
<%= button_to t('invoices.show.pay'), oneoff_invoice_path(uuid: @invoice.uuid), class: 'c-btn c-btn--green', data: {turbo: false} if @invoice.payable? %>
<% if @invoice.payable? %>
<% if @invoice.partial_payments? %>
<%= form_with url: oneoff_invoice_path(uuid: @invoice.uuid), data: {turbo: false} do |f| %>
<%= component 'common/form/number_field', form: f, **amount_field_properties %>
<%= f.submit t('invoices.show.pay'), class: 'c-btn c-btn--green' %>
<% end %>
<% else %>
<%= button_to t('invoices.show.pay'), oneoff_invoice_path(uuid: @invoice.uuid), class: 'c-btn c-btn--green', data: {turbo: false} %>
<% end %>
<% end %>
</div>
</div>

Expand Down
12 changes: 12 additions & 0 deletions app/components/modals/pay_invoice/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ def initialize(invoice:)

@invoice = invoice
end

def amount_field_properties
{
attribute: :amount,
options: {
min: 0.0,
step: 0.01,
value: number_with_precision(invoice.due_amount.to_f, precision: 2, delimiter: '', separator: '.'),
disabled: false
}
}
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@

</thead>
<tbody>

<% @invoice.items.each_with_index do |item, index| %>
<tr>
<td><%= index + 1 %></td>
<td><%= index += 1 %></td>
<td><%= I18n.t('invoice_items.name',
domain_name: item.invoice.result.auction.domain_name,
auction_end: item.invoice.result.auction.ends_at.to_date) %>
Expand All @@ -66,30 +65,44 @@
<% end %>

<tr>
<td>2</td>
<td></td>
<td><%= t('invoices.vat_amount') %> <%= number_to_percentage(@invoice.vat_rate * 100, precision: 0) %></td>
<td><%= t('offers.price_in_currency', price: @invoice.vat) %></td>
</tr>

<tr>
<td>3</td>
<td></td>
<td><%= t('invoices.total') %></td>
<td><%= t('offers.price_in_currency', price: @invoice.total + (@invoice.enable_deposit? ? @invoice.deposit : 0.0)) %></td>
</tr>

<% if @invoice.enable_deposit? %>
<tr>
<td>4</td>
<td><</td>
<td><%= t('invoices.deposit') %></td>
<td><%= t('offers.price_in_currency', price: @invoice.deposit) %></td>
</tr>
<% end %>

<% if @invoice.partial_payments? && [email protected]? %>
<tr>
<td></td>
<td><%= t('invoices.total_paid') %></td>
<td><%= t('offers.price_in_currency', price: Money.from_amount(@invoice.paid_amount || 0, @invoice.auction_currency)) %></td>
</tr>
<% end %>

</tbody>
<tfoot>
<tr>
<td colspan="2"><%= t('invoices.show.total_amount') %></td>
<td><%= t('offers.price_in_currency', price: @invoice.total) %></td>
<td>
<% if @invoice.partial_payments? && [email protected]? %>
<%= t('offers.price_in_currency', price: @invoice.due_amount) %>
<% else %>
<%= t('offers.price_in_currency', price: @invoice.total) %>
<% end %>
</td>
</tr>
</tfoot>
</table>
Expand Down
20 changes: 18 additions & 2 deletions app/controllers/admin/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
module Admin
class InvoicesController < BaseController
before_action :authorize_user
before_action :create_invoice_if_needed
before_action :set_invoice, only: %i[show download update edit]
before_action :create_invoice_if_needed, except: :toggle_partial_payments
before_action :set_invoice, only: %i[show download update edit toggle_partial_payments]
before_action :authorize_for_update, only: %i[edit update]

# GET /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b
Expand Down Expand Up @@ -67,6 +67,22 @@ def update
end
end

# POST /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b/toggle_partial_payments
def toggle_partial_payments
respond_to do |format|
if @invoice.toggle(:partial_payments).save
format.html do
action = @invoice.partial_payments? ? 'activated' : 'deactivated'
redirect_to admin_invoice_path(@invoice), notice: t("invoices.partial_payments_#{action}")
end
format.json { render :show, status: :ok, location: @invoice }
else
format.html { redirect_to admin_invoice_path(@invoice), notice: t(:something_went_wrong) }
format.json { render json: @invoice.errors, status: :unprocessable_entity }
end
end
end

private

def set_invoice
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/eis_billing/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def state_machine
when 'unpaid'
@invoice.update(status: 'issued', paid_at: nil)
when 'paid'
return if @invoice.paid?

@invoice.payable? ? @invoice.mark_as_paid_at(Time.zone.now) : @invoice.errors.add(:base, 'Invoice is not payable')
when 'cancelled'
@invoice.update(status: 'cancelled', paid_at: nil)
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/eis_billing/payment_status_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ def pay_mulitply(data)
end

def payment_process(invoice:)
payment_order = PaymentOrder.find_by(invoice_id: invoice.id) ||
existing_po = invoice.partial_payments? ? nil : PaymentOrder.find_by(invoice_id: invoice.id)
payment_order = existing_po ||
PaymentOrders::EveryPay.create(invoices: [invoice], user: invoice.user)

payment_order.response = params
Expand Down
26 changes: 18 additions & 8 deletions app/controllers/invoices_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
class InvoicesController < ApplicationController
before_action :authenticate_user!
before_action :authorize_user
before_action :set_invoice, except: %i[index pay_all_bills oneoff pay_deposit]
before_action :set_invoice, except: %i[index pay_all_bills pay_deposit]
before_action :validate_amount, only: :oneoff

# GET /invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b/edit
def edit; end
Expand Down Expand Up @@ -97,9 +98,10 @@ def pay_deposit
end

def oneoff
invoice = Invoice.accessible_by(current_ability).find_by!(uuid: params[:uuid])
response = EisBilling::OneoffService.call(invoice_number: invoice.number.to_s,
customer_url: linkpay_callback_url)
response = EisBilling::OneoffService.call(invoice_number: @invoice.number.to_s,
customer_url: linkpay_callback_url,
amount: params[:amount])


if response.result?
redirect_to response.instance['oneoff_redirect_link'], allow_other_host: true, format: :html
Expand All @@ -110,13 +112,12 @@ def oneoff
end

def send_e_invoice
invoice = Invoice.accessible_by(current_ability).find_by!(uuid: params[:uuid])
response = EisBilling::SendEInvoice.call(invoice:, payable: !invoice.paid?)
response = EisBilling::SendEInvoice.call(invoice: @invoice, payable: [email protected]?)

if response.result?
redirect_to invoice_path(invoice.uuid), notice: t('.sent_to_omniva')
redirect_to invoice_path(@invoice.uuid), notice: t('.sent_to_omniva')
else
redirect_to invoice_path(invoice.uuid), alert: response.errors
redirect_to invoice_path(@invoice.uuid), alert: response.errors
end
end

Expand Down Expand Up @@ -146,4 +147,13 @@ def authorize_user
authorize! :read, Invoice
authorize! :update, Invoice
end

def validate_amount
return if params[:amount].nil?

alert = I18n.t('invoices.amount_must_be_positive') if params[:amount].to_f <= 0
alert = I18n.t('invoices.amount_is_too_big') if params[:amount].to_f > @invoice.due_amount.to_f

redirect_to invoice_path(@invoice.uuid), alert: alert if alert
end
end
3 changes: 2 additions & 1 deletion app/models/bank_transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def autobind_invoice

payment_order = PaymentOrder.find_by(invoice_id: invoice.id) ||
PaymentOrders::EveryPay.create(invoices: [invoice], user: invoice.user)
payment_order.response = { 'transaction_time' => Time.zone.now, 'payment_state' => 'settled' }
payment_order.response = { 'transaction_time' => Time.zone.now, 'payment_state' => 'settled',
'initial_amount' => invoice.total.to_f }

payment_order.save
payment_order.mark_invoice_as_paid
Expand Down
61 changes: 37 additions & 24 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,6 @@ def items=(items)
self.invoice_items = items
end

def user_id_must_be_the_same_as_on_billing_profile_or_nil
return unless billing_profile
return unless user

return if billing_profile.user_id == user_id

errors.add(:billing_profile, I18n.t('invoices.billing_profile_must_belong_to_user'))
end

def auction_currency
Rails.cache.fetch(cache_key_with_version, expires_in: 12.hours) do
Setting.find_by(code: 'auction_currency').retrieve
Expand All @@ -185,11 +176,14 @@ def price
end

def total
if result.auction.enable_deposit?
(price * (1 + vat_rate) - deposit).round(2)
else
(price * (1 + vat_rate)).round(2)
end
total_amount = price * (1 + vat_rate)
total_amount -= deposit if result.auction.enable_deposit?

total_amount.round(2)
end

def due_amount
total - Money.from_amount(paid_amount || 0, auction_currency)
end

def vat
Expand Down Expand Up @@ -224,24 +218,36 @@ def billing_vat_rate
# paid in the user interface.
def mark_as_paid_at(time)
ActiveRecord::Base.transaction do
prepare_payment_fields(time)
self.vat_rate = billing_profile.present? ? billing_profile.vat_rate : vat_rate
self.paid_amount = total
self.paid_at = time

result.mark_as_payment_received(time) unless cancelled?
clear_linked_ban
paid!
end

ResultStatusUpdateJob.perform_now
end

def mark_as_paid_at_with_payment_order(time, payment_order)
ActiveRecord::Base.transaction do
prepare_payment_fields(time)
self.vat_rate = billing_profile.present? ? billing_profile.vat_rate : vat_rate
initial_amount = payment_order.response['initial_amount'].to_f
self.paid_amount += initial_amount.to_f

self.paid_with_payment_order = payment_order

result.mark_as_payment_received(time) unless cancelled?
clear_linked_ban
paid!

if finally_paid?
self.paid_at = time
result.mark_as_payment_received(time) unless cancelled?
clear_linked_ban
paid!
else
save!
end
end

ResultStatusUpdateJob.perform_now
end

Expand Down Expand Up @@ -291,15 +297,22 @@ def self.numeric?(string)

private

def user_id_must_be_the_same_as_on_billing_profile_or_nil
return unless billing_profile
return unless user

return if billing_profile.user_id == user_id

errors.add(:billing_profile, I18n.t('invoices.billing_profile_must_belong_to_user'))
end

def clear_linked_ban
ban = Ban.find_by(invoice_id: id)
ban.lift if ban.present?
end

def prepare_payment_fields(time)
self.paid_at = time
self.vat_rate = billing_profile.present? ? billing_profile.vat_rate : vat_rate
self.paid_amount = total
def finally_paid?
due_amount <= 0
end
end
# rubocop:enable Metrics/ClassLength
6 changes: 1 addition & 5 deletions app/models/payment_orders/every_pay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,11 @@ def mark_invoice_as_paid

Invoice.transaction do
invoices.each do |invoice|
process_payment(invoice, time)
invoice.mark_as_paid_at_with_payment_order(time, self)
end
end
end

def process_payment(invoice, time)
invoice.mark_as_paid_at_with_payment_order(time, self)
end

# Check if the intermediary reports payment as settled and we can expect money on
# our accounts
def settled_payment?
Expand Down
16 changes: 10 additions & 6 deletions app/services/eis_billing/oneoff_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ class OneoffService
include EisBilling::Request
include EisBilling::BaseService

attr_reader :invoice_number, :customer_url
attr_reader :invoice_number, :customer_url, :amount

def initialize(invoice_number:, customer_url:)
def initialize(invoice_number:, customer_url:, amount: nil)
@invoice_number = invoice_number
@customer_url = customer_url
@amount = amount
end

def self.call(invoice_number:, customer_url:)
new(invoice_number: invoice_number, customer_url: customer_url).call
def self.call(invoice_number:, customer_url:, amount: nil)
new(invoice_number: invoice_number, customer_url: customer_url, amount: amount).call
end

def call
Expand All @@ -25,8 +26,11 @@ def fetch
end

def params
{ invoice_number: invoice_number,
customer_url: customer_url }
{
invoice_number: invoice_number,
customer_url: customer_url,
amount: amount
}
end

def invoice_oneoff_url
Expand Down
4 changes: 2 additions & 2 deletions app/views/admin/invoices/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
<td><%= invoice.notes %></td>
<td><%= t('offers.price_in_currency', price: invoice.total) %></td>
<td><%= number_to_percentage(invoice.vat_rate * 100, precision: 0) if invoice.paid? %></td>
<td><%= t('offers.price_in_currency', price: invoice.paid_amount) if invoice.paid? %></td>
<td><%= invoice.paid_with_payment_order&.channel if invoice.paid? %></td>
<td><%= t('offers.price_in_currency', price: invoice.paid_amount) if invoice.paid? || invoice.partial_payments? %></td>
<td><%= invoice.paid_with_payment_order&.channel if invoice.paid? || invoice.partial_payments? %></td>
</tr>
<% end %>
<% end %>
Expand Down
Loading
Loading