Skip to content
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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@
"lint": "eslint \"src/**/*.ts\" --fix"
},
"dependencies": {
"@nestjs/cli": "^10.3.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^11.2.0",
"@nestjs/cli": "^10.3.0",
"@prisma/client": "^5.17.0",
"@types/express": "^4.17.21",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"date-fns": "^3.6.0",
"json-stringify-safe": "^5.0.1",
"pg": "^8.11.5",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.4",
"@types/express": "^4.17.21"
"tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

41 changes: 41 additions & 0 deletions sql/reports/sfdc/payments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
SELECT
p.payment_id as "paymentId",
p.created_at as "paymentDate",
p.billing_account as "billingAccountId",
p.payment_status as "paymentStatus",
p.challenge_fee as "challengeFee",
p.total_amount as "paymentAmount",
w.external_id as "challengeId",
w.category,
(w.category = 'TASK_PAYMENT') AS "isTask",
c.name AS "challengeName",
m.handle AS "winnerHandle",
m."userId" as "winnerId",
m."firstName" as "winnerFirstName",
m."lastName" as "winnerLastName"
FROM finance.payment p
LEFT JOIN finance.winnings w
ON w.winning_id = p.winnings_id
LEFT JOIN challenges."Challenge" c
ON c.id = w.external_id
LEFT JOIN members.member m
ON m."userId" = w.winner_id::bigint
WHERE
($1::text[] IS NULL OR p.billing_account = ANY($1::text[]))
AND ($2::text[] IS NULL OR c.id = ANY($2::text[]))
AND ($3::text[] IS NULL OR w.winner_id::text IN (
SELECT m2."userId"::text
FROM members.member m2
WHERE m2.handle = ANY($3::text[])
))
AND ($4::text IS NULL OR w.external_id IN (
SELECT c2.id
FROM challenges."Challenge" c2
WHERE c2.name ILIKE '%' || $4 || '%'
))
AND ($5::timestamptz IS NULL OR p.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR p.created_at <= $6::timestamptz)
AND ($7::numeric IS NULL OR p.total_amount >= $7::numeric)
AND ($8::numeric IS NULL OR p.total_amount <= $8::numeric)
ORDER BY p.created_at DESC
LIMIT 1000;
7 changes: 5 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { ReportsModule } from "./reports/reports.module";
import { DbModule } from "./db/db.module";
import { AuthMiddleware } from "./auth/auth.middleware";
import { HealthModule } from "./health/health.module";

import { TopgearReportsModule } from "./topgear-reports/topgear-reports.module";
import { SfdcReportsModule } from "./sfdc-reports/sfdc-reports.module";

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DbModule,
ReportsModule,
TopgearReportsModule,
SfdcReportsModule,
HealthModule,
],
})
Expand Down
30 changes: 30 additions & 0 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Logger as NestLogger } from "@nestjs/common";
import * as stringify from "json-stringify-safe";
console.log('here', stringify);


export class Logger extends NestLogger {
log(...messages: any[]): void {
super.log(this.formatMessages(messages));
}

debug(...messages: any[]): void {
super.debug(this.formatMessages(messages));
}

info(...messages: any[]): void {
super.log(this.formatMessages(messages)); // NestJS doesn't have a dedicated `info` method, so we use `log`.
}

error(...messages: any[]): void {
super.error(this.formatMessages(messages));
}

private formatMessages(messages: any[]): string {
return [...messages]
.map((msg) =>
typeof msg === "object" ? stringify(msg, null, 2) : String(msg),
)
.join(" ");
}
}
30 changes: 30 additions & 0 deletions src/dto/api-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiProperty } from "@nestjs/swagger";

export enum ResponseStatusType {
SUCCESS = "success",
ERROR = "error",
}

export class Error {
code: number;
message: string;
}

export class ResponseDto<T> {
@ApiProperty({
description: "Type of the response",
enum: ResponseStatusType,
example: ResponseStatusType.SUCCESS,
})
status: ResponseStatusType;

@ApiProperty({
description: "The response data",
})
data: T;

@ApiProperty({
description: "The error message",
})
error: Error;
}
10 changes: 0 additions & 10 deletions src/reports/reports.module.ts

This file was deleted.

47 changes: 47 additions & 0 deletions src/sfdc-reports/sfdc-reports.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Controller, Get, Query, UseGuards } from "@nestjs/common";

import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";

import { PermissionsGuard } from "../auth/guards/permissions.guard";
import { Scopes } from "../auth/decorators/scopes.decorator";
import { Scopes as AppScopes } from "../app-constants";

import { SfdcReportsService } from "./sfdc-reports.service";
import {
PaymentsReportQueryDto,
PaymentsReportResponse,
} from "./sfdc-reports.dto";
import { ResponseDto } from "src/dto/api-response.dto";

