Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 385f811
Author: Ivan Novosad <[email protected]>
Date:   Thu Jan 23 17:21:04 2025 +0100

    feat(manual-payments): Add payment resolver, and filter by customer external_id

commit 00781e2
Author: Ivan Novosad <[email protected]>
Date:   Thu Jan 23 14:55:38 2025 +0100

    feat(manual-payments): Refactor payment model

commit 07af881
Author: Ivan Novosad <[email protected]>
Date:   Wed Jan 22 09:34:38 2025 +0100

    feat(manual-payments): Add more resolver specs

commit b4fee07
Author: Ivan Novosad <[email protected]>
Date:   Wed Jan 22 08:52:04 2025 +0100

    feat(manual-payments): Add premium integration types enum spec

commit ac22ef4
Author: Ivan Novosad <[email protected]>
Date:   Tue Jan 21 17:31:14 2025 +0100

    feat(manual-payments): Add positive_due_amount to invoices resolver

commit 7c2b00f
Author: Ivan Novosad <[email protected]>
Date:   Tue Jan 21 15:26:02 2025 +0100

    feat(manual-payments): Add customer to Payment object

commit 1087f64
Author: Ivan Novosad <[email protected]>
Date:   Mon Jan 20 17:36:59 2025 +0100

    feat(manual-payments): Add paymentProviderType to Payment object

commit ba5c623
Author: Ivan Novosad <[email protected]>
Date:   Mon Jan 20 11:14:37 2025 +0100

    feat(manual-payments): Fix rubocop warnings and specs

commit b890f28
Author: Miguel Pinto <[email protected]>
Date:   Fri Jan 17 11:27:16 2025 +0000

    feat: merge pr

commit 65f0c61
Author: Ivan Novosad <[email protected]>
Date:   Fri Jan 3 11:45:57 2025 +0100

    feat(manual-payments): WIP - Add GQL enums, inputs and types

