Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
45c9a41
feat: add admin approve modal for enableAutopublish
KrissDrawing May 6, 2026
25af5b1
refactor: rename id's
KrissDrawing May 6, 2026
01cd512
test: use new approve and publish path
KrissDrawing May 6, 2026
4e4de65
test: fake time inside test
KrissDrawing May 11, 2026
90525d5
fix: use timezone for scheduledPublishAt backend error
KrissDrawing May 11, 2026
8ec6d65
fix: modal copy work irrespective of timezone
KrissDrawing May 13, 2026
f74006a
fix: add scheduled listing status
KrissDrawing May 13, 2026
61a3a1b
fix: validate date from listing service
KrissDrawing May 18, 2026
35d4a1a
Merge branch 'main' into 6246/Admin-approval-dialogs
KrissDrawing May 18, 2026
e9b84d5
Merge branch 'main' into 6246/Admin-approval-dialogs
KrissDrawing May 19, 2026
20cf9df
Merge branch '6246/Admin-approval-dialogs' into 6247/New-listing-status
KrissDrawing May 19, 2026
54a8184
fix: remove actions for scheduled state
KrissDrawing May 19, 2026
1288f27
fix: handle scheduled public at for create listing
KrissDrawing May 19, 2026
14e84cd
Merge branch '6246/Admin-approval-dialogs' into 6247/New-listing-status
KrissDrawing May 19, 2026
4360038
fix: add scheduled action buttons with dialogs
KrissDrawing May 26, 2026
dc2c0a4
fix: publish listing when remove scheduled date on save
KrissDrawing May 26, 2026
461c788
fix: save when scheduledPublishedAt removed
KrissDrawing May 26, 2026
633e2c9
Merge branch 'main' into 6181/Admin-updates-when-in-scheduled-status
KrissDrawing May 27, 2026
de996da
fix: add cron job for autopublish
KrissDrawing May 29, 2026
4e9cffb
fix: update comment
KrissDrawing May 29, 2026
952e771
Merge branch 'main' into 6213/Autopublish-cron-job
KrissDrawing Jun 1, 2026
2766470
fix: adjust retry cron string example to 2AM
KrissDrawing Jun 3, 2026
221db4e
Merge branch 'main' into 6213/Autopublish-cron-job
KrissDrawing Jun 3, 2026
3d74a08
fix: use just one cron job for autopublish listing
KrissDrawing Jun 9, 2026
1ce9a97
Merge branch 'main' into 6213/Autopublish-cron-job
KrissDrawing Jun 9, 2026
3988513
Apply suggestion from @ludtkemorgan
KrissDrawing Jun 10, 2026
fd6a671
fix: lint issues
KrissDrawing Jun 10, 2026
44e5862
fix: align api test to new solution
KrissDrawing Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ PARTNERS_PORTAL_URL=http://localhost:3001
EMAIL_API_KEY=SG.ExampleApiKey
# controls the repetition of the listing cron job
LISTING_PROCESSING_CRON_STRING=0 * * * *
# controls the scheduled listing auto-publish cron job
LISTING_SCHEDULED_PUBLISH_CRON_STRING=1 0,1 * * *
# controls the repetition of the lottery publish cron job
LOTTERY_PUBLISH_PROCESSING_CRON_STRING=58 23 * * *
# controls the repetition of the lottery cron job
Expand Down
80 changes: 80 additions & 0 deletions api/src/services/listing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ includeViews.full = {
};

const LISTING_CRON_JOB_NAME = 'LISTING_CRON_JOB';
const LISTING_SCHEDULED_PUBLISH_CRON_JOB_NAME =
'LISTING_SCHEDULED_PUBLISH_CRON_STRING';
/*
this is the service for listings
it handles all the backend's business logic for reading in listing(s)
Expand Down Expand Up @@ -268,6 +270,11 @@ export class ListingService implements OnModuleInit {
process.env.LISTING_PROCESSING_CRON_STRING,
this.closeListings.bind(this),
);
this.cronJobService.startCronJob(
LISTING_SCHEDULED_PUBLISH_CRON_JOB_NAME,
process.env.LISTING_SCHEDULED_PUBLISH_CRON_STRING,
this.publishScheduledListingsCronJob.bind(this),
);
}

/*
Expand Down Expand Up @@ -3347,6 +3354,79 @@ export class ListingService implements OnModuleInit {
};
}

/**
* Runs the cron job to publish listings in 'scheduled' status whose
* scheduledPublishAt date is in the past.
*/
async publishScheduledListingsCronJob(): Promise<SuccessDTO> {
const logName = 'publishScheduledListingsCron';
this.logger.warn(`${logName} job running`);
await this.cronJobService.markCronJobAsStarted(
LISTING_SCHEDULED_PUBLISH_CRON_JOB_NAME,
);

const listings = await this.prisma.listings.findMany({
select: { id: true },
where: {
status: ListingsStatusEnum.scheduled,
AND: [
{ scheduledPublishAt: { not: null } },
{ scheduledPublishAt: { lte: new Date() } },
],
},
});

const listingIds = listings.map((listing) => listing.id);
this.logger.warn(
`${logName} found ${listingIds.length} listing(s) ready to publish`,
);

if (!listingIds.length) {
return { success: true };
}

this.logger.log(
`${logName} listing IDs to publish: ${listingIds.join(', ')}`,
);

for (const listingId of listingIds) {
await this.snapshotCreateService.createListingSnapshot(listingId);
}

const res = await this.prisma.listings.updateMany({
data: {
status: ListingsStatusEnum.active,
publishedAt: new Date(),
},
where: { id: { in: listingIds } },
});

if (listingIds.length > 0) {
await this.prisma.activityLog.createMany({
data: listingIds.map((id) => ({
module: 'listing',
recordId: id,
action: 'update',
metadata: { status: ListingsStatusEnum.active },
})),
});
}

this.logger.warn(
`${logName} published ${res?.count} listing(s) to active status`,
);

if (res?.count) {
await this.cachePurge(
ListingsStatusEnum.active,
ListingsStatusEnum.scheduled,
'',
);
}

return { success: true };
}

