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

Add qfFilter with estimatedMatchingView #1137

Merged
merged 4 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 30 additions & 4 deletions src/entities/ProjectEstimatedMatchingView.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
import { Field, ObjectType } from 'type-graphql';
import {
Entity,
Column,
Index,
PrimaryColumn,
BaseEntity,
ViewEntity,
ManyToOne,
RelationId,
ViewColumn,
JoinColumn,
} from 'typeorm';
import { Project } from './project';

@Entity({ name: 'project_estimated_matching_view' })
@ViewEntity('project_estimated_matching_view', { synchronize: false })
@Index('project_estimated_matching_view_project_id_qfround_id', [
'projectId',
'qfRoundId',
Expand All @@ -13,28 +26,41 @@ import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
@Index('project_estimated_matching_view_unique_donation_count', [
'uniqueDonationCount',
])
export class ProjectEstimatedMatchingView {
// Project ID associated with the donations
@ObjectType()
export class ProjectEstimatedMatchingView extends BaseEntity {
@Field(type => Project)
@ManyToOne(type => Project, project => project.projectEstimatedMatchingView)
@JoinColumn({ referencedColumnName: 'id' })
project: Project;

@Field()
@ViewColumn()
@PrimaryColumn()
projectId: number;

// QF Round ID associated with the donations
@ViewColumn()
@Field()
@PrimaryColumn()
qfRoundId: number;

// Sum of the square root of the value in USD of the donations
@ViewColumn()
@Column('double precision')
sqrtRootSum: number;

// Count of unique donations per user per project per QF round
@ViewColumn()
@Column('int')
uniqueDonationCount: number;

// Sum of the value in USD of the donations for active QF rounds where the donation status is verified
@ViewColumn()
@Column('double precision')
sumValueUsd: number;

// Count of unique donors who have verified donations for each project
@ViewColumn()
@Column('int')
uniqueDonorsCount: number;
}
2 changes: 2 additions & 0 deletions src/entities/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ProjectInstantPowerView } from '../views/projectInstantPowerView';
import { QfRound } from './qfRound';
import { ReferredEvent } from './referredEvent';
import { QfRoundHistory } from './qfRoundHistory';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';

export const getEntities = (): DataSourceOptions['entities'] => {
return [
Expand Down Expand Up @@ -81,6 +82,7 @@ export const getEntities = (): DataSourceOptions['entities'] => {
LastSnapshotProjectPowerView,
ProjectInstantPowerView,
ProjectUserInstantPowerView,
ProjectEstimatedMatchingView,

// historic snapshots
PowerSnapshotHistory,
Expand Down
9 changes: 9 additions & 0 deletions src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
} from '../repositories/qfRoundRepository';
import { EstimatedMatching } from '../types/qfTypes';
import { Campaign } from './campaign';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';
// tslint:disable-next-line:no-var-requires
const moment = require('moment');

Expand All @@ -76,6 +77,7 @@ export enum SortingField {
QualityScore = 'QualityScore',
GIVPower = 'GIVPower',
InstantBoosting = 'InstantBoosting',
ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds',
}

export enum FilterField {
Expand Down Expand Up @@ -341,6 +343,13 @@ export class Project extends BaseEntity {
@OneToMany(type => SocialProfile, socialProfile => socialProfile.project)
socialProfiles?: SocialProfile[];

@Field(type => [ProjectEstimatedMatchingView], { nullable: true })
@OneToMany(
type => ProjectEstimatedMatchingView,
projectEstimatedMatchingView => projectEstimatedMatchingView.project,
)
projectEstimatedMatchingView?: ProjectEstimatedMatchingView[];

@Field(type => Float)
@Column({ type: 'real' })
totalDonations: number;
Expand Down
30 changes: 24 additions & 6 deletions src/repositories/projectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import {
} from '../entities/project';
import { ProjectVerificationForm } from '../entities/projectVerificationForm';
import { ProjectAddress } from '../entities/projectAddress';
import {
errorMessages,
i18n,
translationErrorMessagesKeys,
} from '../utils/errorMessages';
import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages';
import { User, publicSelectionFields } from '../entities/user';
import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver';
import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver';

export const findProjectById = (projectId: number): Promise<Project | null> => {
// return Project.findOne({ id: projectId });

Expand Down Expand Up @@ -59,6 +54,7 @@ export type FilterProjectQueryInputParams = {
slugArray?: string[];
sortingBy?: SortingField;
qfRoundId?: number;
activeQfRoundId?: number;
};
export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
const {
Expand All @@ -71,6 +67,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
sortingBy,
slugArray,
qfRoundId,
activeQfRoundId,
} = params;

let query = Project.createQueryBuilder('project')
Expand Down Expand Up @@ -168,6 +165,27 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
'NULLS LAST',
);
break;
case SortingField.ActiveQfRoundRaisedFunds:
if (activeQfRoundId) {
query
.innerJoin(
'project.projectEstimatedMatchingView',
'projectEstimatedMatchingView',
)
.addSelect([
'projectEstimatedMatchingView.sumValueUsd',
'projectEstimatedMatchingView.qfRoundId',
])
.andWhere('projectEstimatedMatchingView.qfRoundId = :qfRoundId', {
qfRoundId: activeQfRoundId,
})
.orderBy(
'projectEstimatedMatchingView.sumValueUsd',
OrderDirection.DESC,
)
.addOrderBy(`project.verified`, OrderDirection.DESC);
}
break;
default:
query
.orderBy('projectInstantPower.totalPower', OrderDirection.DESC)
Expand Down
68 changes: 68 additions & 0 deletions src/resolvers/projectResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,74 @@ function allProjectsTestCases() {
);
});

it('should return projects, sort by project raised funds in the active QF round DESC', async () => {
const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const project1 = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
});
const project2 = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
});

const qfRound = await QfRound.create({
isActive: true,
name: 'test filter by qfRoundId',
minimumPassportScore: 10,
allocatedFund: 100,
beginDate: new Date(),
endDate: moment().add(1, 'day').toDate(),
}).save();
project1.qfRounds = [qfRound];
await project1.save();
project2.qfRounds = [qfRound];
await project2.save();

const donation1 = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
qfRoundId: qfRound.id,
valueUsd: 2,
},
donor.id,
project1.id,
);

const donation2 = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
qfRoundId: qfRound.id,
valueUsd: 20,
},
donor.id,
project2.id,
);

await refreshProjectEstimatedMatchingView();
await refreshProjectDonationSummaryView();

const result = await axios.post(graphqlUrl, {
query: fetchMultiFilterAllProjectsQuery,
variables: {
sortingBy: SortingField.ActiveQfRoundRaisedFunds,
limit: 10,
},
});

assert.equal(result.data.data.allProjects.projects.length, 2);
assert.equal(result.data.data.allProjects.projects[0].id, project2.id);
result.data.data.allProjects.projects.forEach(project => {
assert.equal(project.qfRounds[0].id, qfRound.id);
});
qfRound.isActive = false;
await qfRound.save();
});

it('should return projects, sort by project instant power DESC', async () => {
await PowerBoosting.clear();
await InstantPowerBalance.clear();
Expand Down
8 changes: 8 additions & 0 deletions src/resolvers/projectResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import { FeaturedUpdate } from '../entities/featuredUpdate';
import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators';
import { calculateGivbackFactor } from '../services/givbackService';
import { ProjectBySlugResponse } from './types/projectResolver';
import { findActiveQfRound } from '../repositories/qfRoundRepository';
import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService';

@ObjectType()
Expand Down Expand Up @@ -761,6 +762,12 @@ export class ProjectResolver {
): Promise<AllProjects> {
let projects: Project[];
let totalCount: number;
let activeQfRoundId: number | undefined;

if (sortingBy === SortingField.ActiveQfRoundRaisedFunds) {
activeQfRoundId = (await findActiveQfRound())?.id;
}

const filterQueryParams: FilterProjectQueryInputParams = {
limit,
skip,
Expand All @@ -770,6 +777,7 @@ export class ProjectResolver {
filters,
sortingBy,
qfRoundId,
activeQfRoundId,
};
let campaign;
if (campaignSlug) {
Expand Down
Loading