commit 8e9e11d
Author: Vincent Pochet <[email protected]>
Date:   Fri Jan 24 16:46:44 2025 +0100

    fix(payment): Ensure payment status is checkd as a string (#3104)

    ## Description

    This PR is related to #3088 it
    ensure that none of the payment `status`, `payable_payment_status` and
    related payables's `payment_status` are updated if an unknown status is
    received from a payment provider

commit ec8ef1f
Author: Anna Velentsevich <[email protected]>
Date:   Fri Jan 24 16:15:29 2025 +0100

    Fix(rev-share): fix bugs found during QA (#3098)

    ## Context

    There were small issues found during the QA

    ## Description

    Fixed invoice numbering switching from customer-based to
    per-organization
    Do not allow edit customers via API if they are not editable

commit 4bbb63b
Author: Anna Velentsevich <[email protected]>
Date:   Fri Jan 24 15:30:06 2025 +0100

    add rescue when failing to update charge filters and charges (#3103)

    ## Context

    When an organization have charges or charge filters with wrong
    properties, the migration fails and it's hard to indicate what exactly
    went wrong

    ## Description

    Added rescue to migration that fixes double properties for charges and
    charges filters

commit 3881346
Author: brunomiguelpinto <[email protected]>
Date:   Fri Jan 24 13:56:37 2025 +0000

    fix: filter payments based on visible invoices (#3102)

    ## Context

    This changes the scope `for_organization` in the `Payment` model,
    allowing payments only for visible invoices to be considered.

    ## Description
    only let the Invoices with statuses defined in
    `Invoice::VISIBLE_STATUS`.

commit 035cea3
Author: Jérémy Denquin <[email protected]>
Date:   Fri Jan 24 12:14:33 2025 +0100

    fix(tasks): Fix signup task env var (#3101)
  • Loading branch information
ivannovosad committed Jan 24, 2025
1 parent fb2f4a6 commit f5a225c
Show file tree
Hide file tree
Showing 46 changed files with 1,563 additions and 16 deletions.
23 changes: 23 additions & 0 deletions app/graphql/mutations/payments/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Mutations
module Payments
class Create < BaseMutation
include AuthenticableApiUser
include RequiredOrganization

REQUIRED_PERMISSION = "payments:create"

graphql_name "CreatePayment"
description "Creates a manual payment"

input_object_class Types::Payments::CreateInput
type Types::Payments::Object

def resolve(**args)
result = ::ManualPayments::CreateService.call(organization: current_organization, params: args)
result.success? ? result.payment : result_error(result)
end
end
end
end
8 changes: 7 additions & 1 deletion app/graphql/resolvers/invoices_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ class InvoicesResolver < Resolvers::BaseResolver
argument :issuing_date_to, GraphQL::Types::ISO8601Date, required: false
argument :limit, Integer, required: false
argument :page, Integer, required: false
argument :partially_paid, Boolean, required: false
argument :payment_dispute_lost, Boolean, required: false
argument :payment_overdue, Boolean, required: false
argument :payment_status, [Types::Invoices::PaymentStatusTypeEnum], required: false
argument :positive_due_amount, Boolean, required: false
argument :search_term, String, required: false
argument :self_billed, Boolean, required: false
argument :status, [Types::Invoices::StatusTypeEnum], required: false
Expand All @@ -41,6 +43,8 @@ def resolve( # rubocop:disable Metrics/ParameterLists
page: nil,
payment_dispute_lost: nil,
payment_overdue: nil,
partially_paid: nil,
positive_due_amount: nil,
payment_status: nil,
search_term: nil,
self_billed: nil,
Expand All @@ -53,13 +57,15 @@ def resolve( # rubocop:disable Metrics/ParameterLists
filters: {
amount_from:,
amount_to:,
partially_paid:,
payment_dispute_lost:,
positive_due_amount:,
currency:,
customer_external_id:,
customer_id:,
invoice_type:,
issuing_date_from:,
issuing_date_to:,
payment_dispute_lost:,
payment_overdue:,
payment_status:,
self_billed:,
Expand Down
22 changes: 22 additions & 0 deletions app/graphql/resolvers/payment_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Resolvers
class PaymentResolver < Resolvers::BaseResolver
include AuthenticableApiUser
include RequiredOrganization

REQUIRED_PERMISSION = 'payments:view'

description 'Query a single Payment'

argument :id, ID, required: true, description: 'Uniq ID of the payment'

type Types::Payments::Object, null: true

def resolve(id:)
Payment.for_organization(current_organization).find(id)
rescue ActiveRecord::RecordNotFound
not_found_error(resource: 'payment')
end
end
end
35 changes: 35 additions & 0 deletions app/graphql/resolvers/payments_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Resolvers
class PaymentsResolver < Resolvers::BaseResolver
include AuthenticableApiUser
include RequiredOrganization

REQUIRED_PERMISSION = "payments:view"

description "Query payments of an organization"

argument :external_customer_id, ID, required: false
argument :invoice_id, ID, required: false
argument :limit, Integer, required: false
argument :page, Integer, required: false

type Types::Payments::Object.collection_type, null: false

def resolve(page: nil, limit: nil, invoice_id: nil, external_customer_id: nil)
result = PaymentsQuery.call(
organization: current_organization,
filters: {
invoice_id:,
external_customer_id:
},
pagination: {
page:,
limit:
}
)

result.payments
end
end
end
2 changes: 2 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class MutationType < Types::BaseObject
field :revoke_membership, mutation: Mutations::Memberships::Revoke
field :update_membership, mutation: Mutations::Memberships::Update

field :create_payment, mutation: Mutations::Payments::Create

field :create_payment_request, mutation: Mutations::PaymentRequests::Create

field :create_password_reset, mutation: Mutations::PasswordResets::Create
Expand Down
22 changes: 22 additions & 0 deletions app/graphql/types/payables/object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Types
module Payables
class Object < Types::BaseUnion
graphql_name 'Payable'

possible_types Types::Invoices::Object, Types::PaymentRequests::Object

def self.resolve_type(object, _context)
case object.class.to_s
when 'Invoice'
Types::Invoices::Object
when 'PaymentRequest'
Types::PaymentRequests::Object
else
raise "Unexpected payable type: #{object.inspect}"
end
end
end
end
end
14 changes: 14 additions & 0 deletions app/graphql/types/payments/create_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Types
module Payments
class CreateInput < Types::BaseInputObject
graphql_name "CreatePaymentInput"

argument :amount_cents, GraphQL::Types::BigInt, required: true
argument :created_at, GraphQL::Types::ISO8601DateTime, required: true
argument :invoice_id, ID, required: true
argument :reference, String, required: true
end
end
end
24 changes: 24 additions & 0 deletions app/graphql/types/payments/object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Types
module Payments
class Object < Types::BaseObject
graphql_name "Payment"

field :id, ID, null: false

field :amount_cents, GraphQL::Types::BigInt, null: false
field :amount_currency, Types::CurrencyEnum, null: false

field :customer, Types::Customers::Object, null: false
field :payable, Types::Payables::Object, null: false
field :payable_payment_status, Types::Payments::PayablePaymentStatusEnum, null: true
field :payment_provider_type, Types::PaymentProviders::ProviderTypeEnum, null: true
field :payment_type, Types::Payments::PaymentTypeEnum, null: false
field :provider_payment_id, GraphQL::Types::String, null: true
field :reference, GraphQL::Types::String, null: true

field :created_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
end
11 changes: 11 additions & 0 deletions app/graphql/types/payments/payable_payment_status_enum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Types
module Payments
class PayablePaymentStatusEnum < Types::BaseEnum
Payment::PAYABLE_PAYMENT_STATUS.each do |type|
value type
end
end
end
end
11 changes: 11 additions & 0 deletions app/graphql/types/payments/payment_type_enum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Types
module Payments
class PaymentTypeEnum < Types::BaseEnum
Payment::PAYMENT_TYPES.keys.each do |type|
value type
end
end
end
end
2 changes: 2 additions & 0 deletions app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ class QueryType < Types::BaseObject
field :organization, resolver: Resolvers::OrganizationResolver
field :overdue_balances, resolver: Resolvers::Analytics::OverdueBalancesResolver
field :password_reset, resolver: Resolvers::PasswordResetResolver
field :payment, resolver: Resolvers::PaymentResolver
field :payment_provider, resolver: Resolvers::PaymentProviderResolver
field :payment_providers, resolver: Resolvers::PaymentProvidersResolver
field :payment_requests, resolver: Resolvers::PaymentRequestsResolver
field :payments, resolver: Resolvers::PaymentsResolver
field :plan, resolver: Resolvers::PlanResolver
field :plans, resolver: Resolvers::PlansResolver
field :subscription, resolver: Resolvers::SubscriptionResolver
Expand Down
2 changes: 1 addition & 1 deletion app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ def generate_organization_sequential_id
) do
# If previous invoice had different numbering, base sequential id is the total number of invoices
organization_sequential_id = if switched_from_customer_numbering?
organization.invoices.with_generated_number.count
organization.invoices.non_self_billed.with_generated_number.count
else
organization
.invoices
Expand Down
2 changes: 1 addition & 1 deletion app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Organization < ApplicationRecord
].freeze

INTEGRATIONS = %w[
netsuite okta anrok xero progressive_billing hubspot auto_dunning revenue_analytics salesforce api_permissions revenue_share
netsuite okta anrok xero progressive_billing hubspot auto_dunning revenue_analytics salesforce api_permissions revenue_share manual_payments
].freeze
PREMIUM_INTEGRATIONS = INTEGRATIONS - %w[anrok]

Expand Down
10 changes: 8 additions & 2 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ class Payment < ApplicationRecord
ON invoices.id = payments.payable_id
AND payments.payable_type = 'Invoice'
AND invoices.organization_id = :org_id
AND invoices.status IN (:visible_statuses)
LEFT JOIN payment_requests
ON payment_requests.id = payments.payable_id
AND payments.payable_type = 'PaymentRequest'
AND payment_requests.organization_id = :org_id
SQL
{org_id: organization.id}
{org_id: organization.id, visible_statuses: Invoice::VISIBLE_STATUS.values}
])
joins(payables_join).where('invoices.id IS NOT NULL OR payment_requests.id IS NOT NULL')
joins(payables_join)
.where('invoices.id IS NOT NULL OR payment_requests.id IS NOT NULL')
}

def should_sync_payment?
Expand All @@ -49,6 +51,10 @@ def should_sync_payment?
payable.finalized? && customer.integration_customers.accounting_kind.any? { |c| c.integration.sync_payments }
end

def payment_provider_type
payment_provider&.payment_type
end

private

def manual_payment_credit_invoice_amount_cents
Expand Down
4 changes: 4 additions & 0 deletions app/models/payment_providers/adyen_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def environment
:test
end
end

def payment_type
'adyen'
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/payment_providers/cashfree_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class CashfreeProvider < BaseProvider
validates :success_redirect_url, url: true, allow_nil: true, length: {maximum: 1024}

secrets_accessors :client_id, :client_secret

def payment_type
'cashfree'
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/payment_providers/gocardless_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def environment
:sandbox
end
end

def payment_type
'gocardless'
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/payment_providers/stripe_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class StripeProvider < BaseProvider

settings_accessors :webhook_id
secrets_accessors :secret_key

def payment_type
'stripe'
end
end
end

Expand Down
11 changes: 11 additions & 0 deletions app/queries/invoices_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def call
invoices = with_amount_range(invoices) if filters.amount_from.present? || filters.amount_to.present?
invoices = with_metadata(invoices) if filters.metadata.present?
invoices = with_partially_paid(invoices) unless filters.partially_paid.nil?
invoices = with_positive_due_amount(invoices) unless filters.positive_due_amount.nil?
invoices = with_self_billed(invoices) unless filters.self_billed.nil?

result.invoices = invoices
Expand Down Expand Up @@ -90,6 +91,16 @@ def with_payment_overdue(scope)
scope.where(payment_overdue: filters.payment_overdue)
end

def with_positive_due_amount(scope)
positive_due_amount = ActiveModel::Type::Boolean.new.cast(filters.positive_due_amount)

if positive_due_amount
scope.where("total_amount_cents - total_paid_amount_cents > 0")
else
scope.where("total_amount_cents - total_paid_amount_cents <= 0")
end
end

def with_partially_paid(scope)
partially_paid = ActiveModel::Type::Boolean.new.cast(filters.partially_paid)

Expand Down
6 changes: 3 additions & 3 deletions app/queries/payments_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ 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
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:)
Expand All @@ -43,7 +43,7 @@ def filter_by_invoice(scope)
invoice_id = filters.invoice_id

scope.joins(<<~SQL)
LEFT JOIN invoices_payment_requests
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:)
Expand Down
2 changes: 1 addition & 1 deletion app/services/customers/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def create_from_api(organization:, params:)
customer.lastname = params[:lastname] if params.key?(:lastname)
customer.customer_type = params[:customer_type] if params.key?(:customer_type)

if customer.organization.revenue_share_enabled?
if customer.organization.revenue_share_enabled? && customer.editable?
customer.account_type = params[:account_type] if params.key?(:account_type)
customer.exclude_from_dunning_campaign = customer.partner_account?
end
Expand Down
Loading

0 comments on commit f5a225c

Please sign in to comment.