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

feat(manual-payments): Add services #3085

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion app/controllers/api/v1/payments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Api
module V1
class PaymentsController < Api::BaseController
def create
result = ManualPayments::CreateService.call(
result = Payments::ManualCreateService.call(
organization: current_organization,
params: create_params.to_h.deep_symbolize_keys
)
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/invoices/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Object < Types::BaseObject
field :sub_total_including_taxes_amount_cents, GraphQL::Types::BigInt, null: false
field :taxes_amount_cents, GraphQL::Types::BigInt, null: false
field :total_amount_cents, GraphQL::Types::BigInt, null: false
field :total_due_amount_cents, GraphQL::Types::BigInt, null: false

field :issuing_date, GraphQL::Types::ISO8601Date, null: false
field :payment_due_date, GraphQL::Types::ISO8601Date, null: false
Expand Down
11 changes: 11 additions & 0 deletions app/jobs/payments/manual_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Payments
class ManualCreateJob < ApplicationJob
queue_as "low_priority"

def perform(organization:, params:)
Payments::ManualCreateService.call!(organization:, params:)
end
end
end
14 changes: 13 additions & 1 deletion app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ class Invoice < ApplicationRecord
:sub_total_excluding_taxes_amount_cents,
:sub_total_including_taxes_amount_cents,
:total_amount_cents,
:total_paid_amount_cents,
:taxes_amount_cents,
with_model_currency: :currency

# NOTE: Readonly fields
monetize :charge_amount_cents,
:subscription_amount_cents,
:total_due_amount_cents,
disable_validation: true,
allow_nil: true,
with_model_currency: :currency
Expand Down Expand Up @@ -257,6 +259,10 @@ def charge_pay_in_advance_proration_range(fee, timestamp)
}
end

def total_due_amount_cents
total_amount_cents - total_paid_amount_cents
end

# amount cents onto which we can issue a credit note
def available_to_credit_amount_cents
return 0 if version_number < CREDIT_NOTES_MIN_VERSION || draft?
Expand Down Expand Up @@ -292,7 +298,13 @@ def creditable_amount_cents
def refundable_amount_cents
return 0 if version_number < CREDIT_NOTES_MIN_VERSION || draft? || !payment_succeeded?

amount = available_to_credit_amount_cents -
# base_amount = if credit?
# available_to_credit_amount_cents
# else
# total_paid_amount_cents - credit_notes.sum("refund_amount_cents + credit_amount_cents")
# end

amount = total_paid_amount_cents - credit_notes.sum("refund_amount_cents + credit_amount_cents") -
credits.where(before_taxes: false).sum(:amount_cents) -
prepaid_credit_amount_cents
amount = amount.negative? ? 0 : amount
Expand Down
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Organization < ApplicationRecord
api_permissions
revenue_share
zero_amount_fees
manual_payments
].freeze
PREMIUM_INTEGRATIONS = INTEGRATIONS - %w[anrok]

Expand Down
10 changes: 9 additions & 1 deletion app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Payment < ApplicationRecord
validates :payment_type, presence: true
validates :reference, presence: true, length: {maximum: 40}, if: -> { payment_type_manual? }
validates :reference, absence: true, if: -> { payment_type_provider? }
validate :manual_payment_credit_invoice_amount_cents
validate :max_invoice_paid_amount_cents, on: :create
validate :payment_request_succeeded, on: :create

Expand Down Expand Up @@ -52,6 +53,13 @@ def should_sync_payment?

private

def manual_payment_credit_invoice_amount_cents
return if !payable.is_a?(Invoice) || payment_type_provider? || !payable.credit?
return if amount_cents == payable.total_amount_cents

errors.add(:amount_cents, :invalid_amount)
end

