diff --git a/api/.env.template b/api/.env.template index e60c09563a..2691b46033 100644 --- a/api/.env.template +++ b/api/.env.template @@ -44,6 +44,8 @@ LOTTERY_PUBLISH_PROCESSING_CRON_STRING=58 23 * * * LOTTERY_PROCESSING_CRON_STRING=0 * * * * # how many days till lottery data expires LOTTERY_DAYS_TILL_EXPIRY=45 +# allow partners and jadmins to create duplicate listings +ALLOW_PARTNERS_TO_DUPLICATE_LISTINGS=FALSE # the list of allowed urls that can make requests to the api (strings must be exact matches) CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"] # spill over list of allowed urls that can make requests to the api (strings are turned into regex) diff --git a/api/src/controllers/listing.controller.ts b/api/src/controllers/listing.controller.ts index 0bfa43c9f8..a64f2d580a 100644 --- a/api/src/controllers/listing.controller.ts +++ b/api/src/controllers/listing.controller.ts @@ -32,6 +32,7 @@ import { PermissionAction } from '../decorators/permission-action.decorator'; import { PermissionTypeDecorator } from '../decorators/permission-type.decorator'; import Listing from '../dtos/listings/listing.dto'; import { ListingCreate } from '../dtos/listings/listing-create.dto'; +import { ListingDuplicate } from '../dtos/listings/listing-duplicate.dto'; import { ListingCsvQueryParams } from '../dtos/listings/listing-csv-query-params.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; @@ -156,6 +157,19 @@ export class ListingController { ); } + @Post('duplicate') + @ApiOperation({ summary: 'Duplicate listing', operationId: 'duplicate' }) + @UseInterceptors(ClassSerializerInterceptor) + @UsePipes(new ListingCreateUpdateValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: Listing }) + @UseGuards(ApiKeyGuard) + async duplicate( + @Request() req: ExpressRequest, + @Body() dto: ListingDuplicate, + ): Promise { + return await this.listingService.duplicate(dto, mapTo(User, req['user'])); + } + @Delete() @ApiOperation({ summary: 'Delete listing by id', operationId: 'delete' }) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) diff --git a/api/src/dtos/listings/listing-duplicate.dto.ts b/api/src/dtos/listings/listing-duplicate.dto.ts new file mode 100644 index 0000000000..54f996338b --- /dev/null +++ b/api/src/dtos/listings/listing-duplicate.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsDefined, + IsString, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { IdDTO } from '../shared/id.dto'; + +export class ListingDuplicate { + @Expose() + @ApiProperty() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + name: string; + + @Expose() + @ApiProperty() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + includeUnits: boolean; + + @Expose() + @ApiProperty() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDTO) + storedListing: IdDTO; +} diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index 33ceb8c388..58c356a61c 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -11,6 +11,7 @@ import { ConfigService } from '@nestjs/config'; import { SchedulerRegistry } from '@nestjs/schedule'; import { LanguagesEnum, + ListingEventsTypeEnum, ListingsStatusEnum, Prisma, ReviewOrderTypeEnum, @@ -25,6 +26,7 @@ import { TranslationService } from './translation.service'; import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import { Listing } from '../dtos/listings/listing.dto'; import { ListingCreate } from '../dtos/listings/listing-create.dto'; +import { ListingDuplicate } from '../dtos/listings/listing-duplicate.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; import { ListingUpdate } from '../dtos/listings/listing-update.dto'; @@ -961,6 +963,104 @@ export class ListingService implements OnModuleInit { return mapTo(Listing, rawListing); } + async duplicate( + dto: ListingDuplicate, + requestingUser: User, + ): Promise { + const storedListing = await this.findOrThrow( + dto.storedListing.id, + ListingViews.details, + ); + if (dto.name.trim() === storedListing.name) { + throw new BadRequestException('New listing name must be unique'); + } + + const userRoles = + process.env.ALLOW_PARTNERS_TO_DUPLICATE_LISTINGS === 'TRUE' && + (requestingUser?.userRoles?.isJurisdictionalAdmin || + requestingUser?.userRoles?.isPartner) + ? { + ...requestingUser.userRoles, + isAdmin: true, + } + : requestingUser?.userRoles; + + await this.permissionService.canOrThrow( + { ...requestingUser, userRoles: userRoles }, + 'listing', + permissionActions.create, + { + jurisdictionId: storedListing.jurisdictions.id, + }, + ); + + const mappedListing = mapTo(ListingCreate, storedListing); + + const listingEvents = mappedListing.listingEvents?.filter( + (event) => event.type !== ListingEventsTypeEnum.lotteryResults, + ); + + const listingImages = mappedListing.listingImages?.map((unsavedImage) => ({ + assets: { + fileId: unsavedImage.assets.fileId, + label: unsavedImage.assets.label, + }, + ordinal: unsavedImage.ordinal, + })); + + if (!dto.includeUnits) { + delete mappedListing['units']; + } + + const newListingData: ListingCreate = { + ...mappedListing, + name: dto.name, + status: ListingsStatusEnum.pending, + listingEvents: listingEvents, + listingMultiselectQuestions: + storedListing.listingMultiselectQuestions?.map((question) => ({ + id: question.multiselectQuestionId, + ordinal: question.ordinal, + })), + listingImages: listingImages, + lotteryLastRunAt: undefined, + lotteryLastPublishedAt: undefined, + lotteryStatus: undefined, + }; + + const res = await this.create(newListingData, { + ...requestingUser, + userRoles: userRoles, + }); + + if ( + process.env.ALLOW_PARTNERS_TO_DUPLICATE_LISTINGS === 'TRUE' && + requestingUser.userRoles?.isPartner + ) { + await this.prisma.userAccounts.update({ + data: { + listings: { + connect: { id: res.id }, + }, + }, + where: { + id: requestingUser.id, + }, + }); + + await this.prisma.activityLog.create({ + data: { + module: 'user', + recordId: requestingUser.id, + action: 'update', + userAccounts: { connect: { id: requestingUser.id } }, + }, + }); + } + + return res; + } + /* deletes a listing */ diff --git a/api/test/integration/listing.e2e-spec.ts b/api/test/integration/listing.e2e-spec.ts index d4a354a5a6..7a55971964 100644 --- a/api/test/integration/listing.e2e-spec.ts +++ b/api/test/integration/listing.e2e-spec.ts @@ -658,6 +658,75 @@ describe('Listing Controller Tests', () => { }); }); + describe('duplicate endpoint', () => { + it('should duplicate listing, include units', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + await reservedCommunityTypeFactoryAll(jurisdictionA.id, prisma); + const listingData = await listingFactory(jurisdictionA.id, prisma, { + numberOfUnits: 2, + }); + const listing = await prisma.listings.create({ + data: listingData, + include: { + units: true, + }, + }); + + const newName = 'duplicate name 1'; + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: newName, + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', adminAccessToken) + .expect(201); + + expect(res.body.name).toEqual(newName); + expect(res.body.units.length).toBe(listing.units.length); + }); + + it('should duplicate listing, exclude units', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + await reservedCommunityTypeFactoryAll(jurisdictionA.id, prisma); + const listingData = await listingFactory(jurisdictionA.id, prisma, { + numberOfUnits: 2, + }); + const listing = await prisma.listings.create({ + data: listingData, + include: { + units: true, + }, + }); + const newName = 'duplicate name 2'; + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: false, + name: newName, + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', adminAccessToken) + .expect(201); + + expect(res.body.name).toEqual(newName); + expect(res.body.units).toEqual([]); + }); + }); + describe('process endpoint', () => { it('should successfully process listings that are past due', async () => { const jurisdictionA = await prisma.jurisdictions.create({ 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 e8df503b16..9218e368e8 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 @@ -1272,6 +1272,42 @@ describe('Testing Permissioning of endpoints as Admin User', () => { expect(activityLogResult).not.toBeNull(); }); + it('should succeed for duplicate endpoint & create an activity log entry', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 180', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(201); + + const activityLogResult = await prisma.activityLog.findFirst({ + where: { + module: 'listing', + action: permissionActions.create, + recordId: res.body.id, + }, + }); + + expect(activityLogResult).not.toBeNull(); + }); + it('should succeed for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 95a3ccf966..2acc5e7281 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 @@ -1161,6 +1161,32 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr expect(activityLogResult).not.toBeNull(); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 181', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should succeed for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 1b5a8b82fa..8c992667a6 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 @@ -1095,6 +1095,32 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron .expect(403); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 182', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should succeed for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 f9d7eca37f..2df8fcc64e 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 @@ -1118,6 +1118,32 @@ describe('Testing Permissioning of endpoints as logged out user', () => { .expect(403); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 183', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should error as forbidden for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 6ebb1c78a0..58b5f0c77d 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 @@ -1100,6 +1100,32 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( .expect(403); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 184', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should error as forbidden for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 4445936c1d..001ae06973 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 @@ -1056,6 +1056,32 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () .expect(403); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 185', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should error as forbidden for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) 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 fc92f2fc3a..20d6af2334 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 @@ -1171,6 +1171,32 @@ describe('Testing Permissioning of endpoints as public user', () => { .expect(403); }); + it('should error as forbidden for duplicate endpoint', async () => { + const jurisdictionA = await generateJurisdiction( + prisma, + 'permission juris 186', + ); + await reservedCommunityTypeFactoryAll(jurisdictionA, prisma); + + const listingData = await listingFactory(jurisdictionA, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .post('/listings/duplicate') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + includeUnits: true, + name: 'name', + storedListing: { + id: listing.id, + }, + }) + .set('Cookie', cookies) + .expect(403); + }); + it('should error as forbidden for process endpoint', async () => { await request(app.getHttpServer()) .put(`/listings/closeListings`) diff --git a/api/test/unit/services/listing.service.spec.ts b/api/test/unit/services/listing.service.spec.ts index e277b4bd4a..45438f56d3 100644 --- a/api/test/unit/services/listing.service.spec.ts +++ b/api/test/unit/services/listing.service.spec.ts @@ -2134,6 +2134,274 @@ describe('Testing listing service', () => { }); }); + describe('Test duplicate endpoint', () => { + it('should duplicate a listing, including units', async () => { + const listing = mockListing(1, { numberToMake: 2, date: new Date() }); + + const newName = 'duplicate name'; + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + ...listing, + jurisdictions: { + id: randomUUID(), + }, + }); + + prisma.listings.create = jest.fn().mockResolvedValue({ + ...listing, + id: 'duplicate id', + name: newName, + }); + + const newListing = await service.duplicate( + { + includeUnits: true, + name: newName, + storedListing: { + id: listing.id.toString(), + }, + }, + user, + ); + + expect(newListing.name).toBe(newName); + expect(newListing.units).toEqual(listing.units); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: listing.id.toString(), + }, + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsApplicationMailingAddress: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + requestedChangesUser: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + }); + }); + + it('should duplicate a listing, excluding units', async () => { + const listing = mockListing(1, { numberToMake: 2, date: new Date() }); + + const newName = 'duplicate name'; + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + ...listing, + jurisdictions: { + id: randomUUID(), + }, + }); + + prisma.listings.create = jest.fn().mockResolvedValue({ + ...listing, + id: 'duplicate id', + name: newName, + units: [], + }); + + const newListing = await service.duplicate( + { + includeUnits: false, + name: newName, + storedListing: { + id: listing.id.toString(), + }, + }, + user, + ); + + expect(newListing.name).toBe(newName); + expect(newListing.units).toEqual([]); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: listing.id.toString(), + }, + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsApplicationMailingAddress: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + requestedChangesUser: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + }); + }); + + it('should fail to duplicate a listing with the same name', async () => { + const listing = mockListing(1, { numberToMake: 2, date: new Date() }); + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + ...listing, + jurisdictions: { + id: randomUUID(), + }, + }); + + await expect( + async () => + await service.duplicate( + { + includeUnits: false, + name: listing.name, + storedListing: { + id: listing.id.toString(), + }, + }, + user, + ), + ).rejects.toThrowError(); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: listing.id.toString(), + }, + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsApplicationMailingAddress: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + requestedChangesUser: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + }); + }); + }); + describe('Test delete endpoint', () => { it('should delete a listing', async () => { const id = randomUUID(); diff --git a/api/test/unit/services/lottery.service.spec.ts b/api/test/unit/services/lottery.service.spec.ts index 1efd34f5d8..296759495d 100644 --- a/api/test/unit/services/lottery.service.spec.ts +++ b/api/test/unit/services/lottery.service.spec.ts @@ -810,7 +810,7 @@ describe('Testing lottery service', () => { }); describe('Test autoPublishResults endpoint', () => { - it('should call the updateMany', async () => { + it('should call the update', async () => { prisma.listings.findMany = jest.fn().mockResolvedValue([ { id: 'example id1', diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 86a36f1d8b..846a0a9a70 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -329,6 +329,28 @@ export class ListingsService { axios(configs, resolve, reject) }) } + /** + * Duplicate listing + */ + duplicate( + params: { + /** requestBody */ + body?: ListingDuplicate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/listings/duplicate" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } /** * Trigger the listing process job */ @@ -3789,6 +3811,17 @@ export interface ListingCreate { requestedChangesUser?: IdDTO } +export interface ListingDuplicate { + /** */ + name: string + + /** */ + includeUnits: boolean + + /** */ + storedListing: IdDTO +} + export interface ListingUpdate { /** */ id: string