diff --git a/app/graphql/inputs/create_buy_crypto_order_attributes_input.rb b/app/graphql/inputs/create_buy_crypto_order_attributes_input.rb new file mode 100644 index 0000000..3b3f37e --- /dev/null +++ b/app/graphql/inputs/create_buy_crypto_order_attributes_input.rb @@ -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 diff --git a/app/graphql/inputs/create_sell_crypto_order_attributes_input.rb b/app/graphql/inputs/create_sell_crypto_order_attributes_input.rb new file mode 100644 index 0000000..e6da70f --- /dev/null +++ b/app/graphql/inputs/create_sell_crypto_order_attributes_input.rb @@ -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 diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index e589946..207afde 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -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 diff --git a/app/graphql/mutations/create_buy_crypto_order.rb b/app/graphql/mutations/create_buy_crypto_order.rb new file mode 100644 index 0000000..9efb152 --- /dev/null +++ b/app/graphql/mutations/create_buy_crypto_order.rb @@ -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 diff --git a/app/graphql/mutations/create_sell_crypto_order.rb b/app/graphql/mutations/create_sell_crypto_order.rb new file mode 100644 index 0000000..9b70114 --- /dev/null +++ b/app/graphql/mutations/create_sell_crypto_order.rb @@ -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 diff --git a/app/graphql/resolvers/model_errors.rb b/app/graphql/resolvers/model_errors.rb new file mode 100644 index 0000000..8156a01 --- /dev/null +++ b/app/graphql/resolvers/model_errors.rb @@ -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 diff --git a/app/graphql/types/buy_crypto_order_type.rb b/app/graphql/types/buy_crypto_order_type.rb new file mode 100644 index 0000000..1c28420 --- /dev/null +++ b/app/graphql/types/buy_crypto_order_type.rb @@ -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 diff --git a/app/graphql/types/fiat_balance_type.rb b/app/graphql/types/fiat_balance_type.rb index 65eaa24..be79b3e 100644 --- a/app/graphql/types/fiat_balance_type.rb +++ b/app/graphql/types/fiat_balance_type.rb @@ -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 diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index c928833..a1405bb 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -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 diff --git a/app/graphql/types/process_status_enum.rb b/app/graphql/types/process_status_enum.rb new file mode 100644 index 0000000..9d5704c --- /dev/null +++ b/app/graphql/types/process_status_enum.rb @@ -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 diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index a87b9e2..480509a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -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 diff --git a/app/graphql/types/record_invalid_type.rb b/app/graphql/types/record_invalid_type.rb new file mode 100644 index 0000000..42ea150 --- /dev/null +++ b/app/graphql/types/record_invalid_type.rb @@ -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 diff --git a/app/graphql/types/sell_crypto_order_type.rb b/app/graphql/types/sell_crypto_order_type.rb new file mode 100644 index 0000000..c70bcde --- /dev/null +++ b/app/graphql/types/sell_crypto_order_type.rb @@ -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 diff --git a/app/graphql/x_stake_schema.rb b/app/graphql/x_stake_schema.rb index e95c99a..e7c28a0 100644 --- a/app/graphql/x_stake_schema.rb +++ b/app/graphql/x_stake_schema.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class XStakeSchema < GraphQL::Schema - # mutation(Types::MutationType) + mutation(Types::MutationType) query(Types::QueryType) use GraphQL::Dataloader diff --git a/app/javascript/__generated__/schema.graphql b/app/javascript/__generated__/schema.graphql index c945e2f..16763b3 100644 --- a/app/javascript/__generated__/schema.graphql +++ b/app/javascript/__generated__/schema.graphql @@ -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 { diff --git a/app/models/user.rb b/app/models/user.rb index cbc1d8d..26d827a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/spec/graphql/inputs/create_buy_crypto_order_attributes_input_spec.rb b/spec/graphql/inputs/create_buy_crypto_order_attributes_input_spec.rb new file mode 100644 index 0000000..84a1cbc --- /dev/null +++ b/spec/graphql/inputs/create_buy_crypto_order_attributes_input_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Inputs::CreateBuyCryptoOrderAttributesInput do + subject { described_class } + + describe "fields" do + it { is_expected.to(accept_argument("currency_id").of_type("ID!")) } + it { is_expected.to(accept_argument("amount_cents").of_type("Int!")) } + end +end diff --git a/spec/graphql/inputs/create_sell_crypto_order_attributes_input_spec.rb b/spec/graphql/inputs/create_sell_crypto_order_attributes_input_spec.rb new file mode 100644 index 0000000..84607d7 --- /dev/null +++ b/spec/graphql/inputs/create_sell_crypto_order_attributes_input_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Inputs::CreateSellCryptoOrderAttributesInput do + subject { described_class } + + describe "fields" do + it { is_expected.to(accept_argument("currency_id").of_type("ID!")) } + it { is_expected.to(accept_argument("amount").of_type("String!")) } + end +end diff --git a/spec/graphql/mutations/create_buy_crypto_order_spec.rb b/spec/graphql/mutations/create_buy_crypto_order_spec.rb new file mode 100644 index 0000000..86ebbb5 --- /dev/null +++ b/spec/graphql/mutations/create_buy_crypto_order_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe(Mutations::CreateBuyCryptoOrder, type: :mutation) do + let(:query_string) do + <<~GQL + mutation($currencyId: ID!, $amountCents: Int!) { + createBuyCryptoOrder(input: { + order: { + currencyId: $currencyId, + amountCents: $amountCents, + } + }) { + errors { + fullMessages + fieldName + messages + path + } + order { + status + paidAmountCents + receivedAmount + currency { + name + } + } + } + } + GQL + end + + context "when the user has enough balance" do + it "withdraws from his account and creates a buy order" do + currency = create(:currency) + user = create( + :user, + fiat_balance: build( + :fiat_balance, + amount_cents: 100_00 + ), + ) + + currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id) + + variables = { + "currencyId": currency_global_id, + "amountCents": 90_00, + } + + context = { current_user: user } + + result = XStakeSchema.execute( + query_string, + variables: variables, + context: context + ).to_h.with_indifferent_access + + expect(result).to(eq({ + "data" => { + "createBuyCryptoOrder" => { + "errors" => nil, + "order" => { + "status" => "PROCESSING", + "paidAmountCents" => 90_00, + "receivedAmount" => nil, + "currency" => { + "name" => "CAKE", + }, + }, + }, + }, + })) + + expect(user.fiat_balance.reload.amount_cents).to(eq(10_00)) + end + end + + context "when the user does not have enough balance" do + it "returns withdrawl error" do + currency = create(:currency) + user = create( + :user, + fiat_balance: build( + :fiat_balance, + amount_cents: 80_00 + ), + ) + + currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id) + + variables = { + "currencyId": currency_global_id, + "amountCents": 90_00, + } + + context = { current_user: user } + + result = XStakeSchema.execute( + query_string, + variables: variables, + context: context + ).to_h.with_indifferent_access + + expect(result).to(eq({ + "data" => { + "createBuyCryptoOrder" => { + "errors" => [{ + "fullMessages" => ["Quantia saldo insuficiente"], + "fieldName" => "amount_cents", + "messages" => ["saldo insuficiente"], + "path" => ["attributes", "amount_cents"], + }], + "order" => nil, + }, + }, + })) + end + end +end diff --git a/spec/graphql/mutations/create_sell_crypto_order_spec.rb b/spec/graphql/mutations/create_sell_crypto_order_spec.rb new file mode 100644 index 0000000..b817e14 --- /dev/null +++ b/spec/graphql/mutations/create_sell_crypto_order_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe(Mutations::CreateSellCryptoOrder, type: :mutation) do + let(:query_string) do + <<~GQL + mutation($currencyId: ID!, $amount: String!) { + createSellCryptoOrder(input: { + order: { + currencyId: $currencyId, + amount: $amount, + } + }) { + errors { + fullMessages + fieldName + messages + path + } + order { + status + paidAmount + receivedAmountCents + currency { + name + } + } + } + } + GQL + end + + context "when the user has enough balance" do + it "withdraws from his account and creates a buy order" do + currency = create(:currency) + user = create( + :user, + balances: [ + build(:balance, currency: currency, amount: 1.0034), + ] + ) + + currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id) + + variables = { + "currencyId": currency_global_id, + "amount": "0.80", + } + + context = { current_user: user } + + result = XStakeSchema.execute( + query_string, + variables: variables, + context: context + ).to_h.with_indifferent_access + + expect(result).to(eq({ + "data" => { + "createSellCryptoOrder" => { + "errors" => nil, + "order" => { + "status" => "PROCESSING", + "paidAmount" => "0.8", + "receivedAmountCents" => nil, + "currency" => { + "name" => "CAKE", + }, + }, + }, + }, + })) + + expect(user.balances.first.reload.amount.to_s).to(eq("0.2034")) + end + end + + context "when the user does not have enough balance" do + it "returns withdrawl error" do + currency = create(:currency) + user = create( + :user, + balances: [ + build(:balance, currency: currency, amount: 0.0034), + ] + ) + + currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id) + + variables = { + "currencyId": currency_global_id, + "amount": "0.80", + } + + context = { current_user: user } + + result = XStakeSchema.execute( + query_string, + variables: variables, + context: context + ).to_h.with_indifferent_access + + expect(result).to(eq({ + "data" => { + "createSellCryptoOrder" => { + "errors" => [{ + "fullMessages" => ["Quantia saldo insuficiente"], + "fieldName" => "amount", + "messages" => ["saldo insuficiente"], + "path" => ["attributes", "amount"], + }], + "order" => nil, + }, + }, + })) + + expect(user.balances.first.reload.amount.to_s).to(eq("0.0034")) + end + end +end diff --git a/spec/graphql/types/buy_crypto_order_type_spec.rb b/spec/graphql/types/buy_crypto_order_type_spec.rb new file mode 100644 index 0000000..27ed225 --- /dev/null +++ b/spec/graphql/types/buy_crypto_order_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe(Types::BuyCryptoOrderType) do + subject { described_class } + + describe "arguments" do + it { is_expected.to(have_a_field(:id).of_type("ID!")) } + it { is_expected.to(have_a_field(:currency).of_type("Currency!")) } + it { is_expected.to(have_a_field(:paid_amount_cents).of_type("Int!")) } + it { is_expected.to(have_a_field(:received_amount).of_type("String")) } + it { is_expected.to(have_a_field(:status).of_type("ProcessStatus!")) } + it { is_expected.to(have_a_field(:created_at).of_type("ISO8601DateTime!")) } + it { is_expected.to(have_a_field(:updated_at).of_type("ISO8601DateTime!")) } + end +end diff --git a/spec/graphql/types/process_status_enum_spec.rb b/spec/graphql/types/process_status_enum_spec.rb new file mode 100644 index 0000000..8bcb0c5 --- /dev/null +++ b/spec/graphql/types/process_status_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Types::ProcessStatusEnum do + describe "values" do + it { expect(described_class.values.keys).to(match(["PROCESSING", "COMPLETED", "CANCELED"])) } + end +end diff --git a/spec/graphql/types/record_invalid_type_spec.rb b/spec/graphql/types/record_invalid_type_spec.rb new file mode 100644 index 0000000..98416ce --- /dev/null +++ b/spec/graphql/types/record_invalid_type_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Types::RecordInvalidType do + subject { described_class } + + describe "fields" do + it { is_expected.to(have_field(:full_messages).of_type("[String!]!")) } + it { is_expected.to(have_field(:field_name).of_type("String")) } + it { is_expected.to(have_field(:messages).of_type("[String!]")) } + it { is_expected.to(have_field(:path).of_type("[String!]")) } + end +end diff --git a/spec/graphql/types/sell_crypto_order_type_spec.rb b/spec/graphql/types/sell_crypto_order_type_spec.rb new file mode 100644 index 0000000..65b2e67 --- /dev/null +++ b/spec/graphql/types/sell_crypto_order_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe(Types::SellCryptoOrderType) do + subject { described_class } + + describe "arguments" do + it { is_expected.to(have_a_field(:id).of_type("ID!")) } + it { is_expected.to(have_a_field(:currency).of_type("Currency!")) } + it { is_expected.to(have_a_field(:paid_amount).of_type("String!")) } + it { is_expected.to(have_a_field(:received_amount_cents).of_type("Int")) } + it { is_expected.to(have_a_field(:status).of_type("ProcessStatus!")) } + it { is_expected.to(have_a_field(:created_at).of_type("ISO8601DateTime!")) } + it { is_expected.to(have_a_field(:updated_at).of_type("ISO8601DateTime!")) } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ce70b21..bc8481d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -31,7 +31,7 @@ RSpec.describe(User, type: :model) do describe "associations" do it { is_expected.to(have_many(:documents)) } - it { is_expected.to(have_one(:balance)) } + it { is_expected.to(have_many(:balances)) } it { is_expected.to(have_one(:fiat_balance)) } end end