Skip to content

Commit

Permalink
use cryptomus payment gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
senid231 committed Jul 2, 2023
1 parent 8fd7864 commit 0fc0e02
Show file tree
Hide file tree
Showing 26 changed files with 591 additions and 28 deletions.
10 changes: 4 additions & 6 deletions app/admin/billing/accounts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,10 @@ def scoped_collection
end

sidebar 'Create Payment', only: [:show] do
active_admin_form_for(Payment.new(account_id: params[:id]),
url: payment_account_path(params[:id]),
as: :payment,
method: :post) do |f|
active_admin_form_for(Payment.new, url: payment_account_path(params[:id]), as: :payment, method: :post) do |f|
f.inputs do
f.input :account_id, as: :hidden
f.input :status_id, as: :hidden, value: Payment::CONST::STATUS_ID_COMPLETED
f.input :account_id, as: :hidden, value: params[:id]
f.input :amount, input_html: { style: 'width: 200px' }
f.input :notes, input_html: { style: 'width: 200px' }
end
Expand Down Expand Up @@ -291,7 +289,7 @@ def scoped_collection

member_action :payment, method: :post do
authorize!
payment_params = params.require(:payment).permit(:account_id, :amount, :notes)
payment_params = params.require(:payment).permit(:account_id, :amount, :notes, :status_id)
payment = Payment.new(payment_params)
if payment.save
flash[:notice] = 'Payment created!'
Expand Down
29 changes: 19 additions & 10 deletions app/admin/billing/payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
config.batch_actions = false
actions :index, :create, :new, :show

permit_params :account_id, :amount, :notes, :private_notes
permit_params :account_id, :amount, :notes, :private_notes, :status_id
scope :all, default: true
scope :today
scope :yesterday
Expand All @@ -17,7 +17,8 @@
[:account_name, proc { |row| row.account.try(:name) }],
:amount,
:notes,
:private_notes
:private_notes,
:status

controller do
def scoped_collection
Expand All @@ -29,6 +30,7 @@ def scoped_collection
f.semantic_errors *f.object.errors.attribute_names

f.inputs form_title do
f.input :status_id, as: :hidden, value: Payment::CONST::STATUS_ID_COMPLETED
f.account_input :account_id
f.input :amount
f.input :private_notes
Expand All @@ -41,15 +43,16 @@ def scoped_collection
id_column
column :created_at
column :account, footer: lambda {
strong do
'Total:'
end
}
strong do
'Total:'
end
}
column :status, :status_formatted, sortabla: :status_id
column :amount, footer: lambda {
strong do
@footer_data[:total_amount]
end
}
strong do
@footer_data[:total_amount]
end
}
column :private_notes
column :notes
column :uuid
Expand All @@ -59,6 +62,11 @@ def scoped_collection
filter :uuid_equals, label: 'UUID'
filter :created_at, as: :date_time_range
account_filter :account_id_eq
filter :status_id,
label: 'Status',
as: :select,
collection: proc { Payment::CONST::STATUS_IDS },
input_html: { class: 'chosen' }
filter :amount
filter :private_notes
filter :notes
Expand All @@ -69,6 +77,7 @@ def scoped_collection
row :uuid
row :created_at
row :account
row :status, :status_formatted
row :amount
row :private_notes
row :notes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Api::Rest::Customer::V1::CryptomusPaymentsController < Api::Rest::Customer::V1::BaseController
end
45 changes: 45 additions & 0 deletions app/controllers/cryptomus_webhooks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

class CryptomusWebhooksController < ActionController::API
include CaptureError::ControllerMethods

rescue_from StandardError, with: :handle_exceptions
rescue_from ActiveRecord::RecordNotFound, with: :handle_exceptions

# https://doc.cryptomus.com/payments/webhook
def create
payload = params.except(:controller, :action, :sign).to_unsafe_h
sign = params[:sign]

unless Cryptomus::WebhookValidator.validate(payload:, sign:)
Rails.logger.info { 'invalid signature' }
head 400
return
end

unless payload['is_final']
Rails.logger.info { "status is not final: #{payload['status']}" }
head 204
return
end

payment = Payment.find(payload['order_id'])
CryptomusPayment::CheckStatus.call(payment:)
head 204
end

private

def handle_exceptions(error)
log_error(error)
capture_error(error)
head 500
end

def capture_extra
{
payload: params.except(:controller, :action, :sign).to_unsafe_h,
sign: params[:sign]
}
end
end
22 changes: 22 additions & 0 deletions app/decorators/payment_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class PaymentDecorator < BillingDecorator
delegate_all
decorates Payment

def status_formatted
status_tag(status, class: status_color)
end

private

def status_color
if model.completed?
:ok
elsif model.pending?
:warning
elsif model.canceled?
:error
end
end
end
59 changes: 59 additions & 0 deletions app/forms/customer_api/cryptomus_payment_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module CustomerApi
class CryptomusPaymentForm < ApplicationForm
EXPIRATION_SEC = 24 * 60 * 60 # expires in 24 hours

def self.policy_class
PaymentPolicy
end

attr_reader :id, :url

attr_accessor :customer_id, :allowed_account_ids

attribute :amount, :decimal
attribute :notes, :string
attribute :account_id, :integer

