add fiat balance

This commit is contained in:
João Geonizeli
2021-08-11 22:53:59 -03:00
parent 1847a276f7
commit fe474dfb08
21 changed files with 596 additions and 116 deletions

View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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<Props> = ({ balancesRef }) => {
const { nodes } = useFragment<Balances_balances$key>(
graphql`
fragment Balances_balances on BalanceConnection {
nodes {
id
amount
currency {
name
}
}
}
`,
balancesRef
);
return (
<div className="-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto">
<div className="inline-block min-w-full shadow rounded-lg overflow-hidden">
<table className="min-w-full leading-normal">
<thead>
<tr>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Moeda
</th>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Saldo
</th>
</tr>
</thead>
<tbody>
{nodes?.map((balance) => {
return (
<tr key={balance?.id}>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<div className="flex items-center">
<div className="flex-shrink-0">
<img
alt={`${balance?.currency.name} icon`}
src={getCurrencyLogo(balance?.currency.name)}
className="mx-auto object-cover rounded-full h-10 w-10 "
/>
</div>
<div className="ml-3">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.currency.name}
</p>
</div>
</div>
</td>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.amount}
</p>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};

View File

@@ -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<Props> = ({ fiatBalancesRef }) => {
const { nodes } = useFragment<FiatBalances_fiatBalances$key>(
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 (
<div className="shadow rounded-lg p-4 bg-white dark:bg-gray-800">
<div className="flex items-center">
<span className="rounded-xl relative p-4 bg-green-200">
<svg
width="40"
fill="currentColor"
height="40"
className="text-green-500 h-4 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
viewBox="0 0 1792 1792"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1362 1185q0 153-99.5 263.5t-258.5 136.5v175q0 14-9 23t-23 9h-135q-13 0-22.5-9.5t-9.5-22.5v-175q-66-9-127.5-31t-101.5-44.5-74-48-46.5-37.5-17.5-18q-17-21-2-41l103-135q7-10 23-12 15-2 24 9l2 2q113 99 243 125 37 8 74 8 81 0 142.5-43t61.5-122q0-28-15-53t-33.5-42-58.5-37.5-66-32-80-32.5q-39-16-61.5-25t-61.5-26.5-62.5-31-56.5-35.5-53.5-42.5-43.5-49-35.5-58-21-66.5-8.5-78q0-138 98-242t255-134v-180q0-13 9.5-22.5t22.5-9.5h135q14 0 23 9t9 23v176q57 6 110.5 23t87 33.5 63.5 37.5 39 29 15 14q17 18 5 38l-81 146q-8 15-23 16-14 3-27-7-3-3-14.5-12t-39-26.5-58.5-32-74.5-26-85.5-11.5q-95 0-155 43t-60 111q0 26 8.5 48t29.5 41.5 39.5 33 56 31 60.5 27 70 27.5q53 20 81 31.5t76 35 75.5 42.5 62 50 53 63.5 31.5 76.5 13 94z" />
</svg>
</span>
<p className="text-md text-black dark:text-white ml-2">Saldo Fiat</p>
</div>
<div className="flex flex-col justify-start">
<p className="text-gray-700 dark:text-gray-100 text-4xl text-left font-bold my-4">
{amount}
<span className="text-sm">{firstResult?.amountCurrency}</span>
</p>
</div>
</div>
);
};

View File

@@ -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<WalletQuery>(
const { fiatBalances, balances } = useLazyLoadQuery<WalletQuery>(
graphql`
query WalletQuery {
fiatBalances {
...FiatBalances_fiatBalances
}
balances {
nodes {
id
amount
currency {
name
}
}
...Balances_balances
}
}
`,
@@ -28,57 +26,8 @@ export const Wallet: FC = () => {
<div className="flex flex-col h-full w-full overflow-x-hidden">
<div className="container mx-auto px-4 sm:px-8 max-w-3xl">
<div className="py-8">
<div className="-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto">
<div className="inline-block min-w-full shadow rounded-lg overflow-hidden">
<table className="min-w-full leading-normal">
<thead>
<tr>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Moeda
</th>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Saldo
</th>
</tr>
</thead>
<tbody>
{balances.nodes?.map((balance) => {
return (
<tr key={balance?.id}>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<div className="flex items-center">
<div className="flex-shrink-0">
<img
alt={`${balance?.currency.name} icon`}
src={getCurrencyLogo(balance?.currency.name)}
className="mx-auto object-cover rounded-full h-10 w-10 "
/>
</div>
<div className="ml-3">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.currency.name}
</p>
</div>
</div>
</td>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.amount}
</p>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
<FiatBalances fiatBalancesRef={fiatBalances} />
<Balances balancesRef={balances} />
</div>
</div>
</div>

View File

@@ -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;

View File

@@ -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;

View File

@@ -3,16 +3,14 @@
// @ts-nocheck
import { ConcreteRequest } from "relay-runtime";
import { FragmentRefs } from "relay-runtime";
export type WalletQueryVariables = {};
export type WalletQueryResponse = {
readonly balances: {
readonly nodes: ReadonlyArray<{
readonly id: string;
readonly amount: number;
readonly currency: {
readonly name: string;
readonly fiatBalances: {
readonly " $fragmentRefs": FragmentRefs<"FiatBalances_fiatBalances">;
};
} | null> | null;
readonly balances: {
readonly " $fragmentRefs": FragmentRefs<"Balances_balances">;
};
};
export type WalletQuery = {
@@ -24,7 +22,15 @@ export type WalletQuery = {
/*
query WalletQuery {
fiatBalances {
...FiatBalances_fiatBalances
}
balances {
...Balances_balances
}
}
fragment Balances_balances on BalanceConnection {
nodes {
id
amount
@@ -33,6 +39,13 @@ query WalletQuery {
id
}
}
}
fragment FiatBalances_fiatBalances on FiatBalanceConnection {
nodes {
id
amountCents
amountCurrency
}
}
*/
@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

12
db/schema.rb generated
View File

@@ -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

View File

@@ -214,3 +214,5 @@ currencies.each do |currency|
amount: random_floating_number
)
end
FiatBalance.create(user_id: user.id)

View File

@@ -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

View File

@@ -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