Skip to content

Commit

Permalink
feat(invoices): Add new filters (#3046)
Browse files Browse the repository at this point in the history
## Context

Adding new filters for Invoices

## Description

Add invoice amount filters to the graphql, export and API
Add invoice metadata filters to API only
  • Loading branch information
floganz authored Jan 15, 2025
1 parent e92601d commit 75ffd72
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 3 deletions.
5 changes: 4 additions & 1 deletion app/controllers/api/v1/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def index
},
search_term: params[:search_term],
filters: {
amount_from: params[:amount_from],
amount_to: params[:amount_to],
payment_status: (params[:payment_status] if valid_payment_status?(params[:payment_status])),
payment_dispute_lost: params[:payment_dispute_lost],
payment_overdue: (params[:payment_overdue] if %w[true false].include?(params[:payment_overdue])),
Expand All @@ -59,7 +61,8 @@ def index
customer_external_id: params[:external_customer_id],
invoice_type: params[:invoice_type],
issuing_date_from: (Date.strptime(params[:issuing_date_from]) if valid_date?(params[:issuing_date_from])),
issuing_date_to: (Date.strptime(params[:issuing_date_to]) if valid_date?(params[:issuing_date_to]))
issuing_date_to: (Date.strptime(params[:issuing_date_to]) if valid_date?(params[:issuing_date_to])),
metadata: params[:metadata]&.permit!.to_h
}
)

Expand Down
6 changes: 6 additions & 0 deletions app/graphql/resolvers/invoices_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class InvoicesResolver < Resolvers::BaseResolver

description 'Query invoices'

argument :amount_from, Integer, required: false
argument :amount_to, Integer, required: false
argument :currency, Types::CurrencyEnum, required: false
argument :customer_external_id, String, required: false
argument :customer_id, ID, required: false, description: 'Uniq ID of the customer'
Expand All @@ -26,6 +28,8 @@ class InvoicesResolver < Resolvers::BaseResolver
type Types::Invoices::Object.collection_type, null: false

