mileston payout emails + some bug fixes
This commit is contained in:
parent
fd9a4c5393
commit
47f827693d
|
@ -77,4 +77,24 @@ export default [
|
|||
title: 'Arbiter assignment',
|
||||
description: 'Sent if someone is made arbiter of a proposal',
|
||||
},
|
||||
{
|
||||
id: 'milestone_request',
|
||||
title: 'Milestone request',
|
||||
description: 'Sent if team member has made a milestone payout request',
|
||||
},
|
||||
{
|
||||
id: 'milestone_accept',
|
||||
title: 'Milestone accept',
|
||||
description: 'Sent if arbiter approves milestone payout',
|
||||
},
|
||||
{
|
||||
id: 'milestone_reject',
|
||||
title: 'Milestone reject',
|
||||
description: 'Sent if arbiter rejects milestone payout',
|
||||
},
|
||||
{
|
||||
id: 'milestone_paid',
|
||||
title: 'Milestone paid',
|
||||
description: 'Sent when milestone is paid',
|
||||
},
|
||||
] as Email[];
|
||||
|
|
|
@ -6,12 +6,19 @@ class FakeUser(object):
|
|||
title = 'Email Example Dude'
|
||||
|
||||
|
||||
class FakeMilestone(object):
|
||||
id = 123
|
||||
index = 0
|
||||
title = 'Example Milestone'
|
||||
|
||||
|
||||
class FakeProposal(object):
|
||||
id = 123
|
||||
title = 'Example proposal'
|
||||
brief = 'This is an example proposal'
|
||||
content = 'Example example example example'
|
||||
target = "100"
|
||||
current_milestone = FakeMilestone()
|
||||
|
||||
|
||||
class FakeContribution(object):
|
||||
|
@ -101,7 +108,27 @@ example_email_args = {
|
|||
},
|
||||
'proposal_arbiter': {
|
||||
'proposal': proposal,
|
||||
'proposal_url': 'http://someproposal.com',
|
||||
'arbitration_url': 'http://arbitrationtab.com',
|
||||
'proposal_url': 'http://zfnd.org/proposals/999',
|
||||
'accept_url': 'http://zfnd.org/email/arbiter?code=blah&proposalId=999',
|
||||
},
|
||||
'milestone_request': {
|
||||
'proposal': proposal,
|
||||
'proposal_milestones_url': 'http://zfnd.org/proposals/999-my-proposal?tab=milestones',
|
||||
},
|
||||
'milestone_reject': {
|
||||
'proposal': proposal,
|
||||
'admin_note': 'We noticed that the tests were failing for the features outlined in this milestone. Please address these issues.',
|
||||
'proposal_milestones_url': 'http://zfnd.org/proposals/999-my-proposal?tab=milestones',
|
||||
},
|
||||
'milestone_accept': {
|
||||
'proposal': proposal,
|
||||
'amount': '33',
|
||||
'proposal_milestones_url': 'http://zfnd.org/proposals/999-my-proposal?tab=milestones',
|
||||
},
|
||||
'milestone_paid': {
|
||||
'proposal': proposal,
|
||||
'amount': '33',
|
||||
'tx_explorer_url': 'http://someblockexplorer.com/tx/271857129857192579125',
|
||||
'proposal_milestones_url': 'http://zfnd.org/proposals/999-my-proposal?tab=milestones',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ from grant.utils.admin import admin_auth_required, admin_is_authed, admin_login,
|
|||
from grant.utils.misc import make_url
|
||||
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus, ProposalArbiterStatus, MilestoneStage
|
||||
from grant.utils import pagination
|
||||
from grant.settings import EXPLORER_URL
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
from .example_emails import example_email_args
|
||||
|
@ -277,15 +278,24 @@ def paid_milestone_payout_request(id, mid, tx_id):
|
|||
for ms in proposal.milestones:
|
||||
if ms.id == int(mid):
|
||||
ms.mark_paid(tx_id)
|
||||
# TODO: email TEAM that payout request was PAID
|
||||
db.session.add(ms)
|
||||
db.session.flush()
|
||||
# check if this is the final ms, and update proposal.stage
|
||||
num_paid = reduce(lambda a, x: a + (1 if x.stage == MilestoneStage.PAID else 0), proposal.milestones, 0)
|
||||
if num_paid == len(proposal.milestones):
|
||||
proposal.stage = ProposalStage.COMPLETED # WIP -> COMPLETED
|
||||
db.session.add(proposal)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
# email TEAM that payout request was PAID
|
||||
amount = Decimal(ms.payout_percent) * Decimal(proposal.target) / 100
|
||||
for member in proposal.team:
|
||||
send_email(member.email_address, 'milestone_paid', {
|
||||
'proposal': proposal,
|
||||
'amount': amount,
|
||||
'tx_explorer_url': f'{EXPLORER_URL}transactions/{tx_id}',
|
||||
'proposal_milestones_url': make_url(f'/proposals/{proposal.id}?tab=milestones'),
|
||||
})
|
||||
return proposal_schema.dump(proposal), 200
|
||||
|
||||
return {"message": "No milestone matching id"}, 404
|
||||
|
|
|
@ -163,6 +163,52 @@ def proposal_arbiter(email_args):
|
|||
}
|
||||
|
||||
|
||||
def milestone_request(email_args):
|
||||
p = email_args['proposal']
|
||||
ms = p.current_milestone
|
||||
return {
|
||||
'subject': f'Payout request for {p.title} - {ms.title} has been made',
|
||||
'title': f'Milestone payout requested',
|
||||
'preview': f'A payout request for milestone {ms.title} has been made.',
|
||||
'subscription': EmailSubscription.ARBITER,
|
||||
}
|
||||
|
||||
|
||||
def milestone_reject(email_args):
|
||||
p = email_args['proposal']
|
||||
ms = p.current_milestone
|
||||
return {
|
||||
'subject': f'Payout rejected for {p.title} - {ms.title}',
|
||||
'title': f'Milestone payout rejected',
|
||||
'preview': f'The payout for milestone {ms.title} has been rejected.',
|
||||
'subscription': EmailSubscription.MY_PROPOSAL_APPROVAL,
|
||||
}
|
||||
|
||||
|
||||
def milestone_accept(email_args):
|
||||
p = email_args['proposal']
|
||||
a = email_args['amount']
|
||||
ms = p.current_milestone
|
||||
return {
|
||||
'subject': f'Payout approved for {p.title} - {ms.title}!',
|
||||
'title': f'Milestone payout approved',
|
||||
'preview': f'The payout of {a} ZEC for milestone {ms.title} has been approved.',
|
||||
'subscription': EmailSubscription.MY_PROPOSAL_APPROVAL,
|
||||
}
|
||||
|
||||
|
||||
def milestone_paid(email_args):
|
||||
p = email_args['proposal']
|
||||
a = email_args['amount']
|
||||
ms = p.current_milestone
|
||||
return {
|
||||
'subject': f'{p.title} - {ms.title} has been paid!',
|
||||
'title': f'Milestone paid',
|
||||
'preview': f'The milestone {ms.title} payout of {a} ZEC has been paid!',
|
||||
'subscription': EmailSubscription.MY_PROPOSAL_FUNDED,
|
||||
}
|
||||
|
||||
|
||||
get_info_lookup = {
|
||||
'signup': signup_info,
|
||||
'team_invite': team_invite_info,
|
||||
|
@ -178,7 +224,11 @@ get_info_lookup = {
|
|||
'contribution_confirmed': contribution_confirmed,
|
||||
'contribution_update': contribution_update,
|
||||
'comment_reply': comment_reply,
|
||||
'proposal_arbiter': proposal_arbiter
|
||||
'proposal_arbiter': proposal_arbiter,
|
||||
'milestone_request': milestone_request,
|
||||
'milestone_reject': milestone_reject,
|
||||
'milestone_accept': milestone_accept,
|
||||
'milestone_paid': milestone_paid
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from dateutil.parser import parse
|
||||
from decimal import Decimal
|
||||
from flask import Blueprint, g, request
|
||||
from flask_yoloapi import endpoint, parameter
|
||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||
|
@ -562,9 +563,13 @@ def request_milestone_payout(proposal_id, milestone_id):
|
|||
for ms in g.current_proposal.milestones:
|
||||
if ms.id == int(milestone_id):
|
||||
ms.request_payout(g.current_user.id)
|
||||
# TODO: email ARBITER to review payout request
|
||||
db.session.add(ms)
|
||||
db.session.commit()
|
||||
# email ARBITER to review payout request
|
||||
send_email(g.current_proposal.arbiter.user.email_address, 'milestone_request', {
|
||||
'proposal': g.current_proposal,
|
||||
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
||||
})
|
||||
return proposal_schema.dump(g.current_proposal), 200
|
||||
|
||||
return {"message": "No milestone matching id"}, 404
|
||||
|
@ -580,9 +585,16 @@ def accept_milestone_payout_request(proposal_id, milestone_id):
|
|||
for ms in g.current_proposal.milestones:
|
||||
if ms.id == int(milestone_id):
|
||||
ms.accept_request(g.current_user.id)
|
||||
# TODO: email TEAM that payout request accepted (maybe, or wait until paid?)
|
||||
db.session.add(ms)
|
||||
db.session.commit()
|
||||
# email TEAM that payout request accepted
|
||||
amount = Decimal(ms.payout_percent) * Decimal(g.current_proposal.target) / 100
|
||||
for member in g.current_proposal.team:
|
||||
send_email(member.email_address, 'milestone_accept', {
|
||||
'proposal': g.current_proposal,
|
||||
'amount': amount,
|
||||
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
||||
})
|
||||
return proposal_schema.dump(g.current_proposal), 200
|
||||
|
||||
return {"message": "No milestone matching id"}, 404
|
||||
|
@ -600,9 +612,15 @@ def reject_milestone_payout_request(proposal_id, milestone_id, reason):
|
|||
for ms in g.current_proposal.milestones:
|
||||
if ms.id == int(milestone_id):
|
||||
ms.reject_request(g.current_user.id, reason)
|
||||
# TODO: email TEAM that payout request was rejected
|
||||
db.session.add(ms)
|
||||
db.session.commit()
|
||||
# email TEAM that payout request was rejected
|
||||
for member in g.current_proposal.team:
|
||||
send_email(member.email_address, 'milestone_reject', {
|
||||
'proposal': g.current_proposal,
|
||||
'admin_note': reason,
|
||||
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
||||
})
|
||||
return proposal_schema.dump(g.current_proposal), 200
|
||||
|
||||
return {"message": "No milestone matching id"}, 404
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
The proposal milestone
|
||||
<a href="{{ args.proposal_milestones_url }}" target="_blank">
|
||||
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
|
||||
</a>
|
||||
payout of <b>{{ args.amount }} ZEC</b> has been approved.
|
||||
</p>
|
||||
|
||||
<p style="margin: 0;">
|
||||
You will receive payment shortly!
|
||||
</p>
|
|
@ -0,0 +1,6 @@
|
|||
The proposal milestone "{{ args.proposal.title }} - {{args.proposal.current_milestone.title }}"
|
||||
payout of {{args.amount}} ZEC has been approved!
|
||||
|
||||
You will receive payment shortly!
|
||||
|
||||
View the milestone: {{ args.proposal_milestones_url }}
|
|
@ -0,0 +1,12 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
Hooray! <b>{{ args.amount }} ZEC</b> has been paid out for
|
||||
<a href="{{ args.proposal_milestones_url }}" target="_blank">
|
||||
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }} </a
|
||||
>! You can view the transaction below:
|
||||
</p>
|
||||
|
||||
<p style="margin: 0 0 20px;">
|
||||
<a href="{{ args.tx_explorer_url }}" target="_blank" rel="nofollow noopener">
|
||||
{{ args.tx_explorer_url }}
|
||||
</a>
|
||||
</p>
|
|
@ -0,0 +1,6 @@
|
|||
Hooray! {{args.amount}} ZEC has been paid out for "{{ args.proposal.title }} - {{args.proposal.current_milestone.title }}"!
|
||||
You can view the transaction below:
|
||||
|
||||
{{ args.tx_explorer_url }}
|
||||
|
||||
View the milestone: {{ args.proposal_milestones_url }}
|
|
@ -0,0 +1,21 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
The payout request for proposal milestone
|
||||
<a href="{{ args.proposal_milestones_url }}" target="_blank">
|
||||
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
|
||||
</a>
|
||||
has been rejected.
|
||||
</p>
|
||||
|
||||
{% if args.admin_note %}
|
||||
<p style="margin: 20px 0 0;">
|
||||
The following reason was provided:
|
||||
</p>
|
||||
<p style="margin: 10px 0; padding: 20px; background: #F8F8F8;">
|
||||
“{{ args.admin_note }}”
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin: 0;">
|
||||
Another request for payment can be made when the above concerns have been
|
||||
addressed.
|
||||
</p>
|
|
@ -0,0 +1,12 @@
|
|||
The payout request for proposal milestone "{{ args.proposal.title }} - {{args.proposal.current_milestone.title }}"
|
||||
has been rejected.
|
||||
|
||||
{% if args.admin_note %}
|
||||
The following reason was provided:
|
||||
|
||||
> {{ args.admin_note }}
|
||||
{% endif %}
|
||||
|
||||
Another request for payment can be made when the above concerns have been addressed.
|
||||
|
||||
View milestone: {{ args.proposal_milestones_url }}
|
|
@ -0,0 +1,33 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
A payout request for the proposal milestone
|
||||
<a href="{{ args.proposal_milestones_url }}" target="_blank">
|
||||
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
|
||||
</a>
|
||||
has been made. As arbiter, you are responsible for reviewing this request.
|
||||
</p>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="border-radius: 3px;"
|
||||
bgcolor="{{ UI.PRIMARY }}"
|
||||
>
|
||||
<a
|
||||
href="{{ args.proposal_milestones_url }}"
|
||||
target="_blank"
|
||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{
|
||||
UI.PRIMARY
|
||||
}}; display: inline-block;"
|
||||
>
|
||||
Review the request
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,4 @@
|
|||
A payout request for the proposal milestone "{{ args.proposal.title }} - {{args.proposal.current_milestone.title }}"
|
||||
has been made. As arbiter, you are responsible for reviewing this request.
|
||||
|
||||
Review the request: {{ args.proposal_milestones_url }}
|
|
@ -7,7 +7,12 @@ import { Alert, Steps, Button, message, Modal, Input } from 'antd';
|
|||
import { AlertProps } from 'antd/lib/alert';
|
||||
import { StepProps } from 'antd/lib/steps';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import { Milestone, ProposalMilestone, MILESTONE_STAGE } from 'types';
|
||||
import {
|
||||
Milestone,
|
||||
ProposalMilestone,
|
||||
MILESTONE_STAGE,
|
||||
PROPOSAL_ARBITER_STATUS,
|
||||
} from 'types';
|
||||
import { PROPOSAL_STAGE } from 'api/constants';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import Loader from 'components/Loader';
|
||||
|
@ -16,6 +21,7 @@ import Placeholder from 'components/Placeholder';
|
|||
import { proposalActions } from 'modules/proposals';
|
||||
import { ProposalDetail } from 'modules/proposals/reducers';
|
||||
import './index.less';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
enum STEP_STATUS {
|
||||
WAIT = 'wait',
|
||||
|
@ -230,6 +236,10 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
isCurrent={activeIsCurrent}
|
||||
isTeamMember={proposal.isTeamMember || false}
|
||||
isArbiter={proposal.isArbiter || false}
|
||||
hasArbiter={
|
||||
!!proposal.arbiter.user &&
|
||||
proposal.arbiter.status === PROPOSAL_ARBITER_STATUS.ACCEPTED
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -301,6 +311,7 @@ interface MilestoneProps extends MSProps {
|
|||
showRejectPayout: (milestoneId: number) => void;
|
||||
isTeamMember: boolean;
|
||||
isArbiter: boolean;
|
||||
hasArbiter: boolean;
|
||||
isCurrent: boolean;
|
||||
proposalId: number;
|
||||
isFunded: boolean;
|
||||
|
@ -382,7 +393,11 @@ const MilestoneAction: React.SFC<MilestoneProps> = p => {
|
|||
if (!p.isCurrent || !p.isFunded || p.stage === MILESTONE_STAGE.PAID) {
|
||||
return null;
|
||||
}
|
||||
if (!p.hasArbiter && !p.isTeamMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TEAM INFO
|
||||
const team = {
|
||||
[MILESTONE_STAGE.IDLE]: () => (
|
||||
<>
|
||||
|
@ -439,6 +454,7 @@ const MilestoneAction: React.SFC<MilestoneProps> = p => {
|
|||
[MILESTONE_STAGE.PAID]: () => <></>,
|
||||
} as { [key in MILESTONE_STAGE]: () => ReactNode };
|
||||
|
||||
// OUTSIDERS/OTHERS INFO
|
||||
const others = {
|
||||
[MILESTONE_STAGE.IDLE]: () => (
|
||||
<>
|
||||
|
@ -473,6 +489,7 @@ const MilestoneAction: React.SFC<MilestoneProps> = p => {
|
|||
[MILESTONE_STAGE.PAID]: () => <></>,
|
||||
} as { [key in MILESTONE_STAGE]: () => ReactNode };
|
||||
|
||||
// ARBITER INFO
|
||||
const arbiter = {
|
||||
[MILESTONE_STAGE.IDLE]: () => (
|
||||
<>
|
||||
|
@ -528,6 +545,26 @@ const MilestoneAction: React.SFC<MilestoneProps> = p => {
|
|||
content = others[p.stage]();
|
||||
}
|
||||
|
||||
// special warning if no arbiter is set for team members
|
||||
if (!p.hasArbiter && p.isTeamMember) {
|
||||
content = (
|
||||
<Alert
|
||||
type="error"
|
||||
message="Arbiter not assigned"
|
||||
description={
|
||||
<p>
|
||||
We are sorry for the inconvenience, but in order to have milestone payouts
|
||||
reviewed an arbiter must be assigned. Please{' '}
|
||||
<Link target="_blank" to="/contact">
|
||||
contact support
|
||||
</Link>{' '}
|
||||
for help.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ProposalMilestones-milestone-divider" />
|
||||
|
|
|
@ -293,7 +293,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
detail: {
|
||||
...state.detail,
|
||||
isAcceptingPayout: true,
|
||||
acceptingPayoutError: '',
|
||||
acceptPayoutError: '',
|
||||
},
|
||||
};
|
||||
case types.PROPOSAL_PAYOUT_ACCEPT_FULFILLED:
|
||||
|
@ -307,7 +307,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
detail: {
|
||||
...state.detail,
|
||||
isAcceptingPayout: false,
|
||||
acceptingPayoutError: (payload && payload.message) || payload.toString(),
|
||||
acceptPayoutError: (payload && payload.message) || payload.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue