Merge pull request #15 from exstake/feature/create-exchange-order-mutations
Feature/create exchange order mutations
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
module Inputs
|
||||
class CreateBuyCryptoOrderAttributesInput < Types::BaseInputObject
|
||||
argument :currency_id, ID, required: true
|
||||
argument :amount_cents, Integer, "Amount to be paid", required: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
module Inputs
|
||||
class CreateSellCryptoOrderAttributesInput < Types::BaseInputObject
|
||||
argument :currency_id, ID, required: true
|
||||
argument :amount, String, "Amount to be paid", required: true
|
||||
end
|
||||
end
|
||||
@@ -6,8 +6,16 @@ module Mutations
|
||||
input_object_class Types::BaseInputObject
|
||||
object_class Types::BaseObject
|
||||
|
||||
field :errors, [String],
|
||||
field :errors, [Types::RecordInvalidType],
|
||||
null: true,
|
||||
description: "Errors encountered during execution of the mutation."
|
||||
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def decode_id(encoded_id)
|
||||
GraphQL::Schema::UniqueWithinType.decode(encoded_id).last
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
26
app/graphql/mutations/create_buy_crypto_order.rb
Normal file
26
app/graphql/mutations/create_buy_crypto_order.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
module Mutations
|
||||
class CreateBuyCryptoOrder < BaseMutation
|
||||
field :order, Types::BuyCryptoOrderType, null: true
|
||||
|
||||
argument :order, Inputs::CreateBuyCryptoOrderAttributesInput, required: true
|
||||
|
||||
def resolve(order:)
|
||||
currency_id = decode_id(order[:currency_id])
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
current_user.fiat_balance.withdrawal!(order[:amount_cents])
|
||||
|
||||
record = BuyCryptoOrder.create!(
|
||||
paid_amount_cents: order[:amount_cents],
|
||||
currency_id: currency_id,
|
||||
user_id: current_user.id,
|
||||
)
|
||||
|
||||
{ order: record }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
{ errors: Resolvers::ModelErrors.from_active_record_model(e.record) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/graphql/mutations/create_sell_crypto_order.rb
Normal file
28
app/graphql/mutations/create_sell_crypto_order.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
module Mutations
|
||||
class CreateSellCryptoOrder < BaseMutation
|
||||
field :order, Types::SellCryptoOrderType, null: true
|
||||
|
||||
argument :order, Inputs::CreateSellCryptoOrderAttributesInput, required: true
|
||||
|
||||
def resolve(order:)
|
||||
currency_id = decode_id(order[:currency_id])
|
||||
amount = BigDecimal(order[:amount])
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
current_user.balances.find_by!(currency_id: currency_id)
|
||||
.withdrawal!(amount)
|
||||
|
||||
record = SellCryptoOrder.create(
|
||||
paid_amount: amount,
|
||||
currency_id: currency_id,
|
||||
user_id: current_user.id,
|
||||
)
|
||||
|
||||
{ order: record }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
{ errors: Resolvers::ModelErrors.from_active_record_model(e.record) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
39
app/graphql/resolvers/model_errors.rb
Normal file
39
app/graphql/resolvers/model_errors.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
module Resolvers
|
||||
class ModelErrors
|
||||
attr_reader :full_messages, :field_name, :messages, :path
|
||||
|
||||
def initialize(args)
|
||||
@full_messages = args[:full_messages]
|
||||
@field_name = args[:field_name]
|
||||
@messages = args[:messages]
|
||||
@path = args[:path]
|
||||
end
|
||||
|
||||
def self.from_active_record_model(model)
|
||||
return if model&.errors.blank?
|
||||
|
||||
model.errors.messages.map do |field, messages|
|
||||
new(
|
||||
full_messages: model.errors.full_messages_for(field),
|
||||
field_name: field,
|
||||
messages: messages,
|
||||
path: ["attributes", field]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_active_record_model_errors(errors)
|
||||
return if errors.blank?
|
||||
|
||||
errors.messages.map do |field, messages|
|
||||
new(
|
||||
full_messages: errors.full_messages_for(field),
|
||||
field_name: field,
|
||||
messages: messages,
|
||||
path: ["attributes", field]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/graphql/types/buy_crypto_order_type.rb
Normal file
22
app/graphql/types/buy_crypto_order_type.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BuyCryptoOrderType < Types::BaseObject
|
||||
implements GraphQL::Types::Relay::Node
|
||||
global_id_field :id
|
||||
|
||||
graphql_name "BuyCryptoOrder"
|
||||
|
||||
field :id, ID, null: false
|
||||
|
||||
field :currency, CurrencyType, null: false
|
||||
def currency
|
||||
dataloader.with(Dataloader::Source, Currency).load(object.currency_id)
|
||||
end
|
||||
|
||||
field :status, ProcessStatusEnum, null: false
|
||||
field :paid_amount_cents, Integer, null: false
|
||||
field :received_amount, String, null: true
|
||||
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,5 @@ module Types
|
||||
field :id, ID, null: false
|
||||
field :amount_cents, Integer, null: false
|
||||
field :amount_currency, String, null: false
|
||||
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
module Types
|
||||
class MutationType < Types::BaseObject
|
||||
field :create_sell_crypto_order, mutation: Mutations::CreateSellCryptoOrder
|
||||
field :create_buy_crypto_order, mutation: Mutations::CreateBuyCryptoOrder
|
||||
end
|
||||
end
|
||||
|
||||
10
app/graphql/types/process_status_enum.rb
Normal file
10
app/graphql/types/process_status_enum.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
module Types
|
||||
class ProcessStatusEnum < Types::BaseEnum
|
||||
graphql_name "ProcessStatus"
|
||||
|
||||
value "PROCESSING", value: "processing"
|
||||
value "COMPLETED", value: "completed"
|
||||
value "CANCELED", value: "canceled"
|
||||
end
|
||||
end
|
||||
@@ -18,5 +18,15 @@ module Types
|
||||
def fiat_balances
|
||||
Pundit.policy_scope(current_user, FiatBalance)
|
||||
end
|
||||
|
||||
field :sell_crypto_orders, SellCryptoOrderType.connection_type, null: false
|
||||
def sell_crypto_orders
|
||||
[]
|
||||
end
|
||||
|
||||
field :buy_crypto_orders, BuyCryptoOrderType.connection_type, null: false
|
||||
def buy_crypto_orders
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
11
app/graphql/types/record_invalid_type.rb
Normal file
11
app/graphql/types/record_invalid_type.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class RecordInvalidType < Types::BaseObject
|
||||
graphql_name "RecordInvalid"
|
||||
field :full_messages, [String], null: false
|
||||
field :field_name, String, null: true
|
||||
field :messages, [String], null: true
|
||||
field :path, [String], null: true
|
||||
end
|
||||
end
|
||||
22
app/graphql/types/sell_crypto_order_type.rb
Normal file
22
app/graphql/types/sell_crypto_order_type.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
module Types
|
||||
class SellCryptoOrderType < Types::BaseObject
|
||||
implements GraphQL::Types::Relay::Node
|
||||
global_id_field :id
|
||||
|
||||
graphql_name "SellCryptoOrder"
|
||||
|
||||
field :id, ID, null: false
|
||||
|
||||
field :currency, CurrencyType, null: false
|
||||
def currency
|
||||
dataloader.with(Dataloader::Source, Currency).load(object.currency_id)
|
||||
end
|
||||
|
||||
field :status, ProcessStatusEnum, null: false
|
||||
field :paid_amount, String, null: false
|
||||
field :received_amount_cents, Integer, null: true
|
||||
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
class XStakeSchema < GraphQL::Schema
|
||||
# mutation(Types::MutationType)
|
||||
mutation(Types::MutationType)
|
||||
query(Types::QueryType)
|
||||
use GraphQL::Dataloader
|
||||
|
||||
|
||||
180
app/javascript/__generated__/schema.graphql
generated
180
app/javascript/__generated__/schema.graphql
generated
@@ -34,19 +34,24 @@ type BalanceEdge {
|
||||
node: Balance!
|
||||
}
|
||||
|
||||
type Currency implements Node {
|
||||
type BuyCryptoOrder implements Node {
|
||||
createdAt: ISO8601DateTime!
|
||||
currency: Currency!
|
||||
id: ID!
|
||||
name: String!
|
||||
paidAmountCents: Int!
|
||||
receivedAmount: Float
|
||||
status: ProcessStatus!
|
||||
updatedAt: ISO8601DateTime!
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for Currency.
|
||||
The connection type for BuyCryptoOrder.
|
||||
"""
|
||||
type CurrencyConnection {
|
||||
type BuyCryptoOrderConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [CurrencyEdge!]!
|
||||
edges: [BuyCryptoOrderEdge!]!
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
@@ -57,7 +62,7 @@ type CurrencyConnection {
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type CurrencyEdge {
|
||||
type BuyCryptoOrderEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
@@ -66,7 +71,82 @@ type CurrencyEdge {
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: Currency!
|
||||
node: BuyCryptoOrder!
|
||||
}
|
||||
|
||||
input CreateBuyCryptoOrderAttributesInput {
|
||||
"""
|
||||
Amount to be paid
|
||||
"""
|
||||
amount: Float!
|
||||
currencyId: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of CreateBuyCryptoOrder
|
||||
"""
|
||||
input CreateBuyCryptoOrderInput {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
order: CreateBuyCryptoOrderAttributesInput!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of CreateBuyCryptoOrder
|
||||
"""
|
||||
type CreateBuyCryptoOrderPayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]
|
||||
order: BuyCryptoOrder!
|
||||
}
|
||||
|
||||
input CreateSellCryptoOrderAttributesInput {
|
||||
"""
|
||||
Amount to be paid
|
||||
"""
|
||||
amount: Float!
|
||||
currencyId: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of CreateSellCryptoOrder
|
||||
"""
|
||||
input CreateSellCryptoOrderInput {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
order: CreateSellCryptoOrderAttributesInput!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of CreateSellCryptoOrder
|
||||
"""
|
||||
type CreateSellCryptoOrderPayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]
|
||||
order: SellCryptoOrder!
|
||||
}
|
||||
|
||||
type Currency implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type FiatBalance implements Node {
|
||||
@@ -112,6 +192,21 @@ An ISO 8601-encoded datetime
|
||||
"""
|
||||
scalar ISO8601DateTime
|
||||
|
||||
type Mutation {
|
||||
createBuyCryptoOrder(
|
||||
"""
|
||||
Parameters for CreateBuyCryptoOrder
|
||||
"""
|
||||
input: CreateBuyCryptoOrderInput!
|
||||
): CreateBuyCryptoOrderPayload
|
||||
createSellCryptoOrder(
|
||||
"""
|
||||
Parameters for CreateSellCryptoOrder
|
||||
"""
|
||||
input: CreateSellCryptoOrderInput!
|
||||
): CreateSellCryptoOrderPayload
|
||||
}
|
||||
|
||||
"""
|
||||
An object with an ID.
|
||||
"""
|
||||
@@ -147,6 +242,12 @@ type PageInfo {
|
||||
startCursor: String
|
||||
}
|
||||
|
||||
enum ProcessStatus {
|
||||
CANCELED
|
||||
COMPLETED
|
||||
PROCESSING
|
||||
}
|
||||
|
||||
type Query {
|
||||
balances(
|
||||
"""
|
||||
@@ -169,7 +270,7 @@ type Query {
|
||||
"""
|
||||
last: Int
|
||||
): BalanceConnection!
|
||||
currencies(
|
||||
buyCryptoOrders(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
@@ -189,7 +290,7 @@ type Query {
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
): CurrencyConnection!
|
||||
): BuyCryptoOrderConnection!
|
||||
currentUser: User
|
||||
fiatBalances(
|
||||
"""
|
||||
@@ -232,6 +333,67 @@ type Query {
|
||||
"""
|
||||
ids: [ID!]!
|
||||
): [Node]!
|
||||
sellCryptoOrders(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
): SellCryptoOrderConnection!
|
||||
}
|
||||
|
||||
type SellCryptoOrder implements Node {
|
||||
createdAt: ISO8601DateTime!
|
||||
currency: Currency!
|
||||
id: ID!
|
||||
paidAmount: Float!
|
||||
receivedAmountCents: Int
|
||||
status: ProcessStatus!
|
||||
updatedAt: ISO8601DateTime!
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for SellCryptoOrder.
|
||||
"""
|
||||
type SellCryptoOrderConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [SellCryptoOrderEdge!]!
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type SellCryptoOrderEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: SellCryptoOrder!
|
||||
}
|
||||
|
||||
type User {
|
||||
|
||||
@@ -25,5 +25,13 @@ class Balance < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :currency
|
||||
|
||||
validates :amount, presence: true
|
||||
validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
def withdrawal!(value)
|
||||
update!(amount: amount - value)
|
||||
end
|
||||
|
||||
def deposit!(value)
|
||||
update!(amount: amount + value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,7 +24,17 @@ class FiatBalance < ApplicationRecord
|
||||
|
||||
monetize :amount_cents
|
||||
|
||||
validates :amount_cents, numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
def amount_formatted
|
||||
amount.format
|
||||
end
|
||||
|
||||
def withdrawal!(value)
|
||||
update!(amount_cents: amount_cents - value)
|
||||
end
|
||||
|
||||
def deposit!(value)
|
||||
update!(amount_cents: amount_cents + value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class User < ApplicationRecord
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
has_many :documents, class_name: "UserDocument", dependent: :destroy
|
||||
has_one :balance, dependent: :restrict_with_error
|
||||
has_many :balances, dependent: :restrict_with_error
|
||||
has_one :fiat_balance, dependent: :restrict_with_error
|
||||
|
||||
validates :first_name, :last_name, :email, presence: true
|
||||
|
||||
Reference in New Issue
Block a user