-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat manual payments controller (#3045)
## Roadmap Task 👉 https://getlago.canny.io/feature-requests/p/log-partial-payments ## Context This PR introduces a new endpoint to list all payments. Additionally, it adds the ability to filter payments by a specific invoice_id, allowing users to narrow down the results to payments associated with a particular invoice. This enhancement improves the flexibility and usability of the payments listing functionality. It also adds `create` and `show` actions in the PaymentsController. ## Description **Added Endpoint:** - Created an endpoint to list, show and create payments. - Filter by invoice_id: - Added functionality to filter payments based on the provided invoice_id. - Included a validation step to ensure the invoice_id is in the correct UUID format before executing the query. **Query Updates:** - Refactored PaymentsQuery to handle dynamic filtering by invoice_id. - Ensured that the query properly joins related tables, such as invoices and payment_requests, to support filtering. --------- Co-authored-by: brunomiguelpinto <[email protected]>
- Loading branch information
1 parent
36315a9
commit b28b40d
Showing
17 changed files
with
682 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# frozen_string_literal: true | ||
|
||
module Queries | ||
class PaymentsQueryFiltersContract < Dry::Validation::Contract | ||
UUID_REGEX = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/ | ||
|
||
params do | ||
required(:filters).hash do | ||
optional(:invoice_id).maybe(:string, format?: UUID_REGEX) | ||
optional(:external_customer_id).maybe(:string) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# frozen_string_literal: true | ||
|
||
module Api | ||
module V1 | ||
class PaymentsController < Api::BaseController | ||
def create | ||
result = ManualPayments::CreateService.call( | ||
organization: current_organization, | ||
params: create_params.to_h.deep_symbolize_keys | ||
) | ||
|
||
if result.success? | ||
render( | ||
json: ::V1::PaymentSerializer.new(result.payment, root_name: resource_name) | ||
) | ||
else | ||
render_error_response(result) | ||
end | ||
end | ||
|
||
def index | ||
result = PaymentsQuery.call( | ||
organization: current_organization, | ||
pagination: { | ||
page: params[:page], | ||
limit: params[:per_page] || PER_PAGE | ||
}, | ||
filters: index_filters | ||
) | ||
|
||
if result.success? | ||
render( | ||
json: ::CollectionSerializer.new( | ||
result.payments, | ||
::V1::PaymentSerializer, | ||
collection_name: resource_name.pluralize, | ||
meta: pagination_metadata(result.payments) | ||
) | ||
) | ||
else | ||
render_error_response(result) | ||
end | ||
end | ||
|
||
def show | ||
payment = Payment.for_organization(current_organization).find_by(id: params[:id]) | ||
return not_found_error(resource: resource_name) unless payment | ||
|
||
render_payment(payment) | ||
end | ||
|
||
private | ||
|
||
def create_params | ||
params.require(:payment).permit( | ||
:invoice_id, | ||
:amount_cents, | ||
:reference, | ||
:paid_at | ||
) | ||
end | ||
|
||
def index_filters | ||
params.permit(:invoice_id, :external_customer_id) | ||
end | ||
|
||
def render_payment(payment) | ||
render( | ||
json: ::V1::PaymentSerializer.new( | ||
payment, | ||
root_name: resource_name | ||
) | ||
) | ||
end | ||
|
||
def resource_name | ||
'payment' | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# frozen_string_literal: true | ||
|
||
class PaymentsQuery < BaseQuery | ||
def call | ||
return result unless validate_filters.success? | ||
|
||
payments = apply_filters(base_scope) | ||
payments = paginate(payments) | ||
payments = apply_consistent_ordering(payments) | ||
|
||
result.payments = payments | ||
result | ||
end | ||
|
||
private | ||
|
||
def filters_contract | ||
@filters_contract ||= Queries::PaymentsQueryFiltersContract.new | ||
end | ||
|
||
def base_scope | ||
Payment.for_organization(organization) | ||
end | ||
|
||
def apply_filters(scope) | ||
scope = filter_by_invoice(scope) if filters.invoice_id.present? | ||
scope = filter_by_customer(scope) if filters.external_customer_id.present? | ||
scope | ||
end | ||
|
||
def filter_by_customer(scope) | ||
external_customer_id = filters.external_customer_id | ||
|
||
scope.joins(<<~SQL) | ||
LEFT JOIN customers ON | ||
(payments.payable_type = 'Invoice' AND customers.id = invoices.customer_id) OR | ||
(payments.payable_type = 'PaymentRequest' AND customers.id = payment_requests.customer_id) | ||
SQL | ||
.where('customers.external_id = :external_customer_id', external_customer_id:) | ||
end | ||
|
||
def filter_by_invoice(scope) | ||
invoice_id = filters.invoice_id | ||
|
||
scope.joins(<<~SQL) | ||
LEFT JOIN invoices_payment_requests | ||
ON invoices_payment_requests.payment_request_id = payment_requests.id | ||
SQL | ||
.where('invoices.id = :invoice_id OR invoices_payment_requests.invoice_id = :invoice_id', invoice_id:) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
spec/contracts/queries/payments_query_filters_contract_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# frozen_string_literal: true | ||
|
||
require "rails_helper" | ||
|
||
RSpec.describe Queries::PaymentsQueryFiltersContract, type: :contract do | ||
subject(:result) { described_class.new.call(filters:) } | ||
|
||
let(:filters) { {} } | ||
|
||
context "when filters are valid" do | ||
context "when invoice_id is valid" do | ||
let(:filters) { {invoice_id: "7b199d93-2663-4e68-beca-203aefcd019b"} } | ||
|
||
it "is valid" do | ||
expect(result.success?).to be(true) | ||
end | ||
end | ||
|
||
context "when invoice_id is blank" do | ||
let(:filters) { {invoice_id: nil} } | ||
|
||
it "is valid" do | ||
expect(result.success?).to be(true) | ||
end | ||
end | ||
|
||
context "when external_customer_id is valid" do | ||
let(:filters) { {external_customer_id: "valid_string"} } | ||
|
||
it "is valid" do | ||
expect(result.success?).to be(true) | ||
end | ||
end | ||
|
||
context "when external_customer_id is blank" do | ||
let(:filters) { {external_customer_id: nil} } | ||
|
||
it "is valid" do | ||
expect(result.success?).to be(true) | ||
end | ||
end | ||
|
||
context "when both invoice_id and external_customer_id are valid" do | ||
let(:filters) { {invoice_id: "7b199d93-2663-4e68-beca-203aefcd019b", external_customer_id: "valid_string"} } | ||
|
||
it "is valid" do | ||
expect(result.success?).to be(true) | ||
end | ||
end | ||
end | ||
|
||
context "when filters are invalid" do | ||
context "when invoice_id is not a UUID" do | ||
let(:filters) { {invoice_id: "invalid_uuid"} } | ||
|
||
it "is invalid" do | ||
expect(result.success?).to be(false) | ||
expect(result.errors.to_h).to include(filters: {invoice_id: ["is in invalid format"]}) | ||
end | ||
end | ||
|
||
context "when external_customer_id is not a string" do | ||
let(:filters) { {external_customer_id: 123} } | ||
|
||
it "is invalid" do | ||
expect(result.success?).to be(false) | ||
expect(result.errors.to_h).to include(filters: {external_customer_id: ["must be a string"]}) | ||
end | ||
end | ||
|
||
context "when both invoice_id and external_customer_id are invalid" do | ||
let(:filters) { {invoice_id: "invalid_uuid", external_customer_id: 123} } | ||
|
||
it "is invalid" do | ||
expect(result.success?).to be(false) | ||
expect(result.errors.to_h).to include( | ||
filters: { | ||
invoice_id: ["is in invalid format"], | ||
external_customer_id: ["must be a string"] | ||
} | ||
) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.