validates :amount, presence: true
validates :amount, numericality: { greater_than_or_equal_to: 0.01 }, allow_nil: true

validates :account, presence: true

# @!method account
define_memoizable :account, apply: lambda {
return if account_id.nil?

scope = Account.where(contractor_id: customer_id)
scope = scope.where(id: allowed_account_ids) if allowed_account_ids.present?
scope.find_by(uuid: account_id)
}

def persisted?
id.present?
end

private

def _save
ApplicationRecord.transaction do
payment = Payment.create!(
account:,
amount:,
notes:,
status: Payment::CONST::STATUS_ID_PENDING
)
crypto_payment = Cryptomus::Client.create_payment(
order_id: payment.id,
amount: amount.to_s,
currency: 'USD',
lifetime: EXPIRATION_SEC,
subtract: 100 # customer will pay 100% of commission
)
@url = crypto_payment[:result][:url]
@id = payment.uuid
end
end
end
end
87 changes: 78 additions & 9 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# uuid :uuid not null
# created_at :timestamptz not null
# account_id :integer(4) not null
# status_id :integer(2) not null
#
# Indexes
#
Expand All @@ -23,19 +24,87 @@
#

class Payment < ApplicationRecord
belongs_to :account
module CONST
STATUS_ID_CANCELED = 10
STATUS_ID_COMPLETED = 20
STATUS_ID_PENDING = 30
STATUS_IDS = {
STATUS_ID_CANCELED => 'canceled',
STATUS_ID_COMPLETED => 'completed',
STATUS_ID_PENDING => 'pending'
}.freeze

freeze
end

include WithPaperTrail

validates :amount, numericality: true
validates :account, presence: true
belongs_to :account, class_name: 'Account'

validates :amount, presence: true
validates :amount, numericality: { greater_than_or_equal_to: 0.01 }, allow_nil: true

validates :status_id, presence: true
validates :status_id, inclusion: { in: CONST::STATUS_IDS.keys }, allow_nil: true

before_save :top_up_balance

# eq not_eq in not_in
scope :status_eq, lambda { |value|
status_id = CONST::STATUS_IDS.key(value)
status_id ? where(status_id:) : none
}

scope :status_not_eq, lambda { |value|
status_id = CONST::STATUS_IDS.key(value)
status_id ? where.not(status_id:) : all
}

scope :status_in, lambda { |values|
status_ids = values.map { |value| CONST::STATUS_IDS.key(value) }.compact
status_ids.present? ? where(status_id: status_ids) : none
}

scope :status_not_in, lambda { |values|
status_ids = values.map { |value| CONST::STATUS_IDS.key(value) }.compact
status_ids.present? ? where.not(status_id: status_ids) : all
}

scope :today, lambda {
where('created_at >= ? ', Time.now.at_beginning_of_day)
}

scope :yesterday, lambda {
where('created_at >= ? and created_at < ?', 1.day.ago.at_beginning_of_day, Time.now.at_beginning_of_day)
}

before_create do
account.lock! # will generate SELECT FOR UPDATE SQL statement
account.balance += amount
throw(:abort) unless account.save
def status
CONST::STATUS_IDS[status_id]
end

scope :today, -> { where('created_at >= ? ', Time.now.at_beginning_of_day) }
scope :yesterday, -> { where('created_at >= ? and created_at < ?', 1.day.ago.at_beginning_of_day, Time.now.at_beginning_of_day) }
def completed?
status_id == CONST::STATUS_ID_COMPLETED
end

def canceled?
status_id == CONST::STATUS_ID_CANCELED
end

def pending?
status_id == CONST::STATUS_ID_PENDING
end

def self.ransackable_scopes(_auth_object = nil)
%i[status_eq status_not_eq status_in status_not_in]
end

private

def top_up_balance
if status_id_changed? && completed?
account.lock! # will generate SELECT FOR UPDATE SQL statement
account.balance += amount
throw(:abort) unless account.save
end
end
end
7 changes: 6 additions & 1 deletion app/resources/api/rest/admin/payment_resource.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# frozen_string_literal: true

class Api::Rest::Admin::PaymentResource < BaseResource
attributes :amount, :notes
attributes :amount, :notes, :status

paginator :paged

has_one :account

ransack_filter :amount, type: :number
ransack_filter :notes, type: :string
ransack_filter :status, type: :enum, collection: Payment::CONST::STATUS_IDS.values

def self.creatable_fields(_context)
%i[
Expand All @@ -17,4 +18,8 @@ def self.creatable_fields(_context)
notes
]
end

def self.sortable_fields(_context)
%i[amount notes]
end
end
23 changes: 23 additions & 0 deletions app/resources/api/rest/customer/v1/cryptomus_payment_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class Api::Rest::Customer::V1::CryptomusPaymentResource < Api::Rest::Customer::V1::BaseResource
model_name 'CustomerApi::CryptomusPaymentForm'

attributes :amount,
:notes,
:url,
:payment_id

has_one :account, foreign_key_on: :related

before_create { _model.customer_id = context[:customer_id] }
before_create { _model.allowed_account_ids = context[:allowed_account_ids] }

def self.creatable_fields(_ctx = nil)
%i[amount notes account]
end

def fetchable_fields
%i[url]
end
end
Loading

0 comments on commit 0fc0e02

Please sign in to comment.