From 19a08cd50e7dab459bda6e9608f9e047cf467e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Geonizeli?= Date: Sun, 15 Aug 2021 23:30:53 -0300 Subject: [PATCH] add mutations to exchange painel --- app/dashboards/fiat_balance_dashboard.rb | 3 +- .../mutations/create_buy_crypto_order.rb | 4 +- .../mutations/create_sell_crypto_order.rb | 6 +- .../src/components/{Poo.tsx => Pool.tsx} | 2 +- app/javascript/src/components/PoolListing.tsx | 2 +- .../CreateExchangeOrderModel/index.ts | 1 - .../src/pages/Orders/Exchange/Exchange.tsx | 21 +- .../ExchangeHistory/ExchangeHistory.tsx | 6 +- .../ExchangePanel.tsx} | 72 ++++-- .../ExchangePanel_balances.graphql.ts} | 37 ++- .../ExchangePanel_fiatBalances.graphql.ts} | 16 +- .../createBuyCryptoOrderMutation.graphql.ts | 160 +++++++++++++ .../createSellCryptoOrderMutation.graphql.ts | 160 +++++++++++++ .../ExchangePanel/createBuyCryptoOrder.tsx | 37 +++ .../ExchangePanel/createSellCryptoOrder.tsx | 35 +++ .../Orders/Exchange/ExchangePanel/index.ts | 1 + .../__generated__/ExchangeQuery.graphql.ts | 69 +++--- app/javascript/stylesheets/tailwind.config.js | 5 +- app/models/buy_crypto_order.rb | 2 +- app/models/sell_crypto_order.rb | 2 +- config/locales/pt-BR.yml | 6 +- ..._sell_crypto_order_recived_amount_cents.rb | 14 ++ ...t_value_to_buy_cripto_paid_amount_cents.rb | 14 ++ db/schema.rb | 6 +- erd.svg | 211 +++++++++--------- spec/factories/buy_crypto_orders.rb | 2 +- spec/factories/sell_crypto_orders.rb | 2 +- spec/models/buy_crypto_order_spec.rb | 2 +- spec/models/sell_crypto_order_spec.rb | 2 +- 29 files changed, 702 insertions(+), 198 deletions(-) rename app/javascript/src/components/{Poo.tsx => Pool.tsx} (98%) delete mode 100644 app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/index.ts rename app/javascript/src/pages/Orders/Exchange/{CreateExchangeOrderModel/CreateExchangeOrderModal.tsx => ExchangePanel/ExchangePanel.tsx} (65%) rename app/javascript/src/pages/Orders/Exchange/{CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_balances.graphql.ts => ExchangePanel/__generated__/ExchangePanel_balances.graphql.ts} (52%) rename app/javascript/src/pages/Orders/Exchange/{CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_fiatBalances.graphql.ts => ExchangePanel/__generated__/ExchangePanel_fiatBalances.graphql.ts} (68%) create mode 100644 app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createBuyCryptoOrderMutation.graphql.ts create mode 100644 app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createSellCryptoOrderMutation.graphql.ts create mode 100644 app/javascript/src/pages/Orders/Exchange/ExchangePanel/createBuyCryptoOrder.tsx create mode 100644 app/javascript/src/pages/Orders/Exchange/ExchangePanel/createSellCryptoOrder.tsx create mode 100644 app/javascript/src/pages/Orders/Exchange/ExchangePanel/index.ts create mode 100644 db/migrate/20210816015139_add_default_value_to_sell_crypto_order_recived_amount_cents.rb create mode 100644 db/migrate/20210816015156_add_default_value_to_buy_cripto_paid_amount_cents.rb diff --git a/app/dashboards/fiat_balance_dashboard.rb b/app/dashboards/fiat_balance_dashboard.rb index 227461b..690e49d 100644 --- a/app/dashboards/fiat_balance_dashboard.rb +++ b/app/dashboards/fiat_balance_dashboard.rb @@ -12,6 +12,7 @@ class FiatBalanceDashboard < Administrate::BaseDashboard id: Field::Number, user: Field::BelongsTo, amount_formatted: Field::String, + amount_cents: Field::String, created_at: Field::DateTime, updated_at: Field::DateTime, }.freeze @@ -30,7 +31,7 @@ class FiatBalanceDashboard < Administrate::BaseDashboard # FORM_ATTRIBUTES # an array of attributes that will be displayed # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = [:user, :amount_formatted].freeze + FORM_ATTRIBUTES = [:user, :amount_cents].freeze # COLLECTION_FILTERS # a hash that defines filters that can be used while searching via the search diff --git a/app/graphql/mutations/create_buy_crypto_order.rb b/app/graphql/mutations/create_buy_crypto_order.rb index 9efb152..0b22ec6 100644 --- a/app/graphql/mutations/create_buy_crypto_order.rb +++ b/app/graphql/mutations/create_buy_crypto_order.rb @@ -9,7 +9,9 @@ module Mutations currency_id = decode_id(order[:currency_id]) ActiveRecord::Base.transaction do - current_user.fiat_balance.withdrawal!(order[:amount_cents]) + current_user + .fiat_balance + .withdrawal!(order[:amount_cents]) record = BuyCryptoOrder.create!( paid_amount_cents: order[:amount_cents], diff --git a/app/graphql/mutations/create_sell_crypto_order.rb b/app/graphql/mutations/create_sell_crypto_order.rb index 9b70114..637031a 100644 --- a/app/graphql/mutations/create_sell_crypto_order.rb +++ b/app/graphql/mutations/create_sell_crypto_order.rb @@ -10,10 +10,12 @@ module Mutations amount = BigDecimal(order[:amount]) ActiveRecord::Base.transaction do - current_user.balances.find_by!(currency_id: currency_id) + current_user + .balances + .find_by!(currency_id: currency_id) .withdrawal!(amount) - record = SellCryptoOrder.create( + record = SellCryptoOrder.create!( paid_amount: amount, currency_id: currency_id, user_id: current_user.id, diff --git a/app/javascript/src/components/Poo.tsx b/app/javascript/src/components/Pool.tsx similarity index 98% rename from app/javascript/src/components/Poo.tsx rename to app/javascript/src/components/Pool.tsx index c559aa9..b51a6c8 100644 --- a/app/javascript/src/components/Poo.tsx +++ b/app/javascript/src/components/Pool.tsx @@ -42,7 +42,7 @@ export const Pool = ({ pool }: PoolProps) => { rewardTokenPrice: earningPrice, stakingTokenPrice: stakingPrice, tokenPerBlock: parseFloat(pool.tokenPerBlock) / 1e-18, - totalStaked: totalStaked, + totalStaked, }); if (aprValue) { diff --git a/app/javascript/src/components/PoolListing.tsx b/app/javascript/src/components/PoolListing.tsx index 56f4aed..f26942b 100644 --- a/app/javascript/src/components/PoolListing.tsx +++ b/app/javascript/src/components/PoolListing.tsx @@ -1,7 +1,7 @@ import React from "react"; import { pools } from "../constants/Pools"; -import { Pool } from "./Poo"; +import { Pool } from "./Pool"; export const PoolListing = () => { return ( diff --git a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/index.ts b/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/index.ts deleted file mode 100644 index a8e24d2..0000000 --- a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./CreateExchangeOrderModal"; diff --git a/app/javascript/src/pages/Orders/Exchange/Exchange.tsx b/app/javascript/src/pages/Orders/Exchange/Exchange.tsx index c2a04ff..e3665b4 100644 --- a/app/javascript/src/pages/Orders/Exchange/Exchange.tsx +++ b/app/javascript/src/pages/Orders/Exchange/Exchange.tsx @@ -1,23 +1,21 @@ /* eslint-disable relay/must-colocate-fragment-spreads */ import { graphql } from "babel-plugin-relay/macro"; -import React, { useState } from "react"; +import React from "react"; import { useLazyLoadQuery } from "react-relay"; -import { CreateExchangeOrderModal } from "./CreateExchangeOrderModel"; +import { ExchangePanel } from "./ExchangePanel"; import { ExchangeHistory } from "./ExchangeHistory"; import type { ExchangeQuery } from "./__generated__/ExchangeQuery.graphql"; export const Exchange = () => { - const [modelOpen] = useState(false); - const data = useLazyLoadQuery( graphql` query ExchangeQuery { fiatBalances { - ...CreateExchangeOrderModal_fiatBalances + ...ExchangePanel_fiatBalances } balances { - ...CreateExchangeOrderModal_balances + ...ExchangePanel_balances } buyCryptoOrders { ...ExchangeHistory_buyCryptoOrders @@ -32,16 +30,15 @@ export const Exchange = () => { return (
+ + - {modelOpen && ( - - )}
); }; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangeHistory/ExchangeHistory.tsx b/app/javascript/src/pages/Orders/Exchange/ExchangeHistory/ExchangeHistory.tsx index f0660c0..dbd3931 100644 --- a/app/javascript/src/pages/Orders/Exchange/ExchangeHistory/ExchangeHistory.tsx +++ b/app/javascript/src/pages/Orders/Exchange/ExchangeHistory/ExchangeHistory.tsx @@ -83,8 +83,8 @@ export const ExchangeHistory: FC = ({ const allResultsOrdeneds = allResults.sort((item1, item2) => { return ( - new Date(item1.node.createdAt as string).getTime() - - new Date(item2.node.createdAt as string).getTime() + new Date(item2.node.createdAt as string).getTime() - + new Date(item1.node.createdAt as string).getTime() ); }) as SellOrBuyOrder[]; @@ -117,6 +117,8 @@ export const ExchangeHistory: FC = ({ return null; }); + if (!orderRows.length) return null; + return (
diff --git a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/CreateExchangeOrderModal.tsx b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/ExchangePanel.tsx similarity index 65% rename from app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/CreateExchangeOrderModal.tsx rename to app/javascript/src/pages/Orders/Exchange/ExchangePanel/ExchangePanel.tsx index 01f34ed..4972a9a 100644 --- a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/CreateExchangeOrderModal.tsx +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/ExchangePanel.tsx @@ -1,14 +1,16 @@ import React, { useState } from "react"; import type { FC } from "react"; import { graphql } from "babel-plugin-relay/macro"; -import { useFragment } from "react-relay"; +import { useFragment, useRelayEnvironment } from "react-relay"; import { BigNumber } from "bignumber.js"; import cx from "classnames"; import { useCurrentUser } from "../../../../contexts/UserProvider"; import { Unauthenticated } from "../../../../messages/Unauthenticated"; -import type { CreateExchangeOrderModal_fiatBalances$key } from "./__generated__/CreateExchangeOrderModal_fiatBalances.graphql"; -import type { CreateExchangeOrderModal_balances$key } from "./__generated__/CreateExchangeOrderModal_balances.graphql"; +import type { ExchangePanel_fiatBalances$key } from "./__generated__/ExchangePanel_fiatBalances.graphql"; +import type { ExchangePanel_balances$key } from "./__generated__/ExchangePanel_balances.graphql"; +import { commitCreateSellCryptoOrderMutation } from "./createSellCryptoOrder"; +import { commitCreateBuyCryptoOrderMutation } from "./createBuyCryptoOrder"; const tabBaseStyles = "w-full text-base font-bold text-black px-4 py-2 focus:ring-blue-500"; @@ -20,22 +22,23 @@ const inputBaseStyles = "rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-3"; type Props = { - fiatBalancesRefs: CreateExchangeOrderModal_fiatBalances$key; - balancesRefs: CreateExchangeOrderModal_balances$key; + fiatBalancesRefs: ExchangePanel_fiatBalances$key; + balancesRefs: ExchangePanel_balances$key; }; -export const CreateExchangeOrderModal: FC = ({ +export const ExchangePanel: FC = ({ fiatBalancesRefs, balancesRefs, }) => { const { isAuthenticated } = useCurrentUser(); + const environment = useRelayEnvironment(); const [exchangeOption, setExchangeOption] = useState<"BUY" | "SELL">("BUY"); const [cryptoDock, setCryptoDock] = useState("0"); const [fiatDock, setFiatDock] = useState("0.00"); - const fiatBalances = useFragment( + const fiatBalances = useFragment( graphql` - fragment CreateExchangeOrderModal_fiatBalances on FiatBalanceConnection { + fragment ExchangePanel_fiatBalances on FiatBalanceConnection { edges { node { amountCents @@ -46,12 +49,15 @@ export const CreateExchangeOrderModal: FC = ({ fiatBalancesRefs ); - const balances = useFragment( + const balances = useFragment( graphql` - fragment CreateExchangeOrderModal_balances on BalanceConnection { + fragment ExchangePanel_balances on BalanceConnection { edges { node { amount + currency { + id + } } } } @@ -60,6 +66,7 @@ export const CreateExchangeOrderModal: FC = ({ ); if (!isAuthenticated) return ; + const [crypto] = balances.edges; const [fiat] = fiatBalances.edges; @@ -83,7 +90,10 @@ export const CreateExchangeOrderModal: FC = ({ }: React.ChangeEvent) => { const newCryptoAmount = new BigNumber(value); - if (newCryptoAmount.isLessThanOrEqualTo(avaliableCrypto)) { + if ( + newCryptoAmount.isLessThanOrEqualTo(avaliableCrypto) && + newCryptoAmount.isGreaterThanOrEqualTo(0) + ) { setCryptoDock(value); } }; @@ -91,9 +101,10 @@ export const CreateExchangeOrderModal: FC = ({ const handleFiatAmountChange = ({ currentTarget: { value }, }: React.ChangeEvent) => { - const newFiatAmount = Number(value); + const newFiatAmount = parseInt(value, 10); + const avaliableFiatAmount = parseInt(avaliableFiat, 10); - if (Number(avaliableFiat) >= newFiatAmount) { + if (newFiatAmount <= avaliableFiatAmount && newFiatAmount >= 0) { setFiatDock(value); } }; @@ -106,8 +117,33 @@ export const CreateExchangeOrderModal: FC = ({ setCryptoDock(avaliableCrypto.toString()); }; + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (exchangeOption === "SELL") { + commitCreateSellCryptoOrderMutation(environment, { + amount: cryptoDock, + currencyId: crypto.node.currency.id, + }); + } + + if (exchangeOption === "BUY") { + const [amountCents] = fiatDock.split("."); + + commitCreateBuyCryptoOrderMutation(environment, { + amountCents: parseInt(amountCents, 10), + currencyId: crypto.node.currency.id, + }); + } + }; + + const submitDisabled = + (exchangeOption === "BUY" && parseInt(fiatDock, 10) <= 0) || + (exchangeOption === "SELL" && + new BigNumber(cryptoDock).isLessThanOrEqualTo(0)); + return ( -
+
-
+ {exchangeOption === "SELL" ? "CAKE" : "BRL"} disponível:{" "} {exchangeOption === "SELL" ? crypto.node.amount : avaliableFiat} @@ -176,8 +215,9 @@ export const CreateExchangeOrderModal: FC = ({
diff --git a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_balances.graphql.ts b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_balances.graphql.ts similarity index 52% rename from app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_balances.graphql.ts rename to app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_balances.graphql.ts index 39b1fbd..f6d2da2 100644 --- a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_balances.graphql.ts +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_balances.graphql.ts @@ -4,18 +4,21 @@ import { ReaderFragment } from "relay-runtime"; import { FragmentRefs } from "relay-runtime"; -export type CreateExchangeOrderModal_balances = { +export type ExchangePanel_balances = { readonly edges: ReadonlyArray<{ readonly node: { readonly amount: string; + readonly currency: { + readonly id: string; + }; }; }>; - readonly " $refType": "CreateExchangeOrderModal_balances"; + readonly " $refType": "ExchangePanel_balances"; }; -export type CreateExchangeOrderModal_balances$data = CreateExchangeOrderModal_balances; -export type CreateExchangeOrderModal_balances$key = { - readonly " $data"?: CreateExchangeOrderModal_balances$data; - readonly " $fragmentRefs": FragmentRefs<"CreateExchangeOrderModal_balances">; +export type ExchangePanel_balances$data = ExchangePanel_balances; +export type ExchangePanel_balances$key = { + readonly " $data"?: ExchangePanel_balances$data; + readonly " $fragmentRefs": FragmentRefs<"ExchangePanel_balances">; }; @@ -24,7 +27,7 @@ const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, - "name": "CreateExchangeOrderModal_balances", + "name": "ExchangePanel_balances", "selections": [ { "alias": null, @@ -48,6 +51,24 @@ const node: ReaderFragment = { "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": "id", + "storageKey": null + } + ], + "storageKey": null } ], "storageKey": null @@ -59,5 +80,5 @@ const node: ReaderFragment = { "type": "BalanceConnection", "abstractKey": null }; -(node as any).hash = '42aad1bd63f1135b4d99ec236cd945b5'; +(node as any).hash = '3be851ad99a353609459a7edc960f272'; export default node; diff --git a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_fiatBalances.graphql.ts b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_fiatBalances.graphql.ts similarity index 68% rename from app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_fiatBalances.graphql.ts rename to app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_fiatBalances.graphql.ts index 4a7860d..9860210 100644 --- a/app/javascript/src/pages/Orders/Exchange/CreateExchangeOrderModel/__generated__/CreateExchangeOrderModal_fiatBalances.graphql.ts +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/ExchangePanel_fiatBalances.graphql.ts @@ -4,18 +4,18 @@ import { ReaderFragment } from "relay-runtime"; import { FragmentRefs } from "relay-runtime"; -export type CreateExchangeOrderModal_fiatBalances = { +export type ExchangePanel_fiatBalances = { readonly edges: ReadonlyArray<{ readonly node: { readonly amountCents: number; }; }>; - readonly " $refType": "CreateExchangeOrderModal_fiatBalances"; + readonly " $refType": "ExchangePanel_fiatBalances"; }; -export type CreateExchangeOrderModal_fiatBalances$data = CreateExchangeOrderModal_fiatBalances; -export type CreateExchangeOrderModal_fiatBalances$key = { - readonly " $data"?: CreateExchangeOrderModal_fiatBalances$data; - readonly " $fragmentRefs": FragmentRefs<"CreateExchangeOrderModal_fiatBalances">; +export type ExchangePanel_fiatBalances$data = ExchangePanel_fiatBalances; +export type ExchangePanel_fiatBalances$key = { + readonly " $data"?: ExchangePanel_fiatBalances$data; + readonly " $fragmentRefs": FragmentRefs<"ExchangePanel_fiatBalances">; }; @@ -24,7 +24,7 @@ const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, - "name": "CreateExchangeOrderModal_fiatBalances", + "name": "ExchangePanel_fiatBalances", "selections": [ { "alias": null, @@ -59,5 +59,5 @@ const node: ReaderFragment = { "type": "FiatBalanceConnection", "abstractKey": null }; -(node as any).hash = 'b3a734bd9e34e02aacfa42b6b95776a5'; +(node as any).hash = '14b79d15c8353d856f3e74bb7c181cf7'; export default node; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createBuyCryptoOrderMutation.graphql.ts b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createBuyCryptoOrderMutation.graphql.ts new file mode 100644 index 0000000..d4fd5be --- /dev/null +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createBuyCryptoOrderMutation.graphql.ts @@ -0,0 +1,160 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from "relay-runtime"; +export type createBuyCryptoOrderMutationVariables = { + currencyId: string; + amountCents: number; +}; +export type createBuyCryptoOrderMutationResponse = { + readonly createBuyCryptoOrder: { + readonly errors: ReadonlyArray<{ + readonly messages: ReadonlyArray | null; + }> | null; + readonly order: { + readonly id: string; + } | null; + } | null; +}; +export type createBuyCryptoOrderMutation = { + readonly response: createBuyCryptoOrderMutationResponse; + readonly variables: createBuyCryptoOrderMutationVariables; +}; + + + +/* +mutation createBuyCryptoOrderMutation( + $currencyId: ID! + $amountCents: Int! +) { + createBuyCryptoOrder(input: {order: {currencyId: $currencyId, amountCents: $amountCents}}) { + errors { + messages + } + order { + id + } + } +} +*/ + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "amountCents" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "currencyId" +}, +v2 = [ + { + "alias": null, + "args": [ + { + "fields": [ + { + "fields": [ + { + "kind": "Variable", + "name": "amountCents", + "variableName": "amountCents" + }, + { + "kind": "Variable", + "name": "currencyId", + "variableName": "currencyId" + } + ], + "kind": "ObjectValue", + "name": "order" + } + ], + "kind": "ObjectValue", + "name": "input" + } + ], + "concreteType": "CreateBuyCryptoOrderPayload", + "kind": "LinkedField", + "name": "createBuyCryptoOrder", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RecordInvalid", + "kind": "LinkedField", + "name": "errors", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "messages", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "BuyCryptoOrder", + "kind": "LinkedField", + "name": "order", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "createBuyCryptoOrderMutation", + "selections": (v2/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "createBuyCryptoOrderMutation", + "selections": (v2/*: any*/) + }, + "params": { + "cacheID": "d7cc0c483d893b14f46781408d9d3f2f", + "id": null, + "metadata": {}, + "name": "createBuyCryptoOrderMutation", + "operationKind": "mutation", + "text": "mutation createBuyCryptoOrderMutation(\n $currencyId: ID!\n $amountCents: Int!\n) {\n createBuyCryptoOrder(input: {order: {currencyId: $currencyId, amountCents: $amountCents}}) {\n errors {\n messages\n }\n order {\n id\n }\n }\n}\n" + } +}; +})(); +(node as any).hash = 'a272ce6d5ae676a9cdc4e38eb7cc3cbe'; +export default node; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createSellCryptoOrderMutation.graphql.ts b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createSellCryptoOrderMutation.graphql.ts new file mode 100644 index 0000000..0ea924e --- /dev/null +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/__generated__/createSellCryptoOrderMutation.graphql.ts @@ -0,0 +1,160 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from "relay-runtime"; +export type createSellCryptoOrderMutationVariables = { + currencyId: string; + amount: string; +}; +export type createSellCryptoOrderMutationResponse = { + readonly createSellCryptoOrder: { + readonly errors: ReadonlyArray<{ + readonly messages: ReadonlyArray | null; + }> | null; + readonly order: { + readonly id: string; + } | null; + } | null; +}; +export type createSellCryptoOrderMutation = { + readonly response: createSellCryptoOrderMutationResponse; + readonly variables: createSellCryptoOrderMutationVariables; +}; + + + +/* +mutation createSellCryptoOrderMutation( + $currencyId: ID! + $amount: String! +) { + createSellCryptoOrder(input: {order: {currencyId: $currencyId, amount: $amount}}) { + errors { + messages + } + order { + id + } + } +} +*/ + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "amount" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "currencyId" +}, +v2 = [ + { + "alias": null, + "args": [ + { + "fields": [ + { + "fields": [ + { + "kind": "Variable", + "name": "amount", + "variableName": "amount" + }, + { + "kind": "Variable", + "name": "currencyId", + "variableName": "currencyId" + } + ], + "kind": "ObjectValue", + "name": "order" + } + ], + "kind": "ObjectValue", + "name": "input" + } + ], + "concreteType": "CreateSellCryptoOrderPayload", + "kind": "LinkedField", + "name": "createSellCryptoOrder", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RecordInvalid", + "kind": "LinkedField", + "name": "errors", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "messages", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "SellCryptoOrder", + "kind": "LinkedField", + "name": "order", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "createSellCryptoOrderMutation", + "selections": (v2/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "createSellCryptoOrderMutation", + "selections": (v2/*: any*/) + }, + "params": { + "cacheID": "2c81ae5adf76b4fa157bc5453df40fcc", + "id": null, + "metadata": {}, + "name": "createSellCryptoOrderMutation", + "operationKind": "mutation", + "text": "mutation createSellCryptoOrderMutation(\n $currencyId: ID!\n $amount: String!\n) {\n createSellCryptoOrder(input: {order: {currencyId: $currencyId, amount: $amount}}) {\n errors {\n messages\n }\n order {\n id\n }\n }\n}\n" + } +}; +})(); +(node as any).hash = '073e3f84d5921279ded3149dd9ec7db9'; +export default node; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createBuyCryptoOrder.tsx b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createBuyCryptoOrder.tsx new file mode 100644 index 0000000..e5a098d --- /dev/null +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createBuyCryptoOrder.tsx @@ -0,0 +1,37 @@ +import { graphql } from "babel-plugin-relay/macro"; +import type { Environment } from "react-relay"; +import { commitMutation } from "react-relay"; + +import type { createBuyCryptoOrderMutationVariables } from "./__generated__/createBuyCryptoOrderMutation.graphql"; + +export const commitCreateBuyCryptoOrderMutation = ( + environment: Environment, + variables: createBuyCryptoOrderMutationVariables +) => { + return commitMutation(environment, { + mutation: graphql` + mutation createBuyCryptoOrderMutation( + $currencyId: ID! + $amountCents: Int! + ) { + createBuyCryptoOrder( + input: { + order: { currencyId: $currencyId, amountCents: $amountCents } + } + ) { + errors { + messages + } + order { + id + } + } + } + `, + variables: { ...variables }, + onCompleted: (_response) => { + window.location.reload(); + }, + onError: (_error) => {}, + }); +}; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createSellCryptoOrder.tsx b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createSellCryptoOrder.tsx new file mode 100644 index 0000000..d285f43 --- /dev/null +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/createSellCryptoOrder.tsx @@ -0,0 +1,35 @@ +import { graphql } from "babel-plugin-relay/macro"; +import type { Environment } from "react-relay"; +import { commitMutation } from "react-relay"; + +import type { createSellCryptoOrderMutationVariables } from "./__generated__/createSellCryptoOrderMutation.graphql"; + +export const commitCreateSellCryptoOrderMutation = ( + environment: Environment, + variables: createSellCryptoOrderMutationVariables +) => { + return commitMutation(environment, { + mutation: graphql` + mutation createSellCryptoOrderMutation( + $currencyId: ID! + $amount: String! + ) { + createSellCryptoOrder( + input: { order: { currencyId: $currencyId, amount: $amount } } + ) { + errors { + messages + } + order { + id + } + } + } + `, + variables: { ...variables }, + onCompleted: (_response) => { + window.location.reload(); + }, + onError: (_error) => {}, + }); +}; diff --git a/app/javascript/src/pages/Orders/Exchange/ExchangePanel/index.ts b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/index.ts new file mode 100644 index 0000000..a013887 --- /dev/null +++ b/app/javascript/src/pages/Orders/Exchange/ExchangePanel/index.ts @@ -0,0 +1 @@ +export * from "./ExchangePanel"; diff --git a/app/javascript/src/pages/Orders/Exchange/__generated__/ExchangeQuery.graphql.ts b/app/javascript/src/pages/Orders/Exchange/__generated__/ExchangeQuery.graphql.ts index 6d55f20..3ce39a2 100644 --- a/app/javascript/src/pages/Orders/Exchange/__generated__/ExchangeQuery.graphql.ts +++ b/app/javascript/src/pages/Orders/Exchange/__generated__/ExchangeQuery.graphql.ts @@ -7,10 +7,10 @@ import { FragmentRefs } from "relay-runtime"; export type ExchangeQueryVariables = {}; export type ExchangeQueryResponse = { readonly fiatBalances: { - readonly " $fragmentRefs": FragmentRefs<"CreateExchangeOrderModal_fiatBalances">; + readonly " $fragmentRefs": FragmentRefs<"ExchangePanel_fiatBalances">; }; readonly balances: { - readonly " $fragmentRefs": FragmentRefs<"CreateExchangeOrderModal_balances">; + readonly " $fragmentRefs": FragmentRefs<"ExchangePanel_balances">; }; readonly buyCryptoOrders: { readonly " $fragmentRefs": FragmentRefs<"ExchangeHistory_buyCryptoOrders">; @@ -29,10 +29,10 @@ export type ExchangeQuery = { /* query ExchangeQuery { fiatBalances { - ...CreateExchangeOrderModal_fiatBalances + ...ExchangePanel_fiatBalances } balances { - ...CreateExchangeOrderModal_balances + ...ExchangePanel_balances } buyCryptoOrders { ...ExchangeHistory_buyCryptoOrders @@ -42,24 +42,6 @@ query ExchangeQuery { } } -fragment CreateExchangeOrderModal_balances on BalanceConnection { - edges { - node { - amount - id - } - } -} - -fragment CreateExchangeOrderModal_fiatBalances on FiatBalanceConnection { - edges { - node { - amountCents - id - } - } -} - fragment ExchangeHistory_buyCryptoOrders on BuyCryptoOrderConnection { edges { node { @@ -93,6 +75,27 @@ fragment ExchangeHistory_sellCryptoOrders on SellCryptoOrderConnection { } } } + +fragment ExchangePanel_balances on BalanceConnection { + edges { + node { + amount + currency { + id + } + id + } + } +} + +fragment ExchangePanel_fiatBalances on FiatBalanceConnection { + edges { + node { + amountCents + id + } + } +} */ const node: ConcreteRequest = (function(){ @@ -161,7 +164,7 @@ return { { "args": null, "kind": "FragmentSpread", - "name": "CreateExchangeOrderModal_fiatBalances" + "name": "ExchangePanel_fiatBalances" } ], "storageKey": null @@ -177,7 +180,7 @@ return { { "args": null, "kind": "FragmentSpread", - "name": "CreateExchangeOrderModal_balances" + "name": "ExchangePanel_balances" } ], "storageKey": null @@ -296,6 +299,18 @@ return { "name": "amount", "storageKey": null }, + { + "alias": null, + "args": null, + "concreteType": "Currency", + "kind": "LinkedField", + "name": "currency", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null + }, (v0/*: any*/) ], "storageKey": null @@ -413,14 +428,14 @@ return { ] }, "params": { - "cacheID": "ddb6670ea93a9fdc62c7627c3ed09925", + "cacheID": "424352bbffec52284c44f109ce54a441", "id": null, "metadata": {}, "name": "ExchangeQuery", "operationKind": "query", - "text": "query ExchangeQuery {\n fiatBalances {\n ...CreateExchangeOrderModal_fiatBalances\n }\n balances {\n ...CreateExchangeOrderModal_balances\n }\n buyCryptoOrders {\n ...ExchangeHistory_buyCryptoOrders\n }\n sellCryptoOrders {\n ...ExchangeHistory_sellCryptoOrders\n }\n}\n\nfragment CreateExchangeOrderModal_balances on BalanceConnection {\n edges {\n node {\n amount\n id\n }\n }\n}\n\nfragment CreateExchangeOrderModal_fiatBalances on FiatBalanceConnection {\n edges {\n node {\n amountCents\n id\n }\n }\n}\n\nfragment ExchangeHistory_buyCryptoOrders on BuyCryptoOrderConnection {\n edges {\n node {\n id\n status\n createdAt\n paidAmountCents\n receivedAmount\n currency {\n name\n id\n }\n __typename\n }\n }\n}\n\nfragment ExchangeHistory_sellCryptoOrders on SellCryptoOrderConnection {\n edges {\n node {\n id\n status\n paidAmount\n receivedAmountCents\n createdAt\n currency {\n name\n id\n }\n __typename\n }\n }\n}\n" + "text": "query ExchangeQuery {\n fiatBalances {\n ...ExchangePanel_fiatBalances\n }\n balances {\n ...ExchangePanel_balances\n }\n buyCryptoOrders {\n ...ExchangeHistory_buyCryptoOrders\n }\n sellCryptoOrders {\n ...ExchangeHistory_sellCryptoOrders\n }\n}\n\nfragment ExchangeHistory_buyCryptoOrders on BuyCryptoOrderConnection {\n edges {\n node {\n id\n status\n createdAt\n paidAmountCents\n receivedAmount\n currency {\n name\n id\n }\n __typename\n }\n }\n}\n\nfragment ExchangeHistory_sellCryptoOrders on SellCryptoOrderConnection {\n edges {\n node {\n id\n status\n paidAmount\n receivedAmountCents\n createdAt\n currency {\n name\n id\n }\n __typename\n }\n }\n}\n\nfragment ExchangePanel_balances on BalanceConnection {\n edges {\n node {\n amount\n currency {\n id\n }\n id\n }\n }\n}\n\nfragment ExchangePanel_fiatBalances on FiatBalanceConnection {\n edges {\n node {\n amountCents\n id\n }\n }\n}\n" } }; })(); -(node as any).hash = 'cc0eaddc68f5bd14d39ce9e148876535'; +(node as any).hash = '3d09bb5b003cac17750666dc76088801'; export default node; diff --git a/app/javascript/stylesheets/tailwind.config.js b/app/javascript/stylesheets/tailwind.config.js index 9e56d95..edc2f76 100644 --- a/app/javascript/stylesheets/tailwind.config.js +++ b/app/javascript/stylesheets/tailwind.config.js @@ -18,7 +18,10 @@ module.exports = { }, }, variants: { - extend: {}, + extend: { + opacity: ["disabled"], + cursor: ["disabled"], + }, }, plugins: [], }; diff --git a/app/models/buy_crypto_order.rb b/app/models/buy_crypto_order.rb index 9bbd027..58aa0d2 100644 --- a/app/models/buy_crypto_order.rb +++ b/app/models/buy_crypto_order.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount_cents :integer default(0), not null -# received_amount :decimal(20, 10) +# received_amount :decimal(20, 10) default(0.0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/sell_crypto_order.rb b/app/models/sell_crypto_order.rb index 3d0b175..7a9233e 100644 --- a/app/models/sell_crypto_order.rb +++ b/app/models/sell_crypto_order.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount :decimal(20, 10) default(0.0), not null -# received_amount_cents :integer +# received_amount_cents :integer default(0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index ee233df..14d0ea8 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -32,7 +32,7 @@ pt-BR: fiat_balance: amount_formatted: Quantia - amount_cents: Quantia + amount_cents: Quantia em centavos currency: name: Nome @@ -42,8 +42,8 @@ pt-BR: balance: attributes: amount: - greater_than_or_equal_to: "saldo insuficiente" + greater_than_or_equal_to: saldo insuficiente fiat_balance: attributes: amount_cents: - greater_than_or_equal_to: "saldo insuficiente" \ No newline at end of file + greater_than_or_equal_to: saldo insuficiente \ No newline at end of file diff --git a/db/migrate/20210816015139_add_default_value_to_sell_crypto_order_recived_amount_cents.rb b/db/migrate/20210816015139_add_default_value_to_sell_crypto_order_recived_amount_cents.rb new file mode 100644 index 0000000..be3cf56 --- /dev/null +++ b/db/migrate/20210816015139_add_default_value_to_sell_crypto_order_recived_amount_cents.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +class AddDefaultValueToSellCryptoOrderRecivedAmountCents < ActiveRecord::Migration[6.1] + def change + # rubocop:disable Rails/ReversibleMigration + execute(<<-SQL.squish) + UPDATE sell_crypto_orders + SET received_amount_cents = 0 + WHERE received_amount_cents IS NULL + SQL + + change_column_default(:sell_crypto_orders, :received_amount_cents, from: nil, to: 0) + change_column_null(:sell_crypto_orders, :received_amount_cents, false) + end +end diff --git a/db/migrate/20210816015156_add_default_value_to_buy_cripto_paid_amount_cents.rb b/db/migrate/20210816015156_add_default_value_to_buy_cripto_paid_amount_cents.rb new file mode 100644 index 0000000..68da299 --- /dev/null +++ b/db/migrate/20210816015156_add_default_value_to_buy_cripto_paid_amount_cents.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +class AddDefaultValueToBuyCriptoPaidAmountCents < ActiveRecord::Migration[6.1] + def change + # rubocop:disable Rails/ReversibleMigration + execute(<<-SQL.squish) + UPDATE buy_crypto_orders + SET received_amount = 0 + WHERE received_amount IS NULL + SQL + + change_column_default(:buy_crypto_orders, :received_amount, from: nil, to: 0) + change_column_null(:buy_crypto_orders, :received_amount, false) + end +end diff --git a/db/schema.rb b/db/schema.rb index 7af2e1f..4516172 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_14_194513) do +ActiveRecord::Schema.define(version: 2021_08_16_015156) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -70,7 +70,7 @@ ActiveRecord::Schema.define(version: 2021_08_14_194513) do t.bigint "currency_id", null: false t.string "status", null: false t.integer "paid_amount_cents", default: 0, null: false - t.decimal "received_amount", precision: 20, scale: 10 + t.decimal "received_amount", precision: 20, scale: 10, default: "0.0", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["currency_id"], name: "index_buy_crypto_orders_on_currency_id" @@ -97,7 +97,7 @@ ActiveRecord::Schema.define(version: 2021_08_14_194513) do t.bigint "currency_id", null: false t.string "status", null: false t.decimal "paid_amount", precision: 20, scale: 10, default: "0.0", null: false - t.integer "received_amount_cents" + t.integer "received_amount_cents", default: 0, null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["currency_id"], name: "index_sell_crypto_orders_on_currency_id" diff --git a/erd.svg b/erd.svg index 129d7bd..4ea3eee 100644 --- a/erd.svg +++ b/erd.svg @@ -1,178 +1,179 @@ - - + XStake - -XStake domain model + +XStake domain model m_AdminUser - -AdminUser - -email -string ∗ U -encrypted_password -string ∗ -remember_created_at -datetime -reset_password_sent_at -datetime -reset_password_token -string + +AdminUser + +email +string ∗ U +encrypted_password +string ∗ +remember_created_at +datetime +reset_password_sent_at +datetime +reset_password_token +string m_Balance - -Balance - -amount -decimal (20,10) ∗ -currency_id -integer (8) ∗ FK -user_id -integer (8) ∗ FK + +Balance + +amount +decimal (20,10) ∗ +currency_id +integer (8) ∗ FK +user_id +integer (8) ∗ FK m_BuyCryptoOrder - -BuyCryptoOrder - -currency_id -integer (8) ∗ FK -paid_amount_cents -integer ∗ -received_amount -decimal (20,10) ∗ -status -string ∗ -user_id -integer (8) ∗ FK + +BuyCryptoOrder + +currency_id +integer (8) ∗ FK +paid_amount_cents +integer ∗ +received_amount +decimal (20,10) ∗ +status +string ∗ +user_id +integer (8) ∗ FK m_Currency - -Currency - -name -string ∗ + +Currency + +name +string ∗ m_Currency->m_Balance - - + + m_Currency->m_BuyCryptoOrder - - + + m_SellCryptoOrder - -SellCryptoOrder - -currency_id -integer (8) ∗ FK -paid_amount -decimal (20,10) ∗ -received_amount_cents -integer ∗ -status -string ∗ -user_id -integer (8) ∗ FK + +SellCryptoOrder + +currency_id +integer (8) ∗ FK +paid_amount +decimal (20,10) ∗ +received_amount_cents +integer ∗ +status +string ∗ +user_id +integer (8) ∗ FK m_Currency->m_SellCryptoOrder - - + + m_FiatBalance - -FiatBalance - -amount_cents -integer ∗ -amount_currency -string ∗ -user_id -integer (8) ∗ FK + +FiatBalance + +amount_cents +integer ∗ +amount_currency +string ∗ +user_id +integer (8) ∗ FK m_User - -User - -email -string ∗ U -encrypted_password -string ∗ -first_name -string ∗ -last_name -string ∗ -remember_created_at -datetime -reset_password_sent_at -datetime -reset_password_token -string + +User + +email +string ∗ U +encrypted_password +string ∗ +first_name +string ∗ +last_name +string ∗ +remember_created_at +datetime +reset_password_sent_at +datetime +reset_password_token +string m_User->m_Balance - + + m_User->m_BuyCryptoOrder - - + + m_User->m_FiatBalance - + m_User->m_SellCryptoOrder - - + + m_UserDocument - -UserDocument - -status -string ∗ -user_id -integer (8) ∗ FK + +UserDocument + +status +string ∗ +user_id +integer (8) ∗ FK m_User->m_UserDocument - - + + diff --git a/spec/factories/buy_crypto_orders.rb b/spec/factories/buy_crypto_orders.rb index 0e05664..aad2aa1 100644 --- a/spec/factories/buy_crypto_orders.rb +++ b/spec/factories/buy_crypto_orders.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount_cents :integer default(0), not null -# received_amount :decimal(20, 10) +# received_amount :decimal(20, 10) default(0.0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/spec/factories/sell_crypto_orders.rb b/spec/factories/sell_crypto_orders.rb index 4d9e369..fc702fb 100644 --- a/spec/factories/sell_crypto_orders.rb +++ b/spec/factories/sell_crypto_orders.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount :decimal(20, 10) default(0.0), not null -# received_amount_cents :integer +# received_amount_cents :integer default(0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/spec/models/buy_crypto_order_spec.rb b/spec/models/buy_crypto_order_spec.rb index 9ba1a66..c17bd56 100644 --- a/spec/models/buy_crypto_order_spec.rb +++ b/spec/models/buy_crypto_order_spec.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount_cents :integer default(0), not null -# received_amount :decimal(20, 10) +# received_amount :decimal(20, 10) default(0.0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/spec/models/sell_crypto_order_spec.rb b/spec/models/sell_crypto_order_spec.rb index 651a618..c453847 100644 --- a/spec/models/sell_crypto_order_spec.rb +++ b/spec/models/sell_crypto_order_spec.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # paid_amount :decimal(20, 10) default(0.0), not null -# received_amount_cents :integer +# received_amount_cents :integer default(0), not null # status :string not null # created_at :datetime not null # updated_at :datetime not null