Skip to content

Commit

Permalink
feat: moneyhash provider
Browse files Browse the repository at this point in the history
feat(moneyhash): generate graphql schema for moneyhash

fix(moneyhash) customer creation

fix(moneyhash): webhook

feat(moneyhash): update required fields for connection creation/update

feat(moneyhash): build webhook_end_point based on LAGO_API_URL

feat(moneyhash): Use flow_id if exists

fix(moneyhash): correct amount before sending it

feat(moneyhash): remove redirect_url

cleanup: remove commented code

feat(moneyhash): make flow_id required during connection creation

feat(moneyhash): Only use flow_id instead of operation

feat(moneyhash): add more to custom_fields

feat(moneyhash): Implement moneyhash/payments/create_service

fix(moneyhash): get agreement_id from subscription_external_id

fix(payment_providers): possible types

regenerate graphql schema

fix formatting according to rubocop

revert unrelated change

shorten Types::Customers::Object#provider_customer

better refactor Types::Customers::Object#provider_customer

feat(moneyhash): put-back-success-url-field

Expected by code shared between providers

feat(moneyhash): revert a refactor, disable rubocop rule

cleanup: remove unncessary newline

remove potential duplicate email calls

Send email when can't create a payment, too

Fix payment_url for one-off invoices

Remove expire_after_seconds for invoice payments

Revamp PaymentProviders::Moneyhash::Payments::CreateService
  • Loading branch information
shahwan42 committed Feb 18, 2025
1 parent c0e6050 commit d0bf0a7
Show file tree
Hide file tree
Showing 36 changed files with 1,776 additions and 4 deletions.
18 changes: 18 additions & 0 deletions app/controllers/webhooks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,22 @@ def adyen
def adyen_params
params["notificationItems"]&.first&.dig("NotificationRequestItem")&.permit!
end

def moneyhash
result = PaymentProviders::Moneyhash::HandleIncomingWebhookService.call(
organization_id: params[:organization_id],
code: params[:code].presence,
body: JSON.parse(request.body.read)
)

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
end
20 changes: 20 additions & 0 deletions app/graphql/mutations/payment_providers/moneyhash/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Mutations
module PaymentProviders
module Moneyhash
class Base < BaseMutation
include AuthenticableApiUser
include RequiredOrganization

def resolve(**args)
result = ::PaymentProviders::MoneyhashService
.new(context[:current_user])
.create_or_update(**args.merge(organization: current_organization))

result.success? ? result.moneyhash_provider : result_error(result)
end
end
end
end
end
18 changes: 18 additions & 0 deletions app/graphql/mutations/payment_providers/moneyhash/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Mutations
module PaymentProviders
module Moneyhash
class Create < Base
REQUIRED_PERMISSION = "organization:integrations:create"

graphql_name "AddMoneyhashPaymentProvider"
description "Add Moneyhash payment provider"

input_object_class Types::PaymentProviders::MoneyhashInput

type Types::PaymentProviders::Moneyhash
end
end
end
end
18 changes: 18 additions & 0 deletions app/graphql/mutations/payment_providers/moneyhash/update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Mutations
module PaymentProviders
module Moneyhash
class Update < Base
REQUIRED_PERMISSION = "organization:integrations:update"

graphql_name "UpdateMoneyhashPaymentProvider"
description "Update Moneyhash payment provider"

input_object_class Types::PaymentProviders::UpdateInput

type Types::PaymentProviders::Moneyhash
end
end
end
end
2 changes: 2 additions & 0 deletions app/graphql/resolvers/payment_providers_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def provider_type(type)
PaymentProviders::GocardlessProvider.to_s
when "cashfree"
PaymentProviders::CashfreeProvider.to_s
when "moneyhash"
PaymentProviders::MoneyhashProvider.to_s
else
raise(NotImplementedError)
end
Expand Down
4 changes: 3 additions & 1 deletion app/graphql/types/customers/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def active_subscriptions_count
object.active_subscriptions.count
end

def provider_customer
def provider_customer # rubocop:disable GraphQL/ResolverMethodLength
case object&.payment_provider&.to_sym
when :stripe
object.stripe_customer
Expand All @@ -135,6 +135,8 @@ def provider_customer
object.cashfree_customer
when :adyen
object.adyen_customer
when :moneyhash
object.moneyhash_customer
end
end