@ApiTags("Sfdc Reports")
@Controller("/sfdc")
export class SfdcReportsController {
constructor(private readonly reportsService: SfdcReportsService) {}

@Get("/payments")
@UseGuards(PermissionsGuard)
@Scopes(AppScopes.AllReports)
@ApiBearerAuth()
@ApiOperation({
summary: "Export search winnings result in csv file format",
description: "Roles: Payment Admin, Payment Editor, Payment Viewer",
})
@ApiQuery({
type: PaymentsReportQueryDto,
})
@ApiResponse({
status: 200,
description: "Export winnings successfully.",
type: ResponseDto<PaymentsReportResponse>,
})
async getPaymentsReport(@Query() query: PaymentsReportQueryDto) {
const report = await this.reportsService.getPaymentsReport(query);
return report;
}
}
112 changes: 112 additions & 0 deletions src/sfdc-reports/sfdc-reports.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import {
IsOptional,
IsString,
IsNotEmpty,
IsNumber,
IsDateString,
} from "class-validator";

const transformArray = ({ value }: { value: string }) =>
Array.isArray(value) ? value : [value];

export class PaymentsReportQueryDto {
@ApiProperty({
required: false,
description:
"List of billing account IDs associated with the payments to retrieve",
example: ["80001012"],
})
@IsOptional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@Transform(transformArray)
billingAccountIds?: string[];

@ApiProperty({
required: false,
description: "Challenge name to search for",
example: ["Task Payment for member"],
})
@IsOptional()
@IsString()
@IsNotEmpty()
challengeName?: string;

@ApiProperty({
required: false,
description: "List of challenge IDs",
example: ["e74c3e37-73c9-474e-a838-a38dd4738906"],
})
@IsOptional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@Transform(transformArray)
challengeIds?: string[];

@ApiProperty({
required: false,
description: "Start date for the report query in ISO format",
example: "2023-01-01T00:00:00.000Z",
})
@IsOptional()
@IsDateString()
startDate?: Date;

@ApiProperty({
required: false,
description: "End date for the report query in ISO format",
example: "2023-01-31T23:59:59.000Z",
})
@IsOptional()
@IsDateString()
endDate?: Date;

@ApiProperty({
required: false,
description: "List of user handles",
example: ["user_01", "user_02"],
})
@IsOptional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@Transform(transformArray)
handles?: string[];

@ApiProperty({
required: false,
description: "Minimum payment amount for filtering the report",
example: 100,
})
@IsOptional()
@IsNumber()
@Transform(({ value }) => parseFloat(value))
minPaymentAmount?: number;

@ApiProperty({
required: false,
description: "Maximum payment amount for filtering the report",
example: 1000,
})
@IsOptional()
@IsNumber()
@Transform(({ value }) => parseFloat(value))
maxPaymentAmount?: number;
}

export class PaymentsReportResponse {
billingAccountId: string;
challengeName: string;
challengeId: string;
paymentDate: string;
paymentId: string;
paymentStatus: string;
winnerId: string;
winnerHandle: string;
winnerFirstName: string;
winnerLastName: string;
isTask: boolean;
challengeFee: number;
paymentAmount: number;
}
10 changes: 10 additions & 0 deletions src/sfdc-reports/sfdc-reports.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { SfdcReportsController } from "./sfdc-reports.controller";
import { SfdcReportsService } from "./sfdc-reports.service";
import { SqlLoaderService } from "../common/sql-loader.service";

@Module({
controllers: [SfdcReportsController],
providers: [SfdcReportsService, SqlLoaderService],
})
export class SfdcReportsModule {}
53 changes: 53 additions & 0 deletions src/sfdc-reports/sfdc-reports.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Injectable } from "@nestjs/common";
import { DbService } from "../db/db.service";
import { Logger } from "src/common/logger";
import { PaymentsReportQueryDto } from "./sfdc-reports.dto";
import { SqlLoaderService } from "src/common/sql-loader.service";

@Injectable()
export class SfdcReportsService {
private readonly logger = new Logger(SfdcReportsService.name);

constructor(
private readonly db: DbService,
private readonly sql: SqlLoaderService,
) {}

async getPaymentsReport(filters: PaymentsReportQueryDto) {
this.logger.debug("Starting getPaymentsReport with filters:", filters);

const query = this.sql.load("reports/sfdc/payments.sql");

const payments = await this.db.query<{
billingAccountId: string,
challengeName: string,
challengeId: string,
paymentDate: Date,
paymentId: string,
paymentStatus: string,
winnerId: string,
winnerHandle: string,
winnerFirstName: string,
winnerLastName: string,
isTask: boolean,
challengeFee: number,
paymentAmount: number,
}>(
query,
[
filters.billingAccountIds,
filters.challengeIds,
filters.handles,
filters.challengeName,
filters.startDate,
filters.endDate,
filters.minPaymentAmount,
filters.maxPaymentAmount,
]
);

this.logger.debug("Mapped payments to the final report format");

return payments;
}
}
Loading