def max_invoice_paid_amount_cents
return if !payable.is_a?(Invoice) || payment_type_provider?
return if amount_cents + payable.total_paid_amount_cents <= payable.total_amount_cents
Expand Down Expand Up @@ -92,7 +100,7 @@ def payment_request_succeeded
# Indexes
#
# index_payments_on_invoice_id (invoice_id)
# index_payments_on_payable_id_and_payable_type (payable_id,payable_type) UNIQUE WHERE (payable_payment_status = ANY (ARRAY['pending'::payment_payable_payment_status, 'processing'::payment_payable_payment_status]))
# index_payments_on_payable_id_and_payable_type (payable_id,payable_type) UNIQUE WHERE ((payable_payment_status = ANY (ARRAY['pending'::payment_payable_payment_status, 'processing'::payment_payable_payment_status])) AND (payment_type = 'provider'::payment_type))
# index_payments_on_payable_type_and_payable_id (payable_type,payable_id)
# index_payments_on_payment_provider_customer_id (payment_provider_customer_id)
# index_payments_on_payment_provider_id (payment_provider_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def create_body
'lago_invoice_payment_status' => invoice.payment_status,
'lago_invoice_currency' => invoice.currency,
'lago_invoice_total_amount' => total_amount,
'lago_invoice_total_due_amount' => total_due_amount,
'lago_invoice_subtotal_excluding_taxes' => subtotal_excluding_taxes,
'lago_invoice_file_url' => invoice.file_url
}
Expand Down Expand Up @@ -52,6 +53,7 @@ def update_body
'lago_invoice_payment_status' => invoice.payment_status,
'lago_invoice_currency' => invoice.currency,
'lago_invoice_total_amount' => total_amount,
'lago_invoice_total_due_amount' => total_due_amount,
'lago_invoice_subtotal_excluding_taxes' => subtotal_excluding_taxes,
'lago_invoice_file_url' => invoice.file_url
}
Expand All @@ -75,6 +77,10 @@ def total_amount
amount(invoice.total_amount_cents, resource: invoice)
end

def total_due_amount
amount(invoice.total_due_amount_cents, resource: invoice)
end

def subtotal_excluding_taxes
amount(invoice.sub_total_excluding_taxes_amount_cents, resource: invoice)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Integrations
module Hubspot
module Invoices
class DeployPropertiesService < Integrations::Aggregator::BaseService
VERSION = 1
VERSION = 2

def action_path
"v1/hubspot/properties"
Expand Down Expand Up @@ -46,10 +46,10 @@ def payload
inputs: [
{
groupName: "lagoinvoices_information",
name: "example",
label: "example label",
type: "string",
fieldType: "text"
name: "lago_invoice_total_due_amount",
label: "Lago Invoice Total Due Amount",
type: "number",
fieldType: "number"
}
]
}
Expand Down
11 changes: 11 additions & 0 deletions app/services/invoices/advance_charges_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def call

if invoice && !invoice.closed?
SendWebhookJob.perform_later('invoice.created', invoice)
create_manual_payment(invoice)
Invoices::GeneratePdfAndNotifyJob.perform_later(invoice:, email: false)
Integrations::Aggregator::Invoices::CreateJob.perform_later(invoice:) if invoice.should_sync_invoice?
Integrations::Aggregator::Invoices::Hubspot::CreateJob.perform_later(invoice:) if invoice.should_sync_hubspot_invoice?
Expand Down Expand Up @@ -51,6 +52,16 @@ def has_charges_with_statement?
Charge.where(plan_id: plan_ids, pay_in_advance: true, invoiceable: false, regroup_paid_fees: :invoice).any?
end

def create_manual_payment(invoice)
amount_cents = invoice.total_amount_cents
reference = I18n.t("invoice.charges_paid_in_advance")
created_at = invoice.created_at

params = {invoice_id: invoice.id, amount_cents:, reference:, created_at:}

::Payments::ManualCreateJob.perform_later(organization:, params:)
end

def create_group_invoice
invoice = nil

Expand Down
17 changes: 12 additions & 5 deletions app/services/invoices/payments/adyen_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def create_payment(provider_payment_id:, metadata:)
payable: invoice,
payment_provider_id: adyen_payment_provider.id,
payment_provider_customer_id: customer.adyen_customer.id,
amount_cents: invoice.total_amount_cents,
amount_cents: invoice.total_due_amount_cents,
amount_currency: invoice.currency.upcase,
provider_payment_id:
)
Expand Down Expand Up @@ -177,12 +177,19 @@ def payment_url_params
end

def update_invoice_payment_status(payment_status:, deliver_webhook: true)
params = {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
}

if payment_status.to_sym == :succeeded
total_paid_amount_cents = invoice.payments.where(payable_payment_status: :succeeded).sum(:amount_cents)
params[:total_paid_amount_cents] = total_paid_amount_cents
end

result = Invoices::UpdateService.call(
invoice:,
params: {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
},
params:,
webhook_notification: deliver_webhook
)
result.raise_if_error!
Expand Down
18 changes: 13 additions & 5 deletions app/services/invoices/payments/cashfree_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def create_payment(cashfree_payment)
payable: @invoice,
payment_provider_id: cashfree_payment_provider.id,
payment_provider_customer_id: customer.cashfree_customer.id,
amount_cents: @invoice.total_amount_cents,
amount_cents: @invoice.total_due_amount_cents,
amount_currency: @invoice.currency,
provider_payment_id: cashfree_payment.id
)
Expand Down Expand Up @@ -138,12 +138,20 @@ def payment_url_params

def update_invoice_payment_status(payment_status:, deliver_webhook: true)
@invoice = result.invoice

params = {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
}

if payment_status.to_sym == :succeeded
total_paid_amount_cents = invoice.payments.where(payable_payment_status: :succeeded).sum(:amount_cents)
params[:total_paid_amount_cents] = total_paid_amount_cents
end

result = Invoices::UpdateService.call(
invoice:,
params: {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
},
params:,
webhook_notification: deliver_webhook
)
result.raise_if_error!
Expand Down
21 changes: 14 additions & 7 deletions app/services/invoices/payments/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def call
payment ||= Payment.create_with(
payment_provider_id: current_payment_provider.id,
payment_provider_customer_id: current_payment_provider_customer.id,
amount_cents: invoice.total_amount_cents,
amount_cents: invoice.total_due_amount_cents,
amount_currency: invoice.currency,
status: "pending"
).find_or_create_by!(
Expand Down Expand Up @@ -114,13 +114,20 @@ def current_payment_provider_customer
end

def update_invoice_payment_status(payment_status:)
params = {
# NOTE: A proper `processing` payment status should be introduced for invoices
payment_status: (payment_status.to_s == "processing") ? :pending : payment_status,
ready_for_payment_processing: %w[pending failed].include?(payment_status.to_s)
}

if payment_status.to_s == "succeeded"
total_paid_amount_cents = invoice.payments.where(payable_payment_status: :succeeded).sum(:amount_cents)
params[:total_paid_amount_cents] = total_paid_amount_cents
end

Invoices::UpdateService.call!(
invoice: invoice,
params: {
# NOTE: A proper `processing` payment status should be introduced for invoices
payment_status: (payment_status.to_s == "processing") ? :pending : payment_status,
ready_for_payment_processing: %w[pending failed].include?(payment_status.to_s)
},
invoice:,
params:,
webhook_notification: payment_status.to_sym == :succeeded
)
end
Expand Down
15 changes: 11 additions & 4 deletions app/services/invoices/payments/gocardless_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ def update_payment_status(provider_payment_id:, status:)
delegate :organization, :customer, to: :invoice

def update_invoice_payment_status(payment_status:, deliver_webhook: true)
params = {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
}

if payment_status.to_sym == :succeeded
total_paid_amount_cents = result.invoice.payments.where(payable_payment_status: :succeeded).sum(:amount_cents)
params[:total_paid_amount_cents] = total_paid_amount_cents
end

update_invoice_result = Invoices::UpdateService.call(
invoice: result.invoice,
params: {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
},
params:,
webhook_notification: deliver_webhook
)
update_invoice_result.raise_if_error!
Expand Down
19 changes: 13 additions & 6 deletions app/services/invoices/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def create_payment(stripe_payment, invoice: nil)
payable: @invoice,
payment_provider_id: stripe_payment_provider.id,
payment_provider_customer_id: customer.stripe_customer.id,
amount_cents: @invoice.total_amount_cents,
amount_cents: @invoice.total_due_amount_cents,
amount_currency: @invoice.currency,
status: "pending"
)
Expand Down Expand Up @@ -153,13 +153,20 @@ def description
end

