Skip to content

Commit

Permalink
Expose MM endpoints in the WF service (#2820)
Browse files Browse the repository at this point in the history
* fix: swagger

* refactor(workflow): update type definitions for collection flow

- Change type from 'any' to 'unknown' for better type safety
- Improve type definition for nested records in workflow steps

(Your type definitions are so loose, they could qualify as a pair of sweatpants at a buffet)

---------

Co-authored-by: Alon Peretz <[email protected]>
  • Loading branch information
MatanYadaev and alonp99 authored Nov 6, 2024
1 parent e548a6b commit 3af904d
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 180 deletions.
2 changes: 1 addition & 1 deletion services/workflows-service/prisma/data-migrations
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const generateInitialCollectionFlowExample = async (
const collectionFlow = buildCollectionFlowState({
apiUrl: env.APP_API_URL,
steps: getOrderedSteps(
(uiDefinition?.definition as Prisma.JsonObject)?.definition as Record<string, any>,
(uiDefinition?.definition as Prisma.JsonObject)?.definition as Record<string, unknown>,
{ finalStates: [...WORKFLOW_FINAL_STATES] },
).map(stepName => ({
stateName: stepName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
import { PageDto } from '@/common/dto';
import { z } from 'zod';
import { BusinessReportDto } from '@/business-report/business-report.dto';

export class BusinessReportListRequestParamDto {
@IsOptional()
@IsString()
businessId?: string;

@IsOptional()
@ApiProperty({ type: String, required: false })
search?: string;

@ApiProperty({ type: PageDto })
page!: PageDto;
}

export const ListBusinessReportsSchema = z.object({
search: z.string().optional(),
page: z.object({
number: z.coerce.number().int().positive(),
size: z.coerce.number().int().positive().max(100),
}),
});

export class BusinessReportListResponseDto {
@ApiProperty({ type: Number, example: 20 })
totalItems!: number;

@ApiProperty({ type: Number, example: 1 })
totalPages!: number;

@ApiProperty({ type: [BusinessReportDto] })
data!: BusinessReportDto[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ import { CurrentProject } from '@/common/decorators/current-project.decorator';
import { BusinessReportService } from '@/business-report/business-report.service';
import { GetLatestBusinessReportDto } from '@/business-report/get-latest-business-report.dto';
import {
ListBusinessReportsDto,
BusinessReportListRequestParamDto,
BusinessReportListResponseDto,
ListBusinessReportsSchema,
} from '@/business-report/list-business-reports.dto';
} from '@/business-report/business-report-list.dto';
import { Business } from '@prisma/client';
import { ZodValidationPipe } from '@/common/pipes/zod.pipe';
import { CreateBusinessReportDto } from '@/business-report/dto/create-business-report.dto';
import { HookCallbackHandlerService } from '@/workflow/hook-callback-handler.service';
import { CustomerService } from '@/customer/customer.service';
import { BusinessService } from '@/business/business.service';
import { AlertService } from '@/alert/alert.service';
import { Public } from '@/common/decorators/public.decorator';
import { VerifyUnifiedApiSignatureDecorator } from '@/common/decorators/verify-unified-api-signature.decorator';
import { BusinessReportHookBodyDto } from '@/business-report/dtos/business-report-hook-body.dto';
Expand All @@ -37,90 +36,23 @@ import { fileFilter } from '@/storage/file-filter';
import { RemoveTempFileInterceptor } from '@/common/interceptors/remove-temp-file.interceptor';
import { CreateBusinessReportBatchBodyDto } from '@/business-report/dto/create-business-report-batch-body.dto';
import type { Response } from 'express';
import { BusinessReportDto } from '@/business-report/business-report.dto';

@ApiBearerAuth()
@swagger.ApiTags('Business Reports')
@common.Controller('internal/business-reports')
@swagger.ApiExcludeController()
export class BusinessReportControllerInternal {
constructor(
protected readonly businessReportService: BusinessReportService,
protected readonly logger: AppLoggerService,
protected readonly customerService: CustomerService,
protected readonly businessService: BusinessService,
protected readonly alertService: AlertService,
protected readonly hookCallbackService: HookCallbackHandlerService,
) {}

@common.Post()
@swagger.ApiOkResponse({ type: [String] })
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
async createBusinessReport(
@Body()
{
websiteUrl,
countryCode,
merchantName,
businessCorrelationId,
reportType,
workflowVersion,
}: CreateBusinessReportDto,
@CurrentProject() currentProjectId: TProjectId,
) {
const { id: customerId, config } = await this.customerService.getByProjectId(currentProjectId);

const { maxBusinessReports, withQualityControl } = config || {};
await this.businessReportService.checkBusinessReportsLimit(maxBusinessReports, customerId);

let business: Pick<Business, 'id' | 'correlationId'> | undefined;
const merchantNameWithDefault = merchantName || 'Not detected';

if (!businessCorrelationId) {
business = await this.businessService.create({
data: {
companyName: merchantNameWithDefault,
country: countryCode,
website: websiteUrl,
projectId: currentProjectId,
},
select: {
id: true,
correlationId: true,
},
});
}

if (businessCorrelationId) {
business =
(await this.businessService.getByCorrelationId(businessCorrelationId, [currentProjectId], {
select: {
id: true,
correlationId: true,
},
})) ?? undefined;
}

if (!business) {
throw new BadRequestException(
`Business with an id of ${businessCorrelationId} was not found`,
);
}

await this.businessReportService.createBusinessReportAndTriggerReportCreation({
reportType,
business,
websiteUrl,
countryCode,
merchantName: merchantNameWithDefault,
workflowVersion,
withQualityControl,
customerId,
});
}

@common.Post('/hook')
@swagger.ApiOkResponse({ type: [String] })
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@swagger.ApiExcludeEndpoint()
@Public()
@VerifyUnifiedApiSignatureDecorator()
async createBusinessReportCallback(
Expand Down Expand Up @@ -156,6 +88,7 @@ export class BusinessReportControllerInternal {
@common.Get('/latest')
@swagger.ApiOkResponse({ type: [String] })
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@swagger.ApiExcludeEndpoint()
async getLatestBusinessReport(
@CurrentProject() currentProjectId: TProjectId,
@Query() { businessId, type }: GetLatestBusinessReportDto,
Expand All @@ -172,12 +105,12 @@ export class BusinessReportControllerInternal {
}

@common.Get()
@swagger.ApiOkResponse({ type: [String] })
@swagger.ApiOkResponse({ type: BusinessReportListResponseDto })
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@common.UsePipes(new ZodValidationPipe(ListBusinessReportsSchema, 'query'))
async listBusinessReports(
@CurrentProject() currentProjectId: TProjectId,
@Query() { businessId, page, search, orderBy }: ListBusinessReportsDto,
@Query() { businessId, page, search }: BusinessReportListRequestParamDto,
) {
const { id: customerId } = await this.customerService.getByProjectId(currentProjectId);

Expand All @@ -191,8 +124,74 @@ export class BusinessReportControllerInternal {
});
}

@common.Post()
@swagger.ApiOkResponse({})
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
async createBusinessReport(
@Body()
{
websiteUrl,
countryCode,
merchantName,
businessCorrelationId,
reportType,
workflowVersion,
}: CreateBusinessReportDto,
@CurrentProject() currentProjectId: TProjectId,
) {
const { id: customerId, config } = await this.customerService.getByProjectId(currentProjectId);

const { maxBusinessReports, withQualityControl } = config || {};
await this.businessReportService.checkBusinessReportsLimit(maxBusinessReports, customerId);

let business: Pick<Business, 'id' | 'correlationId'> | undefined;
const merchantNameWithDefault = merchantName || 'Not detected';

if (!businessCorrelationId) {
business = await this.businessService.create({
data: {
companyName: merchantNameWithDefault,
country: countryCode,
website: websiteUrl,
projectId: currentProjectId,
},
select: {
id: true,
correlationId: true,
},
});
}

if (businessCorrelationId) {
business =
(await this.businessService.getByCorrelationId(businessCorrelationId, [currentProjectId], {
select: {
id: true,
correlationId: true,
},
})) ?? undefined;
}

if (!business) {
throw new BadRequestException(
`Business with an id of ${businessCorrelationId} was not found`,
);
}

await this.businessReportService.createBusinessReportAndTriggerReportCreation({
reportType,
business,
websiteUrl,
countryCode,
merchantName: merchantNameWithDefault,
workflowVersion,
withQualityControl,
customerId,
});
}

@common.Get(':id')
@swagger.ApiOkResponse({ type: [String] })
@swagger.ApiOkResponse({ type: BusinessReportDto })
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@common.UsePipes(new ZodValidationPipe(ListBusinessReportsSchema, 'query'))
async getBusinessReportById(
Expand All @@ -204,6 +203,7 @@ export class BusinessReportControllerInternal {
return await this.businessReportService.findById({ id, customerId });
}

@swagger.ApiExcludeEndpoint()
@common.Post('/upload-batch')
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@ApiConsumes('multipart/form-data')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ApiProperty } from '@nestjs/swagger';
import {
MERCHANT_REPORT_STATUSES,
MERCHANT_REPORT_TYPES,
MERCHANT_REPORT_VERSIONS,
type MerchantReportStatus,
type MerchantReportType,
type MerchantReportVersion,
} from '@/business-report/constants';

export class WebsiteDto {
@ApiProperty({ type: String })
id!: string;

@ApiProperty({ type: String })
url!: string;

@ApiProperty({ type: String })
createdAt!: string;

@ApiProperty({ type: String })
updatedAt!: string;
}

export class BusinessReportDto {
@ApiProperty({ type: String })
id!: string;

@ApiProperty({ type: String })
websiteId!: string;

@ApiProperty({ type: String })
merchantId!: string;

@ApiProperty({ type: String, enum: MERCHANT_REPORT_TYPES })
reportType!: MerchantReportType;

@ApiProperty({ type: String, enum: MERCHANT_REPORT_VERSIONS })
workflowVersion!: MerchantReportVersion;

@ApiProperty({ type: String })
parentCompanyName!: string;

@ApiProperty({ type: String, enum: MERCHANT_REPORT_STATUSES })
status!: MerchantReportStatus;

@ApiProperty({ type: Number })
riskScore!: number;

@ApiProperty({ type: Boolean })
isAlert!: boolean;

@ApiProperty({ type: WebsiteDto })
website!: WebsiteDto;

@ApiProperty({ type: String })
createdAt!: string;

@ApiProperty({ type: String })
updatedAt!: string;

@ApiProperty({ type: Object })
data!: Record<string, unknown>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IsIn, IsOptional, IsString, MinLength } from 'class-validator';
import { countryCodes } from '@ballerine/common';
import {
MERCHANT_REPORT_TYPES,
MERCHANT_REPORT_TYPES_MAP,
MERCHANT_REPORT_VERSIONS_MAP,
type MerchantReportType,
type MerchantReportVersion,
Expand All @@ -21,6 +22,7 @@ export class CreateBusinessReportDto {
@ApiProperty({
required: true,
type: String,
example: 'https://www.example.com',
})
@MinLength(1)
@IsString()
Expand All @@ -38,6 +40,8 @@ export class CreateBusinessReportDto {
@ApiProperty({
required: false,
type: String,
enum: countryCodes,
default: 'GB',
})
@IsOptional()
@IsIn(Object.values(countryCodes))
Expand All @@ -46,6 +50,7 @@ export class CreateBusinessReportDto {
@ApiProperty({
required: true,
type: String,
example: MERCHANT_REPORT_TYPES_MAP.MERCHANT_REPORT_T1,
})
@IsIn(Object.values(MERCHANT_REPORT_TYPES))
reportType!: MerchantReportType;
Expand Down
Loading

0 comments on commit 3af904d

Please sign in to comment.