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(invoices): Add new filters #3046

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading