From ea85e625b12193567df10ca54329bbb3bafb07f6 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 20 Oct 2023 00:22:39 +0300 Subject: [PATCH 1/2] campaign statistics endpoints --- apps/api/src/app/app.module.ts | 2 + .../statistics/dto/donation-statistics.dto.ts | 36 +++++++++++ apps/api/src/statistics/dto/group-by.dto.ts | 5 ++ .../src/statistics/statistics.controller.ts | 38 ++++++++++++ apps/api/src/statistics/statistics.module.ts | 12 ++++ apps/api/src/statistics/statistics.service.ts | 59 +++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 apps/api/src/statistics/dto/donation-statistics.dto.ts create mode 100644 apps/api/src/statistics/dto/group-by.dto.ts create mode 100644 apps/api/src/statistics/statistics.controller.ts create mode 100644 apps/api/src/statistics/statistics.module.ts create mode 100644 apps/api/src/statistics/statistics.service.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index e3d8985e6..61aba5f94 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -56,6 +56,7 @@ import { CacheModule } from '@nestjs/cache-manager' import { CampaignNewsModule } from '../campaign-news/campaign-news.module' import { CampaignNewsFileModule } from '../campaign-news-file/campaign-news-file.module' import { MarketingNotificationsModule } from '../notifications/notifications.module' +import { StatisticsModule } from '../statistics/statistics.module' @Module({ imports: [ @@ -107,6 +108,7 @@ import { MarketingNotificationsModule } from '../notifications/notifications.mod JwtModule, NotificationModule, BankTransactionsModule, + StatisticsModule, CacheModule.registerAsync({ imports: [ConfigModule], useFactory: async (config: ConfigService) => ({ diff --git a/apps/api/src/statistics/dto/donation-statistics.dto.ts b/apps/api/src/statistics/dto/donation-statistics.dto.ts new file mode 100644 index 000000000..8ce3af249 --- /dev/null +++ b/apps/api/src/statistics/dto/donation-statistics.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Expose } from 'class-transformer' + +export class GroupedDonationsDto { + @ApiProperty() + @Expose() + sum: number + + @ApiProperty() + @Expose() + count: number + + @ApiProperty() + @Expose() + date: Date +} + +export class UniqueDonationsDto { + @ApiProperty() + @Expose() + amount: number + + @ApiProperty() + @Expose() + count: number +} + +export class HourlyDonationsDto { + @ApiProperty() + @Expose() + hour: number + + @ApiProperty() + @Expose() + count: number +} diff --git a/apps/api/src/statistics/dto/group-by.dto.ts b/apps/api/src/statistics/dto/group-by.dto.ts new file mode 100644 index 000000000..ea01ef32d --- /dev/null +++ b/apps/api/src/statistics/dto/group-by.dto.ts @@ -0,0 +1,5 @@ +export enum GroupBy { + DAY = 'day', + WEEK = 'week', + MONTH = 'month', +} diff --git a/apps/api/src/statistics/statistics.controller.ts b/apps/api/src/statistics/statistics.controller.ts new file mode 100644 index 000000000..8bf37221a --- /dev/null +++ b/apps/api/src/statistics/statistics.controller.ts @@ -0,0 +1,38 @@ +import { Public } from 'nest-keycloak-connect' +import { Controller, Get, Param, Query, UseInterceptors } from '@nestjs/common' +import { CacheInterceptor } from '@nestjs/cache-manager' + +import { StatisticsService } from './statistics.service' +import { ApiQuery, ApiTags } from '@nestjs/swagger' +import { GroupBy } from './dto/group-by.dto' + +@ApiTags('statistics') +@Controller('statistics') +export class StatisticsController { + constructor(private readonly statisticsService: StatisticsService) {} + + @Get('donations/:campaignId') + @UseInterceptors(CacheInterceptor) + @Public() + @ApiQuery({ name: 'groupBy', required: false, enum: GroupBy }) + async findGroupedDonations( + @Param('campaignId') campaignId: string, + @Query('groupBy') groupBy?: GroupBy, + ) { + return await this.statisticsService.listGroupedDonations(campaignId, groupBy) + } + + @Get('unique-donations/:campaignId') + @UseInterceptors(CacheInterceptor) + @Public() + async findUniqueDonations(@Param('campaignId') campaignId: string) { + return await this.statisticsService.listUniqueDonations(campaignId) + } + + @Get('hourly-donations/:campaignId') + @UseInterceptors(CacheInterceptor) + @Public() + async findHourlyDonations(@Param('campaignId') campaignId: string) { + return await this.statisticsService.listHourlyDonations(campaignId) + } +} diff --git a/apps/api/src/statistics/statistics.module.ts b/apps/api/src/statistics/statistics.module.ts new file mode 100644 index 000000000..21a93881d --- /dev/null +++ b/apps/api/src/statistics/statistics.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common' +import { PrismaService } from '../prisma/prisma.service' +import { StatisticsController } from './statistics.controller' +import { StatisticsService } from './statistics.service' + +@Module({ + controllers: [StatisticsController], + providers: [StatisticsService, PrismaService], + + exports: [StatisticsService], +}) +export class StatisticsModule {} diff --git a/apps/api/src/statistics/statistics.service.ts b/apps/api/src/statistics/statistics.service.ts new file mode 100644 index 000000000..3bc6149a6 --- /dev/null +++ b/apps/api/src/statistics/statistics.service.ts @@ -0,0 +1,59 @@ +import { Prisma } from '@prisma/client' +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../prisma/prisma.service' + +import { GroupBy } from './dto/group-by.dto' +import { + GroupedDonationsDto, + HourlyDonationsDto, + UniqueDonationsDto, +} from './dto/donation-statistics.dto' + +@Injectable() +export class StatisticsService { + constructor(private prisma: PrismaService) {} + + async listGroupedDonations( + campaignId: string, + groupBy?: GroupBy, + ): Promise { + const date = + groupBy === GroupBy.MONTH + ? Prisma.sql`DATE_TRUNC('MONTH', created_at) date` + : groupBy === GroupBy.WEEK + ? Prisma.sql`DATE_TRUNC('WEEK', created_at) date` + : Prisma.sql`DATE_TRUNC('DAY', created_at) date` + + const group = + groupBy === GroupBy.MONTH + ? Prisma.sql`GROUP BY DATE_TRUNC('MONTH', created_at)` + : groupBy === GroupBy.WEEK + ? Prisma.sql`GROUP BY DATE_TRUNC('WEEK', created_at)` + : Prisma.sql`GROUP BY DATE_TRUNC('DAY', created_at)` + + return this.prisma.$queryRaw` + SELECT SUM(amount)::INTEGER, COUNT(id)::INTEGER, ${date} + FROM api.donations WHERE status = 'succeeded' + ${Prisma.sql`AND target_vault_id IN ( SELECT id from api.vaults WHERE campaign_id = ${campaignId}::uuid)`} + ${group} + ORDER BY date ASC ` + } + + async listUniqueDonations(campaignId: string): Promise { + return this.prisma.$queryRaw` + SELECT amount::INTEGER, COUNT(id)::INTEGER AS count + FROM api.donations WHERE status = 'succeeded' + ${Prisma.sql`AND target_vault_id IN ( SELECT id from api.vaults WHERE campaign_id = ${campaignId}::uuid)`} + GROUP BY amount + ORDER BY amount ASC` + } + + async listHourlyDonations(campaignId: string): Promise { + return this.prisma.$queryRaw` + SELECT EXTRACT(HOUR from created_at)::INTEGER AS hour, COUNT(id)::INTEGER AS count + FROM api.donations where status = 'succeeded' + ${Prisma.sql`AND target_vault_id IN ( SELECT id from api.vaults WHERE campaign_id = ${campaignId}::uuid)`} + GROUP BY hour + ORDER BY hour ASC` + } +} From 6ab3855958956eee60a8ea6fb06654f1a273251a Mon Sep 17 00:00:00 2001 From: quantum-grit <91589884+quantum-grit@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:45:02 +0300 Subject: [PATCH 2/2] updated undici to latest to silence dependabot warning (#567) --- package.json | 2 +- yarn.lock | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e2c6f9ce6..b47885830 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ ] }, "resolutions": { - "undici": "^5.8.2", + "undici": "^5.26.2", "semver": "^7.5.2" }, "packageManager": "yarn@3.3.0", diff --git a/yarn.lock b/yarn.lock index 5a8c42ed4..142dcd5ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2415,6 +2415,13 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^2.0.0": + version: 2.0.0 + resolution: "@fastify/busboy@npm:2.0.0" + checksum: 41879937ce1dee6421ef9cd4da53239830617e1f0bb7a0e843940772cd72827205d05e518af6adabe6e1ea19301285fff432b9d11bad01a531e698bea95c781b + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -6767,7 +6774,7 @@ __metadata: languageName: node linkType: hard -"busboy@npm:^1.0.0, busboy@npm:^1.6.0": +"busboy@npm:^1.0.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" dependencies: @@ -16216,12 +16223,12 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.8.2": - version: 5.22.1 - resolution: "undici@npm:5.22.1" +"undici@npm:^5.26.2": + version: 5.26.4 + resolution: "undici@npm:5.26.4" dependencies: - busboy: ^1.6.0 - checksum: 048a3365f622be44fb319316cedfaa241c59cf7f3368ae7667a12323447e1822e8cc3d00f6956c852d1478a6fde1cbbe753f49e05f2fdaed229693e716ebaf35 + "@fastify/busboy": ^2.0.0 + checksum: 4d37f14ce56837d332ab1623be751f2a5b439069705671cc60b441133300b05bcb8803668132e7b3f98800db19cd6cb76b48831facbdbc2d73271b12383ab3cd languageName: node linkType: hard