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

View File

@ -2,17 +2,17 @@ import { pick } from 'lodash';
import { store } from 'react-easy-state';
import axios, { AxiosError } from 'axios';
import {
User,
Proposal,
CCR,
CommentArgs,
Contribution,
ContributionArgs,
EmailExample,
PageData,
PageQuery,
Proposal,
RFP,
RFPArgs,
EmailExample,
PageQuery,
PageData,
CommentArgs,
User,
} from './types';
// API
@ -142,6 +142,11 @@ async function approveDiscussion(
return data;
}
async function approveProposalKYC(id: number) {
const { data } = await api.put(`/admin/proposals/${id}/approve-kyc`);
return data;
}
async function acceptProposal(
id: number,
isAccepted: boolean,
@ -345,6 +350,7 @@ const app = store({
proposalDetailApprovingDiscussion: false,
proposalDetailMarkingChangesAsResolved: false,
proposalDetailAcceptingProposal: false,
proposalDetailApprovingKyc: false,
proposalDetailMarkingMilestonePaid: false,
proposalDetailCanceling: false,
proposalDetailUpdating: false,
@ -688,6 +694,25 @@ const app = store({
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(
isAccepted: boolean,
withFunding: boolean,
@ -975,6 +1000,7 @@ function createDefaultPageData<T>(sort: string): PageData<T> {
}
type FNFetchPage = (params: PageQuery) => Promise<any>;
interface PageParent<T> {
page: PageData<T>;
}

View File

@ -123,6 +123,7 @@ export interface Proposal {
isVersionTwo: boolean;
changesRequestedDiscussion: boolean | null;
changesRequestedDiscussionReason: string | null;
kycApproved: null | boolean;
}
export interface Comment {
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)
@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'])
@body({
"isAccepted": fields.Bool(required=True),

View File

@ -391,6 +391,8 @@ class Proposal(db.Model):
date_approved = db.Column(db.DateTime)
date_published = db.Column(db.DateTime)
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)
changes_requested_discussion = db.Column(db.Boolean(), 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",
"changes_requested_discussion",
"changes_requested_discussion_reason",
"live_draft_id"
"live_draft_id",
"kyc_approved"
)
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>
</div>
<div className="Header-links-button is-desktop">
{false && <div className="Header-links-button is-desktop">
<Link to="/create-request">
{Array.isArray(ccrDrafts) && ccrDrafts.length > 0 ? (
<Button type={'primary'}>My Requests</Button>
@ -113,7 +113,7 @@ class Header extends React.Component<Props, State> {
<Button type={'primary'}>Create a Request</Button>
)}
</Link>
</div>
</div>}
<HeaderAuth/>
</div>

View File

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