Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: create project personnel #1796

Merged
merged 9 commits into from
May 21, 2024
17 changes: 4 additions & 13 deletions src/back/models/Personnel.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'

export type PersonnelAttributes = {
import { TeamMember } from '../../entities/Grant/types'

export type PersonnelAttributes = TeamMember & {
id: string
project_id: string
address?: string
name: string
role: string
description: string
link?: string
status: PersonnelStatus
deleted: boolean
updated_by?: string
updated_at?: Date
created_at: Date
}

export enum PersonnelStatus {
Deleted = 'deleted',
Unassigned = 'unassigned',
Assigned = 'assigned',
}

export default class PersonnelModel extends Model<PersonnelAttributes> {
static tableName = 'personnel'
static withTimestamps = false
Expand Down
45 changes: 38 additions & 7 deletions src/back/models/Project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'
import { SQL } from 'decentraland-gatsby/dist/entities/Database/utils/sql'
import { SQL, table } from 'decentraland-gatsby/dist/entities/Database/utils'

import CoauthorModel from '../../entities/Coauthor/model'
import { CoauthorStatus } from '../../entities/Coauthor/types'
import { ProjectStatus } from '../../entities/Grant/types'
import ProposalModel from '../../entities/Proposal/model'

import PersonnelModel, { PersonnelAttributes } from './Personnel'

export type ProjectAttributes = {
id: string
Expand All @@ -18,6 +23,7 @@ export type ProjectAttributes = {

// TODO: add here all data from other tables (updates, personnel, milestones, etc)
export type Project = ProjectAttributes & {
personnel: PersonnelAttributes[]
author: string
coauthors: string[] | null
}
Expand All @@ -29,14 +35,39 @@ export default class ProjectModel extends Model<ProjectAttributes> {

static async getProject(id: string) {
const query = SQL`
SELECT pr.*, p.user as author, array_agg(co.address) as coauthors
FROM projects pr
JOIN proposals p on pr.proposal_id = p.id
LEFT JOIN coauthors co on pr.proposal_id = co.proposal_id AND co.status = 'APPROVED'
WHERE pr.id = ${id}
GROUP BY pr.id, p.user;
SELECT
pr.*,
p.user as author,
COALESCE(json_agg(
json_build_object(
'id', pe.id,
'project_id', pe.project_id,
'address', pe.address,
'name', pe.name,
'role', pe.role,
'about', pe.about,
'relevantLink', pe."relevantLink",
'status', pe.status,
'updated_by', pe.updated_by,
'updated_at', pe.updated_at,
'created_at', pe.created_at
) ORDER BY pe.id) FILTER (WHERE pe.id IS NOT NULL), '[]') AS personnel,
COALESCE(array_agg(co.address) FILTER (WHERE co.address IS NOT NULL), '{}') AS coauthors
FROM ${table(ProjectModel)} pr
JOIN ${table(ProposalModel)} p ON pr.proposal_id = p.id
LEFT JOIN ${table(PersonnelModel)} pe ON pr.id = pe.project_id AND pe.deleted = false
LEFT JOIN ${table(CoauthorModel)} co ON pr.proposal_id = co.proposal_id AND co.status = ${
CoauthorStatus.APPROVED
}
WHERE pr.id = ${id}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we validating this is an id?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are in the route handler, but I'll add a check in the model for xtra safu

GROUP BY pr.id, p.user;
`

const result = await this.namedQuery<Project>(`get_project`, query)
if (!result || result.length === 0) {
throw new Error(`Project not found: "${id}"`)
}

return result[0]
}
}
1 change: 1 addition & 0 deletions src/back/routes/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async function getProject(req: Request<{ project: string }>) {
try {
return await ProjectService.getProject(id)
} catch (e) {
console.log(`Error getting project: ${e}`) //TODO: remove before merging projects to main
throw new RequestError(`Project "${id}" not found`, RequestError.NotFound)
}
}
Expand Down
25 changes: 11 additions & 14 deletions src/entities/Bid/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { BudgetBreakdownConcept, GrantRequestDueDiligenceSchema, GrantRequestTeamSchema } from '../Grant/types'
import {
BudgetBreakdownConcept,
GrantRequestDueDiligenceSchema,
GrantRequestTeamSchema,
ProposalRequestTeam,
} from '../Grant/types'

import { BID_MIN_PROJECT_DURATION } from './constants'

Expand Down Expand Up @@ -39,30 +44,22 @@ export type BidRequestGeneralInfo = {
coAuthors?: string[]
}

export type TeamMember = {
name: string
role: string
about: string
address?: string
relevantLink?: string
}

export type BidRequestTeam = {
members: TeamMember[]
}

export type BidRequestDueDiligence = {
budgetBreakdown: BudgetBreakdownConcept[]
}

export type BidRequest = BidRequestFunding &
BidRequestGeneralInfo &
BidRequestTeam &
ProposalRequestTeam &
BidRequestDueDiligence & {
linked_proposal_id: string
coAuthors?: string[]
}

export type BidProposalConfiguration = BidRequest & { bid_number: number } & { created_at: string } & {
choices: string[]
}

export const BidRequestFundingSchema = {
funding: {
type: 'integer',
Expand Down
6 changes: 3 additions & 3 deletions src/entities/Grant/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export type GrantRequest = {
category: NewGrantCategory | null
} & GrantRequestFunding &
GrantRequestGeneralInfo &
GrantRequestTeam &
ProposalRequestTeam &
GrantRequestCategoryAssessment &
GrantRequestDueDiligence

Expand Down Expand Up @@ -435,9 +435,9 @@ export type GrantRequestDueDiligence = {

export type TeamMember = {
name: string
address?: string
role: string
about: string
address?: string
relevantLink?: string
}

Expand All @@ -447,7 +447,7 @@ type Milestone = {
delivery_date: string
}

export type GrantRequestTeam = {
export type ProposalRequestTeam = {
members: TeamMember[]
}

Expand Down
4 changes: 2 additions & 2 deletions src/entities/Proposal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
CategoryAssessmentQuestions,
GrantRequestDueDiligence,
GrantRequestGeneralInfo,
GrantRequestTeam,
GrantTierType,
PaymentToken,
ProjectStatus,
ProposalGrantCategory,
ProposalRequestTeam,
SubtypeOptions,
VestingStartDate,
} from '../Grant/types'
Expand Down Expand Up @@ -680,7 +680,7 @@ export const ProposalRequiredVP = {

export type GrantProposalConfiguration = GrantRequestGeneralInfo &
GrantRequestDueDiligence &
GrantRequestTeam & {
ProposalRequestTeam & {
category: ProposalGrantCategory | null
size: number
paymentToken?: PaymentToken
Expand Down
16 changes: 7 additions & 9 deletions src/migrations/1715019261618_create-personnel-table.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { MigrationBuilder } from "node-pg-migrate"

import Model, { PersonnelStatus } from "../back/models/Personnel"
import Model from "../back/models/Personnel"
import ProjectModel from "../back/models/Project"

const STATUS_TYPE = 'personnel_status_type'

export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.createType(STATUS_TYPE, Object.values(PersonnelStatus))
pgm.createTable(Model.tableName, {
id: {
type: 'TEXT',
Expand All @@ -28,16 +25,17 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
type: 'TEXT',
notNull: true
},
description: {
about: {
type: 'TEXT',
notNull: true
},
link: {
relevantLink: {
type: 'TEXT',
},
status: {
type: STATUS_TYPE,
deleted: {
type: 'BOOLEAN',
notNull: true,
default: false
},
updated_at: {
type: 'TIMESTAMPTZ',
Expand All @@ -59,5 +57,5 @@ export async function up(pgm: MigrationBuilder): Promise<void> {

export async function down(pgm: MigrationBuilder): Promise<void> {
pgm.dropTable(Model.tableName)
pgm.dropType(STATUS_TYPE, { cascade: true })
pgm.dropType('personnel_status_type', { cascade: true })
}
19 changes: 11 additions & 8 deletions src/services/BidService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import JobContext from 'decentraland-gatsby/dist/entities/Job/context'

import UnpublishedBidModel from '../entities/Bid/model'
import { UnpublishedBidAttributes, UnpublishedBidStatus } from '../entities/Bid/types'
import { BidProposalConfiguration, UnpublishedBidAttributes, UnpublishedBidStatus } from '../entities/Bid/types'
import { GATSBY_GRANT_VP_THRESHOLD } from '../entities/Grant/constants'
import ProposalModel from '../entities/Proposal/model'
import { ProposalType } from '../entities/Proposal/types'
Expand All @@ -15,6 +15,7 @@ import { ErrorService } from './ErrorService'
import { ProposalService } from './ProposalService'

const MINIMUM_BIDS_TO_PUBLISH = Number(process.env.MINIMUM_BIDS_TO_PUBLISH || 0)

export default class BidService {
static async createBid(
linked_proposal_id: string,
Expand Down Expand Up @@ -102,16 +103,18 @@ export default class BidService {
? Number(GATSBY_GRANT_VP_THRESHOLD)
: getHighBudgetVpThreshold(Number(bid_proposal_data.funding))

const bidProposalConfiguration: BidProposalConfiguration = {
bid_number,
linked_proposal_id,
created_at,
...bid_proposal_data,
choices: DEFAULT_CHOICES,
}

await ProposalService.createProposal({
type: ProposalType.Bid,
user: author_address,
configuration: {
bid_number,
linked_proposal_id,
created_at,
...bid_proposal_data,
choices: DEFAULT_CHOICES,
},
configuration: bidProposalConfiguration,
required_to_pass,
finish_at,
})
Expand Down
38 changes: 33 additions & 5 deletions src/services/ProjectService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import crypto from 'crypto'

import PersonnelModel, { PersonnelAttributes } from '../back/models/Personnel'
import ProjectModel, { ProjectAttributes } from '../back/models/Project'
import { TransparencyVesting } from '../clients/Transparency'
import UnpublishedBidModel from '../entities/Bid/model'
import { BidProposalConfiguration } from '../entities/Bid/types'
import { GrantTier } from '../entities/Grant/GrantTier'
import { GRANT_PROPOSAL_DURATION_IN_SECONDS } from '../entities/Grant/constants'
import { GrantRequest, ProjectStatus, TransparencyProjectStatus } from '../entities/Grant/types'
Expand Down Expand Up @@ -164,20 +166,46 @@ export class ProjectService {
}

private static async createProject(proposal: ProposalWithOutcome) {
const newProject: ProjectAttributes = {
const creationDate = new Date()
const newProject = await ProjectModel.create({
id: crypto.randomUUID(),
proposal_id: proposal.id,
title: proposal.title,
status: ProjectStatus.Pending,
links: [],
created_at: new Date(),
}
created_at: creationDate,
})

await ProjectService.createPersonnel(proposal, newProject, creationDate)

return await ProjectModel.create(newProject)
return newProject
}

private static async createPersonnel(
proposal: ProposalWithOutcome,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't we pass just members or personnel here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at it as if finding/parsing the info needed to create the personnel was part of this method's responsibility (so createProject only handles the project attributes, then delegates the rest)

newProject: ProjectAttributes,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be always a new project, isn't it? Maybe we can call it project here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I agree on the name change, but in this case we will always use a new project, since the createProject method is the only one calling this, and it uses the personnel from an existing proposal to create all the personnel for a new project.

If we want to create new personnel for a project that already exists, we should have another method that creates them one by one (this one uses createMany)

WDYT?

creationDate: Date
) {
const newPersonnel: PersonnelAttributes[] = []
const config =
proposal.type === ProposalType.Grant
? (proposal.configuration as GrantProposalConfiguration)
: (proposal.configuration as BidProposalConfiguration)
config.members?.forEach((member) => {
if (member) {
newPersonnel.push({
...member,
id: crypto.randomUUID(),
project_id: newProject.id,
created_at: creationDate,
deleted: false,
})
}
})
await PersonnelModel.createMany(newPersonnel)
}

static async getProject(id: string) {
//TODO: add all data to project from other tables (updates, personnel, milestones, etc) & return Project type, instead of ProjectAttributes
const project = await ProjectModel.getProject(id)
if (!project) {
throw new Error(`Project not found: "${id}"`)
Expand Down