KYC acceptance property and admin UI

This commit is contained in:
Daniel Ternyak 2020-12-28 15:07:37 -06:00
parent 97b0cbc4b3
commit ed7a3343c9
No known key found for this signature in database
GPG Key ID: DF212D2DC5D0E245
8 changed files with 130 additions and 38 deletions

View File

@ -3,31 +3,31 @@ import BN from 'bn.js';
import { view } from 'react-easy-state'; import { view } from 'react-easy-state';
import { RouteComponentProps, withRouter } from 'react-router'; import { RouteComponentProps, withRouter } from 'react-router';
import { import {
Row,
Col,
Card,
Alert, Alert,
Button, Button,
Card,
Col,
Collapse, Collapse,
Popconfirm,
Input, Input,
Tag,
message, message,
Popconfirm,
Row,
Tag,
} from 'antd'; } from 'antd';
import TextArea from 'antd/lib/input/TextArea'; import TextArea from 'antd/lib/input/TextArea';
import store from 'src/store'; import store from 'src/store';
import { formatDateSeconds, formatDurationSeconds } from 'util/time'; import { formatDateSeconds, formatDurationSeconds } from 'util/time';
import { import {
PROPOSAL_STATUS,
PROPOSAL_ARBITER_STATUS,
MILESTONE_STAGE, MILESTONE_STAGE,
PROPOSAL_ARBITER_STATUS,
PROPOSAL_STAGE, PROPOSAL_STAGE,
PROPOSAL_STATUS,
} from 'src/types'; } from 'src/types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Back from 'components/Back'; import Back from 'components/Back';
import Markdown from 'components/Markdown'; import Markdown from 'components/Markdown';
import ArbiterControl from 'components/ArbiterControl'; import ArbiterControl from 'components/ArbiterControl';
import { toZat, fromZat } from 'src/util/units'; import { fromZat, toZat } from 'src/util/units';
import FeedbackModal from '../FeedbackModal'; import FeedbackModal from '../FeedbackModal';
import { formatUsd } from 'util/formatters'; import { formatUsd } from 'util/formatters';
import './index.less'; import './index.less';
@ -45,9 +45,11 @@ type State = typeof STATE;
class ProposalDetailNaked extends React.Component<Props, State> { class ProposalDetailNaked extends React.Component<Props, State> {
state = STATE; state = STATE;
rejectInput: null | TextArea = null; rejectInput: null | TextArea = null;
componentDidMount() { componentDidMount() {
this.loadDetail(); this.loadDetail();
} }
render() { render() {
const id = this.getIdFromQuery(); const id = this.getIdFromQuery();
const { proposalDetail: p, proposalDetailFetching } = store; const { proposalDetail: p, proposalDetailFetching } = store;
@ -183,7 +185,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Alert <Alert
showIcon showIcon
type={p.rfpOptIn ? 'success' : 'error'} type={p.rfpOptIn ? 'success' : 'error'}
message={p.rfpOptIn ? 'KYC accepted' : 'KYC rejected'} message={p.rfpOptIn ? 'KYC Accepted by user' : 'KYC rejected'}
description={ description={
<div> <div>
{p.rfpOptIn ? ( {p.rfpOptIn ? (
@ -272,24 +274,38 @@ class ProposalDetailNaked extends React.Component<Props, State> {
description={ description={
<div> <div>
<p>Please review this proposal and render your judgment.</p> <p>Please review this proposal and render your judgment.</p>
<Button {!p.kycApproved ? (
className="ProposalDetail-review" <Button
loading={store.proposalDetailAcceptingProposal} className="ProposalDetail-review"
icon="check" loading={store.proposalDetailApprovingKyc}
type="primary" icon="check"
onClick={() => this.handleAcceptProposal(true, true)} type="primary"
> onClick={() => this.handleApproveKYC()}
Approve With Funding >
</Button> KYC Approved
<Button </Button>
className="ProposalDetail-review" ) : (
loading={store.proposalDetailAcceptingProposal} <>
icon="check" <Button
type="default" className="ProposalDetail-review"
onClick={() => this.handleAcceptProposal(true, false)} loading={store.proposalDetailAcceptingProposal}
> icon="check"
Approve Without Funding type="primary"
</Button> onClick={() => this.handleAcceptProposal(true, true)}
>
Approve With Funding
</Button>
<Button
className="ProposalDetail-review"
loading={store.proposalDetailAcceptingProposal}
icon="check"
type="default"
onClick={() => this.handleAcceptProposal(true, false)}
>
Approve Without Funding
</Button>
</>
)}
<Button <Button
className="ProposalDetail-review" className="ProposalDetail-review"
loading={store.proposalDetailMarkingChangesAsResolved} loading={store.proposalDetailMarkingChangesAsResolved}
@ -716,6 +732,11 @@ class ProposalDetailNaked extends React.Component<Props, State> {
message.info('Proposal rejected permanently'); message.info('Proposal rejected permanently');
}; };
private handleApproveKYC = async () => {
await store.approveProposalKYC();
message.info(`Proposal KYC approved`);
};
private handleAcceptProposal = async ( private handleAcceptProposal = async (
isAccepted: boolean, isAccepted: boolean,
withFunding: boolean, withFunding: boolean,

View File

@ -2,17 +2,17 @@ import { pick } from 'lodash';
import { store } from 'react-easy-state'; import { store } from 'react-easy-state';
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
import { import {
User,
Proposal,
CCR, CCR,
CommentArgs,
Contribution, Contribution,
ContributionArgs, ContributionArgs,
EmailExample,
PageData,
PageQuery,
Proposal,
RFP, RFP,
RFPArgs, RFPArgs,
EmailExample, User,
PageQuery,
PageData,
CommentArgs,
} from './types'; } from './types';
// API // API
@ -142,6 +142,11 @@ async function approveDiscussion(
return data; return data;
} }
async function approveProposalKYC(id: number) {
const { data } = await api.put(`/admin/proposals/${id}/approve-kyc`);
return data;
}
async function acceptProposal( async function acceptProposal(
id: number, id: number,
isAccepted: boolean, isAccepted: boolean,
@ -345,6 +350,7 @@ const app = store({
proposalDetailApprovingDiscussion: false, proposalDetailApprovingDiscussion: false,
proposalDetailMarkingChangesAsResolved: false, proposalDetailMarkingChangesAsResolved: false,
proposalDetailAcceptingProposal: false, proposalDetailAcceptingProposal: false,
proposalDetailApprovingKyc: false,
proposalDetailMarkingMilestonePaid: false, proposalDetailMarkingMilestonePaid: false,
proposalDetailCanceling: false, proposalDetailCanceling: false,
proposalDetailUpdating: false, proposalDetailUpdating: false,
@ -688,6 +694,25 @@ const app = store({
handleApiError(e); handleApiError(e);
} }
}, },
async approveProposalKYC() {
if (!app.proposalDetail) {
const m = 'store.acceptProposal(): Expected proposalDetail to be populated!';
app.generalError.push(m);
console.error(m);
return;
}
app.proposalDetailApprovingKyc = true;
try {
const { proposalId } = app.proposalDetail;
const res = await approveProposalKYC(proposalId);
app.updateProposalInStore(res);
} catch (e) {
handleApiError(e);
}
app.proposalDetailApprovingKyc = false;
},
async acceptProposal( async acceptProposal(
isAccepted: boolean, isAccepted: boolean,
withFunding: boolean, withFunding: boolean,
@ -975,6 +1000,7 @@ function createDefaultPageData<T>(sort: string): PageData<T> {
} }
type FNFetchPage = (params: PageQuery) => Promise<any>; type FNFetchPage = (params: PageQuery) => Promise<any>;
interface PageParent<T> { interface PageParent<T> {
page: PageData<T>; page: PageData<T>;
} }

View File

@ -123,6 +123,7 @@ export interface Proposal {
isVersionTwo: boolean; isVersionTwo: boolean;
changesRequestedDiscussion: boolean | null; changesRequestedDiscussion: boolean | null;
changesRequestedDiscussionReason: string | null; changesRequestedDiscussionReason: string | null;
kycApproved: null | boolean;
} }
export interface Comment { export interface Comment {
id: number; id: number;

View File

@ -377,6 +377,19 @@ def open_proposal_for_discussion(proposal_id, is_open_for_discussion, reject_rea
return proposal_schema.dump(proposal) return proposal_schema.dump(proposal)
@blueprint.route('/proposals/<id>/approve-kyc', methods=['PUT'])
@admin.admin_auth_required
def approve_proposal_kyc(id):
proposal = Proposal.query.get(id)
if not proposal:
return {"message": "No proposal found."}, 404
proposal.kyc_approved = True
db.session.add(proposal)
db.session.commit()
return proposal_schema.dump(proposal)
@blueprint.route('/proposals/<id>/accept', methods=['PUT']) @blueprint.route('/proposals/<id>/accept', methods=['PUT'])
@body({ @body({
"isAccepted": fields.Bool(required=True), "isAccepted": fields.Bool(required=True),

View File

@ -391,6 +391,8 @@ class Proposal(db.Model):
date_approved = db.Column(db.DateTime) date_approved = db.Column(db.DateTime)
date_published = db.Column(db.DateTime) date_published = db.Column(db.DateTime)
reject_reason = db.Column(db.String()) reject_reason = db.Column(db.String())
kyc_approved = db.Column(db.Boolean(), nullable=True, default=False)
accepted_with_funding = db.Column(db.Boolean(), nullable=True) accepted_with_funding = db.Column(db.Boolean(), nullable=True)
changes_requested_discussion = db.Column(db.Boolean(), nullable=True) changes_requested_discussion = db.Column(db.Boolean(), nullable=True)
changes_requested_discussion_reason = db.Column(db.String(255), nullable=True) changes_requested_discussion_reason = db.Column(db.String(255), nullable=True)
@ -1096,7 +1098,8 @@ class ProposalSchema(ma.Schema):
"tip_jar_view_key", "tip_jar_view_key",
"changes_requested_discussion", "changes_requested_discussion",
"changes_requested_discussion_reason", "changes_requested_discussion_reason",
"live_draft_id" "live_draft_id",
"kyc_approved"
) )
date_created = ma.Method("get_date_created") date_created = ma.Method("get_date_created")

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: d03c91f3038d
Revises: bea5c35d0cd6
Create Date: 2020-12-27 15:48:36.787259
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd03c91f3038d'
down_revision = 'bea5c35d0cd6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('proposal', sa.Column('kyc_approved', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('proposal', 'kyc_approved')
# ### end Alembic commands ###

View File

@ -105,7 +105,7 @@ class Header extends React.Component<Props, State> {
)} )}
</Link> </Link>
</div> </div>
<div className="Header-links-button is-desktop"> {false && <div className="Header-links-button is-desktop">
<Link to="/create-request"> <Link to="/create-request">
{Array.isArray(ccrDrafts) && ccrDrafts.length > 0 ? ( {Array.isArray(ccrDrafts) && ccrDrafts.length > 0 ? (
<Button type={'primary'}>My Requests</Button> <Button type={'primary'}>My Requests</Button>
@ -113,7 +113,7 @@ class Header extends React.Component<Props, State> {
<Button type={'primary'}>Create a Request</Button> <Button type={'primary'}>Create a Request</Button>
)} )}
</Link> </Link>
</div> </div>}
<HeaderAuth/> <HeaderAuth/>
</div> </div>

View File

@ -30,8 +30,8 @@ const HomeIntro: React.SFC<Props> = ({ t, authUser }) => (
{t('home.intro.signup')} {t('home.intro.signup')}
</Link> </Link>
)} )}
<Link className="HomeIntro-content-buttons-button" to="/create-request"> <Link className="HomeIntro-content-buttons-button" to="/create">
{t('home.intro.ccr')} Create a Proposal
</Link> </Link>
</div> </div>
</div> </div>