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` + } +}