Expand Down
2 changes: 2 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ class MutationType < Types::BaseObject
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_moneyhash_payment_provider, mutation: Mutations::PaymentProviders::Moneyhash::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_moneyhash_payment_provider, mutation: Mutations::PaymentProviders::Moneyhash::Update
field :update_stripe_payment_provider, mutation: Mutations::PaymentProviders::Stripe::Update

field :destroy_payment_provider, mutation: Mutations::PaymentProviders::Destroy
Expand Down
22 changes: 22 additions & 0 deletions app/graphql/types/payment_providers/moneyhash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Types
module PaymentProviders
class Moneyhash < Types::BaseObject
graphql_name "MoneyhashProvider"

field :api_key, String, null: true, permission: "organization:integrations:view"
field :code, String, null: false
field :flow_id, String, null: true, permission: "organization:integrations:view"
field :id, ID, null: false
field :name, String, null: false
field :success_redirect_url, String, null: true, permission: "organization:integrations:view"

# NOTE: Api key is a sensitive information. It should not be sent back to the
# front end application. Instead we send an obfuscated value
def api_key
"#{"•" * 8}#{object.api_key[-3..]}"
end
end
end
end
15 changes: 15 additions & 0 deletions app/graphql/types/payment_providers/moneyhash_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Types
module PaymentProviders
class MoneyhashInput < BaseInputObject
description "Moneyhash input arguments"

argument :api_key, String, required: true
argument :code, String, required: true
argument :flow_id, String, required: true
argument :name, String, required: true
argument :success_redirect_url, String, required: false
end
end
end
5 changes: 4 additions & 1 deletion app/graphql/types/payment_providers/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class Object < Types::BaseUnion
possible_types Types::PaymentProviders::Adyen,
Types::PaymentProviders::Gocardless,
Types::PaymentProviders::Stripe,
Types::PaymentProviders::Cashfree
Types::PaymentProviders::Cashfree,
Types::PaymentProviders::Moneyhash

def self.resolve_type(object, _context)
case object.class.to_s
Expand All @@ -20,6 +21,8 @@ def self.resolve_type(object, _context)
Types::PaymentProviders::Gocardless
when "PaymentProviders::CashfreeProvider"
Types::PaymentProviders::Cashfree
when "PaymentProviders::MoneyhashProvider"
Types::PaymentProviders::Moneyhash
else
raise "Unexpected Payment provider type: #{object.inspect}"
end
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/payment_providers/update_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class UpdateInput < BaseInputObject
description "Update input arguments"

argument :code, String, required: false
argument :flow_id, String, required: false
argument :id, ID, required: true
argument :name, String, required: false
argument :success_redirect_url, String, required: false
Expand Down
18 changes: 18 additions & 0 deletions app/jobs/invoices/payments/moneyhash_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Invoices
module Payments
class MoneyhashCreateJob < ApplicationJob
queue_as "providers"

unique :until_executed

retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6

def perform(invoice)
result = Invoices::Payments::MoneyhashService.new(invoice).create
result.raise_if_error!
end
end
end
end
14 changes: 14 additions & 0 deletions app/jobs/payment_provider_customers/moneyhash_checkout_url_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module PaymentProviderCustomers
class MoneyhashCheckoutUrlJob < ApplicationJob
queue_as :providers

retry_on ActiveJob::DeserializationError

def perform(moneyhash_customer)
result = PaymentProviderCustomers::MoneyhashService.new(moneyhash_customer).generate_checkout_url
result.raise_if_error!
end
end
end
15 changes: 15 additions & 0 deletions app/jobs/payment_provider_customers/moneyhash_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module PaymentProviderCustomers
class MoneyhashCreateJob < ApplicationJob
queue_as :providers

retry_on ActiveJob::DeserializationError

def perform(moneyhash_customer)
result = PaymentProviderCustomers::MoneyhashService.new(moneyhash_customer).create

result.raise_if_error!
end
end
end
14 changes: 14 additions & 0 deletions app/jobs/payment_providers/moneyhash/handle_event_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module PaymentProviders
module Moneyhash
class HandleEventJob < ApplicationJob
queue_as "providers"

