Skip to content

Commit

Permalink
chore: check for undisclosed funds before validating on front (#1701)
Browse files Browse the repository at this point in the history
* chore: check for undisclosed funds before validating on front

* chore: create updates without associated financial records

* chore: financial records backend validation

* TODO removed

---------

Co-authored-by: ncomerci <[email protected]>
  • Loading branch information
1emu and ncomerci authored Mar 14, 2024
1 parent e06e8fe commit 242b7fd
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 97 deletions.
80 changes: 58 additions & 22 deletions src/back/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,32 @@ import { Request } from 'express'
import ProposalModel from '../../entities/Proposal/model'
import { ProposalAttributes } from '../../entities/Proposal/types'
import {
FinancialRecord,
FinancialUpdateSectionSchema,
GeneralUpdateSection,
GeneralUpdateSectionSchema,
UpdateGeneralSection,
} from '../../entities/Updates/types'
import {
getCurrentUpdate,
getFundsReleasedSinceLatestUpdate,
getLatestUpdate,
getNextPendingUpdate,
getPendingUpdates,
getPublicUpdates,
getReleases,
isBetweenLateThresholdDate,
} from '../../entities/Updates/utils'
import { DiscourseService } from '../../services/DiscourseService'
import { ErrorService } from '../../services/ErrorService'
import { FinancialService } from '../../services/FinancialService'
import { VestingService } from '../../services/VestingService'
import Time from '../../utils/date/Time'
import { ErrorCategory } from '../../utils/errorCategories'
import { CoauthorService } from '../services/coauthor'
import { UpdateService } from '../services/update'

import { getProposal } from './proposal'

export default routes((route) => {
const withAuth = auth()
route.get('/proposals/:proposal/updates', handleAPI(getProposalUpdates))
Expand Down Expand Up @@ -103,12 +110,8 @@ async function getProposalUpdateComments(req: Request<{ update_id: string }>) {
}

const generalSectionValidator = schema.compile(GeneralUpdateSectionSchema)
async function createProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
const { author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<GeneralUpdateSection>(
generalSectionValidator,
body
)

function parseFinancialRecords(financial_records: unknown) {
const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records })
if (!parsedResult.success) {
ErrorService.report('Submission of invalid financial records', {
Expand All @@ -117,27 +120,60 @@ async function createProposalUpdate(req: WithAuth<Request<{ proposal: string }>>
})
throw new RequestError(`Invalid financial records`, RequestError.BadRequest)
}
const parsedRecords = parsedResult.data.financial_records
return parsedResult.data.financial_records
}

return await UpdateService.create(
{
proposal_id: req.params.proposal,
author,
health,
introduction,
highlights,
blockers,
next_steps,
additional_notes,
financial_records: parsedRecords,
},
req.auth!
async function validateFinancialRecords(
proposal: ProposalAttributes,
financial_records: unknown
): Promise<FinancialRecord[] | null> {
const [vestingData, updates] = await Promise.all([
VestingService.getVestingInfo(proposal.vesting_addresses),
UpdateService.getAllByProposalId(proposal.id),
])

const releases = vestingData ? getReleases(vestingData) : undefined
const publicUpdates = getPublicUpdates(updates)
const latestUpdate = getLatestUpdate(publicUpdates || [])
const { releasedFunds } = getFundsReleasedSinceLatestUpdate(latestUpdate, releases)
return releasedFunds > 0 ? parseFinancialRecords(financial_records) : null
}

async function createProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
const { author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<UpdateGeneralSection>(
generalSectionValidator,
body
)
try {
const proposal = await getProposal(req)
const financialRecords = await validateFinancialRecords(proposal, financial_records)
return await UpdateService.create(
{
proposal_id: req.params.proposal,
author,
health,
introduction,
highlights,
blockers,
next_steps,
additional_notes,
financial_records: financialRecords,
},
req.auth!
)
} catch (error) {
ErrorService.report('Error creating update', {
error,
category: ErrorCategory.Update,
})
throw new RequestError(`Something wnt wrong: ${error}`, RequestError.InternalServerError)
}
}

async function updateProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
const { id, author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<GeneralUpdateSection>(
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<UpdateGeneralSection>(
generalSectionValidator,
body
)
Expand Down
5 changes: 3 additions & 2 deletions src/back/services/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class UpdateService {
}
const update = await UpdateModel.createUpdate(data)
try {
await FinancialService.createRecords(update.id, financial_records || [])
if (financial_records) await FinancialService.createRecords(update.id, financial_records)
await DiscourseService.createUpdate(update, proposal.title)
await EventsService.updateCreated(update.id, proposal.id, proposal.title, user)
DiscordService.newUpdate(proposal.id, proposal.title, update.id, user)
Expand Down Expand Up @@ -190,7 +190,8 @@ export class UpdateService {
const { author, health, introduction, highlights, blockers, next_steps, additional_notes, financial_records } =
newUpdate

await FinancialService.createRecords(update.id, financial_records || update.financial_records!)
const records = financial_records || update.financial_records
if (records) await FinancialService.createRecords(update.id, records)

await UpdateModel.update<UpdateAttributes>(
{
Expand Down
8 changes: 4 additions & 4 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import { QuarterBudgetAttributes } from '../entities/QuarterBudget/types'
import { SubscriptionAttributes } from '../entities/Subscription/types'
import { Topic } from '../entities/SurveyTopic/types'
import {
FinancialUpdateSection,
GeneralUpdateSection,
UpdateAttributes,
UpdateFinancialSection,
UpdateGeneralSection,
UpdateResponse,
UpdateSubmissionDetails,
} from '../entities/Updates/types'
Expand Down Expand Up @@ -259,7 +259,7 @@ export class Governance extends API {

async createProposalUpdate(
proposal_id: string,
update: UpdateSubmissionDetails & GeneralUpdateSection & FinancialUpdateSection
update: UpdateSubmissionDetails & UpdateGeneralSection & UpdateFinancialSection
) {
return await this.fetchApiResponse<UpdateAttributes>(`/proposals/${proposal_id}/update`, {
method: 'POST',
Expand All @@ -270,7 +270,7 @@ export class Governance extends API {

async updateProposalUpdate(
proposal_id: string,
update: UpdateSubmissionDetails & GeneralUpdateSection & FinancialUpdateSection
update: UpdateSubmissionDetails & UpdateGeneralSection & UpdateFinancialSection
) {
return await this.fetchApiResponse<UpdateAttributes>(`/proposals/${proposal_id}/update`, {
method: 'PATCH',
Expand Down
24 changes: 11 additions & 13 deletions src/components/Updates/FinancialCardsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { useIntl } from 'react-intl'

import { VestingLog } from '../../clients/VestingData'
import { UpdateAttributes } from '../../entities/Updates/types'
import { getFundsReleasedSinceLatestUpdate } from '../../entities/Updates/utils'
import { CURRENCY_FORMAT_OPTIONS } from '../../helpers'
import useFormatMessage from '../../hooks/useFormatMessage'
import { formatDate } from '../../utils/date/Time'
Expand All @@ -11,20 +8,21 @@ import FinancialCard from './FinancialCard'
import './FinancialCardsSection.css'

interface Props {
releases?: VestingLog[]
previousUpdate?: Omit<UpdateAttributes, 'id' | 'proposal_id'>
currentUpdate?: Omit<UpdateAttributes, 'id' | 'proposal_id'>
releasedFundsValue: number
latestTimestamp?: string
txAmount: number
disclosedFunds: number
undisclosedFunds: number
}

function FinancialCardsSection({ releases, previousUpdate, currentUpdate, disclosedFunds }: Props) {
function FinancialCardsSection({
releasedFundsValue,
latestTimestamp,
txAmount,
disclosedFunds,
undisclosedFunds,
}: Props) {
const t = useFormatMessage()
const {
value: releasedFundsValue,
txAmount,
latestTimestamp,
} = getFundsReleasedSinceLatestUpdate(previousUpdate, releases, currentUpdate?.completion_date)
const undisclosedFunds = disclosedFunds <= releasedFundsValue ? releasedFundsValue - disclosedFunds : 0
const { formatNumber } = useIntl()
return (
<div className="FinancialCardsSection__CardsContainer">
Expand Down
40 changes: 24 additions & 16 deletions src/components/Updates/FinancialSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { useCallback, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { usePapaParse } from 'react-papaparse'

import sum from 'lodash/sum'
import toNumber from 'lodash/toNumber'

import { VestingLog } from '../../clients/VestingData'
import {
FinancialRecord,
FinancialUpdateSection,
FinancialUpdateSectionSchema,
UpdateAttributes,
UpdateFinancialSection,
} from '../../entities/Updates/types'
import { getDisclosedAndUndisclosedFunds, getFundsReleasedSinceLatestUpdate } from '../../entities/Updates/utils'
import useFormatMessage from '../../hooks/useFormatMessage'
import CSVDragAndDrop from '../Common/CSVDragAndDrop'
import NumberedTextArea from '../Common/NumberedTextArea'
Expand All @@ -25,10 +25,10 @@ import './FinancialSection.css'
import SummaryItems from './SummaryItems'

interface Props {
onValidation: (data: FinancialUpdateSection, sectionValid: boolean) => void
onValidation: (data: UpdateFinancialSection, sectionValid: boolean) => void
isFormDisabled: boolean
sectionNumber: number
intialValues?: Partial<FinancialUpdateSection>
intialValues?: Partial<UpdateFinancialSection>
releases?: VestingLog[]
latestUpdate?: Omit<UpdateAttributes, 'id' | 'proposal_id'>
csvInputField: string | undefined
Expand All @@ -40,7 +40,7 @@ type Error = {
text: string
}

const UPDATE_FINANCIAL_INITIAL_STATE: FinancialUpdateSection = {
const UPDATE_FINANCIAL_INITIAL_STATE: UpdateFinancialSection = {
financial_records: [],
}

Expand Down Expand Up @@ -80,13 +80,19 @@ function FinancialSection({
formState: { isValid, isDirty },
setValue,
watch,
} = useForm<FinancialUpdateSection>({
} = useForm<UpdateFinancialSection>({
defaultValues: intialValues || UPDATE_FINANCIAL_INITIAL_STATE,
mode: 'onTouched',
})

const [errors, setErrors] = useState<Error[]>([])
const financial_records = watch('financial_records')
const financialRecords: FinancialRecord[] | null = watch('financial_records')

const { releasedFunds, releasesTxCount, latestReleaseTimestamp } = getFundsReleasedSinceLatestUpdate(
latestUpdate,
releases
)
const { disclosedFunds, undisclosedFunds } = getDisclosedAndUndisclosedFunds(releasedFunds, financialRecords)

let typingTimeout: NodeJS.Timeout | null = null

Expand Down Expand Up @@ -119,12 +125,12 @@ function FinancialSection({
}

useEffect(() => {
if (financial_records.length > 0 && errors.length === 0) {
onValidation({ financial_records: financial_records }, true)
if ((undisclosedFunds === 0 || (financialRecords && financialRecords.length > 0)) && errors.length === 0) {
onValidation({ financial_records: financialRecords }, true)
} else {
onValidation({ financial_records: financial_records }, false)
onValidation({ financial_records: financialRecords }, false)
}
}, [onValidation, financial_records, errors.length])
}, [onValidation, financialRecords, undisclosedFunds, errors.length])

const csvInputHandler = useCallback(
(data: string[][]) => {
Expand Down Expand Up @@ -219,9 +225,11 @@ function FinancialSection({
>
<ContentSection>
<FinancialCardsSection
previousUpdate={latestUpdate}
releases={releases}
disclosedFunds={sum(financial_records.map(({ amount }) => amount))}
releasedFundsValue={releasedFunds}
latestTimestamp={latestReleaseTimestamp}
txAmount={releasesTxCount}
undisclosedFunds={undisclosedFunds}
disclosedFunds={disclosedFunds}
/>
</ContentSection>
<ContentSection>
Expand All @@ -239,10 +247,10 @@ function FinancialSection({
<CSVDragAndDrop onUploadAccepted={handleFileUpload} onRemoveFile={handleRemoveFile} />
</div>
</ContentSection>
{financial_records.length > 0 && (
{financialRecords && financialRecords.length > 0 && (
<ContentSection>
<Label>{t('page.proposal_update.summary_label')}</Label>
<SummaryItems financialRecords={financial_records} itemsInitiallyExpanded />
<SummaryItems financialRecords={financialRecords} itemsInitiallyExpanded />
</ContentSection>
)}
</ProjectRequestSection>
Expand Down
16 changes: 8 additions & 8 deletions src/components/Updates/GeneralSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { useForm, useWatch } from 'react-hook-form'

import { GeneralUpdateSection, GeneralUpdateSectionSchema, ProjectHealth } from '../../entities/Updates/types'
import { GeneralUpdateSectionSchema, ProjectHealth, UpdateGeneralSection } from '../../entities/Updates/types'
import useFormatMessage from '../../hooks/useFormatMessage'
import Label from '../Common/Typography/Label'
import MarkdownField from '../Form/MarkdownFieldSection'
Expand All @@ -11,13 +11,13 @@ import ProjectRequestSection from '../ProjectRequest/ProjectRequestSection'
import ProjectHealthButton from './ProjectHealthButton'

interface Props {
onValidation: (data: GeneralUpdateSection, sectionValid: boolean) => void
onValidation: (data: UpdateGeneralSection, sectionValid: boolean) => void
isFormDisabled: boolean
sectionNumber: number
intialValues?: Partial<GeneralUpdateSection>
intialValues?: Partial<UpdateGeneralSection>
}

const UPDATE_GENERAL_INITIAL_STATE: GeneralUpdateSection = {
const UPDATE_GENERAL_INITIAL_STATE: UpdateGeneralSection = {
health: ProjectHealth.OnTrack,
introduction: '',
highlights: '',
Expand All @@ -26,7 +26,7 @@ const UPDATE_GENERAL_INITIAL_STATE: GeneralUpdateSection = {
additional_notes: '',
}

const schema: Record<keyof GeneralUpdateSection, Record<string, unknown>> = GeneralUpdateSectionSchema.properties
const schema: Record<keyof UpdateGeneralSection, Record<string, unknown>> = GeneralUpdateSectionSchema.properties

function GeneralSection({ onValidation, isFormDisabled, sectionNumber, intialValues }: Props) {
const t = useFormatMessage()
Expand All @@ -35,7 +35,7 @@ function GeneralSection({ onValidation, isFormDisabled, sectionNumber, intialVal
control,
setValue,
watch,
} = useForm<GeneralUpdateSection>({
} = useForm<UpdateGeneralSection>({
defaultValues: intialValues || UPDATE_GENERAL_INITIAL_STATE,
mode: 'onTouched',
})
Expand All @@ -45,7 +45,7 @@ function GeneralSection({ onValidation, isFormDisabled, sectionNumber, intialVal
const handleHealthChange = (health: ProjectHealth) => setValue('health', health)
const health = watch('health')

const getFieldProps = (fieldName: keyof GeneralUpdateSection, isRequired = true) => ({
const getFieldProps = (fieldName: keyof UpdateGeneralSection, isRequired = true) => ({
control,
name: fieldName,
error: !!errors[fieldName],
Expand All @@ -72,7 +72,7 @@ function GeneralSection({ onValidation, isFormDisabled, sectionNumber, intialVal
})

useEffect(() => {
onValidation({ ...(values as GeneralUpdateSection) }, isValid)
onValidation({ ...(values as UpdateGeneralSection) }, isValid)
}, [values, isValid, onValidation])
return (
<ProjectRequestSection
Expand Down
Loading

0 comments on commit 242b7fd

Please sign in to comment.