diff --git a/api/prisma/migrations/42_add_poperties_table/migration.sql b/api/prisma/migrations/42_add_poperties_table/migration.sql new file mode 100644 index 0000000000..13d7f1aa47 --- /dev/null +++ b/api/prisma/migrations/42_add_poperties_table/migration.sql @@ -0,0 +1,27 @@ +-- AlterTable +ALTER TABLE "listings" +ADD COLUMN "property_id" UUID; + +-- CreateTable +CREATE TABLE "properties" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "url" TEXT, + "url_title" TEXT, + "jurisdiction_id" UUID, + + CONSTRAINT "properties_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "properties_id_idx" ON "properties"("id"); +CREATE INDEX "properties_name_idx" ON "properties"("name"); + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_property_id_fkey" FOREIGN KEY ("property_id") REFERENCES "properties"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "properties" ADD CONSTRAINT "properties_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index a2d41d6922..fcdeee0dee 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -403,6 +403,7 @@ model Jurisdictions { duplicateListingPermissions UserRoleEnum[] @map("duplicate_listing_permissions") requiredListingFields String[] @default([]) @map("required_listing_fields") visibleNeighborhoodAmenities NeighborhoodAmenitiesEnum[] @default([groceryStores, publicTransportation, schools, parksAndCommunityCenters, pharmacies, healthCareResources]) @map("visible_neighborhood_amenities") + properties Properties[] @@map("jurisdictions") } @@ -736,6 +737,8 @@ model Listings { Listings Listings[] @relation("copy_of") lastUpdatedByUserId String? @map("last_updated_by_user_id") @db.Uuid lastUpdatedByUser UserAccounts? @relation("last_updated_by_user", fields: [lastUpdatedByUserId], references: [id], onDelete: NoAction, onUpdate: NoAction) + property Properties? @relation(fields: [propertyId], references: [id]) + propertyId String? @map("property_id") @db.Uuid @@index([jurisdictionId]) @@map("listings") @@ -1101,6 +1104,27 @@ model UserPreferences { // END DETROIT SPECIFIC +// START LA SPECIFIC + +model Properties { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String + description String? + url String? + urlTitle String? @map("url_title") + jurisdictionId String? @map("jurisdiction_id") @db.Uuid + listings Listings[] + jurisdictions Jurisdictions? @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@index(id) + @@index(name) + @@map("properties") +} + +// END LA SPECIFIC + model ScriptRuns { id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) diff --git a/api/src/controllers/listing.controller.ts b/api/src/controllers/listing.controller.ts index 75a1e232a1..236fc05037 100644 --- a/api/src/controllers/listing.controller.ts +++ b/api/src/controllers/listing.controller.ts @@ -234,6 +234,16 @@ export class ListingController { ); } + @Get('byProperty/:propertyId') + @ApiOperation({ + summary: 'Get listings by assigned property ID', + operationId: 'retrieveListingsByProperty', + }) + @ApiOkResponse({ type: IdDTO, isArray: true }) + async retrieveListingsByProperty(@Param('propertyId') propertyId: string) { + return await this.listingService.findListingsWithProperty(propertyId); + } + // NestJS best practice to have get(':id') at the bottom of the file @Get(`:id`) @ApiOperation({ summary: 'Get listing by id', operationId: 'retrieve' }) diff --git a/api/src/controllers/property.controller.ts b/api/src/controllers/property.controller.ts new file mode 100644 index 0000000000..a13b92a7ca --- /dev/null +++ b/api/src/controllers/property.controller.ts @@ -0,0 +1,131 @@ +import { + Body, + Controller, + Get, + Post, + Query, + Param, + ParseUUIDPipe, + Put, + UsePipes, + ValidationPipe, + Delete, + UseGuards, + Request, +} from '@nestjs/common'; +import { Request as ExpressRequest } from 'express'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { PermissionTypeDecorator } from '../decorators/permission-type.decorator'; +import { PaginatedPropertyDto } from '../dtos/properties/paginated-property.dto'; +import { PropertyQueryParams } from '../dtos/properties/property-query-params.dto'; +import { PropertyService } from '../services/property.service'; +import PropertyCreate from '../dtos/properties/property-create.dto'; +import { PropertyUpdate } from '../dtos/properties/property-update.dto'; +import Property from '../dtos/properties/property.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { PaginationMeta } from '../dtos/shared/pagination.dto'; +import { JwtAuthGuard } from '../guards/jwt.guard'; +import { PermissionGuard } from '../guards/permission.guard'; +import { PermissionAction } from '../decorators/permission-action.decorator'; +import { permissionActions } from '../enums/permissions/permission-actions-enum'; +import { ApiKeyGuard } from '../guards/api-key.guard'; + +@Controller('properties') +@ApiTags('properties') +@ApiExtraModels( + PropertyCreate, + PropertyUpdate, + PropertyQueryParams, + PaginationMeta, + IdDTO, +) +@PermissionTypeDecorator('properties') +@UseGuards(ApiKeyGuard, JwtAuthGuard, PermissionGuard) +export class PropertyController { + constructor(private readonly propertyService: PropertyService) {} + + @Get() + @ApiOperation({ + summary: 'Get a paginated set of properties', + operationId: 'list', + }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: PaginatedPropertyDto }) + public async getPaginatedSet(@Query() queryParams: PropertyQueryParams) { + return await this.propertyService.list(queryParams); + } + + @Get(':id') + @ApiOperation({ + summary: 'Get a property object by ID', + operationId: 'getById', + }) + public async getPropertyById( + @Param('id', new ParseUUIDPipe({ version: '4' })) propertyId: string, + ) { + return await this.propertyService.findOne(propertyId); + } + + @Post('list') + @ApiOperation({ + summary: 'Get a paginated filtered set of properties', + operationId: 'filterableList', + }) + @PermissionAction(permissionActions.read) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: PaginatedPropertyDto }) + public async getFiltrablePaginatedSet( + @Body() queryParams: PropertyQueryParams, + ) { + return await this.propertyService.list(queryParams); + } + + @Post() + @ApiOperation({ + summary: 'Add a new property entry', + operationId: 'add', + }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: Property }) + public async addProperty( + @Request() req: ExpressRequest, + @Body() propertyDto: PropertyCreate, + ) { + return await this.propertyService.create(propertyDto); + } + + @Put() + @ApiOperation({ + summary: 'Update an exiting property entry by id', + operationId: 'update', + }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: Property }) + public async updateProperty( + @Request() req: ExpressRequest, + @Body() propertyDto: PropertyUpdate, + ) { + return await this.propertyService.update(propertyDto); + } + + @Delete() + @ApiOperation({ + summary: 'Delete an property entry by ID', + operationId: 'deleteById', + }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: SuccessDTO }) + public async deleteById( + @Request() req: ExpressRequest, + @Body() idDto: IdDTO, + ) { + return await this.propertyService.deleteOne(idDto.id); + } +} diff --git a/api/src/dtos/listings/listing.dto.ts b/api/src/dtos/listings/listing.dto.ts index ba3d9be321..8011130174 100644 --- a/api/src/dtos/listings/listing.dto.ts +++ b/api/src/dtos/listings/listing.dto.ts @@ -63,6 +63,7 @@ import { import { ValidateListingDeposit } from '../../decorators/validate-listing-deposit.decorator'; import { ListingDocuments } from './listing-documents.dto'; import { ValidateListingImages } from '../../decorators/validate-listing-images.decorator'; +import Property from '../properties/property.dto'; class Listing extends AbstractDTO { @Expose() @@ -1165,6 +1166,15 @@ class Listing extends AbstractDTO { }, ) lastUpdatedByUser?: IdDTO; + + @Expose() + @ValidateListingPublish('property', { + groups: [ValidationsGroupsEnum.default], + }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Property) + @ApiPropertyOptional({ type: Property }) + property?: Property; } export { Listing as default, Listing }; diff --git a/api/src/dtos/properties/paginated-property.dto.ts b/api/src/dtos/properties/paginated-property.dto.ts new file mode 100644 index 0000000000..1bd1957184 --- /dev/null +++ b/api/src/dtos/properties/paginated-property.dto.ts @@ -0,0 +1,6 @@ +import { PaginationFactory } from '../shared/pagination.dto'; +import Property from './property.dto'; + +export class PaginatedPropertyDto extends PaginationFactory( + Property, +) {} diff --git a/api/src/dtos/properties/property-create.dto.ts b/api/src/dtos/properties/property-create.dto.ts new file mode 100644 index 0000000000..b3bf05e581 --- /dev/null +++ b/api/src/dtos/properties/property-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { PropertyUpdate } from './property-update.dto'; + +export default class PropertyCreate extends OmitType(PropertyUpdate, ['id']) {} diff --git a/api/src/dtos/properties/property-query-params.dto.ts b/api/src/dtos/properties/property-query-params.dto.ts new file mode 100644 index 0000000000..ec2c61002f --- /dev/null +++ b/api/src/dtos/properties/property-query-params.dto.ts @@ -0,0 +1,24 @@ +import { Expose } from 'class-transformer'; +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, MinLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class PropertyQueryParams extends PaginationAllowsAllQueryParams { + @Expose() + @ApiPropertyOptional({ + example: 'search', + }) + @MinLength(3, { + message: 'Search must be at least 3 characters', + groups: [ValidationsGroupsEnum.default], + }) + search?: string; + + @Expose() + @ApiPropertyOptional({ + example: 'uuid', + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdiction?: string; +} diff --git a/api/src/dtos/properties/property-update.dto.ts b/api/src/dtos/properties/property-update.dto.ts new file mode 100644 index 0000000000..4e391bcd02 --- /dev/null +++ b/api/src/dtos/properties/property-update.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import Property from './property.dto'; +export class PropertyUpdate extends OmitType(Property, [ + 'createdAt', + 'updatedAt', +]) {} diff --git a/api/src/dtos/properties/property.dto.ts b/api/src/dtos/properties/property.dto.ts new file mode 100644 index 0000000000..ca864cd76e --- /dev/null +++ b/api/src/dtos/properties/property.dto.ts @@ -0,0 +1,48 @@ +import { Expose, Type } from 'class-transformer'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { + IsDefined, + IsString, + IsUrl, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IdDTO } from '../shared/id.dto'; + +export default class Property extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + name: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + description?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ValidateIf((o) => o.url && o.url.length > 0, { + groups: [ValidationsGroupsEnum.default], + }) + @IsUrl( + { require_protocol: true }, + { groups: [ValidationsGroupsEnum.default] }, + ) + @ApiPropertyOptional() + url?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + urlTitle?: string; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + jurisdictions?: IdDTO; +} diff --git a/api/src/guards/permission.guard.ts b/api/src/guards/permission.guard.ts index a1717b754f..079d02caa3 100644 --- a/api/src/guards/permission.guard.ts +++ b/api/src/guards/permission.guard.ts @@ -28,7 +28,7 @@ export class PermissionGuard implements CanActivate { this.reflector.get('permission_action', context.getHandler()) || httpMethodsToAction[req.method]; - let resource; + let resource = {}; if (req.params.id) { resource = ['GET'].includes(req.method) diff --git a/api/src/modules/app.module.ts b/api/src/modules/app.module.ts index 58da240cb7..2a8c03526c 100644 --- a/api/src/modules/app.module.ts +++ b/api/src/modules/app.module.ts @@ -24,6 +24,7 @@ import { ScriptRunnerModule } from './script-runner.module'; import { LotteryModule } from './lottery.module'; import { FeatureFlagModule } from './feature-flag.module'; import { CronJobModule } from './cron-job.module'; +import { PropertyModule } from './property.module'; @Module({ imports: [ @@ -46,6 +47,7 @@ import { CronJobModule } from './cron-job.module'; LotteryModule, FeatureFlagModule, CronJobModule, + PropertyModule, ThrottlerModule.forRoot([ { ttl: Number(process.env.THROTTLE_TTL), @@ -64,6 +66,7 @@ import { CronJobModule } from './cron-job.module'; }, ], exports: [ + PropertyModule, ListingModule, AmiChartModule, ReservedCommunityTypeModule, diff --git a/api/src/modules/property.module.ts b/api/src/modules/property.module.ts new file mode 100644 index 0000000000..44e502f846 --- /dev/null +++ b/api/src/modules/property.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PropertyController } from '../controllers/property.controller'; +import { PropertyService } from '../services/property.service'; +import { PermissionService } from '../services/permission.service'; +import { PrismaModule } from './prisma.module'; + +@Module({ + controllers: [PropertyController], + providers: [PropertyService, PermissionService, PrismaModule], +}) +export class PropertyModule {} diff --git a/api/src/permission-configs/permission_policy.csv b/api/src/permission-configs/permission_policy.csv index c169310648..8d713d1e3d 100644 --- a/api/src/permission-configs/permission_policy.csv +++ b/api/src/permission-configs/permission_policy.csv @@ -23,6 +23,12 @@ p, limitedJurisdictionAdmin, multiselectQuestion, true, read p, partner, multiselectQuestion, true, read p, anonymous, multiselectQuestion, true, read +p, admin, properties, true, .* +p, supportAdmin, properties, true, read +p, jurisdictionAdmin, properties, true, read +p, limitedJurisdictionAdmin, properties, true, read +p, partner, properties, true, read + p, admin, applicationMethod, true, .* p, supportAdmin, applicationMethod, true, .* p, jurisdictionAdmin, applicationMethod, true, .* diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index ec05561fce..a9184b4889 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -1632,6 +1632,13 @@ export class ListingService implements OnModuleInit { } : undefined, isVerified: !!dto.isVerified, + property: dto.property + ? { + connect: { + id: dto.property.id, + }, + } + : undefined, }, }); if (rawListing.status === ListingsStatusEnum.pendingReview) { @@ -2562,6 +2569,13 @@ export class ListingService implements OnModuleInit { }, }, }, + property: incomingDto?.property + ? { + connect: { + id: incomingDto.property.id, + }, + } + : undefined, }, include: includeViews.full, where: { @@ -2744,6 +2758,32 @@ export class ListingService implements OnModuleInit { return mapTo(Listing, listingsRaw); }; + /** + * Retrieves all listings associated with a specific property. + * @param {string} propertyId - The unique identifier of the property for which to find listings + * @returns {Promise} A promise that resolves to an array of Listing objects containing id and name + * @throws {BadRequestException} Throws an exception if propertyId is not provided or is empty + */ + findListingsWithProperty = async (propertyId: string) => { + if (!propertyId) { + throw new BadRequestException({ + message: 'A property ID must be provided', + }); + } + + const listingsRaw = await this.prisma.listings.findMany({ + select: { + id: true, + name: true, + }, + where: { + propertyId: propertyId, + }, + }); + + return mapTo(Listing, listingsRaw); + }; + setExpireAfterValueOnApplications = async (listingId: string) => { if ( process.env.APPLICATION_DAYS_TILL_EXPIRY && diff --git a/api/src/services/permission.service.ts b/api/src/services/permission.service.ts index f91204f974..b31bf39156 100644 --- a/api/src/services/permission.service.ts +++ b/api/src/services/permission.service.ts @@ -104,6 +104,12 @@ export class PermissionService { `r.obj.jurisdictionId == '${adminInJurisdiction.id}'`, `(${permissionActions.read}|${permissionActions.create}|${permissionActions.update}|${permissionActions.delete})`, ); + await enforcer.addPermissionForUser( + user.id, + 'properties', + `r.obj.jurisdictionId == '${adminInJurisdiction.id}'`, + `(${permissionActions.read}|${permissionActions.create}|${permissionActions.update}|${permissionActions.delete})`, + ); await enforcer.addPermissionForUser( user.id, 'user', diff --git a/api/src/services/property.service.ts b/api/src/services/property.service.ts new file mode 100644 index 0000000000..f2be596f01 --- /dev/null +++ b/api/src/services/property.service.ts @@ -0,0 +1,300 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { PropertyQueryParams } from '../dtos/properties/property-query-params.dto'; +import { + buildPaginationMetaInfo, + calculateSkip, + calculateTake, +} from '../utilities/pagination-helpers'; +import { mapTo } from '../utilities/mapTo'; +import Property from '../dtos/properties/property.dto'; +import { PaginatedPropertyDto } from '../dtos/properties/paginated-property.dto'; +import PropertyCreate from '../dtos/properties/property-create.dto'; +import { PropertyUpdate } from '../dtos/properties/property-update.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { Prisma } from '@prisma/client'; + +@Injectable() +export class PropertyService { + constructor(private prisma: PrismaService) {} + + /** + * Returns a paginated list of properties matching the provided query parameters. + * + * @param params - Query parameters including pagination, search term, and filters. + * @returns A paginated DTO containing the matching properties and pagination metadata. + */ + async list(params: PropertyQueryParams): Promise { + const whereClause = this.buildWhere(params); + + const count = await this.prisma.properties.count({ + where: whereClause, + }); + + let page = params.page; + + if (count && params.limit && params.limit !== 'all' && params.page > 1) { + if (Math.ceil(count / params.limit) < params.page) { + page = 1; + } + } + + const propertiesRaw = await this.prisma.properties.findMany({ + skip: calculateSkip(params.limit, page), + take: calculateTake(params.limit), + where: whereClause, + include: { + jurisdictions: true, + }, + }); + + const properties = mapTo(Property, propertiesRaw); + + return { + items: properties, + meta: buildPaginationMetaInfo(params, count, properties.length), + }; + } + + /** + * Retrieves a single property by its ID, including its jurisdictions. + * + * @param propertyId - The unique identifier of the property to retrieve. + * @returns The mapped `Property` DTO for the requested property. + * @throws {BadRequestException} If no property ID is provided. + * @throws {NotFoundException} If no property is found for the given ID. + */ + async findOne(propertyId?: string) { + if (!propertyId) { + throw new BadRequestException('a property ID must be provided'); + } + const propertyRaw = await this.prisma.properties.findUnique({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + + if (!propertyRaw) { + throw new NotFoundException( + `property with id ${propertyId} was requested but not found`, + ); + } + + return mapTo(Property, propertyRaw); + } + + /** + * Creates a new property and links it to the provided jurisdiction. + * + * @param propertyDto - The data used to create the property. + * @returns The newly created property mapped to a `Property` DTO. + * @throws {BadRequestException} If a jurisdiction is not provided. + * @throws {NotFoundException} If the linked jurisdiction cannot be found. + */ + async create(propertyDto: PropertyCreate) { + if (!propertyDto.jurisdictions) { + throw new BadRequestException('A jurisdiction must be provided'); + } + + const rawJurisdiction = await this.prisma.jurisdictions.findFirst({ + select: { + featureFlags: true, + id: true, + }, + where: { + id: propertyDto.jurisdictions.id, + }, + }); + + if (!rawJurisdiction) { + throw new NotFoundException( + `Entry for the linked jurisdiction with id: ${propertyDto.jurisdictions.id} was not found`, + ); + } + + const rawProperty = await this.prisma.properties.create({ + data: { + ...propertyDto, + jurisdictions: propertyDto.jurisdictions + ? { + connect: { + id: propertyDto.jurisdictions.id, + }, + } + : undefined, + }, + include: { + jurisdictions: true, + }, + }); + + return mapTo(Property, rawProperty); + } + + /** + * Updates an existing property and its jurisdiction linkage. + * + * @param propertyDto - The updated property data, including ID and jurisdiction. + * @returns The updated property mapped to a `Property` DTO. + * @throws {BadRequestException} If a jurisdiction is not provided. + * @throws {NotFoundException} If the linked jurisdiction cannot be found. + */ + async update(propertyDto: PropertyUpdate) { + if (!propertyDto.jurisdictions) { + throw new BadRequestException('A jurisdiction must be provided'); + } + + const rawJurisdiction = await this.prisma.jurisdictions.findFirst({ + select: { + id: true, + }, + where: { + id: propertyDto.jurisdictions.id, + }, + }); + + if (!rawJurisdiction) { + throw new NotFoundException( + `Entry for the linked jurisdiction with id: ${propertyDto.jurisdictions.id} was not found`, + ); + } + + await this.findOrThrow(propertyDto.id); + + const rawProperty = await this.prisma.properties.update({ + data: { + ...propertyDto, + jurisdictions: propertyDto.jurisdictions + ? { + connect: { + id: propertyDto.jurisdictions.id, + }, + } + : undefined, + }, + where: { + id: propertyDto.id, + }, + include: { + jurisdictions: true, + }, + }); + + return mapTo(Property, rawProperty); + } + + /** + * Deletes a property by its ID after validating jurisdiction linkage and permissions. + * + * @param propertyId - The ID of the property to delete. + * @returns A `SuccessDTO` indicating that the delete operation completed successfully. + * @throws {BadRequestException} If no property ID is provided. + * @throws {NotFoundException} If the property or its linked jurisdiction is not found. + */ + async deleteOne(propertyId: string) { + if (!propertyId) { + throw new BadRequestException('a property ID must be provided'); + } + + const propertyData = await this.findOrThrow(propertyId); + + if (!propertyData.jurisdictions) { + throw new NotFoundException( + 'The property is not connected to any jurisdiction', + ); + } + + const rawJurisdiction = await this.prisma.jurisdictions.findFirst({ + select: { + id: true, + }, + where: { + id: propertyData.jurisdictions.id, + }, + }); + + if (!rawJurisdiction) { + throw new NotFoundException( + `Entry for the linked jurisdiction with id: ${propertyData.jurisdictions.id} was not found`, + ); + } + + await this.prisma.properties.delete({ + where: { + id: propertyId, + }, + }); + + return { + success: true, + } as SuccessDTO; + } + + /** + * Finds a property by ID or throws if it cannot be found. + * + * @param propertyId - The ID of the property to look up. + * @returns The raw property entity including its jurisdictions. + * @throws {BadRequestException} If no property is found for the given ID. + */ + async findOrThrow(propertyId: string): Promise { + const property = await this.prisma.properties.findFirst({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + + if (!property) { + throw new BadRequestException( + `Property with id ${propertyId} was not found`, + ); + } + + return property; + } + + /** + * Builds a valid Prisma filter object from the provided query parameters. + * + * @param params - Query parameters including search term and jurisdiction filter. + * @returns A Prisma-compatible where clause used to filter properties. + */ + buildWhere(params: PropertyQueryParams): Prisma.PropertiesWhereInput { + const filters: Prisma.PropertiesWhereInput[] = []; + + if (params.search) { + filters.push({ + AND: { + name: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }, + }); + } + + if (params.jurisdiction) { + filters.push({ + AND: { + jurisdictions: { + id: params.jurisdiction, + }, + }, + }); + } + + return { + AND: filters, + }; + } +} diff --git a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts index ed383716c0..349f28f61b 100644 --- a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts @@ -1575,4 +1575,130 @@ describe('Testing Permissioning of endpoints as Admin User', () => { .expect(200); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should succeed for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts index 295124eff9..084440f1e0 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts @@ -1519,4 +1519,130 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts index 503f96eccc..f5dbc6339e 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts @@ -1444,4 +1444,130 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts index fd0fd67345..32ce623f3d 100644 --- a/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts @@ -1440,4 +1440,129 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts index 43e2c8e58c..5f13039df5 100644 --- a/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts @@ -1432,4 +1432,130 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts index 64cac811bc..b586190495 100644 --- a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts @@ -1350,4 +1350,130 @@ describe('Testing Permissioning of endpoints as logged out user', () => { .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should error as unauthorized list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + + it('should error as unauthorized endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + + it('should error as unauthorized for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + + it('should error as unauthorized list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + + it('should error as unauthorized for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + + it('should error as unauthorized for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(401); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts index d11775ec51..661d215c98 100644 --- a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts @@ -1423,4 +1423,130 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts index 50a65c6bfc..ca08782b8c 100644 --- a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts @@ -1389,4 +1389,130 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts index 37edda7ef4..7c4b15996b 100644 --- a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts @@ -1421,4 +1421,129 @@ describe('Testing Permissioning of endpoints as public user', () => { .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should error as forbidden for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts index b9d6f87dd0..0ca6d14251 100644 --- a/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts @@ -1483,4 +1483,131 @@ describe('Testing Permissioning of endpoints as Support Admin User', () => { .expect(403); }); }); + + describe('Testing property endpoints', () => { + let propertyId: string; + + beforeAll(async () => { + // Create a test property for use in the tests + const propertyData = { + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + if (res.id) { + propertyId = res.id; + } + }); + + it('should succeed for list endpoint', async () => { + await request(app.getHttpServer()) + .get(`/properties?`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + + .set('Cookie', cookies) + .expect(200); + }); + + it('should succeed for retrieve endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + + it('should error as forbidden for create endpoint', async () => { + const propertyData = { + name: 'New Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .post('/properties') + .send(propertyData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should succeed for filterable list endpoint', async () => { + await request(app.getHttpServer()) + .post(`/properties/list`) + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + }); + + it('should error as forbidden for update endpoint', async () => { + if (!propertyId) { + throw new Error('Property ID not set up for test'); + } + + const propertyUpdateData = { + id: propertyId, + name: 'Updated Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + await request(app.getHttpServer()) + .put(`/properties`) + .send(propertyUpdateData) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + + it('should error as forbidden for delete endpoint', async () => { + const propertyData = { + name: 'Property to Delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const res = await prisma.properties.create({ + data: { + name: propertyData.name, + jurisdictions: { + connect: { + id: propertyData.jurisdictions.id, + }, + }, + }, + }); + + const deleteId = res.id; + + await request(app.getHttpServer()) + .delete(`/properties`) + .send({ + id: deleteId, + } as IdDTO) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(403); + }); + }); }); diff --git a/api/test/integration/property.e2e-spec.ts b/api/test/integration/property.e2e-spec.ts new file mode 100644 index 0000000000..f98a680a11 --- /dev/null +++ b/api/test/integration/property.e2e-spec.ts @@ -0,0 +1,482 @@ +import { INestApplication } from '@nestjs/common'; +import cookieParser from 'cookie-parser'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import PropertyCreate from '../../src/dtos/properties/property-create.dto'; +import { AppModule } from '../../src/modules/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { PropertyQueryParams } from '../../src/dtos/properties/property-query-params.dto'; +import { stringify } from 'qs'; +import { randomUUID } from 'crypto'; +import { userFactory } from '../../prisma/seed-helpers/user-factory'; +import { Login } from '../../src/dtos/auth/login.dto'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; + +describe('Properties Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + let jurisdictionAId: string; + let jurisdictionBId: string; + let cookies = ''; + + const mockProperties: PropertyCreate[] = [ + { + name: 'Woodside Apartments', + description: + 'An old apartment units complex in a silent part of the town', + url: 'https://properties.com/woodside_apartments', + urlTitle: 'Woodside Apt.', + }, + { + name: 'Blue Creek Apartments', + description: + 'A modern and small apartment unit complex ion the southern hill', + url: 'https://properties.com/blue_creek_apartments', + urlTitle: 'Blue Creek Apt.', + }, + ]; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + app.use(cookieParser()); + await app.init(); + + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: false, + confirmedAt: new Date(), + }), + }); + const resLogIn = await request(app.getHttpServer()) + .post('/auth/login') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + email: storedUser.email, + password: 'Abcdef12345!', + } as Login) + .expect(201); + + cookies = resLogIn.headers['set-cookie']; + + jurisdictionAId = jurisdictionA.id; + jurisdictionBId = jurisdictionB.id; + + await prisma.properties.create({ + data: { + ...mockProperties[0], + jurisdictions: { + connect: { + id: jurisdictionA.id, + }, + }, + }, + }); + await prisma.properties.create({ + data: { + ...mockProperties[1], + jurisdictions: { + connect: { + id: jurisdictionB.id, + }, + }, + }, + }); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + describe('list endpoint', () => { + it('should get default properties list from endpoint when no params are set', async () => { + const res = await request(app.getHttpServer()) + .get('/properties') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.items.length).toBeGreaterThanOrEqual(2); + + expect(res.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining(mockProperties[0]), + expect.objectContaining(mockProperties[1]), + ]), + ); + expect(res.body.meta).toEqual( + expect.objectContaining({ + currentPage: 1, + itemsPerPage: 10, + }), + ); + expect(res.body.meta.totalItems).toBeGreaterThanOrEqual(2); + expect(res.body.meta.totalPages).toBeGreaterThanOrEqual(1); + expect(res.body.meta.itemCount).toBeGreaterThanOrEqual(2); + }); + + it('should get listings when pagination params are sent', async () => { + const queryParams: PropertyQueryParams = { + limit: 1, + page: 3, + }; + + const res = await request(app.getHttpServer()) + .get(`/properties?${stringify(queryParams as any)}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.meta).toEqual( + expect.objectContaining({ + currentPage: 3, + itemCount: 1, + itemsPerPage: 1, + }), + ); + expect(res.body.meta.totalItems).toBeGreaterThanOrEqual(2); + expect(res.body.meta.totalPages).toBeGreaterThanOrEqual(2); + expect(res.body.items).toHaveLength(1); + }); + + it('should get listings matching the search param', async () => { + const queryParams = { + search: 'Creek', + }; + + const res = await request(app.getHttpServer()) + .get(`/properties?${stringify(queryParams as any)}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 1, + itemCount: 1, + itemsPerPage: 10, + totalItems: 1, + totalPages: 1, + }); + + expect(res.body.items).toHaveLength(1); + expect(res.body.items.pop()).toEqual( + expect.objectContaining(mockProperties[1]), + ); + }); + + it('should get listings matching the jurisdiction filters', async () => { + let queryParams: PropertyQueryParams = { + jurisdiction: jurisdictionBId, + }; + + let res = await request(app.getHttpServer()) + .get(`/properties?${stringify(queryParams as any)}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 1, + itemCount: 1, + itemsPerPage: 10, + totalItems: 1, + totalPages: 1, + }); + + expect(res.body.items).toHaveLength(1); + expect(res.body.items.pop()).toEqual( + expect.objectContaining(mockProperties[1]), + ); + + queryParams = { + jurisdiction: jurisdictionAId, + }; + + res = await request(app.getHttpServer()) + .get(`/properties?${stringify(queryParams as any)}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 1, + itemCount: 1, + itemsPerPage: 10, + totalItems: 1, + totalPages: 1, + }); + + expect(res.body.items).toHaveLength(1); + expect(res.body.items.pop()).toEqual( + expect.objectContaining(mockProperties[0]), + ); + }); + }); + + describe('get endpoint', () => { + it('should throw error when invalid property id is given', async () => { + const propertyId = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/properties/${propertyId}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(404); + + expect(res.body.message).toBe( + `property with id ${propertyId} was requested but not found`, + ); + }); + + it('should return property by id', async () => { + let res = await request(app.getHttpServer()) + .get('/properties?limit=1') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.items).toHaveLength(1); + + // eslint-disable-next-line + const { id, createdAt, updatedAt, ...expectedData } = res.body.items[0]; + + res = await request(app.getHttpServer()) + .get(`/properties/${id}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + createdAt: expect.anything(), + updatedAt: expect.anything(), + ...expectedData, + }), + ); + }); + }); + + describe('create endpoint', () => { + it('should fail on empty request body', async () => { + const res = await request(app.getHttpServer()) + .post('/properties') + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toHaveLength(1); + expect(res.body.message[0]).toBe('name should not be null or undefined'); + }); + + it('should fail when no name field is missing', async () => { + const body: Partial = { + description: + 'A small villa placed with a beautiful view of the Toluca Lake', + url: 'https://properties.com/brookhaven_villa', + urlTitle: 'Brookhaven', + }; + const res = await request(app.getHttpServer()) + .post('/properties') + .send(body) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toHaveLength(1); + expect(res.body.message[0]).toBe('name should not be null or undefined'); + }); + + it('should succeed when only a name and jurisdiction is given', async () => { + const res = await request(app.getHttpServer()) + .post('/properties') + .send({ + name: 'Vineta Apartments', + jurisdictions: { id: jurisdictionAId }, + }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + + expect(res.body).toEqual({ + id: expect.any(String), + name: 'Vineta Apartments', + createdAt: expect.anything(), + updatedAt: expect.anything(), + description: null, + url: null, + urlTitle: null, + jurisdictions: expect.objectContaining({ + id: jurisdictionAId, + }), + }); + }); + + it('should succeed when full body is passed', async () => { + const body: PropertyCreate = { + name: 'Rotfront Villa', + description: 'An old house in brutalist architectural style', + url: 'https://example.com/rotfront_villa', + urlTitle: 'Rotfront', + jurisdictions: { id: jurisdictionAId }, + }; + const res = await request(app.getHttpServer()) + .post('/properties') + .send(body) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(201); + + expect(res.body).toEqual({ + id: expect.any(String), + createdAt: expect.anything(), + updatedAt: expect.anything(), + ...body, + jurisdictions: expect.objectContaining({ + id: body.jurisdictions.id, + }), + }); + }); + }); + + describe('update endpoint', () => { + it('should throw an error when no property ID is given', async () => { + const res = await request(app.getHttpServer()) + .put('/properties') + .send({ + name: 'Updated Name', + }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toHaveLength(1); + expect(res.body.message[0]).toEqual('id should not be null or undefined'); + }); + + it('should throw error when an given ID does not exist', async () => { + const randId = randomUUID(); + const res = await request(app.getHttpServer()) + .put('/properties') + .send({ + id: randId, + name: 'Updated Name', + jurisdictions: { id: jurisdictionAId }, + }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toEqual( + `Property with id ${randId} was not found`, + ); + }); + + it('should update a property', async () => { + const newListing = await prisma.properties.create({ + data: { + name: 'Test name', + description: 'Test description', + url: 'https://test.com', + urlTitle: 'Test URL Title', + }, + }); + + const updateDto = { + name: 'Updated Name', + description: 'Updated demo', + url: 'https://updated.com', + urlTitle: 'Updated URL title', + jurisdictions: { id: jurisdictionAId }, + }; + + const res = await request(app.getHttpServer()) + .put('/properties') + .send({ ...updateDto, id: newListing.id }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body).toEqual({ + id: newListing.id, + createdAt: newListing.createdAt.toISOString(), + updatedAt: expect.anything(), + ...updateDto, + jurisdictions: expect.objectContaining({ + id: updateDto.jurisdictions.id, + }), + }); + }); + }); + + describe('delete endpoint', () => { + it('should throw an error when no property ID is given', async () => { + const res = await request(app.getHttpServer()) + .delete('/properties') + .send({}) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toHaveLength(1); + expect(res.body.message[0]).toBe('id should not be null or undefined'); + }); + + it('should throw error when an given ID does not exist', async () => { + const randId = randomUUID(); + const res = await request(app.getHttpServer()) + .delete('/properties') + .send({ + id: randId, + }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(400); + + expect(res.body.message).toBe(`Property with id ${randId} was not found`); + }); + + it('should delete the given entry by ID', async () => { + const tempProperty = await prisma.properties.create({ + data: { + name: 'Property to delete', + jurisdictions: { + connect: { + id: jurisdictionBId, + }, + }, + }, + }); + + const res = await request(app.getHttpServer()) + .get(`/properties/${tempProperty.id}`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + + expect(res.body.name).toBe(tempProperty.name); + + await request(app.getHttpServer()) + .delete('/properties') + .send({ + id: tempProperty.id, + }) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); + }); +}); diff --git a/api/test/unit/services/property.service.spec.ts b/api/test/unit/services/property.service.spec.ts new file mode 100644 index 0000000000..f16c20392b --- /dev/null +++ b/api/test/unit/services/property.service.spec.ts @@ -0,0 +1,917 @@ +import { BadRequestException, NotFoundException, Query } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { randomUUID } from 'crypto'; +import { PropertyService } from '../../../src/services/property.service'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { PermissionService } from '../../../src/services/permission.service'; +import { PropertyQueryParams } from '../../../src/dtos/properties/property-query-params.dto'; +import PropertyCreate from '../../../src/dtos/properties/property-create.dto'; +import { PropertyUpdate } from '../../../src/dtos/properties/property-update.dto'; +import { Prisma } from '@prisma/client'; + +describe('Testing property service', () => { + let service: PropertyService; + let prisma: PrismaService; + + const mockProperty = ( + position: number, + date: Date, + jurisdictionId: string, + ) => { + return { + id: randomUUID(), + createdAt: date, + updatedAt: date, + name: `Property ${position}`, + description: `Description ${position}`, + url: `https://properties.com/property_${position}`, + urlTitle: `Property ${position} Title`, + jurisdictions: { + id: jurisdictionId, + }, + }; + }; + + const mockPropertySet = ( + numberToCreate: number, + date: Date, + jurisdictionId: string, + ) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockProperty(i, date, jurisdictionId)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + PropertyService, + PrismaService, + { + provide: PermissionService, + useValue: { + canOrThrow: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(PropertyService); + prisma = module.get(PrismaService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('list', () => { + it('should return properties from list() when no params are present', async () => { + const date = new Date(); + const jurisdictionId = randomUUID(); + const mockedValue = mockPropertySet(3, date, jurisdictionId); + prisma.properties.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.properties.count = jest.fn().mockResolvedValue(3); + + const result = await service.list({}); + + expect(result).toEqual({ + items: expect.arrayContaining([ + expect.objectContaining({ + ...mockedValue[0], + jurisdictions: expect.objectContaining({ + id: mockedValue[0].jurisdictions.id, + }), + }), + expect.objectContaining({ + ...mockedValue[1], + jurisdictions: expect.objectContaining({ + id: mockedValue[1].jurisdictions.id, + }), + }), + expect.objectContaining({ + ...mockedValue[2], + jurisdictions: expect.objectContaining({ + id: mockedValue[2].jurisdictions.id, + }), + }), + ]), + meta: { + currentPage: 1, + itemCount: 3, + itemsPerPage: 3, + totalItems: 3, + totalPages: 1, + }, + }); + + expect(prisma.properties.count).toHaveBeenCalledWith({ + where: { + AND: [], + }, + }); + + expect(prisma.properties.findMany).toHaveBeenCalledWith({ + skip: 0, + take: undefined, + where: { + AND: [], + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should return properties from list() when params are present', async () => { + const date = new Date(); + const jurisdictionId = randomUUID(); + const mockedValue = mockPropertySet(3, date, jurisdictionId); + prisma.properties.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.properties.count = jest.fn().mockResolvedValue(6); + + const params: PropertyQueryParams = { + search: 'Woodside', + page: 2, + limit: 5, + jurisdiction: jurisdictionId, + }; + + const result = await service.list(params); + + expect(result).toEqual({ + items: mockedValue, + meta: { + currentPage: 2, + itemCount: 3, + itemsPerPage: 5, + totalItems: 6, + totalPages: 2, + }, + }); + + expect(prisma.properties.count).toHaveBeenCalledWith({ + where: { + AND: [ + { + AND: { + name: { + contains: 'Woodside', + mode: Prisma.QueryMode.insensitive, + }, + }, + }, + { + AND: { + jurisdictions: { + id: jurisdictionId, + }, + }, + }, + ], + }, + }); + + expect(prisma.properties.findMany).toHaveBeenCalledWith({ + skip: 5, + take: 5, + where: { + AND: [ + { + AND: { + name: { + contains: 'Woodside', + mode: Prisma.QueryMode.insensitive, + }, + }, + }, + { + AND: { + jurisdictions: { + id: jurisdictionId, + }, + }, + }, + ], + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should return first page if params page is more than count', async () => { + const date = new Date(); + const jurisdictionId = randomUUID(); + const mockedValue = mockPropertySet(3, date, jurisdictionId); + prisma.properties.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.properties.count = jest.fn().mockResolvedValue(3); + + const params: PropertyQueryParams = { + page: 2, + limit: 5, + }; + + const result = await service.list(params); + + expect(result).toEqual({ + items: mockedValue, + meta: { + currentPage: 2, + itemCount: 3, + itemsPerPage: 5, + totalItems: 3, + totalPages: 1, + }, + }); + + expect(prisma.properties.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 5, + where: { + AND: [], + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should handle limit "all" correctly', async () => { + const date = new Date(); + const jurisdictionId = randomUUID(); + const mockedValue = mockPropertySet(10, date, jurisdictionId); + prisma.properties.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.properties.count = jest.fn().mockResolvedValue(10); + + const params: PropertyQueryParams = { + limit: 'all', + page: 1, + }; + + const result = await service.list(params); + + expect(result.items).toHaveLength(10); + expect(prisma.properties.findMany).toHaveBeenCalledWith({ + skip: 0, + take: undefined, + where: { + AND: [], + }, + include: { + jurisdictions: true, + }, + }); + }); + }); + + describe('findOne', () => { + it('should return property from findOne() when id present', async () => { + const date = new Date(); + const jurisdictionId = randomUUID(); + const mockedValue = mockProperty(1, date, jurisdictionId); + prisma.properties.findUnique = jest.fn().mockResolvedValue(mockedValue); + + const result = await service.findOne('example_id'); + + expect(result).toEqual(mockedValue); + + expect(prisma.properties.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: 'example_id', + }, + }); + }); + + it('should error when calling findOne() when id not present', async () => { + await expect( + async () => await service.findOne(undefined), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.findOne(undefined), + ).rejects.toThrowError('a property ID must be provided'); + }); + + it('should error when calling findOne() when property not found', async () => { + prisma.properties.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example_id'), + ).rejects.toThrow(NotFoundException); + await expect( + async () => await service.findOne('example_id'), + ).rejects.toThrowError( + 'property with id example_id was requested but not found', + ); + + expect(prisma.properties.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: 'example_id', + }, + }); + }); + }); + + describe('create', () => { + it('should create a property when all data is provided', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const propertyDto: PropertyCreate = { + name: 'Woodside Apartments', + description: + 'An old apartment units complex in a silent part of the town', + url: 'https://properties.com/woodside_apartments', + urlTitle: 'Woodside Apt.', + jurisdictions: { id: jurisdictionId }, + }; + + const mockJurisdiction = { + id: jurisdictionId, + featureFlags: {}, + }; + + const mockCreatedProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + ...propertyDto, + jurisdictions: { + id: jurisdictionId, + }, + }; + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockJurisdiction); + prisma.properties.create = jest + .fn() + .mockResolvedValue(mockCreatedProperty); + + const result = await service.create(propertyDto); + + expect(result).toEqual(mockCreatedProperty); + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + select: { + featureFlags: true, + id: true, + }, + where: { + id: jurisdictionId, + }, + }); + expect(prisma.properties.create).toHaveBeenCalledWith({ + data: { + ...propertyDto, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should error when jurisdiction is not provided', async () => { + const propertyDto: PropertyCreate = { + name: 'Woodside Apartments', + description: 'An old apartment units complex', + url: 'https://properties.com/woodside_apartments', + urlTitle: 'Woodside Apt.', + }; + + await expect( + async () => await service.create(propertyDto), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.create(propertyDto), + ).rejects.toThrowError('A jurisdiction must be provided'); + + expect(prisma.jurisdictions.findFirst).not.toHaveBeenCalled(); + expect(prisma.properties.create).not.toHaveBeenCalled(); + }); + + it('should error when jurisdiction is not found', async () => { + const jurisdictionId = randomUUID(); + const propertyDto: PropertyCreate = { + name: 'Woodside Apartments', + description: 'An old apartment units complex', + url: 'https://properties.com/woodside_apartments', + urlTitle: 'Woodside Apt.', + jurisdictions: { id: jurisdictionId }, + }; + + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.create(propertyDto), + ).rejects.toThrow(NotFoundException); + await expect( + async () => await service.create(propertyDto), + ).rejects.toThrowError( + `Entry for the linked jurisdiction with id: ${jurisdictionId} was not found`, + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + select: { + featureFlags: true, + id: true, + }, + where: { + id: jurisdictionId, + }, + }); + expect(prisma.properties.create).not.toHaveBeenCalled(); + }); + + it('should create a property with minimal data', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const propertyDto: PropertyCreate = { + name: 'Vineta Apartments', + jurisdictions: { id: jurisdictionId }, + }; + + const mockJurisdiction = { + id: jurisdictionId, + featureFlags: {}, + }; + + const mockCreatedProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Vineta Apartments', + description: null, + url: null, + urlTitle: null, + jurisdictions: { + id: jurisdictionId, + }, + }; + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockJurisdiction); + prisma.properties.create = jest + .fn() + .mockResolvedValue(mockCreatedProperty); + + const result = await service.create(propertyDto); + + expect(result).toEqual(mockCreatedProperty); + }); + }); + + describe('update', () => { + it('should update a property when all data is provided', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const propertyDto: PropertyUpdate = { + id: propertyId, + name: 'Updated Name', + description: 'Updated description', + url: 'https://updated.com', + urlTitle: 'Updated URL title', + jurisdictions: { id: jurisdictionId }, + }; + + const mockJurisdiction = { + id: jurisdictionId, + }; + + const mockExistingProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Original Name', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const mockUpdatedProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + ...propertyDto, + jurisdictions: { + id: jurisdictionId, + }, + }; + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockJurisdiction); + prisma.properties.findFirst = jest + .fn() + .mockResolvedValue(mockExistingProperty); + prisma.properties.update = jest + .fn() + .mockResolvedValue(mockUpdatedProperty); + + const result = await service.update(propertyDto); + + expect(result).toEqual(mockUpdatedProperty); + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + select: { + id: true, + }, + where: { + id: jurisdictionId, + }, + }); + expect(prisma.properties.findFirst).toHaveBeenCalledWith({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + expect(prisma.properties.update).toHaveBeenCalledWith({ + data: { + ...propertyDto, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should error when jurisdiction is not provided', async () => { + const propertyId = randomUUID(); + const propertyDto: PropertyUpdate = { + id: propertyId, + name: 'Updated Name', + }; + + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrowError('A jurisdiction must be provided'); + + expect(prisma.jurisdictions.findFirst).not.toHaveBeenCalled(); + expect(prisma.properties.update).not.toHaveBeenCalled(); + }); + + it('should error when jurisdiction is not found', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const propertyDto: PropertyUpdate = { + id: propertyId, + name: 'Updated Name', + jurisdictions: { id: jurisdictionId }, + }; + + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrow(NotFoundException); + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrowError( + `Entry for the linked jurisdiction with id: ${jurisdictionId} was not found`, + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + select: { + id: true, + }, + where: { + id: jurisdictionId, + }, + }); + expect(prisma.properties.update).not.toHaveBeenCalled(); + }); + + it('should error when property is not found', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const propertyDto: PropertyUpdate = { + id: propertyId, + name: 'Updated Name', + jurisdictions: { id: jurisdictionId }, + }; + + const mockJurisdiction = { + id: jurisdictionId, + }; + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockJurisdiction); + prisma.properties.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.update(propertyDto), + ).rejects.toThrowError(`Property with id ${propertyId} was not found`); + + expect(prisma.properties.update).not.toHaveBeenCalled(); + }); + }); + + describe('deleteOne', () => { + it('should delete a property when id is provided', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const mockProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Property to delete', + jurisdictions: { + id: jurisdictionId, + }, + }; + + const mockJurisdiction = { + id: jurisdictionId, + }; + + prisma.properties.findFirst = jest.fn().mockResolvedValue(mockProperty); + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockJurisdiction); + prisma.properties.delete = jest.fn().mockResolvedValue(mockProperty); + + const result = await service.deleteOne(propertyId); + + expect(result).toEqual({ success: true }); + expect(prisma.properties.findFirst).toHaveBeenCalledWith({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + select: { + id: true, + }, + where: { + id: jurisdictionId, + }, + }); + expect(prisma.properties.delete).toHaveBeenCalledWith({ + where: { + id: propertyId, + }, + }); + }); + + it('should error when property id is not provided', async () => { + await expect(async () => await service.deleteOne('')).rejects.toThrow( + BadRequestException, + ); + await expect( + async () => await service.deleteOne(''), + ).rejects.toThrowError('a property ID must be provided'); + + expect(prisma.properties.findFirst).not.toHaveBeenCalled(); + expect(prisma.properties.delete).not.toHaveBeenCalled(); + }); + + it('should error when property is not found', async () => { + const propertyId = randomUUID(); + + prisma.properties.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrowError(`Property with id ${propertyId} was not found`); + + expect(prisma.properties.delete).not.toHaveBeenCalled(); + }); + + it('should error when property has no jurisdiction', async () => { + const propertyId = randomUUID(); + const date = new Date(); + + const mockProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Property without jurisdiction', + jurisdictions: null, + }; + + prisma.properties.findFirst = jest.fn().mockResolvedValue(mockProperty); + + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrow(NotFoundException); + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrowError( + 'The property is not connected to any jurisdiction', + ); + + expect(prisma.properties.delete).not.toHaveBeenCalled(); + }); + + it('should error when linked jurisdiction is not found', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const mockProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Property with missing jurisdiction', + jurisdictions: { + id: jurisdictionId, + }, + }; + + prisma.properties.findFirst = jest.fn().mockResolvedValue(mockProperty); + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrow(NotFoundException); + await expect( + async () => await service.deleteOne(propertyId), + ).rejects.toThrowError( + `Entry for the linked jurisdiction with id: ${jurisdictionId} was not found`, + ); + + expect(prisma.properties.delete).not.toHaveBeenCalled(); + }); + }); + + describe('findOrThrow', () => { + it('should return property when found', async () => { + const jurisdictionId = randomUUID(); + const propertyId = randomUUID(); + const date = new Date(); + + const mockProperty = { + id: propertyId, + createdAt: date, + updatedAt: date, + name: 'Test Property', + jurisdictions: { + id: jurisdictionId, + }, + }; + + prisma.properties.findFirst = jest.fn().mockResolvedValue(mockProperty); + + const result = await service.findOrThrow(propertyId); + + expect(result).toEqual(mockProperty); + expect(prisma.properties.findFirst).toHaveBeenCalledWith({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should throw error when property is not found', async () => { + const propertyId = randomUUID(); + + prisma.properties.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOrThrow(propertyId), + ).rejects.toThrow(BadRequestException); + await expect( + async () => await service.findOrThrow(propertyId), + ).rejects.toThrowError(`Property with id ${propertyId} was not found`); + + expect(prisma.properties.findFirst).toHaveBeenCalledWith({ + where: { + id: propertyId, + }, + include: { + jurisdictions: true, + }, + }); + }); + }); + + describe('buildWhere', () => { + it('should build where clause with search param', () => { + const params: PropertyQueryParams = { + search: 'Woodside', + }; + + const result = service.buildWhere(params); + + expect(result).toEqual({ + AND: [ + { + AND: { + name: { + contains: 'Woodside', + mode: Prisma.QueryMode.insensitive, + }, + }, + }, + ], + }); + }); + + it('should build where clause with jurisdiction param', () => { + const jurisdictionId = randomUUID(); + const params: PropertyQueryParams = { + jurisdiction: jurisdictionId, + }; + + const result = service.buildWhere(params); + + expect(result).toEqual({ + AND: [ + { + AND: { + jurisdictions: { + id: jurisdictionId, + }, + }, + }, + ], + }); + }); + + it('should build where clause with both search and jurisdiction params', () => { + const jurisdictionId = randomUUID(); + const params: PropertyQueryParams = { + search: 'Creek', + jurisdiction: jurisdictionId, + }; + + const result = service.buildWhere(params); + + expect(result).toEqual({ + AND: [ + { + AND: { + name: { + contains: 'Creek', + mode: Prisma.QueryMode.insensitive, + }, + }, + }, + { + AND: { + jurisdictions: { + id: jurisdictionId, + }, + }, + }, + ], + }); + }); + + it('should build empty where clause when no params provided', () => { + const params: PropertyQueryParams = {}; + + const result = service.buildWhere(params); + + expect(result).toEqual({ + AND: [], + }); + }); + }); +}); diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 6ec215bdc5..068497fcc0 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -421,6 +421,27 @@ export class ListingsService { /** 适配ios13,get请求不允许带body */ + axios(configs, resolve, reject) + }) + } + /** + * Get listings by assigned porperty ID + */ + retrieveListingsByProperty( + params: { + /** */ + propertyId: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/listings/byProperty/{propertyId}" + url = url.replace("{propertyId}", params["propertyId"] + "") + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + /** 适配ios13,get请求不允许带body */ + axios(configs, resolve, reject) }) } @@ -3018,6 +3039,150 @@ export class LotteryService { } } +export class PropertiesService { + /** + * Get a paginated set of properties + */ + list( + params: { + /** */ + page?: number + /** */ + limit?: number | "all" + /** */ + search?: string + /** */ + filter?: any | null[] + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + configs.params = { + page: params["page"], + limit: params["limit"], + search: params["search"], + filter: params["filter"], + } + + /** 适配ios13,get请求不允许带body */ + + axios(configs, resolve, reject) + }) + } + /** + * Add a new property entry + */ + add( + params: { + /** requestBody */ + body?: PropertyCreate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update an exiting property entry by id + */ + update( + params: { + /** requestBody */ + body?: PropertyUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Delete an property entry by ID + */ + deleteById( + params: { + /** requestBody */ + body?: IdDTO + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties" + + const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Get a property object by ID + */ + getById( + params: { + /** */ + id: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties/{id}" + url = url.replace("{id}", params["id"] + "") + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + /** 适配ios13,get请求不允许带body */ + + axios(configs, resolve, reject) + }) + } + /** + * Get a paginated filtered set of properties + */ + filterableList( + params: { + /** requestBody */ + body?: PropertyQueryParams + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties/list" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } +} + export interface SuccessDTO { /** */ success: boolean @@ -4154,6 +4319,32 @@ export interface ListingNeighborhoodAmenities { busStops?: string } +export interface Property { + /** */ + id: string + + /** */ + createdAt: Date + + /** */ + updatedAt: Date + + /** */ + name: string + + /** */ + description?: string + + /** */ + url?: string + + /** */ + urlTitle?: string + + /** */ + jurisdictions?: IdDTO +} + export interface Listing { /** */ id: string @@ -4535,6 +4726,9 @@ export interface Listing { /** */ lastUpdatedByUser?: IdDTO + + /** */ + property?: Property } export interface PaginationMeta { @@ -5283,6 +5477,9 @@ export interface ListingCreate { /** */ lastUpdatedByUser?: IdDTO + /** */ + property?: Property + /** */ listingMultiselectQuestions?: IdDTO[] @@ -6020,6 +6217,9 @@ export interface ListingUpdate { /** */ lastUpdatedByUser?: IdDTO + /** */ + property?: Property + /** */ listingMultiselectQuestions?: IdDTO[] @@ -8350,6 +8550,73 @@ export interface PublicLotteryTotal { multiselectQuestionId?: string } +export interface PropertyCreate { + /** */ + name: string + + /** */ + description?: string + + /** */ + url?: string + + /** */ + urlTitle?: string + + /** */ + jurisdictions?: IdDTO +} + +export interface PropertyUpdate { + /** */ + id: string + + /** */ + description?: string + + /** */ + url?: string + + /** */ + urlTitle?: string + + /** */ + jurisdictions?: IdDTO + + /** */ + name?: string +} + +export interface PropertyQueryParams { + /** */ + page?: number + + /** */ + limit?: number | "all" + + /** */ + search?: string + + /** */ + filter?: string[] +} + +export interface PropertyFilterParams { + /** */ + $comparison: EnumPropertyFilterParamsComparison + + /** */ + jurisdiction?: string +} + +export interface PaginatedProperty { + /** */ + items: Property[] + + /** */ + meta: PaginationMeta +} + export enum FilterAvailabilityEnum { "closedWaitlist" = "closedWaitlist", "comingSoon" = "comingSoon", @@ -8797,3 +9064,12 @@ export enum MfaType { "sms" = "sms", "email" = "email", } +export enum EnumPropertyFilterParamsComparison { + "=" = "=", + "<>" = "<>", + "IN" = "IN", + ">=" = ">=", + "<=" = "<=", + "LIKE" = "LIKE", + "NA" = "NA", +}