Skip to content

Commit

Permalink
feat(manual-payments): Add create manual payment service
Browse files Browse the repository at this point in the history
  • Loading branch information
ivannovosad committed Jan 7, 2025
1 parent a65e674 commit 0262dcf
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 1 deletion.
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
152 changes: 152 additions & 0 deletions spec/services/manual_payments/create_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe ManualPayments::CreateService, type: :service do
subject(:service) { described_class.new(invoice:, params:) }

let(:invoice) { create(:invoice, customer:, organization:, total_amount_cents: 10000, status: :finalized) }
let(:organization) { create(:organization, premium_integrations:) }
let(:customer) { create(:customer, organization:) }
let(:params) { {amount_cents:, reference: "ref1"} }
let(:amount_cents) { 10000 }

describe "#call" do
context "when organization is not premium" do
let(:premium_integrations) { %w[] }

it "returns forbidden failure" do
result = service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ForbiddenFailure)
end
end
end

context "when organization is premium" do
around { |test| lago_premium!(&test) }

context "with the premium_integration disabled" do
let(:premium_integrations) { %w[] }

it "returns forbidden failure" do
result = service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ForbiddenFailure)
end
end
end

context "with the premium_integration enabled" do
let(:premium_integrations) { %w[manual_payments] }

context "when invoice does not exist" do
let(:invoice) { nil }

it "returns not found failure" do
result = service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::NotFoundFailure)
end
end
end

context "when invoice's payment request is succeeded" do
let(:payment_request) { create(:payment_request, payment_status: "succeeded") }

before do
create(:payment_request_applied_invoice, invoice:, payment_request:)
end

it "returns validation failure" do
result = service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
end
end
end

context "when payment amount cents is greater than invoice's remaining amount cents" do
let(:amount_cents) { 10001 }

it "returns validation failure" do
result = service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
end
end
end

context "when payment amount cents is smaller than invoice remaining amount cents" do
let(:amount_cents) { 2000 }

it "creates a payment" do
result = service.call

expect(result).to be_success
expect(result.payment.payment_type).to eq("manual")
end

it "updates invoice's total paid amount cents" do
expect { service.call }.to change(invoice, :total_paid_amount_cents).from(0).to(amount_cents)
end

context "when there is an integration customer" do
let(:integration) do
create(
:netsuite_integration,
organization:,
settings: {
account_id: "acc_12345",
client_id: "cli_12345",
script_endpoint_url: Faker::Internet.url,
sync_payments: true
}
)
end

before { create(:netsuite_customer, integration:, customer:) }

it "enqueues an aggregator payment job" do
expect { service.call }.to have_enqueued_job(Integrations::Aggregator::Payments::CreateJob)
end
end
end

context "when payment amount cents is equal to invoice remaining amount cents" do
let(:amount_cents) { 10000 }

it "creates a payment" do
result = service.call

expect(result).to be_success
expect(result.payment.payment_type).to eq("manual")
end

it "updates invoice's total paid amount cents" do
expect { service.call }.to change(invoice, :total_paid_amount_cents).from(0).to(amount_cents)
end

it "updates invoice's payment status to suceeded" do
result = service.call

aggregate_failures do
expect(result.payment.payable.payment_status).to eq("succeeded")
expect(result.payment.payable_payment_status).to eq("succeeded")
end
end
end
end
end
end
end

0 comments on commit 0262dcf

Please sign in to comment.