def perform(organization:, event_json:)
result = ::PaymentProviders::MoneyhashService.new.handle_event(organization:, event_json:)
result.raise_if_error!
end
end
end
end
16 changes: 16 additions & 0 deletions app/jobs/payment_requests/payments/moneyhash_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class MoneyhashCreateJob < ApplicationJob
queue_as "providers"

unique :until_executed

def perform(payable)
result = PaymentRequests::Payments::MoneyhashService.new(payable).create
result.raise_if_error!
end
end
end
end
5 changes: 4 additions & 1 deletion app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ class Customer < ApplicationRecord
has_one :xero_customer, class_name: "IntegrationCustomers::XeroCustomer"
has_one :hubspot_customer, class_name: "IntegrationCustomers::HubspotCustomer"
has_one :salesforce_customer, class_name: "IntegrationCustomers::SalesforceCustomer"
has_one :moneyhash_customer, class_name: "PaymentProviderCustomers::MoneyhashCustomer"

PAYMENT_PROVIDERS = %w[stripe gocardless cashfree adyen].freeze
PAYMENT_PROVIDERS = %w[stripe gocardless cashfree adyen moneyhash].freeze

default_scope -> { kept }
sequenced scope: ->(customer) { customer.organization.customers.with_discarded },
Expand Down Expand Up @@ -169,6 +170,8 @@ def provider_customer
cashfree_customer
when :adyen
adyen_customer
when :moneyhash
moneyhash_customer
end
end

Expand Down
33 changes: 33 additions & 0 deletions app/models/payment_provider_customers/moneyhash_customer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module PaymentProviderCustomers
class MoneyhashCustomer < BaseCustomer
settings_accessors :payment_method_id
end
end

# == Schema Information
#
# Table name: payment_provider_customers
#
# id :uuid not null, primary key
# deleted_at :datetime
# settings :jsonb not null
# type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# customer_id :uuid not null
# payment_provider_id :uuid
# provider_customer_id :string
#
# Indexes
#
# index_payment_provider_customers_on_customer_id_and_type (customer_id,type) UNIQUE WHERE (deleted_at IS NULL)
# index_payment_provider_customers_on_payment_provider_id (payment_provider_id)
# index_payment_provider_customers_on_provider_customer_id (provider_customer_id)
#
# Foreign Keys
#
# fk_rails_... (customer_id => customers.id)
# fk_rails_... (payment_provider_id => payment_providers.id)
#
61 changes: 61 additions & 0 deletions app/models/payment_providers/moneyhash_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module PaymentProviders
class MoneyhashProvider < BaseProvider
SUCCESS_REDIRECT_URL = "https://moneyhash.io/"

validates :api_key, presence: true
validates :flow_id, url: true, presence: true, length: {maximum: 20}

secrets_accessors :api_key
settings_accessors :flow_id

def self.api_base_url
if Rails.env.production?
"https://web.moneyhash.io"
else
"https://staging-web.moneyhash.io"
end
end

def webhook_end_point
URI.join(
ENV["LAGO_API_URL"],
"webhooks/moneyhash/#{organization_id}?code=#{URI.encode_www_form_component(code)}"
)
end

def environment
if Rails.env.production? && live_prefix.present?
:live
else
:test
end
end
end
end

# == Schema Information
#
# Table name: payment_providers
#
# id :uuid not null, primary key
# code :string not null
# deleted_at :datetime
# name :string not null
# secrets :string
# settings :jsonb not null
# type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :uuid not null
#
# Indexes
#
# index_payment_providers_on_code_and_organization_id (code,organization_id) UNIQUE WHERE (deleted_at IS NULL)
# index_payment_providers_on_organization_id (organization_id)
#
# Foreign Keys
#
# fk_rails_... (organization_id => organizations.id)
#
3 changes: 3 additions & 0 deletions app/serializers/v1/customer_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def billing_configuration
when :adyen
configuration[:provider_customer_id] = model.adyen_customer&.provider_customer_id
configuration.merge!(model.adyen_customer&.settings&.symbolize_keys || {})
when :moneyhash
configuration[:provider_customer_id] = model.moneyhash_customer&.provider_customer_id
configuration.merge!(model.moneyhash_customer&.settings&.symbolize_keys || {})
end

configuration
Expand Down
Loading

0 comments on commit d0bf0a7

Please sign in to comment.