Skip to content

Commit

Permalink
feat(manual-payments): Add service to create manual payments (getlago…
Browse files Browse the repository at this point in the history
…#3029)

## Roadmap Task

👉  https://getlago.canny.io/feature-requests/p/log-partial-payments

## Description

This PR adds `ManualPayments::CreateService`
  • Loading branch information
ivannovosad authored Jan 8, 2025
1 parent 3e3088d commit 41a994c
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 2 deletions.
2 changes: 1 addition & 1 deletion app/models/analytics/overdue_balance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def query(organization_id, **args)
SELECT
DATE_TRUNC('month', payment_due_date) AS month,
i.currency,
COALESCE(SUM(total_amount_cents), 0) AS total_amount_cents,
COALESCE(SUM(total_amount_cents - total_paid_amount_cents), 0) AS total_amount_cents,
array_agg(DISTINCT i.id) AS ids
FROM invoices i
LEFT JOIN customers c ON i.customer_id = c.id
Expand Down
20 changes: 20 additions & 0 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,31 @@ class Payment < ApplicationRecord

enum payable_payment_status: PAYABLE_PAYMENT_STATUS.map { |s| [s, s] }.to_h

validate :max_invoice_paid_amount_cents, on: :create
validate :payment_request_succeeded, on: :create

def should_sync_payment?
return false unless payable.is_a?(Invoice)

payable.finalized? && customer.integration_customers.accounting_kind.any? { |c| c.integration.sync_payments }
end

private

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

errors.add(:amount_cents, :greater_than)
end

def payment_request_succeeded
return if !payable.is_a?(Invoice) || payment_type_provider?

if payable.payment_requests.where(payment_status: 'succeeded').exists?
errors.add(:base, :payment_request_is_already_succeeded)
end
end
end

# == Schema Information
Expand Down
54 changes: 54 additions & 0 deletions app/services/manual_payments/create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module ManualPayments
class CreateService < BaseService
def initialize(invoice:, params:)
@invoice = invoice
@params = params

super
end

def call
check_preconditions
return result if result.error

amount_cents = params[:amount_cents]

ActiveRecord::Base.transaction do
payment = invoice.payments.create!(
amount_cents:,
reference: params[:reference],
amount_currency: invoice.currency,
status: 'succeeded',
payable_payment_status: 'succeeded',
payment_type: :manual
)

invoice.update!(total_paid_amount_cents: invoice.total_paid_amount_cents + amount_cents)

result.payment = payment

if invoice.payments.where(payable_payment_status: 'succeeded').sum(:amount_cents) == invoice.total_amount_cents
payment.payable.update!(payment_status: 'succeeded')
end

Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if result.payment&.should_sync_payment?
end

result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

private

attr_reader :invoice, :params

def check_preconditions
return result.forbidden_failure! unless License.premium?
return result.not_found_failure!(resource: "invoice") unless invoice
result.forbidden_failure! unless invoice.organization.premium_integrations.include?('manual_payments')
end
end
end
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ en:
not_compatible_with_aggregation_type: not_compatible_with_aggregation_type
not_compatible_with_pay_in_advance: not_compatible_with_pay_in_advance
only_compatible_with_pay_in_advance_and_non_invoiceable: only_compatible_with_pay_in_advance_and_non_invoiceable
payment_request_is_already_succeeded: payment_request_is_already_succeeded
required: relation_must_exist
taken: value_already_exist
too_long: value_is_too_long
Expand Down
128 changes: 127 additions & 1 deletion spec/models/payment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
require 'rails_helper'

RSpec.describe Payment, type: :model do
subject(:payment) { build(:payment, payment_type:, provider_payment_id:, reference:) }
subject(:payment) { build(:payment, payable:, payment_type:, provider_payment_id:, reference:, amount_cents:) }

let(:payable) { create(:invoice, total_amount_cents: 10000) }
let(:payment_type) { 'provider' }
let(:provider_payment_id) { SecureRandom.uuid }
let(:reference) { nil }
let(:amount_cents) { 200 }

it_behaves_like 'paper_trail traceable'

Expand All @@ -29,6 +31,130 @@

before { payment.valid? }

describe 'of max invoice paid amount cents' do
before { payment.save }

context 'when payable is an invoice' do
context 'when payment type is provider' do
let(:payment_type) { 'provider' }

context 'when amount cents + total paid amount cents is smaller or equal than invoice total amount cents' do
let(:payment_request) { create(:payment_request, payment_status: :succeeded) }

it 'does not add an error' do
expect(errors.where(:amount_cents, :greater_than)).not_to be_present
end
end

context 'when amount cents + total paid amount cents is greater than invoice total amount cents' do
let(:amount_cents) { 10001 }

it 'does not add an error' do
expect(errors.where(:amount_cents, :greater_than)).not_to be_present
end
end
end

context 'when payment type is manual' do
let(:payment_type) { 'manual' }

context 'when amount cents + total paid amount cents is smaller or equal than invoice total amount cents' do
let(:payment_request) { create(:payment_request, payment_status: :succeeded) }

it 'does not add an error' do
expect(errors.where(:amount_cents, :greater_than)).not_to be_present
end
end

context 'when amount cents + total paid amount cents is greater than invoice total amount cents' do
let(:amount_cents) { 10001 }

it 'adds an error' do
expect(errors.where(:amount_cents, :greater_than)).to be_present
end
end
end
end
end

describe 'of payment request succeeded' do
context 'when payable is an invoice' do
context 'when payment type is provider' do
let(:payment_type) { 'provider' }

context 'when succeeded payment requests exist' do
let(:payment_request) { create(:payment_request, payment_status: :succeeded) }

before do
create(:payment_request_applied_invoice, payment_request:, invoice: payable)
payment.save
end

it 'does not add an error' do
expect(errors.where(:base, :payment_request_is_already_succeeded)).not_to be_present
end
end

context 'when no succeeded payment requests exist' do
before { payment.save }

it 'does not add an error' do
expect(errors.where(:base, :payment_request_is_already_succeeded)).not_to be_present
end
end
end

context 'when payment type is manual' do
let(:payment_type) { 'manual' }

context 'when succeeded payment request exist' do
let(:payment_request) { create(:payment_request, payment_status: 'succeeded') }

before do
create(:payment_request_applied_invoice, payment_request:, invoice: payable)
payment.save
end

it 'adds an error' do
expect(payment.errors.where(:base, :payment_request_is_already_succeeded)).to be_present
end
end

context 'when no succeeded payment requests exist' do
before { payment.save }

it 'does not add an error' do
expect(errors.where(:base, :payment_request_is_already_succeeded)).not_to be_present
end
end
end
end

context 'when payable is not an invoice' do
let(:payable) { create(:payment_request) }

context 'when payment type is provider' do
let(:payment_type) { 'provider' }

before { payment.save }

it 'does not add an error' do
expect(errors.where(:base, :payment_request_is_already_succeeded)).not_to be_present
end
end

context 'when payment type is manual' do
let(:payment_type) { 'manual' }

before { payment.save }

it 'does not add an error' do
expect(errors.where(:base, :payment_request_is_already_succeeded)).not_to be_present
end
end
end
end

describe 'of reference' do
context 'when payment type is provider' do
context 'when reference is present' do
Expand Down
Loading

0 comments on commit 41a994c

Please sign in to comment.