diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 67fc93b3407d..2c344499ff61 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -20,6 +20,26 @@ def stripe head(:ok) end + def cashfree + result = PaymentProviders::CashfreeService.new.handle_incoming_webhook( + organization_id: params[:organization_id], + code: params[:code].presence, + body: request.body.read, + timestamp: request.headers['X-Cashfree-Timestamp'], + signature: request.headers['X-Cashfree-Signature'] + ) + + unless result.success? + if result.error.is_a?(BaseService::ServiceFailure) && result.error.code == 'webhook_error' + return head(:bad_request) + end + + result.raise_if_error! + end + + head(:ok) + end + def gocardless result = PaymentProviders::GocardlessService.new.handle_incoming_webhook( organization_id: params[:organization_id], diff --git a/app/graphql/mutations/payment_providers/cashfree/base.rb b/app/graphql/mutations/payment_providers/cashfree/base.rb new file mode 100644 index 000000000000..a28059edf27b --- /dev/null +++ b/app/graphql/mutations/payment_providers/cashfree/base.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mutations + module PaymentProviders + module Cashfree + class Base < BaseMutation + include AuthenticableApiUser + include RequiredOrganization + + def resolve(**args) + result = ::PaymentProviders::CashfreeService + .new(context[:current_user]) + .create_or_update(**args.merge(organization: current_organization)) + + result.success? ? result.cashfree_provider : result_error(result) + end + end + end + end +end diff --git a/app/graphql/mutations/payment_providers/cashfree/create.rb b/app/graphql/mutations/payment_providers/cashfree/create.rb new file mode 100644 index 000000000000..b717cfec9941 --- /dev/null +++ b/app/graphql/mutations/payment_providers/cashfree/create.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module PaymentProviders + module Cashfree + class Create < Base + REQUIRED_PERMISSION = 'organization:integrations:create' + + graphql_name 'AddCashfreePaymentProvider' + description 'Add or update Cashfree payment provider' + + input_object_class Types::PaymentProviders::CashfreeInput + + type Types::PaymentProviders::Cashfree + end + end + end +end diff --git a/app/graphql/mutations/payment_providers/cashfree/update.rb b/app/graphql/mutations/payment_providers/cashfree/update.rb new file mode 100644 index 000000000000..325274ddd486 --- /dev/null +++ b/app/graphql/mutations/payment_providers/cashfree/update.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module PaymentProviders + module Cashfree + class Update < Base + REQUIRED_PERMISSION = 'organization:integrations:update' + + graphql_name 'UpdateCashfreePaymentProvider' + description 'Update Cashfree payment provider' + + input_object_class Types::PaymentProviders::UpdateInput + + type Types::PaymentProviders::Cashfree + end + end + end +end diff --git a/app/graphql/resolvers/payment_providers_resolver.rb b/app/graphql/resolvers/payment_providers_resolver.rb index 41ac9c21d1ae..93613c79c221 100644 --- a/app/graphql/resolvers/payment_providers_resolver.rb +++ b/app/graphql/resolvers/payment_providers_resolver.rb @@ -31,6 +31,8 @@ def provider_type(type) PaymentProviders::StripeProvider.to_s when 'gocardless' PaymentProviders::GocardlessProvider.to_s + when 'cashfree' + PaymentProviders::CashfreeProvider.to_s else raise(NotImplementedError) end diff --git a/app/graphql/types/customers/object.rb b/app/graphql/types/customers/object.rb index 14b273d31d9b..00745d5e8554 100644 --- a/app/graphql/types/customers/object.rb +++ b/app/graphql/types/customers/object.rb @@ -113,6 +113,8 @@ def provider_customer object.stripe_customer when :gocardless object.gocardless_customer + when :cashfree + object.cashfree_customer when :adyen object.adyen_customer end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index a382f8a6b786..313834e9a9c4 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -45,10 +45,12 @@ class MutationType < Types::BaseObject field :update_add_on, mutation: Mutations::AddOns::Update field :add_adyen_payment_provider, mutation: Mutations::PaymentProviders::Adyen::Create + field :add_cashfree_payment_provider, mutation: Mutations::PaymentProviders::Cashfree::Create field :add_gocardless_payment_provider, mutation: Mutations::PaymentProviders::Gocardless::Create field :add_stripe_payment_provider, mutation: Mutations::PaymentProviders::Stripe::Create field :update_adyen_payment_provider, mutation: Mutations::PaymentProviders::Adyen::Update + field :update_cashfree_payment_provider, mutation: Mutations::PaymentProviders::Cashfree::Update field :update_gocardless_payment_provider, mutation: Mutations::PaymentProviders::Gocardless::Update field :update_stripe_payment_provider, mutation: Mutations::PaymentProviders::Stripe::Update diff --git a/app/graphql/types/organizations/current_organization_type.rb b/app/graphql/types/organizations/current_organization_type.rb index 233a81bdbbfd..0ceaaaab17c3 100644 --- a/app/graphql/types/organizations/current_organization_type.rb +++ b/app/graphql/types/organizations/current_organization_type.rb @@ -45,6 +45,7 @@ class CurrentOrganizationType < BaseOrganizationType field :taxes, [Types::Taxes::Object], resolver: Resolvers::TaxesResolver, permission: 'organization:taxes:view' field :adyen_payment_providers, [Types::PaymentProviders::Adyen], permission: 'organization:integrations:view' + field :cashfree_payment_providers, [Types::PaymentProviders::Cashfree], permission: 'organization:integrations:view' field :gocardless_payment_providers, [Types::PaymentProviders::Gocardless], permission: 'organization:integrations:view' field :stripe_payment_providers, [Types::PaymentProviders::Stripe], permission: 'organization:integrations:view' diff --git a/app/graphql/types/payment_providers/cashfree.rb b/app/graphql/types/payment_providers/cashfree.rb new file mode 100644 index 000000000000..7d0f02d9aa69 --- /dev/null +++ b/app/graphql/types/payment_providers/cashfree.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module PaymentProviders + class Cashfree < Types::BaseObject + graphql_name 'CashfreeProvider' + + field :code, String, null: false + field :id, ID, null: false + field :name, String, null: false + + field :client_id, String, null: true, permission: 'organization:integrations:view' + field :client_secret, String, null: true, permission: 'organization:integrations:view' + field :success_redirect_url, String, null: true, permission: 'organization:integrations:view' + end + end +end diff --git a/app/graphql/types/payment_providers/cashfree_input.rb b/app/graphql/types/payment_providers/cashfree_input.rb new file mode 100644 index 000000000000..a18f0986d46b --- /dev/null +++ b/app/graphql/types/payment_providers/cashfree_input.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module PaymentProviders + class CashfreeInput < BaseInputObject + description 'Cashfree input arguments' + + argument :client_id, String, required: true + argument :client_secret, String, required: true + argument :code, String, required: true + argument :name, String, required: true + argument :success_redirect_url, String, required: false + end + end +end diff --git a/app/graphql/types/payment_providers/object.rb b/app/graphql/types/payment_providers/object.rb index e1591f4dd2ae..a5ebf1ab4914 100644 --- a/app/graphql/types/payment_providers/object.rb +++ b/app/graphql/types/payment_providers/object.rb @@ -7,7 +7,8 @@ class Object < Types::BaseUnion possible_types Types::PaymentProviders::Adyen, Types::PaymentProviders::Gocardless, - Types::PaymentProviders::Stripe + Types::PaymentProviders::Stripe, + Types::PaymentProviders::Cashfree def self.resolve_type(object, _context) case object.class.to_s @@ -17,6 +18,8 @@ def self.resolve_type(object, _context) Types::PaymentProviders::Stripe when 'PaymentProviders::GocardlessProvider' Types::PaymentProviders::Gocardless + when 'PaymentProviders::CashfreeProvider' + Types::PaymentProviders::Cashfree else raise "Unexpected Payment provider type: #{object.inspect}" end diff --git a/app/jobs/invoices/payments/cashfree_create_job.rb b/app/jobs/invoices/payments/cashfree_create_job.rb new file mode 100644 index 000000000000..ccf8c79d91a3 --- /dev/null +++ b/app/jobs/invoices/payments/cashfree_create_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Invoices + module Payments + class CashfreeCreateJob < ApplicationJob + queue_as 'providers' + + unique :until_executed + + def perform(invoice) + result = Invoices::Payments::CashfreeService.new(invoice).create + result.raise_if_error! + end + end + end +end diff --git a/app/jobs/payment_providers/cashfree/handle_event_job.rb b/app/jobs/payment_providers/cashfree/handle_event_job.rb new file mode 100644 index 000000000000..a58e236cf087 --- /dev/null +++ b/app/jobs/payment_providers/cashfree/handle_event_job.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module PaymentProviders + module Cashfree + class HandleEventJob < ApplicationJob + queue_as 'providers' + + def perform(event_json:) + result = PaymentProviders::CashfreeService.new.handle_event(event_json:) + result.raise_if_error! + end + end + end +end diff --git a/app/models/customer.rb b/app/models/customer.rb index b841f7241e64..8daaf086f440 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -47,12 +47,13 @@ class Customer < ApplicationRecord has_one :stripe_customer, class_name: 'PaymentProviderCustomers::StripeCustomer' has_one :gocardless_customer, class_name: 'PaymentProviderCustomers::GocardlessCustomer' + has_one :cashfree_customer, class_name: 'PaymentProviderCustomers::CashfreeCustomer' has_one :adyen_customer, class_name: 'PaymentProviderCustomers::AdyenCustomer' has_one :netsuite_customer, class_name: 'IntegrationCustomers::NetsuiteCustomer' has_one :anrok_customer, class_name: 'IntegrationCustomers::AnrokCustomer' has_one :xero_customer, class_name: 'IntegrationCustomers::XeroCustomer' - PAYMENT_PROVIDERS = %w[stripe gocardless adyen].freeze + PAYMENT_PROVIDERS = %w[stripe gocardless cashfree adyen].freeze default_scope -> { kept } sequenced scope: ->(customer) { customer.organization.customers.with_discarded }, @@ -121,6 +122,8 @@ def provider_customer stripe_customer when :gocardless gocardless_customer + when :cashfree + cashfree_customer when :adyen adyen_customer end diff --git a/app/models/organization.rb b/app/models/organization.rb index 54b8acf5026e..305dbc90fc5c 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -38,6 +38,7 @@ class Organization < ApplicationRecord has_many :stripe_payment_providers, class_name: 'PaymentProviders::StripeProvider' has_many :gocardless_payment_providers, class_name: 'PaymentProviders::GocardlessProvider' + has_many :cashfree_payment_providers, class_name: 'PaymentProviders::CashfreeProvider' has_many :adyen_payment_providers, class_name: 'PaymentProviders::AdyenProvider' has_many :netsuite_integrations, class_name: 'Integrations::NetsuiteIntegration' @@ -106,6 +107,8 @@ def payment_provider(provider) stripe_payment_provider when 'gocardless' gocardless_payment_provider + when 'cashfree' + cashfree_payment_provider when 'adyen' adyen_payment_provider end diff --git a/app/models/payment_provider_customers/cashfree_customer.rb b/app/models/payment_provider_customers/cashfree_customer.rb new file mode 100644 index 000000000000..a61f45224110 --- /dev/null +++ b/app/models/payment_provider_customers/cashfree_customer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module PaymentProviderCustomers + class CashfreeCustomer < BaseCustomer + end +end diff --git a/app/models/payment_providers/cashfree_provider.rb b/app/models/payment_providers/cashfree_provider.rb new file mode 100644 index 000000000000..5e5d83adaaa5 --- /dev/null +++ b/app/models/payment_providers/cashfree_provider.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module PaymentProviders + class CashfreeProvider < BaseProvider + SUCCESS_REDIRECT_URL = 'https://cashfree.com/' + API_VERSION = "2023-08-01" + BASE_URL = (Rails.env.production? ? 'https://api.cashfree.com/pg/links' : 'https://sandbox.cashfree.com/pg/links') + + validates :client_id, presence: true + validates :client_secret, presence: true + validates :success_redirect_url, url: true, allow_nil: true, length: {maximum: 1024} + + secrets_accessors :client_id, :client_secret + end +end diff --git a/app/serializers/v1/customer_serializer.rb b/app/serializers/v1/customer_serializer.rb index 9fea8081c7e1..beb86dd52c91 100644 --- a/app/serializers/v1/customer_serializer.rb +++ b/app/serializers/v1/customer_serializer.rb @@ -68,6 +68,9 @@ def billing_configuration when :gocardless configuration[:provider_customer_id] = model.gocardless_customer&.provider_customer_id configuration.merge!(model.gocardless_customer&.settings || {}) + when :cashfree + configuration[:provider_customer_id] = model.cashfree_customer&.provider_customer_id + configuration.merge!(model.cashfree_customer&.settings || {}) when :adyen configuration[:provider_customer_id] = model.adyen_customer&.provider_customer_id configuration.merge!(model.adyen_customer&.settings || {}) diff --git a/app/services/customers/create_service.rb b/app/services/customers/create_service.rb index 39dd9725943d..c25de4154b12 100644 --- a/app/services/customers/create_service.rb +++ b/app/services/customers/create_service.rb @@ -277,7 +277,7 @@ def handle_api_billing_configuration(customer, params, new_customer) if billing.key?(:payment_provider) customer.payment_provider = nil - if %w[stripe gocardless adyen].include?(billing[:payment_provider]) + if %w[stripe gocardless cashfree adyen].include?(billing[:payment_provider]) customer.payment_provider = billing[:payment_provider] end end @@ -304,6 +304,8 @@ def create_or_update_provider_customer(customer, billing_configuration = {}) PaymentProviderCustomers::StripeCustomer when 'gocardless' PaymentProviderCustomers::GocardlessCustomer + when 'cashfree' + PaymentProviderCustomers::CashfreeCustomer when 'adyen' PaymentProviderCustomers::AdyenCustomer end diff --git a/app/services/customers/update_service.rb b/app/services/customers/update_service.rb index a65087305eaa..aed02d19de24 100644 --- a/app/services/customers/update_service.rb +++ b/app/services/customers/update_service.rb @@ -186,6 +186,12 @@ def create_or_update_provider_customer(customer, payment_provider, billing_confi return unless handle_provider_customer update_gocardless_customer(customer, billing_configuration) + when 'cashfree' + handle_provider_customer ||= customer.cashfree_customer&.provider_customer_id.present? + + return unless handle_provider_customer + + update_cashfree_customer(customer, billing_configuration) when 'adyen' handle_provider_customer ||= customer.adyen_customer&.provider_customer_id.present? @@ -219,6 +225,18 @@ def update_gocardless_customer(customer, billing_configuration) customer.gocardless_customer&.reload end + def update_cashfree_customer(customer, billing_configuration) + create_result = PaymentProviderCustomers::CreateService.new(customer).create_or_update( + customer_class: PaymentProviderCustomers::CashfreeCustomer, + payment_provider_id: payment_provider(customer)&.id, + params: billing_configuration + ) + create_result.raise_if_error! + + # NOTE: Create service is modifying an other instance of the provider customer + customer.cashfree_customer&.reload + end + def update_adyen_customer(customer, billing_configuration) create_result = PaymentProviderCustomers::CreateService.new(customer).create_or_update( customer_class: PaymentProviderCustomers::AdyenCustomer, diff --git a/app/services/invoices/payments/cashfree_service.rb b/app/services/invoices/payments/cashfree_service.rb new file mode 100644 index 000000000000..fcd112f5c5dd --- /dev/null +++ b/app/services/invoices/payments/cashfree_service.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +module Invoices + module Payments + class CashfreeService < BaseService + include Customers::PaymentProviderFinder + + PENDING_STATUSES = %w[PARTIALLY_PAID].freeze + SUCCESS_STATUSES = %w[PAID].freeze + FAILED_STATUSES = %w[EXPIRED CANCELLED].freeze + + def initialize(invoice = nil) + @invoice = invoice + + super(nil) + end + + def create + Rails.logger.debug "invoice.total_amount_cents 0: #{invoice.total_amount_cents}" + + result.invoice = invoice + return result unless should_process_payment? + + Rails.logger.debug "invoice.total_amount_cents 1: #{invoice.total_amount_cents.positive?}" + + unless invoice.total_amount_cents.positive? + update_invoice_payment_status(payment_status: :succeeded) + return result + end + + Rails.logger.debug "invoice.total_amount_cents 2: #{invoice.total_amount_cents}" + + increment_payment_attempts + + # res = create_cashfree_payment + # return result unless res + + # cashfree_success, _cashfree_error = handle_cashfree_response(res) + # return result unless cashfree_success + + payment = Payment.new( + payable: invoice, + payment_provider_id: cashfree_payment_provider.id, + payment_provider_customer_id: customer.cashfree_customer.id, + amount_cents: invoice.total_amount_cents, + amount_currency: invoice.currency.upcase, + provider_payment_id: invoice.id, + status: :pending + # provider_payment_id: res.response['pspReference'], + ) + payment.save! + + # invoice_payment_status = invoice_payment_status(payment.status) + # update_invoice_payment_status(payment_status: invoice_payment_status) + + # Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment? + + result.payment = payment + result + end + + def update_payment_status(provider_payment_id:, status:, metadata: {}) + payment = if metadata[:payment_type] == 'one-time' + create_payment(provider_payment_id:) + else + Payment.find_by(provider_payment_id:) + end + return result.not_found_failure!(resource: 'cashfree_payment') unless payment + + result.payment = payment + result.invoice = payment.payable + return result if payment.payable.payment_succeeded? + + invoice_payment_status = invoice_payment_status(status) + + payment.update!(status: invoice_payment_status) + update_invoice_payment_status(payment_status: invoice_payment_status) + + result + rescue BaseService::FailedResult => e + result.fail_with_error!(e) + end + + def generate_payment_url + return result unless should_process_payment? + + req = create_post_request + req.body = payment_url_params.to_json + + res = client.request(req) + + result.service_failure!(code: res.code, message: res.msg) unless res.is_a?(Net::HTTPSuccess) + + return result unless result.success? + + result.payment_url = JSON.parse(res.body)["link_url"] + + result + rescue => e + deliver_error_webhook(e) + + result.service_failure!(code: e.code, message: e.msg) + end + + private + + attr_accessor :invoice + + delegate :organization, :customer, to: :invoice + + def create_payment(provider_payment_id:) + @invoice = Invoice.find_by(id: provider_payment_id) + + increment_payment_attempts + + Payment.new( + payable: invoice, + payment_provider_id: cashfree_payment_provider.id, + payment_provider_customer_id: customer.cashfree_customer.id, + amount_cents: invoice.total_amount_cents, + amount_currency: invoice.currency.upcase, + provider_payment_id: + ) + end + + def should_process_payment? + return false if invoice.payment_succeeded? || invoice.voided? + return false if cashfree_payment_provider.blank? + + customer&.cashfree_customer&.provider_customer_id + end + + def client + url = URI(::PaymentProviders::CashfreeProvider::BASE_URL) + http = Net::HTTP.new(url.host, url.port) + http.use_ssl = true + + @client ||= http + end + + def create_post_request + url = URI(::PaymentProviders::CashfreeProvider::BASE_URL) + request = Net::HTTP::Post.new(url) + + request["accept"] = 'application/json' + request["content-type"] = 'application/json' + request["x-client-id"] = cashfree_payment_provider.client_id + request["x-client-secret"] = cashfree_payment_provider.client_secret + request["x-api-version"] = ::PaymentProviders::CashfreeProvider::API_VERSION + + request + end + + def success_redirect_url + cashfree_payment_provider.success_redirect_url.presence || ::PaymentProviders::CashfreeProvider::SUCCESS_REDIRECT_URL + end + + def cashfree_payment_provider + @cashfree_payment_provider ||= payment_provider(customer) + end + + def payment_url_params + # TODO Add settings_accessors if required + { + customer_details: { + customer_phone: customer.phone || "9999999999", + customer_email: customer.email, + customer_name: customer.name + }, + link_notify: { + send_sms: true, + send_email: true + }, + link_meta: { + upi_intent: true, + return_url: success_redirect_url + }, + link_notes: { + lago_customer_id: customer.id, + lago_invoice_id: invoice.id, + invoice_issuing_date: invoice.issuing_date.iso8601 + }, + link_id: "#{SecureRandom.uuid}.#{invoice.payment_attempts}", + link_amount: invoice.total_amount_cents / 100.to_f, + link_currency: invoice.currency.upcase, + link_purpose: invoice.id, + link_expiry_time: (Time.current + 10.minutes).iso8601, + link_partial_payments: false, + link_auto_reminders: true + } + end + + def invoice_payment_status(payment_status) + return :pending if PENDING_STATUSES.include?(payment_status) + return :succeeded if SUCCESS_STATUSES.include?(payment_status) + return :failed if FAILED_STATUSES.include?(payment_status) + + payment_status + end + + def update_invoice_payment_status(payment_status:, deliver_webhook: true) + @invoice = result.invoice + Rails.logger.info("Invoice: #{invoice}") + result = Invoices::UpdateService.call( + invoice:, + params: { + payment_status:, + ready_for_payment_processing: payment_status.to_sym != :succeeded + }, + webhook_notification: deliver_webhook + ) + result.raise_if_error! + end + + def increment_payment_attempts + invoice.update!(payment_attempts: invoice.payment_attempts + 1) + end + + def deliver_error_webhook(cashfree_error) + DeliverErrorWebhookService.call_async(invoice, { + provider_customer_id: customer.cashfree_customer.provider_customer_id, + provider_error: { + message: cashfree_error.msg, + error_code: cashfree_error.code + } + }) + end + end + end +end diff --git a/app/services/invoices/payments/create_service.rb b/app/services/invoices/payments/create_service.rb index f26e3c8f7cc5..cbe2c98496d3 100644 --- a/app/services/invoices/payments/create_service.rb +++ b/app/services/invoices/payments/create_service.rb @@ -13,6 +13,8 @@ def call case payment_provider when :stripe Invoices::Payments::StripeCreateJob.perform_later(invoice) + when :cashfree + Invoices::Payments::CashfreeCreateJob.perform_later(invoice) when :gocardless Invoices::Payments::GocardlessCreateJob.perform_later(invoice) when :adyen diff --git a/app/services/invoices/payments/payment_providers/factory.rb b/app/services/invoices/payments/payment_providers/factory.rb index 1fddd97ccd06..ab71253612ea 100644 --- a/app/services/invoices/payments/payment_providers/factory.rb +++ b/app/services/invoices/payments/payment_providers/factory.rb @@ -16,6 +16,8 @@ def self.service_class(payment_provider) Invoices::Payments::AdyenService when 'gocardless' Invoices::Payments::GocardlessService + when 'cashfree' + Invoices::Payments::CashfreeService else raise(NotImplementedError) end diff --git a/app/services/payment_provider_customers/create_service.rb b/app/services/payment_provider_customers/create_service.rb index 12ce2c71bb85..061a6c7d4a60 100644 --- a/app/services/payment_provider_customers/create_service.rb +++ b/app/services/payment_provider_customers/create_service.rb @@ -65,7 +65,7 @@ def create_customer_on_provider_service(async) return PaymentProviderCustomers::AdyenCreateJob.perform_later(result.provider_customer) if async PaymentProviderCustomers::AdyenCreateJob.perform_now(result.provider_customer) - else + elsif result.provider_customer.type == 'PaymentProviderCustomers::GocardlessCustomer' return PaymentProviderCustomers::GocardlessCreateJob.perform_later(result.provider_customer) if async PaymentProviderCustomers::GocardlessCreateJob.perform_now(result.provider_customer) diff --git a/app/services/payment_providers/cashfree_service.rb b/app/services/payment_providers/cashfree_service.rb new file mode 100644 index 000000000000..82c2752f8af3 --- /dev/null +++ b/app/services/payment_providers/cashfree_service.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module PaymentProviders + class CashfreeService < BaseService + LINK_STATUS_ACTIONS = %w[PAID].freeze + PAYMENT_ACTIONS = %w[SUCCESS FAILED USER_DROPPED CANCELLED VOID PENDING FLAGGED NOT_ATTEMPTED].freeze + # REFUND_ACTIONS = %w[created funds_returned paid refund_settled failed].freeze + + def create_or_update(**args) + payment_provider_result = PaymentProviders::FindService.call( + organization_id: args[:organization].id, + code: args[:code], + id: args[:id], + payment_provider_type: 'cashfree' + ) + + cashfree_provider = if payment_provider_result.success? + payment_provider_result.payment_provider + else + PaymentProviders::CashfreeProvider.new( + organization_id: args[:organization].id, + code: args[:code] + ) + end + + cashfree_provider.client_id = args[:client_id] if args.key?(:client_id) + cashfree_provider.client_secret = args[:client_secret] if args.key?(:client_secret) + cashfree_provider.success_redirect_url = args[:success_redirect_url] if args.key?(:success_redirect_url) + cashfree_provider.code = args[:code] if args.key?(:code) + cashfree_provider.name = args[:name] if args.key?(:name) + cashfree_provider.save! + + result.cashfree_provider = cashfree_provider + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + def handle_incoming_webhook(organization_id:, body:, timestamp:, signature:, code: nil) + payment_provider_result = PaymentProviders::FindService.call( + organization_id:, + code:, + payment_provider_type: 'cashfree' + ) + + return payment_provider_result unless payment_provider_result.success? + + secret_key = payment_provider_result.payment_provider.client_secret + data = "#{timestamp}#{body}" + gen_signature = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', secret_key, data)) + + unless gen_signature == signature + return result.service_failure!(code: 'webhook_error', message: 'Invalid signature') + end + + PaymentProviders::Cashfree::HandleEventJob.perform_later(event_json: body) + + result.event = body + result + end + + def handle_event(event_json:) + event = JSON.parse(event_json) + event_type = event['type'] + + case event_type + when 'PAYMENT_LINK_EVENT' + link_status = event.dig('data', 'link_status') + provider_payment_id = event.dig('data', 'link_notes', 'lago_invoice_id') + + if LINK_STATUS_ACTIONS.include?(link_status) && !provider_payment_id.nil? + update_payment_status_result = Invoices::Payments::CashfreeService + .new.update_payment_status( + provider_payment_id: provider_payment_id, + status: link_status + ) + + return update_payment_status_result unless update_payment_status_result.success? + end + end + + result.raise_if_error! + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 346acdf40874..e0cf972c3c83 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,6 +86,7 @@ resources :webhooks, only: [] do post 'stripe/:organization_id', to: 'webhooks#stripe', on: :collection, as: :stripe + post 'cashfree/:organization_id', to: 'webhooks#cashfree', on: :collection, as: :cashfree post 'gocardless/:organization_id', to: 'webhooks#gocardless', on: :collection, as: :gocardless post 'adyen/:organization_id', to: 'webhooks#adyen', on: :collection, as: :adyen end diff --git a/schema.graphql b/schema.graphql index 1cdc3c305533..52aa0f744f43 100644 --- a/schema.graphql +++ b/schema.graphql @@ -33,6 +33,22 @@ input AddAdyenPaymentProviderInput { successRedirectUrl: String } +""" +Cashfree input arguments +""" +input AddCashfreePaymentProviderInput { + clientId: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + clientSecret: String! + code: String! + name: String! + successRedirectUrl: String +} + """ Gocardless input arguments """ @@ -286,6 +302,15 @@ enum BillingTimeEnum { calendar } +type CashfreeProvider { + clientId: String + clientSecret: String + code: String! + id: ID! + name: String! + successRedirectUrl: String +} + type Charge { billableMetric: BillableMetric! chargeModel: ChargeModelEnum! @@ -3001,6 +3026,7 @@ type CurrentOrganization { adyenPaymentProviders: [AdyenProvider!] apiKey: String billingConfiguration: OrganizationBillingConfiguration + cashfreePaymentProviders: [CashfreeProvider!] city: String country: CountryCode createdAt: ISO8601DateTime! @@ -4392,6 +4418,16 @@ type Mutation { input: AddAdyenPaymentProviderInput! ): AdyenProvider + """ + Add or update Cashfree payment provider + """ + addCashfreePaymentProvider( + """ + Parameters for AddCashfreePaymentProvider + """ + input: AddCashfreePaymentProviderInput! + ): CashfreeProvider + """ Add or update Gocardless payment provider """ @@ -5154,6 +5190,16 @@ type Mutation { input: UpdateBillableMetricInput! ): BillableMetric + """ + Update Cashfree payment provider + """ + updateCashfreePaymentProvider( + """ + Parameters for UpdateCashfreePaymentProvider + """ + input: UpdateCashfreePaymentProviderInput! + ): CashfreeProvider + """ Update an existing coupon """ @@ -5497,7 +5543,7 @@ type OverdueBalanceCollection { metadata: CollectionMetadata! } -union PaymentProvider = AdyenProvider | GocardlessProvider | StripeProvider +union PaymentProvider = AdyenProvider | CashfreeProvider | GocardlessProvider | StripeProvider """ PaymentProviderCollection type @@ -5761,6 +5807,7 @@ enum ProviderPaymentMethodsEnum { enum ProviderTypeEnum { adyen + cashfree gocardless stripe } @@ -7368,6 +7415,20 @@ input UpdateBillableMetricInput { weightedInterval: WeightedIntervalEnum } +""" +Update input arguments +""" +input UpdateCashfreePaymentProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + code: String + id: ID! + name: String + successRedirectUrl: String +} + """ Autogenerated input type of UpdateCoupon """ diff --git a/schema.json b/schema.json index 3b587b9e81dd..6b31f2671c0a 100644 --- a/schema.json +++ b/schema.json @@ -203,6 +203,105 @@ ], "enumValues": null }, + { + "kind": "INPUT_OBJECT", + "name": "AddCashfreePaymentProviderInput", + "description": "Cashfree input arguments", + "interfaces": null, + "possibleTypes": null, + "fields": null, + "inputFields": [ + { + "name": "clientId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientSecret", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "successRedirectUrl", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "enumValues": null + }, { "kind": "INPUT_OBJECT", "name": "AddGocardlessPaymentProviderInput", @@ -2505,6 +2604,115 @@ "inputFields": null, "enumValues": null }, + { + "kind": "OBJECT", + "name": "CashfreeProvider", + "description": null, + "interfaces": [ + + ], + "possibleTypes": null, + "fields": [ + { + "name": "clientId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "clientSecret", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "code", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "successRedirectUrl", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + } + ], + "inputFields": null, + "enumValues": null + }, { "kind": "OBJECT", "name": "Charge", @@ -11291,6 +11499,28 @@ ] }, + { + "name": "cashfreePaymentProviders", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CashfreeProvider", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "city", "description": null, @@ -21428,6 +21658,35 @@ } ] }, + { + "name": "addCashfreePaymentProvider", + "description": "Add or update Cashfree payment provider", + "type": { + "kind": "OBJECT", + "name": "CashfreeProvider", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + { + "name": "input", + "description": "Parameters for AddCashfreePaymentProvider", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AddCashfreePaymentProviderInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, { "name": "addGocardlessPaymentProvider", "description": "Add or update Gocardless payment provider", @@ -23673,6 +23932,35 @@ } ] }, + { + "name": "updateCashfreePaymentProvider", + "description": "Update Cashfree payment provider", + "type": { + "kind": "OBJECT", + "name": "CashfreeProvider", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateCashfreePaymentProvider", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateCashfreePaymentProviderInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, { "name": "updateCoupon", "description": "Update an existing coupon", @@ -25375,6 +25663,11 @@ "name": "AdyenProvider", "ofType": null }, + { + "kind": "OBJECT", + "name": "CashfreeProvider", + "ofType": null + }, { "kind": "OBJECT", "name": "GocardlessProvider", @@ -28326,6 +28619,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "cashfree", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "adyen", "description": null, @@ -34829,6 +35128,81 @@ ], "enumValues": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateCashfreePaymentProviderInput", + "description": "Update input arguments", + "interfaces": null, + "possibleTypes": null, + "fields": null, + "inputFields": [ + { + "name": "code", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "successRedirectUrl", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "enumValues": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateCouponInput", diff --git a/spec/factories/payment_provider_customers.rb b/spec/factories/payment_provider_customers.rb index 9cd9c65e450f..af6614832f62 100644 --- a/spec/factories/payment_provider_customers.rb +++ b/spec/factories/payment_provider_customers.rb @@ -14,6 +14,12 @@ provider_customer_id { SecureRandom.uuid } end + factory :cashfree_customer, class: 'PaymentProviderCustomers::CashfreeCustomer' do + customer + + provider_customer_id { SecureRandom.uuid } + end + factory :adyen_customer, class: 'PaymentProviderCustomers::AdyenCustomer' do customer diff --git a/spec/factories/payment_providers.rb b/spec/factories/payment_providers.rb index 63e8fc73ae6e..ddf0ffbecd1c 100644 --- a/spec/factories/payment_providers.rb +++ b/spec/factories/payment_providers.rb @@ -61,4 +61,23 @@ success_redirect_url { Faker::Internet.url } end end + + factory :cashfree_provider, class: 'PaymentProviders::CashfreeProvider' do + organization + type { 'PaymentProviders::CashfreeProvider' } + code { "cashfree_account_#{SecureRandom.uuid}" } + name { 'Cashfree Account 1' } + + secrets do + {client_id: SecureRandom.uuid, client_secret: SecureRandom.uuid}.to_json + end + + settings do + {success_redirect_url:} + end + + transient do + success_redirect_url { Faker::Internet.url } + end + end end diff --git a/spec/graphql/mutations/payment_providers/cashfree/create_spec.rb b/spec/graphql/mutations/payment_providers/cashfree/create_spec.rb new file mode 100644 index 000000000000..3e22a1324ab0 --- /dev/null +++ b/spec/graphql/mutations/payment_providers/cashfree/create_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Mutations::PaymentProviders::Cashfree::Create, type: :graphql do + let(:required_permission) { 'organization:integrations:create' } + let(:membership) { create(:membership) } + let(:client_id) { '123456_abc' } + let(:client_secret) { 'cfsk_ma_prod_abc_123456' } + let(:code) { 'cashfree_1' } + let(:name) { 'Cashfree 1' } + let(:success_redirect_url) { Faker::Internet.url } + + let(:mutation) do + <<-GQL + mutation($input: AddCashfreePaymentProviderInput!) { + addCashfreePaymentProvider(input: $input) { + id, + code, + name, + clientId, + clientSecret + successRedirectUrl + } + } + GQL + end + + it_behaves_like 'requires current user' + it_behaves_like 'requires current organization' + it_behaves_like 'requires permission', 'organization:integrations:create' + + it 'creates a cashfree provider' do + result = execute_graphql( + current_user: membership.user, + current_organization: membership.organization, + # You wouldn't have `create` without `view` permission + # `view` is necessary to retrieve the created record in the response + permissions: [required_permission, 'organization:integrations:view'], + query: mutation, + variables: { + input: { + code:, + name:, + clientId: client_id, + clientSecret: client_secret, + successRedirectUrl: success_redirect_url + } + } + ) + + result_data = result['data']['addCashfreePaymentProvider'] + + aggregate_failures do + expect(result_data['id']).to be_present + expect(result_data['code']).to eq(code) + expect(result_data['name']).to eq(name) + expect(result_data['clientId']).to eq(client_id) + expect(result_data['clientSecret']).to eq(client_secret) + expect(result_data['successRedirectUrl']).to eq(success_redirect_url) + end + end +end diff --git a/spec/graphql/mutations/payment_providers/cashfree/update_spec.rb b/spec/graphql/mutations/payment_providers/cashfree/update_spec.rb new file mode 100644 index 000000000000..0721628035d7 --- /dev/null +++ b/spec/graphql/mutations/payment_providers/cashfree/update_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Mutations::PaymentProviders::Cashfree::Update, type: :graphql do + let(:required_permission) { 'organization:integrations:update' } + let(:membership) { create(:membership) } + let(:cashfree_provider) { create(:cashfree_provider, organization: membership.organization) } + let(:success_redirect_url) { Faker::Internet.url } + + let(:mutation) do + <<-GQL + mutation($input: UpdateCashfreePaymentProviderInput!) { + updateCashfreePaymentProvider(input: $input) { + id, + successRedirectUrl + } + } + GQL + end + + it_behaves_like 'requires current user' + it_behaves_like 'requires current organization' + it_behaves_like 'requires permission', 'organization:integrations:update' + + it 'updates an cashfree provider' do + result = execute_graphql( + current_user: membership.user, + current_organization: membership.organization, + # You wouldn't have `create` without `view` permission + # `view` is necessary to retrieve the created record in the response + permissions: [required_permission, 'organization:integrations:view'], + query: mutation, + variables: { + input: { + id: cashfree_provider.id, + successRedirectUrl: success_redirect_url + } + } + ) + + result_data = result['data']['updateCashfreePaymentProvider'] + + expect(result_data['successRedirectUrl']).to eq(success_redirect_url) + end + + context 'when success redirect url is nil' do + it 'removes success redirect url from the provider' do + result = execute_graphql( + current_user: membership.user, + current_organization: membership.organization, + permissions: required_permission, + query: mutation, + variables: { + input: { + id: cashfree_provider.id, + successRedirectUrl: nil + } + } + ) + + result_data = result['data']['updateCashfreePaymentProvider'] + + expect(result_data['successRedirectUrl']).to eq(nil) + end + end +end diff --git a/spec/graphql/resolvers/payment_provider_resolver_spec.rb b/spec/graphql/resolvers/payment_provider_resolver_spec.rb index 03d16a75f629..97a9d690ca99 100644 --- a/spec/graphql/resolvers/payment_provider_resolver_spec.rb +++ b/spec/graphql/resolvers/payment_provider_resolver_spec.rb @@ -14,6 +14,12 @@ name __typename } + ... on CashfreeProvider { + id + code + name + __typename + } ... on GocardlessProvider { id code diff --git a/spec/graphql/resolvers/payment_providers_resolver_spec.rb b/spec/graphql/resolvers/payment_providers_resolver_spec.rb index 539d9eb8ebfd..faf438144039 100644 --- a/spec/graphql/resolvers/payment_providers_resolver_spec.rb +++ b/spec/graphql/resolvers/payment_providers_resolver_spec.rb @@ -14,6 +14,11 @@ code __typename } + ... on CashfreeProvider { + id + code + __typename + } ... on GocardlessProvider { id code @@ -34,11 +39,13 @@ let(:membership) { create(:membership) } let(:organization) { membership.organization } let(:adyen_provider) { create(:adyen_provider, organization:) } + let(:cashfree_provider) { create(:cashfree_provider, organization:) } let(:gocardless_provider) { create(:gocardless_provider, organization:) } let(:stripe_provider) { create(:stripe_provider, organization:) } before do adyen_provider + cashfree_provider gocardless_provider stripe_provider end @@ -58,6 +65,11 @@ code __typename } + ... on CashfreeProvider { + id + code + __typename + } ... on GocardlessProvider { id code @@ -106,6 +118,11 @@ code __typename } + ... on CashfreeProvider { + id + code + __typename + } ... on GocardlessProvider { id code @@ -136,6 +153,9 @@ adyen_provider_result = payment_providers_response['collection'].find do |record| record['__typename'] == 'AdyenProvider' end + cashfree_provider_result = payment_providers_response['collection'].find do |record| + record['__typename'] == 'CashfreeProvider' + end gocardless_provider_result = payment_providers_response['collection'].find do |record| record['__typename'] == 'GocardlessProvider' end @@ -147,6 +167,7 @@ expect(payment_providers_response['collection'].count).to eq(3) expect(adyen_provider_result['id']).to eq(adyen_provider.id) + expect(cashfree_provider_result['id']).to eq(cashfree_provider.id) expect(gocardless_provider_result['id']).to eq(gocardless_provider.id) expect(stripe_provider_result['id']).to eq(stripe_provider.id) @@ -165,6 +186,10 @@ ... on AdyenProvider { livePrefix } + ... on CashfreeProvider { + clientId + clientSecret + } ... on GocardlessProvider { hasAccessToken } @@ -188,6 +213,8 @@ ) expect(adyen_provider.live_prefix).to be_a String + expect(cashfree_provider.client_id).to be_a String + expect(cashfree_provider.client_secret).to be_a String expect(gocardless_provider.access_token).to be_a String expect(stripe_provider.success_redirect_url).to be_a String diff --git a/spec/graphql/types/organizations/current_organization_type_spec.rb b/spec/graphql/types/organizations/current_organization_type_spec.rb index acedcaa8c91d..995b3fd9d688 100644 --- a/spec/graphql/types/organizations/current_organization_type_spec.rb +++ b/spec/graphql/types/organizations/current_organization_type_spec.rb @@ -37,6 +37,7 @@ it { is_expected.to have_field(:taxes).of_type('[Tax!]').with_permission('organization:taxes:view') } it { is_expected.to have_field(:adyen_payment_providers).of_type('[AdyenProvider!]').with_permission('organization:integrations:view') } + it { is_expected.to have_field(:cashfree_payment_providers).of_type('[CashfreeProvider!]').with_permission('organization:integrations:view') } it { is_expected.to have_field(:gocardless_payment_providers).of_type('[GocardlessProvider!]').with_permission('organization:integrations:view') } it { is_expected.to have_field(:stripe_payment_providers).of_type('[StripeProvider!]').with_permission('organization:integrations:view') } end diff --git a/spec/graphql/types/payment_providers/cashfree_input_spec.rb b/spec/graphql/types/payment_providers/cashfree_input_spec.rb new file mode 100644 index 000000000000..93de2c1ded30 --- /dev/null +++ b/spec/graphql/types/payment_providers/cashfree_input_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::PaymentProviders::CashfreeInput do + subject { described_class } + + it { is_expected.to accept_argument(:client_id).of_type('String!') } + it { is_expected.to accept_argument(:client_secret).of_type('String!') } + it { is_expected.to accept_argument(:code).of_type('String!') } + it { is_expected.to accept_argument(:name).of_type('String!') } + it { is_expected.to accept_argument(:success_redirect_url).of_type('String') } +end diff --git a/spec/graphql/types/payment_providers/cashfree_spec.rb b/spec/graphql/types/payment_providers/cashfree_spec.rb new file mode 100644 index 000000000000..bd41d543e1a6 --- /dev/null +++ b/spec/graphql/types/payment_providers/cashfree_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::PaymentProviders::Cashfree do + subject { described_class } + + it { is_expected.to have_field(:id).of_type('ID!') } + it { is_expected.to have_field(:code).of_type('String!') } + it { is_expected.to have_field(:name).of_type('String!') } + + it { is_expected.to have_field(:client_id).of_type('String').with_permission('organization:integrations:view') } + it { is_expected.to have_field(:client_secret).of_type('String').with_permission('organization:integrations:view') } + it { is_expected.to have_field(:success_redirect_url).of_type('String').with_permission('organization:integrations:view') } +end