diff --git a/app/graphql/types/credit_notes/object.rb b/app/graphql/types/credit_notes/object.rb index 0df230791c7..1d4f7d63043 100644 --- a/app/graphql/types/credit_notes/object.rb +++ b/app/graphql/types/credit_notes/object.rb @@ -58,7 +58,7 @@ def integration_syncable end def external_integration_id - integration_customer = object.customer&.integration_customers&.accounting_kind&.first + integration_customer = object.customer&.accounting_customer return nil unless integration_customer diff --git a/app/graphql/types/customers/object.rb b/app/graphql/types/customers/object.rb index 0f9438c4cc4..65ba01b738e 100644 --- a/app/graphql/types/customers/object.rb +++ b/app/graphql/types/customers/object.rb @@ -103,14 +103,7 @@ def active_subscriptions_count end def provider_customer - case object&.payment_provider&.to_sym - when :stripe - object.stripe_customer - when :gocardless - object.gocardless_customer - when :adyen - object.adyen_customer - end + object&.payment_provider_customer end def credit_notes_credits_available_count diff --git a/app/graphql/types/integrations/anrok.rb b/app/graphql/types/integrations/anrok.rb index c6ef0810cff..482882a669f 100644 --- a/app/graphql/types/integrations/anrok.rb +++ b/app/graphql/types/integrations/anrok.rb @@ -6,6 +6,7 @@ class Anrok < Types::BaseObject graphql_name 'AnrokIntegration' field :api_key, String, null: false + field :category, String, null: true field :code, String, null: false field :external_account_id, String, null: true field :has_mappings_configured, Boolean diff --git a/app/graphql/types/integrations/netsuite.rb b/app/graphql/types/integrations/netsuite.rb index 20eb5a42144..c53d3f58e53 100644 --- a/app/graphql/types/integrations/netsuite.rb +++ b/app/graphql/types/integrations/netsuite.rb @@ -6,6 +6,7 @@ class Netsuite < Types::BaseObject graphql_name 'NetsuiteIntegration' field :account_id, String, null: true + field :category, String, null: true field :client_id, String, null: true field :client_secret, String, null: true field :code, String, null: false diff --git a/app/graphql/types/integrations/okta.rb b/app/graphql/types/integrations/okta.rb index d7b2f102230..586a1c6e43f 100644 --- a/app/graphql/types/integrations/okta.rb +++ b/app/graphql/types/integrations/okta.rb @@ -5,6 +5,7 @@ module Integrations class Okta < Types::BaseObject graphql_name 'OktaIntegration' + field :category, String, null: true field :client_id, String, null: true field :client_secret, String, null: true field :code, String, null: false diff --git a/app/graphql/types/integrations/xero.rb b/app/graphql/types/integrations/xero.rb index 4e6b5e53435..f8e15be88f8 100644 --- a/app/graphql/types/integrations/xero.rb +++ b/app/graphql/types/integrations/xero.rb @@ -5,6 +5,7 @@ module Integrations class Xero < Types::BaseObject graphql_name 'XeroIntegration' + field :category, String, null: true field :code, String, null: false field :connection_id, ID, null: false field :has_mappings_configured, Boolean diff --git a/app/graphql/types/invoices/object.rb b/app/graphql/types/invoices/object.rb index bc438bc297e..b1df85b5557 100644 --- a/app/graphql/types/invoices/object.rb +++ b/app/graphql/types/invoices/object.rb @@ -66,7 +66,7 @@ def integration_syncable end def external_integration_id - integration_customer = object.customer&.integration_customers&.accounting_kind&.first + integration_customer = object.customer&.accounting_customer return nil unless integration_customer diff --git a/app/models/credit_note.rb b/app/models/credit_note.rb index e8c8796b724..2ab578ff271 100644 --- a/app/models/credit_note.rb +++ b/app/models/credit_note.rb @@ -106,7 +106,7 @@ def add_on_items end def should_sync_credit_note? - finalized? && customer.integration_customers.accounting_kind.any? { |c| c.integration.sync_credit_notes } + finalized? & customer.accounting_customer&.integration&.sync_credit_notes end def voidable? diff --git a/app/models/customer.rb b/app/models/customer.rb index d2a56898332..2e6e798ae88 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -42,6 +42,17 @@ class Customer < ApplicationRecord has_one :anrok_customer, class_name: 'IntegrationCustomers::AnrokCustomer' has_one :xero_customer, class_name: 'IntegrationCustomers::XeroCustomer' + # customer can have only one integration customer per integration_category + has_one :tax_provider_customer, -> { + includes(:integration).where(integration: {category: 'tax_provider'}) + }, class_name: 'IntegrationCustomers::BaseCustomer' + has_one :accounting_customer, -> { + includes(:integration).where(integration: {category: 'accounting'}) + }, class_name: 'IntegrationCustomers::BaseCustomer' + + # customer can have only one payment_provider_customer + has_one :payment_provider_customer, class_name: 'PaymentProviderCustomers::BaseCustomer' + PAYMENT_PROVIDERS = %w[stripe gocardless adyen].freeze default_scope -> { kept } @@ -60,6 +71,8 @@ class Customer < ApplicationRecord validates :payment_provider, inclusion: {in: PAYMENT_PROVIDERS}, allow_nil: true validates :timezone, timezone: true, allow_nil: true + alias_method :provider_customer, :payment_provider_customer + def self.ransackable_attributes(_auth_object = nil) %w[id name external_id email] end @@ -104,17 +117,6 @@ def preferred_document_locale organization.document_locale.to_sym end - def provider_customer - case payment_provider&.to_sym - when :stripe - stripe_customer - when :gocardless - gocardless_customer - when :adyen - adyen_customer - end - end - def shipping_address { address_line1: shipping_address_line1, diff --git a/app/models/integration_customers/base_customer.rb b/app/models/integration_customers/base_customer.rb index dceaa841fe0..f8d9c3f0a8d 100644 --- a/app/models/integration_customers/base_customer.rb +++ b/app/models/integration_customers/base_customer.rb @@ -12,10 +12,6 @@ class BaseCustomer < ApplicationRecord validates :customer_id, uniqueness: {scope: :type} - scope :accounting_kind, -> do - where(type: %w[IntegrationCustomers::NetsuiteCustomer IntegrationCustomers::XeroCustomer]) - end - settings_accessors :sync_with_provider def self.customer_type(type) diff --git a/app/models/integrations/base_integration.rb b/app/models/integrations/base_integration.rb index 5d184003041..0499f72e0ca 100644 --- a/app/models/integrations/base_integration.rb +++ b/app/models/integrations/base_integration.rb @@ -28,6 +28,9 @@ class BaseIntegration < ApplicationRecord validates :code, uniqueness: {scope: :organization_id} validates :name, presence: true + INTEGRATION_CATEGORIES = %w[system accounting tax_provider] + enum category: INTEGRATION_CATEGORIES + def self.integration_type(type) case type when 'netsuite' diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 365a653ec6a..2d580a54def 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -287,14 +287,11 @@ def mark_as_dispute_lost!(timestamp = Time.current) end def should_sync_invoice? - finalized? && customer.integration_customers.accounting_kind.any? { |c| c.integration.sync_invoices } + finalized? & customer.accounting_customer&.integration&.sync_invoices end def should_sync_sales_order? - finalized? && - customer.integration_customers.accounting_kind.any? do |c| - c.integration.respond_to?(:sync_sales_orders) && c.integration.sync_sales_orders - end + finalized? & customer.accounting_customer&.integration&.sync_sales_orders end private diff --git a/app/models/payment.rb b/app/models/payment.rb index 6570d2eba87..fbc78d54c68 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -13,6 +13,6 @@ class Payment < ApplicationRecord delegate :customer, to: :invoice def should_sync_payment? - invoice.finalized? && customer.integration_customers.accounting_kind.any? { |c| c.integration.sync_payments } + invoice.finalized? & customer.accounting_customer&.integration&.sync_payments end end diff --git a/app/serializers/v1/customer_serializer.rb b/app/serializers/v1/customer_serializer.rb index c823fd0f5d6..86adf74a943 100644 --- a/app/serializers/v1/customer_serializer.rb +++ b/app/serializers/v1/customer_serializer.rb @@ -59,19 +59,11 @@ def billing_configuration document_locale: model.document_locale } - case model.payment_provider&.to_sym - when :stripe - configuration[:provider_customer_id] = model.stripe_customer&.provider_customer_id - configuration[:provider_payment_methods] = model.stripe_customer&.provider_payment_methods - configuration.merge!(model.stripe_customer&.settings || {}) - when :gocardless - configuration[:provider_customer_id] = model.gocardless_customer&.provider_customer_id - configuration.merge!(model.gocardless_customer&.settings || {}) - when :adyen - configuration[:provider_customer_id] = model.adyen_customer&.provider_customer_id - configuration.merge!(model.adyen_customer&.settings || {}) + configuration[:provider_customer_id] = model.payment_provider_customer&.provider_customer_id + configuration.merge!(model.payment_provider_customer&.settings || {}) + if model.payment_provider&.to_sym == :stripe + configuration[:provider_payment_methods] = model.payment_provider_customer&.provider_payment_methods end - configuration end diff --git a/app/services/customers/update_service.rb b/app/services/customers/update_service.rb index a65087305ea..4e1ba3afc19 100644 --- a/app/services/customers/update_service.rb +++ b/app/services/customers/update_service.rb @@ -172,25 +172,16 @@ def assign_premium_attributes(customer, args) def create_or_update_provider_customer(customer, payment_provider, billing_configuration = {}) handle_provider_customer = customer.payment_provider.present? handle_provider_customer ||= (billing_configuration || {})[:provider_customer_id].present? + handle_provider_customer ||= customer.payment_provider_customer&.provider_customer_id.present? + + return unless handle_provider_customer case payment_provider when 'stripe' - handle_provider_customer ||= customer.stripe_customer&.provider_customer_id.present? - - return unless handle_provider_customer - update_stripe_customer(customer, billing_configuration) when 'gocardless' - handle_provider_customer ||= customer.gocardless_customer&.provider_customer_id.present? - - return unless handle_provider_customer - update_gocardless_customer(customer, billing_configuration) when 'adyen' - handle_provider_customer ||= customer.adyen_customer&.provider_customer_id.present? - - return unless handle_provider_customer - update_adyen_customer(customer, billing_configuration) end end diff --git a/app/services/integrations/aggregator/invoices/base_service.rb b/app/services/integrations/aggregator/invoices/base_service.rb index 3e7b78ae758..b5ebf018500 100644 --- a/app/services/integrations/aggregator/invoices/base_service.rb +++ b/app/services/integrations/aggregator/invoices/base_service.rb @@ -31,7 +31,7 @@ def integration end def integration_customer - @integration_customer ||= customer&.integration_customers&.accounting_kind&.first + @integration_customer ||= customer&.accounting_customer end def payload(type) diff --git a/app/services/integrations/anrok/create_service.rb b/app/services/integrations/anrok/create_service.rb index ef98a1e7224..784c28c6faf 100644 --- a/app/services/integrations/anrok/create_service.rb +++ b/app/services/integrations/anrok/create_service.rb @@ -15,7 +15,8 @@ def call(**args) name: args[:name], code: args[:code], connection_id: args[:connection_id], - api_key: args[:api_key] + api_key: args[:api_key], + category: 'tax_provider' ) integration.save! diff --git a/app/services/integrations/netsuite/create_service.rb b/app/services/integrations/netsuite/create_service.rb index 48ae5af5fd9..3648b946bfa 100644 --- a/app/services/integrations/netsuite/create_service.rb +++ b/app/services/integrations/netsuite/create_service.rb @@ -24,7 +24,8 @@ def call(**args) sync_credit_notes: ActiveModel::Type::Boolean.new.cast(args[:sync_credit_notes]), sync_invoices: ActiveModel::Type::Boolean.new.cast(args[:sync_invoices]), sync_payments: ActiveModel::Type::Boolean.new.cast(args[:sync_payments]), - sync_sales_orders: ActiveModel::Type::Boolean.new.cast(args[:sync_sales_orders]) + sync_sales_orders: ActiveModel::Type::Boolean.new.cast(args[:sync_sales_orders]), + category: 'accounting' ) integration.save! diff --git a/app/services/integrations/okta/create_service.rb b/app/services/integrations/okta/create_service.rb index 0e68bdd694e..35f54759355 100644 --- a/app/services/integrations/okta/create_service.rb +++ b/app/services/integrations/okta/create_service.rb @@ -17,7 +17,8 @@ def call(**args) client_id: args[:client_id], client_secret: args[:client_secret], domain: args[:domain], - organization_name: args[:organization_name] + organization_name: args[:organization_name], + category: 'system' ) integration.save! diff --git a/app/services/integrations/xero/create_service.rb b/app/services/integrations/xero/create_service.rb index 77388633791..b74999cd716 100644 --- a/app/services/integrations/xero/create_service.rb +++ b/app/services/integrations/xero/create_service.rb @@ -17,7 +17,8 @@ def call(**args) connection_id: args[:connection_id], sync_credit_notes: ActiveModel::Type::Boolean.new.cast(args[:sync_credit_notes]), sync_invoices: ActiveModel::Type::Boolean.new.cast(args[:sync_invoices]), - sync_payments: ActiveModel::Type::Boolean.new.cast(args[:sync_payments]) + sync_payments: ActiveModel::Type::Boolean.new.cast(args[:sync_payments]), + category: 'accounting' ) integration.save! diff --git a/app/services/invoices/compute_amounts_from_fees.rb b/app/services/invoices/compute_amounts_from_fees.rb index ef39865f09c..779b9f74d85 100644 --- a/app/services/invoices/compute_amounts_from_fees.rb +++ b/app/services/invoices/compute_amounts_from_fees.rb @@ -51,7 +51,7 @@ def call attr_reader :invoice, :provider_taxes def customer_provider_taxation? - @customer_provider_taxation ||= invoice.customer.anrok_customer + @customer_provider_taxation ||= invoice.customer.tax_provider_customer end def fee_taxes(fee) diff --git a/db/migrate/20240724154417_add_category_to_integrations.rb b/db/migrate/20240724154417_add_category_to_integrations.rb new file mode 100644 index 00000000000..f9d1ea83f8b --- /dev/null +++ b/db/migrate/20240724154417_add_category_to_integrations.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AddCategoryToIntegrations < ActiveRecord::Migration[7.1] + module Integrations + class BaseIntegration < ApplicationRecord + self.table_name = 'integrations' + INTEGRATION_CATEGORIES = %w[system accounting tax_provider] + enum category: INTEGRATION_CATEGORIES + end + + class AnrokIntegration < BaseIntegration + end + + class NetsuiteIntegration < BaseIntegration + end + + class XeroIntegration < BaseIntegration + end + + class OktaIntegration < BaseIntegration + end + end + + def up + add_column :integrations, :category, :integer + add_index :integrations, :category + + Integrations::AnrokIntegration.update_all(category: 'tax_provider') # rubocop:disable Rails/SkipsModelValidations + Integrations::NetsuiteIntegration.update_all(category: 'accounting') # rubocop:disable Rails/SkipsModelValidations + Integrations::XeroIntegration.update_all(category: 'accounting') # rubocop:disable Rails/SkipsModelValidations + Integrations::OktaIntegration.update_all(category: 'system') # rubocop:disable Rails/SkipsModelValidations + end + + def down + remove_index :integrations, :category + remove_column :integrations, :category + end +end diff --git a/db/schema.rb b/db/schema.rb index 2fabef2b337..85df747dc3c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_07_23_150221) do +ActiveRecord::Schema[7.1].define(version: 2024_07_24_154417) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -664,6 +664,8 @@ t.jsonb "settings", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "category" + t.index ["category"], name: "index_integrations_on_category" t.index ["code", "organization_id"], name: "index_integrations_on_code_and_organization_id", unique: true t.index ["organization_id"], name: "index_integrations_on_organization_id" end diff --git a/schema.graphql b/schema.graphql index 94e73ff1e82..05c4004217d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -146,6 +146,7 @@ type AnrokFeeObjectCollection { type AnrokIntegration { apiKey: String! + category: String code: String! externalAccountId: String hasMappingsConfigured: Boolean @@ -5100,6 +5101,7 @@ type NetsuiteCustomer { type NetsuiteIntegration { accountId: String + category: String clientId: String clientSecret: String code: String! @@ -5142,6 +5144,7 @@ input OktaAuthorizeInput { } type OktaIntegration { + category: String clientId: String clientSecret: String code: String! @@ -7547,6 +7550,7 @@ type XeroCustomer { } type XeroIntegration { + category: String code: String! connectionId: ID! hasMappingsConfigured: Boolean diff --git a/schema.json b/schema.json index f546ffbdfd0..507704510cf 100644 --- a/schema.json +++ b/schema.json @@ -1262,6 +1262,20 @@ ] }, + { + "name": "category", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "code", "description": null, @@ -24014,6 +24028,20 @@ ] }, + { + "name": "category", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "clientId", "description": null, @@ -24365,6 +24393,20 @@ ], "possibleTypes": null, "fields": [ + { + "name": "category", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "clientId", "description": null, @@ -38246,6 +38288,20 @@ ], "possibleTypes": null, "fields": [ + { + "name": "category", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "code", "description": null, diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index ba14602289a..84651902be4 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -6,6 +6,7 @@ type { 'Integrations::NetsuiteIntegration' } code { "netsuite_#{SecureRandom.uuid}" } name { 'Accounting integration 1' } + category { 'accounting' } secrets do {connection_id: SecureRandom.uuid, client_secret: SecureRandom.uuid}.to_json @@ -21,6 +22,7 @@ type { 'Integrations::OktaIntegration' } code { 'okta' } name { 'Okta Integration' } + category { 'system' } settings do {client_id: SecureRandom.uuid, domain: 'foo.test', organization_name: 'Foobar'} @@ -36,6 +38,7 @@ type { 'Integrations::AnrokIntegration' } code { 'anrok' } name { 'Anrok Integration' } + category { 'tax_provider' } secrets do {connection_id: SecureRandom.uuid, api_key: SecureRandom.uuid}.to_json @@ -47,6 +50,7 @@ type { 'Integrations::XeroIntegration' } code { 'xero' } name { 'Xero Integration' } + category { 'accounting' } secrets do {connection_id: SecureRandom.uuid}.to_json