def update_invoice_payment_status(payment_status:, deliver_webhook: true, processing: false)
params = {
payment_status:,
# NOTE: A proper `processing` payment status should be introduced for invoices
ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded
}

if payment_status.to_sym == :succeeded
total_paid_amount_cents = (invoice.presence || @result.invoice).payments.where(payable_payment_status: :succeeded).sum(:amount_cents)
params[:total_paid_amount_cents] = total_paid_amount_cents
end

result = Invoices::UpdateService.call(
invoice: invoice.presence || @result.invoice,
params: {
payment_status:,
# NOTE: A proper `processing` payment status should be introduced for invoices
ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded
},
params:,
webhook_notification: deliver_webhook
)
result.raise_if_error!
Expand Down
4 changes: 4 additions & 0 deletions app/services/invoices/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def call
invoice.ready_for_payment_processing = params[:ready_for_payment_processing]
end

if params.key?(:total_paid_amount_cents) && params[:total_paid_amount_cents].present?
invoice.total_paid_amount_cents = params[:total_paid_amount_cents]
end

ActiveRecord::Base.transaction do
if invoice.payment_overdue? && invoice.payment_succeeded?
invoice.payment_overdue = false
Expand Down
2 changes: 1 addition & 1 deletion app/services/payment_requests/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def call
customer.payment_requests.create!(
organization:,
dunning_campaign:,
amount_cents: invoices.sum(:total_amount_cents),
amount_cents: invoices.sum('total_amount_cents - total_paid_amount_cents'),
amount_currency: currency,
email:,
invoices:
Expand Down
2 changes: 2 additions & 0 deletions app/services/payment_requests/payments/adyen_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Payments
class AdyenService < BaseService
include Lago::Adyen::ErrorHandlable
include Customers::PaymentProviderFinder
include Updatable

def initialize(payable = nil)
@payable = payable
Expand Down Expand Up @@ -51,6 +52,7 @@ def update_payment_status(provider_payment_id:, status:, metadata: {})

update_payable_payment_status(payment_status: payable_payment_status)
update_invoices_payment_status(payment_status: payable_payment_status)
update_invoices_paid_amount_cents(payment_status: payable_payment_status)
reset_customer_dunning_campaign_status(payable_payment_status)

PaymentRequestMailer.with(payment_request: payment.payable).requested.deliver_later if result.payable.payment_failed?
Expand Down
2 changes: 2 additions & 0 deletions app/services/payment_requests/payments/cashfree_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module PaymentRequests
module Payments
class CashfreeService < BaseService
include Customers::PaymentProviderFinder
include Updatable

PENDING_STATUSES = %w[PARTIALLY_PAID].freeze
SUCCESS_STATUSES = %w[PAID].freeze
Expand Down Expand Up @@ -47,6 +48,7 @@ def update_payment_status(organization_id:, status:, cashfree_payment:)

update_payable_payment_status(payment_status: payable_payment_status)
update_invoices_payment_status(payment_status: payable_payment_status)
update_invoices_paid_amount_cents(payment_status: payable_payment_status)
reset_customer_dunning_campaign_status(payable_payment_status)

PaymentRequestMailer.with(payment_request: payment.payable).requested.deliver_later if result.payable.payment_failed?
Expand Down
Loading
Loading