diff --git a/Gemfile b/Gemfile index d12bceb..4801de5 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ gem "devise-i18n" gem "administrate-field-active_storage" gem "tailwindcss-rails" gem "administrate" +gem "money-rails" gem "enumerize" gem "graphql" gem "pundit" diff --git a/Gemfile.lock b/Gemfile.lock index 4d8f785..38f8401 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,6 +157,15 @@ GEM minitest (5.14.4) momentjs-rails (2.20.1) railties (>= 3.1) + monetize (1.9.4) + money (~> 6.12) + money (6.13.8) + i18n (>= 0.6.4, <= 2) + money-rails (1.14.0) + activesupport (>= 3.0) + monetize (~> 1.9.0) + money (~> 6.13.2) + railties (>= 3.0) msgpack (1.4.2) nio4r (2.5.8) nokogiri (1.12.1) @@ -330,6 +339,7 @@ DEPENDENCIES graphql_playground-rails image_processing (~> 1.12) listen (~> 3.3) + money-rails pg (~> 1.1) pry-byebug puma (~> 5.0) diff --git a/app/graphql/types/fiat_balance_type.rb b/app/graphql/types/fiat_balance_type.rb new file mode 100644 index 0000000..65eaa24 --- /dev/null +++ b/app/graphql/types/fiat_balance_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +module Types + class FiatBalanceType < Types::BaseObject + implements GraphQL::Types::Relay::Node + global_id_field :id + + graphql_name "FiatBalance" + + 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/query_type.rb b/app/graphql/types/query_type.rb index 95b6fb0..a87b9e2 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -13,5 +13,10 @@ module Types def balances Pundit.policy_scope(current_user, Balance) end + + field :fiat_balances, FiatBalanceType.connection_type, null: false + def fiat_balances + Pundit.policy_scope(current_user, FiatBalance) + end end end diff --git a/app/graphql/x_stake_schema.rb b/app/graphql/x_stake_schema.rb index 8e5f10d..e95c99a 100644 --- a/app/graphql/x_stake_schema.rb +++ b/app/graphql/x_stake_schema.rb @@ -10,6 +10,8 @@ class XStakeSchema < GraphQL::Schema Types::CurrencyType when Balance Types::BalanceType + when FiatBalance + Types::FiatBalanceType else raise(GraphQL::RequiredImplementationMissingError, "Unexpected object: #{obj}") end diff --git a/app/javascript/__generated__/schema.graphql b/app/javascript/__generated__/schema.graphql index 1594f5e..ebecaf9 100644 --- a/app/javascript/__generated__/schema.graphql +++ b/app/javascript/__generated__/schema.graphql @@ -1,5 +1,5 @@ type Balance implements Node { - amount: Float! + amount: String! currency: Currency! id: ID! } @@ -44,6 +44,54 @@ type Currency implements Node { name: String! } +type FiatBalance implements Node { + amountCents: Int! + amountCurrency: String! + createdAt: ISO8601DateTime! + id: ID! + updatedAt: ISO8601DateTime! +} + +""" +The connection type for FiatBalance. +""" +type FiatBalanceConnection { + """ + A list of edges. + """ + edges: [FiatBalanceEdge] + + """ + A list of nodes. + """ + nodes: [FiatBalance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type FiatBalanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: FiatBalance +} + +""" +An ISO 8601-encoded datetime +""" +scalar ISO8601DateTime + """ An object with an ID. """ @@ -102,6 +150,27 @@ type Query { last: Int ): BalanceConnection! currentUser: User + fiatBalances( + """ + 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 + ): FiatBalanceConnection! """ Fetches an object given its ID. diff --git a/app/javascript/src/pages/Wallet/Balances.tsx b/app/javascript/src/pages/Wallet/Balances.tsx new file mode 100644 index 0000000..da64365 --- /dev/null +++ b/app/javascript/src/pages/Wallet/Balances.tsx @@ -0,0 +1,81 @@ +import { graphql } from "babel-plugin-relay/macro"; +import type { FC } from "react"; +import React from "react"; +import { useFragment } from "react-relay"; + +import { getCurrencyLogo } from "../../utils/getCurrencyLogo"; +import type { Balances_balances$key } from "./__generated__/Balances_balances.graphql"; + +type Props = { + balancesRef: Balances_balances$key; +}; +export const Balances: FC = ({ balancesRef }) => { + const { nodes } = useFragment( + graphql` + fragment Balances_balances on BalanceConnection { + nodes { + id + amount + currency { + name + } + } + } + `, + balancesRef + ); + + return ( +
+
+ + + + + + + + + {nodes?.map((balance) => { + return ( + + + + + ); + })} + +
+ Moeda + + Saldo +
+
+
+ {`${balance?.currency.name} +
+
+

+ {balance?.currency.name} +

+
+
+
+

+ {balance?.amount} +

+
+
+
+ ); +}; diff --git a/app/javascript/src/pages/Wallet/FiatBalances.tsx b/app/javascript/src/pages/Wallet/FiatBalances.tsx new file mode 100644 index 0000000..9c037cd --- /dev/null +++ b/app/javascript/src/pages/Wallet/FiatBalances.tsx @@ -0,0 +1,58 @@ +import { graphql } from "babel-plugin-relay/macro"; +import type { FC } from "react"; +import React from "react"; +import { useFragment } from "react-relay"; + +import type { FiatBalances_fiatBalances$key } from "./__generated__/FiatBalances_fiatBalances.graphql"; + +type Props = { + fiatBalancesRef: FiatBalances_fiatBalances$key; +}; +export const FiatBalances: FC = ({ fiatBalancesRef }) => { + const { nodes } = useFragment( + graphql` + fragment FiatBalances_fiatBalances on FiatBalanceConnection { + nodes { + id + amountCents + amountCurrency + } + } + `, + fiatBalancesRef + ); + + if (!nodes?.length) return null; + + const [firstResult] = nodes; + + const amount = ( + firstResult?.amountCents ? firstResult?.amountCents / 100 : 0 + ).toFixed(2); + + return ( +
+
+ + + + + +

Saldo Fiat

+
+
+

+ {amount} + {firstResult?.amountCurrency} +

+
+
+ ); +}; diff --git a/app/javascript/src/pages/Wallet/Wallet.tsx b/app/javascript/src/pages/Wallet/Wallet.tsx index b4c3cba..361ddbc 100644 --- a/app/javascript/src/pages/Wallet/Wallet.tsx +++ b/app/javascript/src/pages/Wallet/Wallet.tsx @@ -3,21 +3,19 @@ import type { FC } from "react"; import React from "react"; import { useLazyLoadQuery } from "react-relay"; -import { getCurrencyLogo } from "../../utils/getCurrencyLogo"; +import { Balances } from "./Balances"; +import { FiatBalances } from "./FiatBalances"; import type { WalletQuery } from "./__generated__/WalletQuery.graphql"; export const Wallet: FC = () => { - const { balances } = useLazyLoadQuery( + const { fiatBalances, balances } = useLazyLoadQuery( graphql` query WalletQuery { + fiatBalances { + ...FiatBalances_fiatBalances + } balances { - nodes { - id - amount - currency { - name - } - } + ...Balances_balances } } `, @@ -28,57 +26,8 @@ export const Wallet: FC = () => {
-
-
- - - - - - - - - {balances.nodes?.map((balance) => { - return ( - - - - - ); - })} - -
- Moeda - - Saldo -
-
-
- {`${balance?.currency.name} -
-
-

- {balance?.currency.name} -

-
-
-
-

- {balance?.amount} -

-
-
-
+ +
diff --git a/app/javascript/src/pages/Wallet/__generated__/Balances_balances.graphql.ts b/app/javascript/src/pages/Wallet/__generated__/Balances_balances.graphql.ts new file mode 100644 index 0000000..be272d4 --- /dev/null +++ b/app/javascript/src/pages/Wallet/__generated__/Balances_balances.graphql.ts @@ -0,0 +1,79 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from "relay-runtime"; +import { FragmentRefs } from "relay-runtime"; +export type Balances_balances = { + readonly nodes: ReadonlyArray<{ + readonly id: string; + readonly amount: string; + readonly currency: { + readonly name: string; + }; + } | null> | null; + readonly " $refType": "Balances_balances"; +}; +export type Balances_balances$data = Balances_balances; +export type Balances_balances$key = { + readonly " $data"?: Balances_balances$data; + readonly " $fragmentRefs": FragmentRefs<"Balances_balances">; +}; + + + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "Balances_balances", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Balance", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Currency", + "kind": "LinkedField", + "name": "currency", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "BalanceConnection", + "abstractKey": null +}; +(node as any).hash = '2704da1dc9949b1becbd9ec947c5ec33'; +export default node; diff --git a/app/javascript/src/pages/Wallet/__generated__/FiatBalances_fiatBalances.graphql.ts b/app/javascript/src/pages/Wallet/__generated__/FiatBalances_fiatBalances.graphql.ts new file mode 100644 index 0000000..348a09e --- /dev/null +++ b/app/javascript/src/pages/Wallet/__generated__/FiatBalances_fiatBalances.graphql.ts @@ -0,0 +1,66 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from "relay-runtime"; +import { FragmentRefs } from "relay-runtime"; +export type FiatBalances_fiatBalances = { + readonly nodes: ReadonlyArray<{ + readonly id: string; + readonly amountCents: number; + readonly amountCurrency: string; + } | null> | null; + readonly " $refType": "FiatBalances_fiatBalances"; +}; +export type FiatBalances_fiatBalances$data = FiatBalances_fiatBalances; +export type FiatBalances_fiatBalances$key = { + readonly " $data"?: FiatBalances_fiatBalances$data; + readonly " $fragmentRefs": FragmentRefs<"FiatBalances_fiatBalances">; +}; + + + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "FiatBalances_fiatBalances", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FiatBalance", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amountCents", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amountCurrency", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "FiatBalanceConnection", + "abstractKey": null +}; +(node as any).hash = '0584f36abe6ca6f8612b8c593c4cfb6d'; +export default node; diff --git a/app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts b/app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts index 2e49fb5..e24b668 100644 --- a/app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts +++ b/app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts @@ -3,16 +3,14 @@ // @ts-nocheck import { ConcreteRequest } from "relay-runtime"; +import { FragmentRefs } from "relay-runtime"; export type WalletQueryVariables = {}; export type WalletQueryResponse = { + readonly fiatBalances: { + readonly " $fragmentRefs": FragmentRefs<"FiatBalances_fiatBalances">; + }; readonly balances: { - readonly nodes: ReadonlyArray<{ - readonly id: string; - readonly amount: number; - readonly currency: { - readonly name: string; - }; - } | null> | null; + readonly " $fragmentRefs": FragmentRefs<"Balances_balances">; }; }; export type WalletQuery = { @@ -24,17 +22,32 @@ export type WalletQuery = { /* query WalletQuery { + fiatBalances { + ...FiatBalances_fiatBalances + } balances { - nodes { + ...Balances_balances + } +} + +fragment Balances_balances on BalanceConnection { + nodes { + id + amount + currency { + name id - amount - currency { - name - id - } } } } + +fragment FiatBalances_fiatBalances on FiatBalanceConnection { + nodes { + id + amountCents + amountCurrency + } +} */ const node: ConcreteRequest = (function(){ @@ -44,20 +57,6 @@ var v0 = { "kind": "ScalarField", "name": "id", "storageKey": null -}, -v1 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "amount", - "storageKey": null -}, -v2 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null }; return { "fragment": { @@ -66,6 +65,22 @@ return { "metadata": null, "name": "WalletQuery", "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FiatBalanceConnection", + "kind": "LinkedField", + "name": "fiatBalances", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "FiatBalances_fiatBalances" + } + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -75,29 +90,9 @@ return { "plural": false, "selections": [ { - "alias": null, "args": null, - "concreteType": "Balance", - "kind": "LinkedField", - "name": "nodes", - "plural": true, - "selections": [ - (v0/*: any*/), - (v1/*: any*/), - { - "alias": null, - "args": null, - "concreteType": "Currency", - "kind": "LinkedField", - "name": "currency", - "plural": false, - "selections": [ - (v2/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null + "kind": "FragmentSpread", + "name": "Balances_balances" } ], "storageKey": null @@ -112,6 +107,43 @@ return { "kind": "Operation", "name": "WalletQuery", "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FiatBalanceConnection", + "kind": "LinkedField", + "name": "fiatBalances", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FiatBalance", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amountCents", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amountCurrency", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -129,7 +161,13 @@ return { "plural": true, "selections": [ (v0/*: any*/), - (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "amount", + "storageKey": null + }, { "alias": null, "args": null, @@ -138,7 +176,13 @@ return { "name": "currency", "plural": false, "selections": [ - (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, (v0/*: any*/) ], "storageKey": null @@ -152,14 +196,14 @@ return { ] }, "params": { - "cacheID": "6b8d0c664bd2d9df4d323c19c4a823a5", + "cacheID": "82d013e2bf418b53aeec5412f2f92661", "id": null, "metadata": {}, "name": "WalletQuery", "operationKind": "query", - "text": "query WalletQuery {\n balances {\n nodes {\n id\n amount\n currency {\n name\n id\n }\n }\n }\n}\n" + "text": "query WalletQuery {\n fiatBalances {\n ...FiatBalances_fiatBalances\n }\n balances {\n ...Balances_balances\n }\n}\n\nfragment Balances_balances on BalanceConnection {\n nodes {\n id\n amount\n currency {\n name\n id\n }\n }\n}\n\nfragment FiatBalances_fiatBalances on FiatBalanceConnection {\n nodes {\n id\n amountCents\n amountCurrency\n }\n}\n" } }; })(); -(node as any).hash = '428f4f1ab769f9056dd38ec641a30733'; +(node as any).hash = '855efce679c691a77938b64376a1a805'; export default node; diff --git a/app/models/fiat_balance.rb b/app/models/fiat_balance.rb new file mode 100644 index 0000000..6b7fb78 --- /dev/null +++ b/app/models/fiat_balance.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fiat_balances +# +# id :bigint not null, primary key +# amount_cents :integer default(0), not null +# amount_currency :string default("BRL"), not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_fiat_balances_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class FiatBalance < ApplicationRecord + belongs_to :user + + monetize :amount_cents + + validates :amount, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index be95b3b..26d827a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ class User < ApplicationRecord has_many :documents, class_name: "UserDocument", dependent: :destroy has_many :balances, dependent: :restrict_with_error + has_one :fiat_balance, dependent: :restrict_with_error validates :first_name, :last_name, :email, presence: true validates :email, uniqueness: true diff --git a/app/policies/fiat_balance_policy.rb b/app/policies/fiat_balance_policy.rb new file mode 100644 index 0000000..0da917a --- /dev/null +++ b/app/policies/fiat_balance_policy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +class FiatBalancePolicy < ApplicationPolicy + class Scope < Scope + def resolve + return scope.none if user.nil? + + scope.where(user_id: user.id) + end + end +end diff --git a/config/initializers/money.rb b/config/initializers/money.rb new file mode 100644 index 0000000..6cbd21c --- /dev/null +++ b/config/initializers/money.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +MoneyRails.configure do |config| + config.default_currency = :brl + config.rounding_mode = BigDecimal::ROUND_HALF_UP + config.locale_backend = :i18n +end diff --git a/db/migrate/20210812011039_create_fiat_balances.rb b/db/migrate/20210812011039_create_fiat_balances.rb new file mode 100644 index 0000000..d139f0a --- /dev/null +++ b/db/migrate/20210812011039_create_fiat_balances.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class CreateFiatBalances < ActiveRecord::Migration[6.1] + def change + create_table(:fiat_balances) do |t| + t.references(:user, null: false, foreign_key: true) + t.monetize(:amount) + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 166df6e..586766c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_08_11_121726) do +ActiveRecord::Schema.define(version: 2021_08_12_011039) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -71,6 +71,15 @@ ActiveRecord::Schema.define(version: 2021_08_11_121726) do t.datetime "updated_at", precision: 6, null: false end + create_table "fiat_balances", force: :cascade do |t| + t.bigint "user_id", null: false + t.integer "amount_cents", default: 0, null: false + t.string "amount_currency", default: "BRL", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["user_id"], name: "index_fiat_balances_on_user_id" + end + create_table "user_documents", force: :cascade do |t| t.string "status", null: false t.bigint "user_id", null: false @@ -97,5 +106,6 @@ ActiveRecord::Schema.define(version: 2021_08_11_121726) do add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "balances", "currencies" add_foreign_key "balances", "users" + add_foreign_key "fiat_balances", "users" add_foreign_key "user_documents", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index 3a1a6de..caa5441 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -214,3 +214,5 @@ currencies.each do |currency| amount: random_floating_number ) end + +FiatBalance.create(user_id: user.id) diff --git a/spec/models/fiat_balance_spec.rb b/spec/models/fiat_balance_spec.rb new file mode 100644 index 0000000..f82fe37 --- /dev/null +++ b/spec/models/fiat_balance_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fiat_balances +# +# id :bigint not null, primary key +# amount_cents :integer default(0), not null +# amount_currency :string default("BRL"), not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_fiat_balances_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +require "rails_helper" + +RSpec.describe(FiatBalance, type: :model) do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/policies/fiat_balance_policy_spec.rb b/spec/policies/fiat_balance_policy_spec.rb new file mode 100644 index 0000000..9bf7e08 --- /dev/null +++ b/spec/policies/fiat_balance_policy_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe(FiatBalancePolicy, type: :policy) do + pending "add some examples to (or delete) #{__FILE__}" +end