/**
*
* @param listingId
Expand Down
93 changes: 93 additions & 0 deletions api/test/unit/services/listing.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7060,6 +7060,99 @@ describe('Testing listing service', () => {
});
});

describe('Test publishScheduledListings', () => {
it('should publish due scheduled listings, write snapshots, activity logs and purge cache', async () => {
prisma.listings.findMany = jest
.fn()
.mockResolvedValue([
{ id: 'scheduled-id-1' },
{ id: 'scheduled-id-2' },
]);
prisma.listings.findUnique = jest
.fn()
.mockResolvedValue({ id: 'scheduled-id-1' });
prisma.listings.updateMany = jest.fn().mockResolvedValue({ count: 2 });
prisma.activityLog.createMany = jest.fn().mockResolvedValue({ count: 2 });
prisma.cronJob.findFirst = jest
.fn()
.mockResolvedValue({ id: randomUUID() });
prisma.cronJob.update = jest.fn().mockResolvedValue(true);
prisma.listingSnapshot.create = jest
.fn()
.mockResolvedValue({ id: 'snapshot-id' });

process.env.PROXY_URL = 'https://www.google.com';
await service.publishScheduledListingsCronJob();

expect(prisma.listings.findMany).toHaveBeenCalledWith({
select: { id: true },
where: {
status: ListingsStatusEnum.scheduled,
AND: [
{ scheduledPublishAt: { not: null } },
{ scheduledPublishAt: { lte: expect.any(Date) } },
],
},
});
expect(prisma.listings.updateMany).toHaveBeenCalledWith({
data: {
status: ListingsStatusEnum.active,
publishedAt: expect.any(Date),
},
where: { id: { in: ['scheduled-id-1', 'scheduled-id-2'] } },
});
expect(prisma.activityLog.createMany).toHaveBeenCalledWith({
data: [
{
module: 'listing',
recordId: 'scheduled-id-1',
action: 'update',
metadata: { status: ListingsStatusEnum.active },
},
{
module: 'listing',
recordId: 'scheduled-id-2',
action: 'update',
metadata: { status: ListingsStatusEnum.active },
},
],
});
expect(prisma.listingSnapshot.create).toHaveBeenCalledTimes(2);
expect(httpServiceMock.request).toHaveBeenCalledWith({
baseURL: 'https://www.google.com',
method: 'PURGE',
url: `/listings?*`,
});
expect(prisma.cronJob.findFirst).toHaveBeenCalled();
expect(prisma.cronJob.findFirst).toHaveBeenCalledWith({
where: {
name: 'LISTING_SCHEDULED_PUBLISH_CRON_STRING',
},
});
expect(prisma.cronJob.update).toHaveBeenCalled();
process.env.PROXY_URL = undefined;
});

it('should not purge cache or write activity logs when no listings are due', async () => {
prisma.listings.findMany = jest.fn().mockResolvedValue([]);
prisma.listings.updateMany = jest.fn().mockResolvedValue({ count: 0 });
prisma.activityLog.createMany = jest.fn().mockResolvedValue({ count: 0 });
prisma.cronJob.findFirst = jest
.fn()
.mockResolvedValue({ id: randomUUID() });
prisma.cronJob.update = jest.fn().mockResolvedValue(true);

process.env.PROXY_URL = 'https://www.google.com';
const result = await service.publishScheduledListingsCronJob();

expect(result).toEqual({ success: true });
expect(prisma.listings.updateMany).not.toHaveBeenCalled();
expect(httpServiceMock.request).not.toHaveBeenCalled();
expect(prisma.activityLog.createMany).not.toHaveBeenCalled();
process.env.PROXY_URL = undefined;
});
});

describe('Test updateListingEvents endpoint', () => {
it('should clear asset from listing events if they are present', async () => {
prisma.listingEvents.findMany = jest.fn().mockResolvedValue([
Expand Down
Loading