def resolve( # rubocop:disable Metrics/ParameterLists
amount_from: nil,
amount_to: nil,
currency: nil,
customer_external_id: nil,
customer_id: nil,
Expand All @@ -45,6 +49,8 @@ def resolve( # rubocop:disable Metrics/ParameterLists
pagination: {page:, limit:},
search_term:,
filters: {
amount_from:,
amount_to:,
payment_status:,
payment_dispute_lost:,
payment_overdue:,
Expand Down
2 changes: 2 additions & 0 deletions app/graphql/types/data_exports/invoices/filters_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class FiltersInput < BaseInputObject
graphql_name 'DataExportInvoiceFiltersInput'
description 'Export Invoices search query and filters input argument'

argument :amount_from, Integer, required: false
argument :amount_to, Integer, required: false
argument :currency, Types::CurrencyEnum, required: false
argument :customer_external_id, String, required: false
argument :invoice_type, [Types::Invoices::InvoiceTypeEnum], required: false
Expand Down
27 changes: 27 additions & 0 deletions app/queries/invoices_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def call
invoices = with_payment_status(invoices) if filters.payment_status.present?
invoices = with_payment_dispute_lost(invoices) unless filters.payment_dispute_lost.nil?
invoices = with_payment_overdue(invoices) unless filters.payment_overdue.nil?
invoices = with_amount_range(invoices) if filters.amount_from.present? || filters.amount_to.present?
invoices = with_metadata(invoices) if filters.metadata.present?

result.invoices = invoices
result
Expand Down Expand Up @@ -92,6 +94,31 @@ def with_issuing_date_range(scope)
scope
end

def with_amount_range(scope)
scope = scope.where("invoices.total_amount_cents >= ?", filters.amount_from) if filters.amount_from
scope = scope.where("invoices.total_amount_cents <= ?", filters.amount_to) if filters.amount_to
scope
end

def with_metadata(scope)
base_scope = scope.joins(:metadata)
subquery = base_scope

filters.metadata.each_with_index do |(key, value), index|
subquery = if index.zero?
base_scope.where(metadata: {key:, value:})
else
subquery.or(base_scope.where(metadata: {key:, value:}))
end
end

subquery = subquery
.group("invoices.id")
.having("COUNT(DISTINCT metadata.key) = ?", filters.metadata.size)

scope.where(id: subquery.select(:id))
end

def issuing_date_from
@issuing_date_from ||= parse_datetime_filter(:issuing_date_from)
end
Expand Down
4 changes: 4 additions & 0 deletions schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions spec/factories/invoice_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
factory :invoice_metadata, class: 'Metadata::InvoiceMetadata' do
invoice

key { 'lead_name' }
value { 'John Doe' }
key { Faker::Commerce.color }
value { rand(100) }
end
end
2 changes: 2 additions & 0 deletions spec/graphql/mutations/data_exports/invoices/create_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
format: 'csv',
resourceType: 'invoices',
filters: {
amountFrom: 0,
amountTo: 10000,
currency: 'USD',
customerExternalId: 'abc123',
invoiceType: ['one_off'],
Expand Down
38 changes: 38 additions & 0 deletions spec/graphql/resolvers/invoices_resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,42 @@
end
end
end

context 'with both amount_from and amount_to' do
subject(:result) do
execute_graphql(
current_user: membership.user,
current_organization: organization,
permissions: required_permission,
query:
)
end

let(:query) do
<<~GQL
query {
invoices(
limit: 5,
amountFrom: #{invoices.second.total_amount_cents},
amountTo: #{invoices.fourth.total_amount_cents}
) {
collection { id }
metadata { currentPage, totalCount }
}
}
GQL
end

let!(:invoices) do
(1..5).to_a.map do |i|
create(:invoice, total_amount_cents: i.succ * 1_000, organization:)
end # from smallest to biggest
end

it 'returns visible invoices total cents amount in provided range' do
collection = result['data']['invoices']['collection']

expect(collection.pluck('id')).to match_array invoices[1..3].pluck(:id)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
RSpec.describe Types::DataExports::Invoices::FiltersInput do
subject { described_class }

it { is_expected.to accept_argument(:amount_from).of_type('Int') }
it { is_expected.to accept_argument(:amount_to).of_type('Int') }
it { is_expected.to accept_argument(:currency).of_type('CurrencyEnum') }
it { is_expected.to accept_argument(:customer_external_id).of_type('String') }
it { is_expected.to accept_argument(:invoice_type).of_type('[InvoiceTypeEnum!]') }
Expand Down
122 changes: 122 additions & 0 deletions spec/queries/invoices_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,126 @@
end
end
end

context "when amount filters applied" do
let(:filters) { {amount_from:, amount_to:} }

let!(:invoices) do
(1..5).to_a.map do |i|
create(:invoice, total_amount_cents: i.succ * 1_000, organization:)
end # from smallest to biggest
end

context "when only amount from provided" do
let(:amount_from) { invoices.second.total_amount_cents }
let(:amount_to) { nil }

it "returns invoices with total cents amount bigger or equal to provided value" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to match_array invoices[1..].pluck(:id)
end
end

context "when only amount to provided" do
let(:amount_from) { 100 }
let(:amount_to) { invoices.fourth.total_amount_cents }

it "returns invoices with total cents amount lower or equal to provided value" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to match_array invoices[..3].pluck(:id)
end
end

context "when both amount from and amount to provided" do
let(:amount_from) { invoices.second.total_amount_cents }
let(:amount_to) { invoices.fourth.total_amount_cents }

it "returns invoices with total cents amount in provided range" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to match_array invoices[1..3].pluck(:id)
end
end
end

context "when metadata filters applied" do
let(:filters) { {metadata:} }

context "when single filter provided" do
let(:metadata) { {red: 5} }

let!(:matching_invoice) { create(:invoice, organization:) }

before do
create(:invoice_metadata, invoice: matching_invoice, key: :red, value: 5)

create(:invoice, organization:) do |invoice|
create(:invoice_metadata, invoice:)
end
end

it "returns invoices with matching metadata filters" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to contain_exactly matching_invoice.id
end
end

context "when multiple filters provided" do
let(:metadata) do
{
red: 5,
orange: 3
}
end

let!(:matching_invoices) { create_pair(:invoice, organization:) }

before do
matching_invoices.each do |invoice|
metadata.each do |key, value|
create(:invoice_metadata, invoice:, key:, value:)
end
end

create(:invoice, organization:) do |invoice|
create(:invoice_metadata, invoice:, key: :red, value: 5)
end
end

it "returns invoices with matching metadata filters" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to match_array matching_invoices.pluck(:id)
end
end
end

context "with multiple filters applied at the same time" do
let(:search_term) { invoice.number.first(5) }

let(:filters) do
{
currency: invoice.currency,
customer_external_id: invoice.customer.external_id,
customer_id: invoice.customer.id,
invoice_type: invoice.invoice_type,
issuing_date_from: invoice.issuing_date,
issuing_date_to: invoice.issuing_date,
status: invoice.status,
payment_status: invoice.payment_status,
payment_dispute_lost: invoice.payment_dispute_lost_at.present?,
payment_overdue: invoice.payment_overdue,
amount_from: invoice.total_amount_cents,
amount_to: invoice.total_amount_cents,
metadata: invoice.metadata.to_h { |item| [item.key, item.value] }
}
end

let!(:invoice) { create(:invoice, currency: "EUR", organization:) }

before { create(:invoice, currency: "USD", organization:) }

it "returns invoices matching all provided filters" do
expect(result).to be_success
expect(result.invoices.pluck(:id)).to contain_exactly invoice.id
end
end
end
Loading

0 comments on commit 75ffd72

Please sign in to comment.