This commit is contained in:
2025-08-18 15:55:35 +05:30
parent 75be8335d0
commit 30e880d44e
51 changed files with 2321 additions and 813 deletions

View File

@@ -1,7 +1,12 @@
{
"permissions": {
"allow": [
"Bash(npm run lint)"
"Bash(npm run lint)",
"Bash(npm install:*)",
"Bash(npm run migration:generate:*)",
"Bash(npm run migration:run:*)",
"Bash(npm run build:*)",
"Bash(npx typeorm migration:generate:*)"
],
"deny": []
}

53
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/multer": "^1.4.12",
"axios": "^1.11.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
@@ -5030,9 +5031,19 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@@ -6917,7 +6928,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -7364,7 +7374,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -7650,7 +7659,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -8388,6 +8396,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -8433,15 +8461,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true,
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -8462,7 +8490,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -8472,7 +8499,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -8917,7 +8943,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -11861,6 +11886,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",

View File

@@ -37,6 +37,7 @@
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/multer": "^1.4.12",
"axios": "^1.11.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",

View File

@@ -14,7 +14,7 @@ import { VideoAdModule } from 'src/video-ad/video-ad.module';
UserModule,
LineAdModule,
PosterAdModule,
VideoAdModule
VideoAdModule,
],
controllers: [AdCommentController],
providers: [AdCommentService],

View File

@@ -152,11 +152,11 @@ export class AdCommentService {
isActive: boolean = true,
): Promise<AdComment[]> {
console.log('adId', adId);
let where: FindOptionsWhere<AdComment> = {};
const where: FindOptionsWhere<AdComment> = {};
if (isActive) {
where.isActive = true;
}
let relations = ['user'];
const relations = ['user'];
if (adType === AdType.LINE) {
where.lineAd = { id: adId };
relations.push('lineAd');

View File

@@ -1,4 +1,15 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, HttpException, HttpStatus } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { AdPositionService } from './ad-position.service';
import { CreateAdPositionDto } from './dto/create-ad-position.dto';
import { UpdateAdPositionDto } from './dto/update-ad-position.dto';
@@ -7,13 +18,13 @@ import { PositionType } from 'src/common/enums/position-type.enum';
import {
AdSlotsOverviewResponseDto,
LineAdsResponseDto,
SlotDetailsResponseDto
SlotDetailsResponseDto,
} from './dto/ad-slots-overview-response.dto';
import {
DateBasedAdSlotsOverviewDto,
DateBasedLineAdsDto,
AvailableDatesDto,
DateBasedSlotDetailsDto
DateBasedSlotDetailsDto,
} from './dto/date-based-ad-slots.dto';
@Controller('ad-position')
@@ -36,7 +47,10 @@ export class AdPositionController {
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateAdPositionDto: UpdateAdPositionDto) {
update(
@Param('id') id: string,
@Body() updateAdPositionDto: UpdateAdPositionDto,
) {
return this.adPositionService.update(id, updateAdPositionDto);
}
@@ -52,25 +66,28 @@ export class AdPositionController {
@Get('ad-slots/overview')
async getAdSlotsOverview(
@Query('pageType') pageType?: PageType,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<AdSlotsOverviewResponseDto> {
try {
// Validate pageType if provided
if (pageType && !Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
return await this.adPositionService.getAdSlotsOverview(pageType, category);
return await this.adPositionService.getAdSlotsOverview(
pageType,
category,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to fetch ad slots overview',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -82,14 +99,14 @@ export class AdPositionController {
@Get('ad-slots/line-ads')
async getLineAds(
@Query('pageType') pageType?: PageType,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<LineAdsResponseDto> {
try {
// Validate pageType if provided
if (pageType && !Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -100,7 +117,7 @@ export class AdPositionController {
}
throw new HttpException(
'Failed to fetch line ads',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -114,14 +131,14 @@ export class AdPositionController {
@Param('pageType') pageType: PageType,
@Param('side') side: PositionType,
@Param('position') position: string,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<SlotDetailsResponseDto> {
try {
// Validate pageType
if (!Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -129,7 +146,7 @@ export class AdPositionController {
if (!Object.values(PositionType).includes(side)) {
throw new HttpException(
`Invalid side. Must be one of: ${Object.values(PositionType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -138,26 +155,35 @@ export class AdPositionController {
if (isNaN(positionNum) || positionNum < 1 || positionNum > 6) {
throw new HttpException(
'Invalid position. Must be a number between 1 and 6',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
// Validate position constraints for CENTER positions
if ((side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) && positionNum !== 1) {
if (
(side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM) &&
positionNum !== 1
) {
throw new HttpException(
'CENTER_TOP and CENTER_BOTTOM positions only support position 1',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
return await this.adPositionService.getSlotDetails(pageType, side, positionNum, category);
return await this.adPositionService.getSlotDetails(
pageType,
side,
positionNum,
category,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to fetch slot details',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -176,7 +202,7 @@ export class AdPositionController {
}
throw new HttpException(
'Failed to fetch available dates',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -189,14 +215,14 @@ export class AdPositionController {
async getAdSlotsByDate(
@Query('date') date: string,
@Query('pageType') pageType?: PageType,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<DateBasedAdSlotsOverviewDto> {
try {
// Validate date format
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new HttpException(
'Invalid date format. Use YYYY-MM-DD',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -204,18 +230,22 @@ export class AdPositionController {
if (pageType && !Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
return await this.adPositionService.getAdSlotsByDate(date, pageType, category);
return await this.adPositionService.getAdSlotsByDate(
date,
pageType,
category,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to fetch ad slots by date',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -228,14 +258,14 @@ export class AdPositionController {
async getLineAdsByDate(
@Query('date') date: string,
@Query('pageType') pageType?: PageType,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<DateBasedLineAdsDto> {
try {
// Validate date format
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new HttpException(
'Invalid date format. Use YYYY-MM-DD',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -243,18 +273,22 @@ export class AdPositionController {
if (pageType && !Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
return await this.adPositionService.getLineAdsByDate(date, pageType, category);
return await this.adPositionService.getLineAdsByDate(
date,
pageType,
category,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to fetch line ads by date',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -269,14 +303,14 @@ export class AdPositionController {
@Param('pageType') pageType: PageType,
@Param('side') side: PositionType,
@Param('position') position: string,
@Query('category') category?: string
@Query('category') category?: string,
): Promise<DateBasedSlotDetailsDto> {
try {
// Validate date format
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new HttpException(
'Invalid date format. Use YYYY-MM-DD',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -284,7 +318,7 @@ export class AdPositionController {
if (!Object.values(PageType).includes(pageType)) {
throw new HttpException(
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -292,7 +326,7 @@ export class AdPositionController {
if (!Object.values(PositionType).includes(side)) {
throw new HttpException(
`Invalid side. Must be one of: ${Object.values(PositionType).join(', ')}`,
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -301,26 +335,36 @@ export class AdPositionController {
if (isNaN(positionNum) || positionNum < 1 || positionNum > 6) {
throw new HttpException(
'Invalid position. Must be a number between 1 and 6',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
// Validate position constraints for CENTER positions
if ((side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) && positionNum !== 1) {
if (
(side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM) &&
positionNum !== 1
) {
throw new HttpException(
'CENTER_TOP and CENTER_BOTTOM positions only support position 1',
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
return await this.adPositionService.getSlotDetailsByDate(date, pageType, side, positionNum, category);
return await this.adPositionService.getSlotDetailsByDate(
date,
pageType,
side,
positionNum,
category,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to fetch slot details by date',
HttpStatus.INTERNAL_SERVER_ERROR
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

View File

@@ -9,7 +9,15 @@ import { LineAd } from 'src/line-ad/entities/line-ad.entity';
import { MainCategory } from 'src/category/entities/main-category.entity';
@Module({
imports: [TypeOrmModule.forFeature([AdPosition, PosterAd, VideoAd, LineAd, MainCategory])],
imports: [
TypeOrmModule.forFeature([
AdPosition,
PosterAd,
VideoAd,
LineAd,
MainCategory,
]),
],
controllers: [AdPositionController],
providers: [AdPositionService],
exports: [AdPositionService],

View File

@@ -14,7 +14,7 @@ import {
LineAdsResponseDto,
LineAdSummaryDto,
SlotDetailsResponseDto,
SlotAdDetailDto
SlotAdDetailDto,
} from './dto/ad-slots-overview-response.dto';
import {
DateBasedAdSlotsOverviewDto,
@@ -24,7 +24,7 @@ import {
AvailableDatesDto,
DateBasedSlotDetailsDto,
DateBasedSlotAdDetailDto,
CategoryDto
CategoryDto,
} from './dto/date-based-ad-slots.dto';
import { PosterAd } from 'src/poster-ad/entities/poster-ad.entity';
import { VideoAd } from 'src/video-ad/entities/video-ad.entity';
@@ -64,7 +64,10 @@ export class AdPositionService {
});
}
async update(id: string, updateAdPositionDto: UpdateAdPositionDto): Promise<AdPosition|null> {
async update(
id: string,
updateAdPositionDto: UpdateAdPositionDto,
): Promise<AdPosition | null> {
await this.adPositionRepository.update(id, updateAdPositionDto);
return this.findOne(id);
}
@@ -80,7 +83,7 @@ export class AdPositionService {
if (!ad.isActive || ad.status !== AdStatus.PUBLISHED) {
return false;
}
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
return false;
}
@@ -101,14 +104,16 @@ export class AdPositionService {
expectedStatus: AdStatus.PUBLISHED,
statusMatch: ad?.status === AdStatus.PUBLISHED,
datesLength: ad?.dates?.length,
dates: ad?.dates
dates: ad?.dates,
});
if (!ad.isActive || ad.status !== AdStatus.PUBLISHED) {
console.log(`[DEBUG] Ad ${ad?.id} rejected - not active or not published`);
console.log(
`[DEBUG] Ad ${ad?.id} rejected - not active or not published`,
);
return false;
}
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
console.log(`[DEBUG] Ad ${ad?.id} rejected - no valid dates array`);
return false;
@@ -117,16 +122,20 @@ export class AdPositionService {
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const todayISOString = today.toISOString().split('T')[0];
console.log(`[DEBUG] Checking dates for ad ${ad?.id}. Today: ${todayISOString}`);
console.log(
`[DEBUG] Checking dates for ad ${ad?.id}. Today: ${todayISOString}`,
);
const isActiveToday = ad.dates.some((dateStr: string) => {
const dateOnly = dateStr.split('T')[0];
const matches = dateOnly === todayISOString;
console.log(`[DEBUG] Date ${dateOnly} matches today ${todayISOString}: ${matches}`);
console.log(
`[DEBUG] Date ${dateOnly} matches today ${todayISOString}: ${matches}`,
);
return matches;
});
console.log(`[DEBUG] Ad ${ad?.id} is active today: ${isActiveToday}`);
return isActiveToday;
}
@@ -138,11 +147,11 @@ export class AdPositionService {
if (!Array.isArray(dates) || dates.length === 0) {
return undefined;
}
const latestDate = dates.reduce((latest, current) => {
return new Date(current) > new Date(latest) ? current : latest;
});
return new Date(latestDate);
}
@@ -153,7 +162,7 @@ export class AdPositionService {
return {
id: category.id,
name: category.name,
color: category.categories_color || undefined
color: category.categories_color || undefined,
};
}
@@ -162,39 +171,60 @@ export class AdPositionService {
*/
private extractUniqueCategories(ads: any[]): CategoryDto[] {
const categoriesMap = new Map<string, CategoryDto>();
ads.forEach(ad => {
ads.forEach((ad) => {
if (ad.mainCategory) {
categoriesMap.set(ad.mainCategory.id, this.categoryToDto(ad.mainCategory));
categoriesMap.set(
ad.mainCategory.id,
this.categoryToDto(ad.mainCategory),
);
}
});
return Array.from(categoriesMap.values());
}
/**
* Get ad slots overview with occupancy status
*/
async getAdSlotsOverview(pageType?: PageType, category?: string): Promise<AdSlotsOverviewResponseDto> {
async getAdSlotsOverview(
pageType?: PageType,
category?: string,
): Promise<AdSlotsOverviewResponseDto> {
const slots: SlotOccupancyDto[] = [];
const pageTypes = pageType ? [pageType] : [PageType.HOME, PageType.CATEGORY];
const sides = [PositionType.LEFT_SIDE, PositionType.RIGHT_SIDE, PositionType.CENTER_TOP, PositionType.CENTER_BOTTOM];
const pageTypes = pageType
? [pageType]
: [PageType.HOME, PageType.CATEGORY];
const sides = [
PositionType.LEFT_SIDE,
PositionType.RIGHT_SIDE,
PositionType.CENTER_TOP,
PositionType.CENTER_BOTTOM,
];
const maxCapacity = 5; // Maximum 5 ads per slot
for (const page of pageTypes) {
for (const side of sides) {
const maxPositions = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 1 : 6;
const maxPositions =
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 1
: 6;
for (let position = 1; position <= maxPositions; position++) {
// Get all ads for this slot
const adPositions = await this.adPositionRepository.find({
where: {
pageType: page,
side,
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
adType: AdType.POSTER // Only poster and video ads use positioned slots
position:
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 0
: position,
adType: AdType.POSTER, // Only poster and video ads use positioned slots
},
relations: ['posterAd', 'videoAd']
relations: ['posterAd', 'videoAd'],
});
// Also get video ads for this slot
@@ -202,16 +232,20 @@ export class AdPositionService {
where: {
pageType: page,
side,
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
adType: AdType.VIDEO
position:
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 0
: position,
adType: AdType.VIDEO,
},
relations: ['videoAd']
relations: ['videoAd'],
});
const allPositions = [...adPositions, ...videoAdPositions];
// Filter for active ads
const activeAds = allPositions.filter(pos => {
const activeAds = allPositions.filter((pos) => {
const ad = pos.posterAd || pos.videoAd;
return ad && this.isAdActive(ad);
});
@@ -219,18 +253,22 @@ export class AdPositionService {
// Calculate expiry dates
let earliestExpiryDate: Date | undefined;
let latestExpiryDate: Date | undefined;
if (activeAds.length > 0) {
const expiryDates = activeAds
.map(pos => {
.map((pos) => {
const ad = pos.posterAd || pos.videoAd;
return ad ? this.getExpiryDate(ad.dates) : undefined;
})
.filter(date => date !== undefined) as Date[];
.filter((date) => date !== undefined);
if (expiryDates.length > 0) {
earliestExpiryDate = new Date(Math.min(...expiryDates.map(d => d.getTime())));
latestExpiryDate = new Date(Math.max(...expiryDates.map(d => d.getTime())));
earliestExpiryDate = new Date(
Math.min(...expiryDates.map((d) => d.getTime())),
);
latestExpiryDate = new Date(
Math.max(...expiryDates.map((d) => d.getTime())),
);
}
}
@@ -249,7 +287,7 @@ export class AdPositionService {
earliestExpiryDate: earliestExpiryDate?.toISOString(),
latestExpiryDate: latestExpiryDate?.toISOString(),
categoryId,
categoryName
categoryName,
});
}
}
@@ -258,27 +296,32 @@ export class AdPositionService {
// Filter slots by category if specified and pageType is CATEGORY
let filteredSlots = slots;
if (category && pageType === PageType.CATEGORY) {
filteredSlots = slots.filter(slot => slot.categoryId === category);
filteredSlots = slots.filter((slot) => slot.categoryId === category);
}
const totalSlots = filteredSlots.length;
const occupiedSlots = filteredSlots.filter(slot => slot.isOccupied).length;
const occupiedSlots = filteredSlots.filter(
(slot) => slot.isOccupied,
).length;
const freeSlots = totalSlots - occupiedSlots;
return {
totalSlots,
occupiedSlots,
freeSlots,
slots: filteredSlots
slots: filteredSlots,
};
}
/**
* Get line ads grouped by page type
*/
async getLineAds(pageType?: PageType, category?: string): Promise<LineAdsResponseDto> {
async getLineAds(
pageType?: PageType,
category?: string,
): Promise<LineAdsResponseDto> {
const whereConditions: any = {
adType: AdType.LINE
adType: AdType.LINE,
};
if (pageType) {
@@ -290,21 +333,21 @@ export class AdPositionService {
relations: ['lineAd', 'lineAd.customer', 'lineAd.customer.user'],
order: {
lineAd: {
updated_at: 'DESC'
}
}
updated_at: 'DESC',
},
},
});
const activeLineAds = lineAdPositions
.filter(pos => pos.lineAd && this.isAdActive(pos.lineAd))
.map(pos => pos.lineAd);
.filter((pos) => pos.lineAd && this.isAdActive(pos.lineAd))
.map((pos) => pos.lineAd);
const homeAds: LineAdSummaryDto[] = [];
const categoryAds: LineAdSummaryDto[] = [];
for (const ad of activeLineAds) {
const adPosition = await this.adPositionRepository.findOne({
where: { lineAd: { id: ad.id } }
where: { lineAd: { id: ad.id } },
});
// For category pages, we'll need to get actual category data from the ad
@@ -314,7 +357,8 @@ export class AdPositionService {
const summary: LineAdSummaryDto = {
id: ad.id,
title: ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''), // Use first 50 chars as title
title:
ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''), // Use first 50 chars as title
content: ad.content,
pageType: adPosition?.pageType || PageType.HOME,
status: ad.status,
@@ -322,7 +366,7 @@ export class AdPositionService {
createdAt: ad.created_at,
updatedAt: ad.updated_at,
categoryId,
categoryName
categoryName,
};
if (adPosition?.pageType === PageType.HOME) {
@@ -335,22 +379,32 @@ export class AdPositionService {
// Filter category ads by category if specified
let filteredCategoryAds = categoryAds;
if (category && pageType === PageType.CATEGORY) {
filteredCategoryAds = categoryAds.filter(ad => ad.categoryId === category);
filteredCategoryAds = categoryAds.filter(
(ad) => ad.categoryId === category,
);
}
return {
homeAds,
categoryAds: filteredCategoryAds,
totalCount: homeAds.length + filteredCategoryAds.length
totalCount: homeAds.length + filteredCategoryAds.length,
};
}
/**
* Get detailed information about a specific slot
*/
async getSlotDetails(pageType: PageType, side: PositionType, position: number, category?: string): Promise<SlotDetailsResponseDto> {
async getSlotDetails(
pageType: PageType,
side: PositionType,
position: number,
category?: string,
): Promise<SlotDetailsResponseDto> {
const maxCapacity = 5;
const actualPosition = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position;
const actualPosition =
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
? 0
: position;
// Get poster ads
const posterAdPositions = await this.adPositionRepository.find({
@@ -358,9 +412,9 @@ export class AdPositionService {
pageType,
side,
position: actualPosition,
adType: AdType.POSTER
adType: AdType.POSTER,
},
relations: ['posterAd', 'posterAd.customer', 'posterAd.customer.user']
relations: ['posterAd', 'posterAd.customer', 'posterAd.customer.user'],
});
// Get video ads
@@ -369,15 +423,15 @@ export class AdPositionService {
pageType,
side,
position: actualPosition,
adType: AdType.VIDEO
adType: AdType.VIDEO,
},
relations: ['videoAd', 'videoAd.customer', 'videoAd.customer.user']
relations: ['videoAd', 'videoAd.customer', 'videoAd.customer.user'],
});
const allPositions = [...posterAdPositions, ...videoAdPositions];
const ads: SlotAdDetailDto[] = allPositions
.map(pos => {
.map((pos) => {
const ad = pos.posterAd || pos.videoAd;
if (!ad) return null;
@@ -388,7 +442,9 @@ export class AdPositionService {
return {
id: ad.id,
title: pos.posterAd ? `Poster Ad by ${ad.postedBy}` : `Video Ad by ${ad.postedBy}`, // Create title from available data
title: pos.posterAd
? `Poster Ad by ${ad.postedBy}`
: `Video Ad by ${ad.postedBy}`, // Create title from available data
content: undefined, // Poster and Video ads don't have content field
status: ad.status,
isActive: this.isAdActive(ad),
@@ -398,18 +454,18 @@ export class AdPositionService {
customerName: ad.customer?.user?.name || 'Unknown',
adType: pos.posterAd ? 'POSTER' : 'VIDEO',
categoryId,
categoryName
categoryName,
};
})
.filter(ad => ad !== null) as SlotAdDetailDto[];
.filter((ad) => ad !== null) as SlotAdDetailDto[];
return {
pageType,
side,
position,
maxCapacity,
currentOccupancy: ads.filter(ad => ad.isActive).length,
ads
currentOccupancy: ads.filter((ad) => ad.isActive).length,
ads,
};
}
@@ -421,23 +477,23 @@ export class AdPositionService {
const [lineAds, posterAds, videoAds] = await Promise.all([
this.lineAdRepository.find({
where: { isActive: true, status: AdStatus.PUBLISHED },
select: ['dates']
select: ['dates'],
}),
this.posterAdRepository.find({
where: { isActive: true, status: AdStatus.PUBLISHED },
select: ['dates']
select: ['dates'],
}),
this.videoAdRepository.find({
where: { isActive: true, status: AdStatus.PUBLISHED },
select: ['dates']
})
select: ['dates'],
}),
]);
const allDates = new Set<string>();
[...lineAds, ...posterAds, ...videoAds].forEach(ad => {
[...lineAds, ...posterAds, ...videoAds].forEach((ad) => {
if (ad.dates && Array.isArray(ad.dates)) {
ad.dates.forEach(dateStr => {
ad.dates.forEach((dateStr) => {
const dateOnly = dateStr.split('T')[0];
allDates.add(dateOnly);
});
@@ -445,60 +501,95 @@ export class AdPositionService {
});
const sortedDates = Array.from(allDates).sort();
return {
dates: sortedDates,
totalDates: sortedDates.length
totalDates: sortedDates.length,
};
}
/**
* Get ad slots overview for a specific date
*/
async getAdSlotsByDate(date: string, pageType?: PageType, category?: string): Promise<DateBasedAdSlotsOverviewDto> {
async getAdSlotsByDate(
date: string,
pageType?: PageType,
category?: string,
): Promise<DateBasedAdSlotsOverviewDto> {
const slots: DateBasedSlotOccupancyDto[] = [];
const pageTypes = pageType ? [pageType] : [PageType.HOME, PageType.CATEGORY];
const sides = [PositionType.LEFT_SIDE, PositionType.RIGHT_SIDE, PositionType.CENTER_TOP, PositionType.CENTER_BOTTOM];
const pageTypes = pageType
? [pageType]
: [PageType.HOME, PageType.CATEGORY];
const sides = [
PositionType.LEFT_SIDE,
PositionType.RIGHT_SIDE,
PositionType.CENTER_TOP,
PositionType.CENTER_BOTTOM,
];
const maxCapacity = 5;
const allCategories = new Set<string>();
for (const page of pageTypes) {
for (const side of sides) {
const maxPositions = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 1 : 6;
const maxPositions =
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 1
: 6;
for (let position = 1; position <= maxPositions; position++) {
// Get all ads for this slot
const adPositions = await this.adPositionRepository.find({
where: {
pageType: page,
side,
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
adType: AdType.POSTER
position:
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 0
: position,
adType: AdType.POSTER,
},
relations: ['posterAd', 'posterAd.mainCategory', 'posterAd.categoryOne', 'posterAd.categoryTwo', 'posterAd.categoryThree']
relations: [
'posterAd',
'posterAd.mainCategory',
'posterAd.categoryOne',
'posterAd.categoryTwo',
'posterAd.categoryThree',
],
});
const videoAdPositions = await this.adPositionRepository.find({
where: {
pageType: page,
side,
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
adType: AdType.VIDEO
position:
side === PositionType.CENTER_TOP ||
side === PositionType.CENTER_BOTTOM
? 0
: position,
adType: AdType.VIDEO,
},
relations: ['videoAd', 'videoAd.mainCategory', 'videoAd.categoryOne', 'videoAd.categoryTwo', 'videoAd.categoryThree']
relations: [
'videoAd',
'videoAd.mainCategory',
'videoAd.categoryOne',
'videoAd.categoryTwo',
'videoAd.categoryThree',
],
});
const allPositions = [...adPositions, ...videoAdPositions];
// Filter for ads active on this specific date
const activeAds = allPositions.filter(pos => {
const activeAds = allPositions.filter((pos) => {
const ad = pos.posterAd || pos.videoAd;
return ad && this.isAdActiveForDate(ad, date);
});
// Extract categories from active ads
const slotCategories: CategoryDto[] = [];
activeAds.forEach(pos => {
activeAds.forEach((pos) => {
const ad = pos.posterAd || pos.videoAd;
if (ad && ad.mainCategory) {
const categoryDto = this.categoryToDto(ad.mainCategory);
@@ -509,7 +600,7 @@ export class AdPositionService {
// Filter by category if specified
if (category && page === PageType.CATEGORY) {
const hasCategory = activeAds.some(pos => {
const hasCategory = activeAds.some((pos) => {
const ad = pos.posterAd || pos.videoAd;
return ad && ad.mainCategory && ad.mainCategory.id === category;
});
@@ -523,18 +614,20 @@ export class AdPositionService {
activeAdsCount: activeAds.length,
maxCapacity,
isOccupied: activeAds.length > 0,
categories: slotCategories
categories: slotCategories,
});
}
}
}
const totalSlots = slots.length;
const occupiedSlots = slots.filter(slot => slot.isOccupied).length;
const occupiedSlots = slots.filter((slot) => slot.isOccupied).length;
const freeSlots = totalSlots - occupiedSlots;
// Convert Set back to array of CategoryDto
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
const uniqueCategories = Array.from(allCategories).map(
(catStr) => JSON.parse(catStr) as CategoryDto,
);
return {
date,
@@ -542,16 +635,20 @@ export class AdPositionService {
occupiedSlots,
freeSlots,
slots,
categories: uniqueCategories
categories: uniqueCategories,
};
}
/**
* Get line ads for a specific date
*/
async getLineAdsByDate(date: string, pageType?: PageType, category?: string): Promise<DateBasedLineAdsDto> {
async getLineAdsByDate(
date: string,
pageType?: PageType,
category?: string,
): Promise<DateBasedLineAdsDto> {
const whereConditions: any = {
adType: AdType.LINE
adType: AdType.LINE,
};
if (pageType) {
@@ -567,18 +664,18 @@ export class AdPositionService {
'lineAd.mainCategory',
'lineAd.categoryOne',
'lineAd.categoryTwo',
'lineAd.categoryThree'
'lineAd.categoryThree',
],
order: {
lineAd: {
updated_at: 'DESC'
}
}
updated_at: 'DESC',
},
},
});
const activeLineAds = lineAdPositions
.filter(pos => pos.lineAd && this.isAdActiveForDate(pos.lineAd, date))
.map(pos => ({ ad: pos.lineAd, pageType: pos.pageType }));
.filter((pos) => pos.lineAd && this.isAdActiveForDate(pos.lineAd, date))
.map((pos) => ({ ad: pos.lineAd, pageType: pos.pageType }));
const homeAds: DateBasedLineAdDto[] = [];
const categoryAds: DateBasedLineAdDto[] = [];
@@ -587,16 +684,25 @@ export class AdPositionService {
for (const { ad, pageType: adPageType } of activeLineAds) {
const adDto: DateBasedLineAdDto = {
id: ad.id,
title: ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''),
title:
ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''),
content: ad.content,
pageType: adPageType || PageType.HOME,
status: ad.status,
createdAt: ad.created_at,
updatedAt: ad.updated_at,
mainCategory: ad.mainCategory ? this.categoryToDto(ad.mainCategory) : undefined,
categoryOne: ad.categoryOne ? { id: ad.categoryOne.id, name: ad.categoryOne.name } : undefined,
categoryTwo: ad.categoryTwo ? { id: ad.categoryTwo.id, name: ad.categoryTwo.name } : undefined,
categoryThree: ad.categoryThree ? { id: ad.categoryThree.id, name: ad.categoryThree.name } : undefined
mainCategory: ad.mainCategory
? this.categoryToDto(ad.mainCategory)
: undefined,
categoryOne: ad.categoryOne
? { id: ad.categoryOne.id, name: ad.categoryOne.name }
: undefined,
categoryTwo: ad.categoryTwo
? { id: ad.categoryTwo.id, name: ad.categoryTwo.name }
: undefined,
categoryThree: ad.categoryThree
? { id: ad.categoryThree.id, name: ad.categoryThree.name }
: undefined,
};
// Track categories
@@ -605,7 +711,11 @@ export class AdPositionService {
}
// Filter by category if specified
if (category && adPageType === PageType.CATEGORY && (!ad.mainCategory || ad.mainCategory.id !== category)) {
if (
category &&
adPageType === PageType.CATEGORY &&
(!ad.mainCategory || ad.mainCategory.id !== category)
) {
continue;
}
@@ -616,14 +726,16 @@ export class AdPositionService {
}
}
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
const uniqueCategories = Array.from(allCategories).map(
(catStr) => JSON.parse(catStr) as CategoryDto,
);
return {
date,
homeAds,
categoryAds,
totalCount: homeAds.length + categoryAds.length,
categories: uniqueCategories
categories: uniqueCategories,
};
}
@@ -635,10 +747,13 @@ export class AdPositionService {
pageType: PageType,
side: PositionType,
position: number,
category?: string
category?: string,
): Promise<DateBasedSlotDetailsDto> {
const maxCapacity = 5;
const actualPosition = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position;
const actualPosition =
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
? 0
: position;
// Get poster ads
const posterAdPositions = await this.adPositionRepository.find({
@@ -646,7 +761,7 @@ export class AdPositionService {
pageType,
side,
position: actualPosition,
adType: AdType.POSTER
adType: AdType.POSTER,
},
relations: [
'posterAd',
@@ -655,8 +770,8 @@ export class AdPositionService {
'posterAd.mainCategory',
'posterAd.categoryOne',
'posterAd.categoryTwo',
'posterAd.categoryThree'
]
'posterAd.categoryThree',
],
});
// Get video ads
@@ -665,7 +780,7 @@ export class AdPositionService {
pageType,
side,
position: actualPosition,
adType: AdType.VIDEO
adType: AdType.VIDEO,
},
relations: [
'videoAd',
@@ -674,33 +789,41 @@ export class AdPositionService {
'videoAd.mainCategory',
'videoAd.categoryOne',
'videoAd.categoryTwo',
'videoAd.categoryThree'
]
'videoAd.categoryThree',
],
});
const allPositions = [...posterAdPositions, ...videoAdPositions];
const allCategories = new Set<string>();
const ads: DateBasedSlotAdDetailDto[] = allPositions
.map(pos => {
.map((pos) => {
const ad = pos.posterAd || pos.videoAd;
if (!ad) return null;
const isActive = this.isAdActiveForDate(ad, date);
// Track categories
if (ad.mainCategory) {
allCategories.add(JSON.stringify(this.categoryToDto(ad.mainCategory)));
allCategories.add(
JSON.stringify(this.categoryToDto(ad.mainCategory)),
);
}
// Filter by category if specified
if (category && pageType === PageType.CATEGORY && (!ad.mainCategory || ad.mainCategory.id !== category)) {
if (
category &&
pageType === PageType.CATEGORY &&
(!ad.mainCategory || ad.mainCategory.id !== category)
) {
return null;
}
return {
id: ad.id,
title: pos.posterAd ? `Poster Ad by ${ad.postedBy}` : `Video Ad by ${ad.postedBy}`,
title: pos.posterAd
? `Poster Ad by ${ad.postedBy}`
: `Video Ad by ${ad.postedBy}`,
content: undefined,
status: ad.status,
isActive,
@@ -708,15 +831,25 @@ export class AdPositionService {
updatedAt: ad.updated_at,
customerName: ad.customer?.user?.name || 'Unknown',
adType: pos.posterAd ? 'POSTER' : 'VIDEO',
mainCategory: ad.mainCategory ? this.categoryToDto(ad.mainCategory) : undefined,
categoryOne: ad.categoryOne ? { id: ad.categoryOne.id, name: ad.categoryOne.name } : undefined,
categoryTwo: ad.categoryTwo ? { id: ad.categoryTwo.id, name: ad.categoryTwo.name } : undefined,
categoryThree: ad.categoryThree ? { id: ad.categoryThree.id, name: ad.categoryThree.name } : undefined
mainCategory: ad.mainCategory
? this.categoryToDto(ad.mainCategory)
: undefined,
categoryOne: ad.categoryOne
? { id: ad.categoryOne.id, name: ad.categoryOne.name }
: undefined,
categoryTwo: ad.categoryTwo
? { id: ad.categoryTwo.id, name: ad.categoryTwo.name }
: undefined,
categoryThree: ad.categoryThree
? { id: ad.categoryThree.id, name: ad.categoryThree.name }
: undefined,
};
})
.filter(ad => ad !== null) as DateBasedSlotAdDetailDto[];
.filter((ad) => ad !== null) as DateBasedSlotAdDetailDto[];
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
const uniqueCategories = Array.from(allCategories).map(
(catStr) => JSON.parse(catStr) as CategoryDto,
);
return {
date,
@@ -724,9 +857,9 @@ export class AdPositionService {
side,
position,
maxCapacity,
currentOccupancy: ads.filter(ad => ad.isActive).length,
currentOccupancy: ads.filter((ad) => ad.isActive).length,
ads,
categories: uniqueCategories
categories: uniqueCategories,
};
}
}

View File

@@ -62,4 +62,4 @@ export class SlotDetailsResponseDto {
maxCapacity: number;
currentOccupancy: number;
ads: SlotAdDetailDto[];
}
}

View File

@@ -79,4 +79,4 @@ export class DateBasedSlotDetailsDto {
currentOccupancy: number;
ads: DateBasedSlotAdDetailDto[];
categories: CategoryDto[]; // All unique categories in this slot
}
}

View File

@@ -7,6 +7,8 @@ import {
UseGuards,
Req,
UnauthorizedException,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { Response, Request } from 'express';
@@ -16,8 +18,6 @@ import { Role } from 'src/common/enums/role.enum';
@Controller('auth')
export class AuthController {
private static DOMAIN = 'paisaads-v2.anujs.dev';
// private static DOMAIN = 'localhost';
constructor(private readonly authService: AuthService) {}
@Public()
@@ -80,4 +80,86 @@ export class AuthController {
});
return { message: 'Viewer login successful', user };
}
@Public()
@Post('send-otp')
async sendOtp(@Body('phone') phone: string) {
if (!phone) {
throw new BadRequestException('Phone number is required');
}
return this.authService.generateOtp(phone);
}
@Public()
@Post('verify-otp')
async verifyOtp(
@Body('phone') phone: string,
@Body('otp') otp: string,
@Res({ passthrough: true }) res: Response,
) {
if (!phone || !otp) {
throw new BadRequestException('Phone number and OTP are required');
}
const result = await this.authService.verifyOtp(phone, otp);
if (result.token) {
res.cookie('token', result.token, {
httpOnly: true,
sameSite: 'strict',
secure: true,
path: '/',
});
}
return { message: result.message };
}
@Public()
@Post('send-verification-otp')
async sendVerificationOtp(
@Body('emailOrPhone') emailOrPhone: string,
@Body('type') type: 'email' | 'phone',
) {
if (!emailOrPhone || !type) {
throw new BadRequestException('Email/Phone and type are required');
}
if (type !== 'email' && type !== 'phone') {
throw new BadRequestException('Type must be either "email" or "phone"');
}
return this.authService.sendVerificationOtp(emailOrPhone, type);
}
@Public()
@Post('verify-account')
async verifyAccount(
@Body('emailOrPhone') emailOrPhone: string,
@Body('otp') otp: string,
@Body('type') type: 'email' | 'phone',
) {
if (!emailOrPhone || !otp || !type) {
throw new BadRequestException('Email/Phone, OTP, and type are required');
}
if (type !== 'email' && type !== 'phone') {
throw new BadRequestException('Type must be either "email" or "phone"');
}
return this.authService.verifyAccount(emailOrPhone, otp, type);
}
@Public()
@Post('auto-verify-phone')
async autoVerifyPhone(
@Body('phone') phone: string,
@Body('otp') otp: string,
) {
if (!phone || !otp) {
throw new BadRequestException('Phone number and OTP are required');
}
return this.authService.autoVerifyPhone(phone, otp);
}
}

View File

@@ -8,11 +8,15 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guard/jwt.guard';
import { RolesGuard } from './guard/role.guard';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Otp } from './entities/otp.entity';
import { SmsService } from './sms.service';
@Module({
imports: [
UserModule,
PassportModule,
TypeOrmModule.forFeature([Otp]),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
@@ -25,6 +29,7 @@ import { RolesGuard } from './guard/role.guard';
controllers: [AuthController],
providers: [
AuthService,
SmsService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,

View File

@@ -1,21 +1,43 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
UnauthorizedException,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
import { ConfigService } from '@nestjs/config';
import { User } from 'src/user/entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, MoreThan } from 'typeorm';
import { Otp } from './entities/otp.entity';
import { SmsService } from './sms.service';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
@InjectRepository(Otp)
private readonly otpRepository: Repository<Otp>,
private readonly smsService: SmsService,
) {}
async validateUser(emailOrPhone: string, password: string) {
const user =
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
if (user && (await bcrypt.compare(password, user.password))) {
// Check if user account is verified (only for USER role)
if (user.role === 'USER') {
if (!user.phone_verified) {
throw new UnauthorizedException('Please verify your phone number.');
}
// if (!user.email_verified) {
// throw new UnauthorizedException('Please verify your email address.');
// }
}
const { password, ...result } = user;
return result;
}
@@ -40,4 +62,262 @@ export class AuthService {
async getUserProfile(userId: string): Promise<User> {
return this.userService.findOneById(userId);
}
async generateOtp(phone: string): Promise<{ message: string }> {
// Check if user exists and their role
const user = await this.userService.findByPhone(phone);
// If user is admin, reject OTP request
if (
user &&
(user.role === 'SUPER_ADMIN' ||
user.role === 'EDITOR' ||
user.role === 'REVIEWER' ||
user.role === 'VIEWER')
) {
throw new BadRequestException(
'Admin users should use regular login, not OTP',
);
}
// If user exists and has an account, they should verify first or use password login
if (user && user.role === 'USER') {
if (!user.phone_verified) {
throw new BadRequestException(
'Please verify your phone number first.',
);
}
}
// Deactivate any existing active LOGIN OTPs for this phone number
await this.otpRepository.update(
{ phone_number: phone, purpose: 'LOGIN', isActive: true },
{ isActive: false },
);
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 5);
const otp = this.otpRepository.create({
phone_number: phone,
otp_code: otpCode,
expires_at: expiresAt,
is_verified: false,
isActive: true,
purpose: 'LOGIN',
user: user ?? undefined, // Link to user if exists
});
await this.otpRepository.save(otp);
// Send SMS using the SMS service
const smsSent = await this.smsService.sendOtp(phone, otpCode);
if (!smsSent) {
// If SMS fails, still log to console as fallback for development
console.log(`SMS failed, OTP for ${phone}: ${otpCode}`);
return { message: 'OTP generated but SMS delivery may have failed' };
}
return { message: 'OTP sent successfully' };
}
async verifyOtp(
phone: string,
otpCode: string,
): Promise<{ message: string; token?: string }> {
const otp = await this.otpRepository.findOne({
where: {
phone_number: phone,
otp_code: otpCode,
purpose: 'LOGIN',
isActive: true,
is_verified: false,
expires_at: MoreThan(new Date()),
},
relations: ['user'],
});
if (!otp) {
throw new BadRequestException('Invalid or expired OTP');
}
await this.otpRepository.update(otp.id, {
is_verified: true,
isActive: false,
});
// If linked to a real user, log them in with their actual data
// Otherwise, create a viewer session
let user;
if (otp.user) {
user = {
id: otp.user.id,
name: otp.user.name,
phone_number: otp.user.phone_number,
role: otp.user.role,
};
} else {
user = { id: null, name: null, phone_number: phone, role: 'VIEWER' };
}
const token = await this.login(user);
return { message: 'OTP verified successfully', token };
}
async sendVerificationOtp(
emailOrPhone: string,
type: 'email' | 'phone',
): Promise<{ message: string }> {
const user =
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
if (!user) {
throw new BadRequestException('User not found');
}
if (type === 'phone' && user.phone_verified) {
throw new BadRequestException('Phone number is already verified');
}
if (type === 'email' && user.email_verified) {
throw new BadRequestException('Email is already verified');
}
const purpose =
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
const contact = type === 'email' ? user.email : user.phone_number;
// Deactivate any existing verification OTPs
await this.otpRepository.update(
{ phone_number: contact, purpose, isActive: true },
{ isActive: false },
);
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 10); // 10 minutes for verification
const otp = this.otpRepository.create({
phone_number: contact,
otp_code: otpCode,
expires_at: expiresAt,
is_verified: false,
isActive: true,
purpose,
user,
});
await this.otpRepository.save(otp);
if (type === 'phone') {
const smsSent = await this.smsService.sendOtp(contact, otpCode);
if (!smsSent) {
console.log(`SMS failed, Verification OTP for ${contact}: ${otpCode}`);
return {
message:
'Verification OTP generated but SMS delivery may have failed',
};
}
} else {
// For email, log to console (you can implement email service later)
console.log(`Email verification OTP for ${contact}: ${otpCode}`);
}
return { message: `Verification OTP sent to your ${type}` };
}
async verifyAccount(
emailOrPhone: string,
otpCode: string,
type: 'email' | 'phone',
): Promise<{ message: string }> {
const purpose =
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
const otp = await this.otpRepository.findOne({
where: {
phone_number: emailOrPhone,
otp_code: otpCode,
purpose,
isActive: true,
is_verified: false,
expires_at: MoreThan(new Date()),
},
relations: ['user'],
});
if (!otp || !otp.user) {
throw new BadRequestException('Invalid or expired verification OTP');
}
// Mark OTP as used
await this.otpRepository.update(otp.id, {
is_verified: true,
isActive: false,
});
// Update user verification status
const updateData =
type === 'email' ? { email_verified: true } : { phone_verified: true };
await this.userService.updateUser(otp.user.id, updateData);
return {
message: `${type === 'email' ? 'Email' : 'Phone number'} verified successfully`,
};
}
async autoVerifyPhone(phone: string, otpCode: string): Promise<{ message: string; user?: any }> {
const user = await this.userService.findByPhone(phone);
if (!user) {
throw new NotFoundException('No account found with this phone number');
}
if (user.phone_verified) {
throw new BadRequestException('Phone number already verified');
}
// Find the OTP
const otp = await this.otpRepository.findOne({
where: {
phone_number: phone,
otp_code: otpCode,
purpose: 'PHONE_VERIFICATION',
isActive: true,
is_verified: false,
expires_at: MoreThan(new Date()),
},
relations: ['user'],
});
if (!otp) {
throw new BadRequestException('Invalid or expired OTP');
}
// Mark OTP as used
await this.otpRepository.update(otp.id, {
is_verified: true,
isActive: false,
});
// Update user phone verification status
await this.userService.updateUser(user.id, { phone_verified: true });
return {
message: 'Phone number verified successfully',
user: {
id: user.id,
name: user.name,
phone_number: user.phone_number,
email: user.email,
role: user.role,
phone_verified: true,
email_verified: user.email_verified,
}
};
}
}

View File

@@ -0,0 +1,28 @@
import { BaseEntity } from 'src/common/types/base-entity';
import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm';
import { User } from 'src/user/entities/user.entity';
@Entity('otps')
export class Otp extends BaseEntity {
@Column()
phone_number: string;
@Column()
otp_code: string;
@Column({ type: 'timestamp' })
expires_at: Date;
@Column({ default: false })
is_verified: boolean;
@Column({ default: true })
isActive: boolean;
@Column({ default: 'LOGIN' })
purpose: string; // 'LOGIN', 'EMAIL_VERIFICATION', 'PHONE_VERIFICATION'
@ManyToOne(() => User, (user) => user.otps, { nullable: true })
@JoinColumn({ name: 'user_id' })
user: User;
}

39
src/auth/sms.service.ts Normal file
View File

@@ -0,0 +1,39 @@
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class SmsService {
private readonly logger = new Logger(SmsService.name);
private readonly SMS_API_URL = 'http://bhashsms.com/api/sendmsg.php';
private readonly SMS_USER = 'paisaads';
private readonly SMS_PASS = '123456';
private readonly SMS_SENDER = 'PSADDS';
async sendOtp(phoneNumber: string, otp: string): Promise<boolean> {
try {
const message = `Your One Time Password (OTP) for login is ${otp}. Please do not share the OTP with anyone. Best Regards, www.PaisaAds.com - contact@PaisaAds.com`;
const params = {
user: this.SMS_USER,
pass: this.SMS_PASS,
sender: this.SMS_SENDER,
phone: phoneNumber,
text: message,
priority: 'ndnd',
stype: 'normal',
};
this.logger.log(`Sending OTP to ${phoneNumber}`);
const response = await axios.get(this.SMS_API_URL, { params });
this.logger.log(`SMS API Response: ${response.data}`);
// Consider successful if status is 200
return response.status === 200;
} catch (error) {
this.logger.error(`Failed to send SMS to ${phoneNumber}:`, error.message);
return false;
}
}
}

View File

@@ -13,6 +13,8 @@ export class CategoryThree extends BaseEntity {
@Column({ default: true })
isActive: boolean;
@ManyToOne(() => CategoryTwo, (catTwo) => catTwo.subCategories, { onDelete: 'CASCADE' })
@ManyToOne(() => CategoryTwo, (catTwo) => catTwo.subCategories, {
onDelete: 'CASCADE',
})
parent: CategoryTwo;
}
}

View File

@@ -1,5 +1,19 @@
import { Body, Controller, Get, Post, Param, Patch, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import {
Body,
Controller,
Get,
Post,
Param,
Patch,
Query,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { ConfigurationsService } from './configurations.service';
import { TermsAndConditionsDto } from './dto/terms-and-conditions';
import { AdPricingDto } from './dto/ad-pricing.dto';
@@ -21,7 +35,10 @@ export class ConfigurationsController {
@Post('terms-and-conditions')
@Roles(Role.SUPER_ADMIN)
@ApiOperation({ summary: 'Create or update terms and conditions' })
@ApiResponse({ status: 201, description: 'Terms and conditions updated successfully' })
@ApiResponse({
status: 201,
description: 'Terms and conditions updated successfully',
})
async createTermsAndConditions(
@Body() termsAndConditionsDto: TermsAndConditionsDto,
) {
@@ -33,7 +50,10 @@ export class ConfigurationsController {
@Get('terms-and-conditions')
@Public()
@ApiOperation({ summary: 'Get current terms and conditions' })
@ApiResponse({ status: 200, description: 'Terms and conditions retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Terms and conditions retrieved successfully',
})
async getTermsAndConditions() {
return this.configurationsService.getTermsAndConditions();
}
@@ -51,7 +71,10 @@ export class ConfigurationsController {
@Get('ad-pricing')
@Public()
@ApiOperation({ summary: 'Get current ad pricing' })
@ApiResponse({ status: 200, description: 'Ad pricing retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Ad pricing retrieved successfully',
})
async getAdPricing() {
return this.configurationsService.getAdPricing();
}
@@ -59,7 +82,10 @@ export class ConfigurationsController {
@Get('ad-pricing/history')
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Get ad pricing history' })
@ApiResponse({ status: 200, description: 'Ad pricing history retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Ad pricing history retrieved successfully',
})
async getAdPricingHistory() {
return this.configurationsService.getAdPricingHistory();
}
@@ -69,15 +95,25 @@ export class ConfigurationsController {
@Post('privacy-policy')
@Roles(Role.SUPER_ADMIN)
@ApiOperation({ summary: 'Create or update privacy policy' })
@ApiResponse({ status: 201, description: 'Privacy policy updated successfully' })
async createOrUpdatePrivacyPolicy(@Body() privacyPolicyDto: PrivacyPolicyDto) {
return this.configurationsService.createOrUpdatePrivacyPolicy(privacyPolicyDto);
@ApiResponse({
status: 201,
description: 'Privacy policy updated successfully',
})
async createOrUpdatePrivacyPolicy(
@Body() privacyPolicyDto: PrivacyPolicyDto,
) {
return this.configurationsService.createOrUpdatePrivacyPolicy(
privacyPolicyDto,
);
}
@Get('privacy-policy')
@Public()
@ApiOperation({ summary: 'Get current privacy policy' })
@ApiResponse({ status: 200, description: 'Privacy policy retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Privacy policy retrieved successfully',
})
async getPrivacyPolicy() {
return this.configurationsService.getPrivacyPolicy();
}
@@ -85,7 +121,10 @@ export class ConfigurationsController {
@Get('privacy-policy/history')
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Get privacy policy history' })
@ApiResponse({ status: 200, description: 'Privacy policy history retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Privacy policy history retrieved successfully',
})
async getPrivacyPolicyHistory() {
return this.configurationsService.getPrivacyPolicyHistory();
}
@@ -95,15 +134,23 @@ export class ConfigurationsController {
@Post('search-slogan')
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Create or update search page slogan' })
@ApiResponse({ status: 201, description: 'Search slogan updated successfully' })
@ApiResponse({
status: 201,
description: 'Search slogan updated successfully',
})
async createOrUpdateSearchSlogan(@Body() searchSloganDto: SearchSloganDto) {
return this.configurationsService.createOrUpdateSearchSlogan(searchSloganDto);
return this.configurationsService.createOrUpdateSearchSlogan(
searchSloganDto,
);
}
@Get('search-slogan')
@Public()
@ApiOperation({ summary: 'Get current search page slogan' })
@ApiResponse({ status: 200, description: 'Search slogan retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Search slogan retrieved successfully',
})
async getSearchSlogan() {
return this.configurationsService.getSearchSlogan();
}
@@ -113,7 +160,10 @@ export class ConfigurationsController {
@Post('about-us')
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Create or update about us page' })
@ApiResponse({ status: 201, description: 'About us page updated successfully' })
@ApiResponse({
status: 201,
description: 'About us page updated successfully',
})
async createOrUpdateAboutUs(@Body() aboutUsDto: AboutUsDto) {
return this.configurationsService.createOrUpdateAboutUs(aboutUsDto);
}
@@ -121,7 +171,10 @@ export class ConfigurationsController {
@Get('about-us')
@Public()
@ApiOperation({ summary: 'Get current about us page content' })
@ApiResponse({ status: 200, description: 'About us content retrieved successfully' })
@ApiResponse({
status: 200,
description: 'About us content retrieved successfully',
})
async getAboutUs() {
return this.configurationsService.getAboutUs();
}
@@ -148,7 +201,10 @@ export class ConfigurationsController {
@Public()
@ApiOperation({ summary: 'Get FAQ by category' })
@ApiParam({ name: 'category', description: 'FAQ category' })
@ApiResponse({ status: 200, description: 'FAQ category retrieved successfully' })
@ApiResponse({
status: 200,
description: 'FAQ category retrieved successfully',
})
async getFaqByCategory(@Param('category') category: string) {
return this.configurationsService.getFaqByCategory(category);
}
@@ -158,12 +214,12 @@ export class ConfigurationsController {
@ApiOperation({ summary: 'Add a new FAQ question' })
@ApiResponse({ status: 201, description: 'FAQ question added successfully' })
async addFaqQuestion(
@Body() body: { question: string; answer: string; category: string }
@Body() body: { question: string; answer: string; category: string },
) {
return this.configurationsService.addFaqQuestion(
body.question,
body.answer,
body.category
body.category,
);
}
@@ -171,20 +227,24 @@ export class ConfigurationsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Update an existing FAQ question' })
@ApiParam({ name: 'index', description: 'Question index' })
@ApiResponse({ status: 200, description: 'FAQ question updated successfully' })
@ApiResponse({
status: 200,
description: 'FAQ question updated successfully',
})
async updateFaqQuestion(
@Param('index') index: string,
@Body() updatedQuestion: Partial<{
@Body()
updatedQuestion: Partial<{
question: string;
answer: string;
category: string;
order: number;
isActive: boolean;
}>
}>,
) {
return this.configurationsService.updateFaqQuestion(
parseInt(index),
updatedQuestion
updatedQuestion,
);
}
//#endregion
@@ -193,7 +253,10 @@ export class ConfigurationsController {
@Post('contact-page')
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
@ApiOperation({ summary: 'Create or update contact page' })
@ApiResponse({ status: 201, description: 'Contact page updated successfully' })
@ApiResponse({
status: 201,
description: 'Contact page updated successfully',
})
async createOrUpdateContactPage(@Body() contactPageDto: ContactPageDto) {
return this.configurationsService.createOrUpdateContactPage(contactPageDto);
}
@@ -201,7 +264,10 @@ export class ConfigurationsController {
@Get('contact-page')
@Public()
@ApiOperation({ summary: 'Get contact page information' })
@ApiResponse({ status: 200, description: 'Contact page retrieved successfully' })
@ApiResponse({
status: 200,
description: 'Contact page retrieved successfully',
})
async getContactPage() {
return this.configurationsService.getContactPage();
}
@@ -211,7 +277,10 @@ export class ConfigurationsController {
@Get('all')
@Public()
@ApiOperation({ summary: 'Get all configurations' })
@ApiResponse({ status: 200, description: 'All configurations retrieved successfully' })
@ApiResponse({
status: 200,
description: 'All configurations retrieved successfully',
})
async getAllConfigurations() {
return this.configurationsService.getAllConfigurations();
}

View File

@@ -7,8 +7,14 @@ import {
TermsAndConditionsSchema,
} from './schemas/terms-and-conditions.schema';
import { AdPricing, AdPricingSchema } from './schemas/ad-pricing.schema';
import { PrivacyPolicy, PrivacyPolicySchema } from './schemas/privacy-policy.schema';
import { SearchSlogan, SearchSloganSchema } from './schemas/search-slogan.schema';
import {
PrivacyPolicy,
PrivacyPolicySchema,
} from './schemas/privacy-policy.schema';
import {
SearchSlogan,
SearchSloganSchema,
} from './schemas/search-slogan.schema';
import { AboutUs, AboutUsSchema } from './schemas/about-us.schema';
import { Faq, FaqSchema } from './schemas/faq.schema';
import { ContactPage, ContactPageSchema } from './schemas/contact-page.schema';

View File

@@ -79,7 +79,9 @@ export class ConfigurationsService {
}
async getAdPricingHistory() {
const pricingHistory = await this.adPricingModel.find().sort({ lastUpdated: -1 });
const pricingHistory = await this.adPricingModel
.find()
.sort({ lastUpdated: -1 });
return pricingHistory;
}
//#endregion
@@ -102,12 +104,16 @@ export class ConfigurationsService {
}
async getPrivacyPolicy() {
const privacyPolicy = await this.privacyPolicyModel.findOne({ isActive: true });
const privacyPolicy = await this.privacyPolicyModel.findOne({
isActive: true,
});
return privacyPolicy;
}
async getPrivacyPolicyHistory() {
const policyHistory = await this.privacyPolicyModel.find().sort({ lastUpdated: -1 });
const policyHistory = await this.privacyPolicyModel
.find()
.sort({ lastUpdated: -1 });
return policyHistory;
}
//#endregion
@@ -130,7 +136,9 @@ export class ConfigurationsService {
}
async getSearchSlogan() {
const searchSlogan = await this.searchSloganModel.findOne({ isActive: true });
const searchSlogan = await this.searchSloganModel.findOne({
isActive: true,
});
return searchSlogan;
}
//#endregion
@@ -185,7 +193,7 @@ export class ConfigurationsService {
if (!faq) return null;
const filteredQuestions = faq.questions.filter(
(q) => q.category === category && q.isActive
(q) => q.category === category && q.isActive,
);
return {
@@ -200,8 +208,8 @@ export class ConfigurationsService {
throw new Error('FAQ document not found. Please create FAQ first.');
}
const maxOrder = Math.max(...faq.questions.map(q => q.order), 0);
const maxOrder = Math.max(...faq.questions.map((q) => q.order), 0);
faq.questions.push({
question,
answer,
@@ -215,13 +223,16 @@ export class ConfigurationsService {
return faq;
}
async updateFaqQuestion(questionIndex: number, updatedQuestion: Partial<{
question: string;
answer: string;
category: string;
order: number;
isActive: boolean;
}>) {
async updateFaqQuestion(
questionIndex: number,
updatedQuestion: Partial<{
question: string;
answer: string;
category: string;
order: number;
isActive: boolean;
}>,
) {
const faq = await this.faqModel.findOne({ isActive: true });
if (!faq || !faq.questions[questionIndex]) {
throw new Error('FAQ or question not found');

View File

@@ -1,5 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
import {
IsString,
IsOptional,
IsBoolean,
IsArray,
IsNumber,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
class TeamMemberDto {
@@ -26,7 +33,9 @@ export class AboutUsDto {
@IsString()
title: string;
@ApiProperty({ description: 'Main about us content in HTML or markdown format' })
@ApiProperty({
description: 'Main about us content in HTML or markdown format',
})
@IsString()
content: string;
@@ -40,7 +49,11 @@ export class AboutUsDto {
@IsString()
vision?: string;
@ApiProperty({ description: 'Company values', type: [String], required: false })
@ApiProperty({
description: 'Company values',
type: [String],
required: false,
})
@IsOptional()
@IsArray()
@IsString({ each: true })
@@ -61,7 +74,11 @@ export class AboutUsDto {
@IsString()
companyDescription?: string;
@ApiProperty({ description: 'Team members', type: [TeamMemberDto], required: false })
@ApiProperty({
description: 'Team members',
type: [TeamMemberDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -73,8 +90,12 @@ export class AboutUsDto {
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether content is active', default: true, required: false })
@ApiProperty({
description: 'Whether content is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -1,5 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString, IsOptional, IsBoolean, Min } from 'class-validator';
import {
IsNumber,
IsString,
IsOptional,
IsBoolean,
Min,
} from 'class-validator';
export class AdPricingDto {
@ApiProperty({ description: 'Price for line ads', example: 100, minimum: 0 })
@@ -7,7 +13,11 @@ export class AdPricingDto {
@Min(0)
lineAdPrice: number;
@ApiProperty({ description: 'Price for poster ads', example: 200, minimum: 0 })
@ApiProperty({
description: 'Price for poster ads',
example: 200,
minimum: 0,
})
@IsNumber()
@Min(0)
posterAdPrice: number;
@@ -17,7 +27,11 @@ export class AdPricingDto {
@Min(0)
videoAdPrice: number;
@ApiProperty({ description: 'Currency code', example: 'INR', required: false })
@ApiProperty({
description: 'Currency code',
example: 'INR',
required: false,
})
@IsOptional()
@IsString()
currency?: string;
@@ -27,8 +41,12 @@ export class AdPricingDto {
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether pricing is active', default: true, required: false })
@ApiProperty({
description: 'Whether pricing is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -1,5 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
import {
IsString,
IsOptional,
IsBoolean,
IsArray,
IsNumber,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
class CoordinatesDto {
@@ -47,11 +54,17 @@ export class ContactPageDto {
@IsString()
companyName: string;
@ApiProperty({ description: 'Primary email address', example: 'contact@paisaads.com' })
@ApiProperty({
description: 'Primary email address',
example: 'contact@paisaads.com',
})
@IsString()
email: string;
@ApiProperty({ description: 'Primary phone number', example: '+91 9876543210' })
@ApiProperty({
description: 'Primary phone number',
example: '+91 9876543210',
})
@IsString()
phone: string;
@@ -84,19 +97,31 @@ export class ContactPageDto {
@IsString()
country?: string;
@ApiProperty({ description: 'GPS coordinates', type: CoordinatesDto, required: false })
@ApiProperty({
description: 'GPS coordinates',
type: CoordinatesDto,
required: false,
})
@IsOptional()
@ValidateNested()
@Type(() => CoordinatesDto)
coordinates?: CoordinatesDto;
@ApiProperty({ description: 'Social media links', type: [String], required: false })
@ApiProperty({
description: 'Social media links',
type: [String],
required: false,
})
@IsOptional()
@IsArray()
@IsString({ each: true })
socialMediaLinks?: string[];
@ApiProperty({ description: 'Business hours', type: BusinessHoursDto, required: false })
@ApiProperty({
description: 'Business hours',
type: BusinessHoursDto,
required: false,
})
@IsOptional()
@ValidateNested()
@Type(() => BusinessHoursDto)
@@ -122,13 +147,20 @@ export class ContactPageDto {
@IsString()
websiteUrl?: string;
@ApiProperty({ description: 'User who updated the contact info', required: false })
@ApiProperty({
description: 'User who updated the contact info',
required: false,
})
@IsOptional()
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether contact info is active', default: true, required: false })
@ApiProperty({
description: 'Whether contact info is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -1,5 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
import {
IsString,
IsOptional,
IsBoolean,
IsArray,
IsNumber,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
class FaqQuestionDto {
@@ -19,24 +26,31 @@ class FaqQuestionDto {
@IsNumber()
order: number;
@ApiProperty({ description: 'Whether question is active', default: true, required: false })
@ApiProperty({
description: 'Whether question is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export class FaqDto {
@ApiProperty({ description: 'FAQ questions and answers', type: [FaqQuestionDto] })
@ApiProperty({
description: 'FAQ questions and answers',
type: [FaqQuestionDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => FaqQuestionDto)
questions: FaqQuestionDto[];
@ApiProperty({
description: 'FAQ categories',
type: [String],
@ApiProperty({
description: 'FAQ categories',
type: [String],
example: ['General', 'Account', 'Payments', 'Ads', 'Technical'],
required: false
required: false,
})
@IsOptional()
@IsArray()
@@ -48,7 +62,10 @@ export class FaqDto {
@IsString()
introduction?: string;
@ApiProperty({ description: 'Contact information for additional help', required: false })
@ApiProperty({
description: 'Contact information for additional help',
required: false,
})
@IsOptional()
@IsString()
contactInfo?: string;
@@ -58,8 +75,12 @@ export class FaqDto {
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether FAQ is active', default: true, required: false })
@ApiProperty({
description: 'Whether FAQ is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -2,7 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean, IsDateString } from 'class-validator';
export class PrivacyPolicyDto {
@ApiProperty({ description: 'Privacy policy content in HTML or markdown format' })
@ApiProperty({
description: 'Privacy policy content in HTML or markdown format',
})
@IsString()
content: string;
@@ -20,8 +22,12 @@ export class PrivacyPolicyDto {
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether policy is active', default: true, required: false })
@ApiProperty({
description: 'Whether policy is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -2,21 +2,36 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean } from 'class-validator';
export class SearchSloganDto {
@ApiProperty({ description: 'Main slogan text', example: 'Find Your Perfect Deal Today!' })
@ApiProperty({
description: 'Main slogan text',
example: 'Find Your Perfect Deal Today!',
})
@IsString()
mainSlogan: string;
@ApiProperty({ description: 'Sub slogan text', required: false, example: 'Discover thousands of listings in your area' })
@ApiProperty({
description: 'Sub slogan text',
required: false,
example: 'Discover thousands of listings in your area',
})
@IsOptional()
@IsString()
subSlogan?: string;
@ApiProperty({ description: 'Show slogan on search page', default: true, required: false })
@ApiProperty({
description: 'Show slogan on search page',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
showOnSearchPage?: boolean;
@ApiProperty({ description: 'Show slogan on home page', default: true, required: false })
@ApiProperty({
description: 'Show slogan on home page',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
showOnHomePage?: boolean;
@@ -26,8 +41,12 @@ export class SearchSloganDto {
@IsString()
updatedBy?: string;
@ApiProperty({ description: 'Whether slogan is active', default: true, required: false })
@ApiProperty({
description: 'Whether slogan is active',
default: true,
required: false,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
}

View File

@@ -29,7 +29,9 @@ export class AboutUs {
@Prop()
companyDescription: string;
@Prop({ type: [{ name: String, position: String, bio: String, image: String }] })
@Prop({
type: [{ name: String, position: String, bio: String, image: String }],
})
teamMembers: Array<{
name: string;
position: string;
@@ -47,4 +49,4 @@ export class AboutUs {
isActive: boolean;
}
export const AboutUsSchema = SchemaFactory.createForClass(AboutUs);
export const AboutUsSchema = SchemaFactory.createForClass(AboutUs);

View File

@@ -27,4 +27,4 @@ export class AdPricing {
isActive: boolean;
}
export const AdPricingSchema = SchemaFactory.createForClass(AdPricing);
export const AdPricingSchema = SchemaFactory.createForClass(AdPricing);

View File

@@ -41,7 +41,17 @@ export class ContactPage {
@Prop({ type: [String] })
socialMediaLinks: string[];
@Prop({ type: { monday: String, tuesday: String, wednesday: String, thursday: String, friday: String, saturday: String, sunday: String } })
@Prop({
type: {
monday: String,
tuesday: String,
wednesday: String,
thursday: String,
friday: String,
saturday: String,
sunday: String,
},
})
businessHours: {
monday: string;
tuesday: string;
@@ -74,4 +84,4 @@ export class ContactPage {
isActive: boolean;
}
export const ContactPageSchema = SchemaFactory.createForClass(ContactPage);
export const ContactPageSchema = SchemaFactory.createForClass(ContactPage);

View File

@@ -5,7 +5,18 @@ export type FaqDocument = Faq & Document;
@Schema({ timestamps: true })
export class Faq {
@Prop({ type: [{ question: String, answer: String, category: String, order: Number, isActive: { type: Boolean, default: true } }], required: true })
@Prop({
type: [
{
question: String,
answer: String,
category: String,
order: Number,
isActive: { type: Boolean, default: true },
},
],
required: true,
})
questions: Array<{
question: string;
answer: string;
@@ -14,7 +25,10 @@ export class Faq {
isActive: boolean;
}>;
@Prop({ type: [String], default: ['General', 'Account', 'Payments', 'Ads', 'Technical'] })
@Prop({
type: [String],
default: ['General', 'Account', 'Payments', 'Ads', 'Technical'],
})
categories: string[];
@Prop()
@@ -33,4 +47,4 @@ export class Faq {
isActive: boolean;
}
export const FaqSchema = SchemaFactory.createForClass(Faq);
export const FaqSchema = SchemaFactory.createForClass(Faq);

View File

@@ -24,4 +24,4 @@ export class PrivacyPolicy {
isActive: boolean;
}
export const PrivacyPolicySchema = SchemaFactory.createForClass(PrivacyPolicy);
export const PrivacyPolicySchema = SchemaFactory.createForClass(PrivacyPolicy);

View File

@@ -27,4 +27,4 @@ export class SearchSlogan {
isActive: boolean;
}
export const SearchSloganSchema = SchemaFactory.createForClass(SearchSlogan);
export const SearchSloganSchema = SchemaFactory.createForClass(SearchSlogan);

View File

@@ -12,4 +12,5 @@ export class TermsAndConditions {
timestamp: Date;
}
export const TermsAndConditionsSchema = SchemaFactory.createForClass(TermsAndConditions);
export const TermsAndConditionsSchema =
SchemaFactory.createForClass(TermsAndConditions);

View File

@@ -32,7 +32,9 @@ export default class LineAdSeeder implements Seeder {
// Check if line ads already exist
const existingLineAds = await lineAdRepo.count();
if (existingLineAds > 0) {
console.log(`${existingLineAds} line ads already exist. Skipping line ad seeder.`);
console.log(
`${existingLineAds} line ads already exist. Skipping line ad seeder.`,
);
return;
}
@@ -66,7 +68,9 @@ export default class LineAdSeeder implements Seeder {
const createdCategories: MainCategory[] = [];
for (const cat of categories) {
let mainCategory = await mainCategoryRepo.findOne({ where: { name: cat.name } });
let mainCategory = await mainCategoryRepo.findOne({
where: { name: cat.name },
});
if (!mainCategory) {
mainCategory = mainCategoryRepo.create({
name: cat.name,
@@ -81,27 +85,113 @@ export default class LineAdSeeder implements Seeder {
// Create subcategories
const subcategories = {
Electronics: ['Mobile Phones', 'Laptops', 'TV & Audio', 'Cameras', 'Gaming', 'Accessories'],
Fashion: ['Men\'s Clothing', 'Women\'s Clothing', 'Accessories', 'Footwear', 'Watches', 'Bags'],
'Real Estate': ['Residential', 'Commercial', 'Land', 'Rental', 'PG/Hostel', 'Office Space'],
Automotive: ['Cars', 'Bikes', 'Auto Parts', 'Services', 'Commercial Vehicles', 'Accessories'],
Entertainment: ['Movies', 'Music', 'Gaming', 'Events', 'Books', 'Tickets'],
'Health & Beauty': ['Healthcare', 'Beauty Products', 'Fitness', 'Wellness', 'Medical Equipment', 'Supplements'],
'Food & Beverages': ['Restaurants', 'Cafes', 'Groceries', 'Catering', 'Food Delivery', 'Beverages'],
Services: ['Home Services', 'Professional Services', 'Repair Services', 'Consulting', 'Legal Services', 'Financial Services'],
Jobs: ['IT Jobs', 'Sales Jobs', 'Marketing Jobs', 'Healthcare Jobs', 'Education Jobs', 'Part Time Jobs'],
Education: ['Tuitions', 'Coaching Classes', 'Online Courses', 'Schools', 'Colleges', 'Skill Development'],
'Home & Garden': ['Furniture', 'Home Decor', 'Appliances', 'Garden Tools', 'Lighting', 'Kitchen Items'],
'Sports & Fitness': ['Gym Equipment', 'Sports Goods', 'Outdoor Sports', 'Fitness Classes', 'Sports Coaching', 'Yoga Classes'],
Electronics: [
'Mobile Phones',
'Laptops',
'TV & Audio',
'Cameras',
'Gaming',
'Accessories',
],
Fashion: [
"Men's Clothing",
"Women's Clothing",
'Accessories',
'Footwear',
'Watches',
'Bags',
],
'Real Estate': [
'Residential',
'Commercial',
'Land',
'Rental',
'PG/Hostel',
'Office Space',
],
Automotive: [
'Cars',
'Bikes',
'Auto Parts',
'Services',
'Commercial Vehicles',
'Accessories',
],
Entertainment: [
'Movies',
'Music',
'Gaming',
'Events',
'Books',
'Tickets',
],
'Health & Beauty': [
'Healthcare',
'Beauty Products',
'Fitness',
'Wellness',
'Medical Equipment',
'Supplements',
],
'Food & Beverages': [
'Restaurants',
'Cafes',
'Groceries',
'Catering',
'Food Delivery',
'Beverages',
],
Services: [
'Home Services',
'Professional Services',
'Repair Services',
'Consulting',
'Legal Services',
'Financial Services',
],
Jobs: [
'IT Jobs',
'Sales Jobs',
'Marketing Jobs',
'Healthcare Jobs',
'Education Jobs',
'Part Time Jobs',
],
Education: [
'Tuitions',
'Coaching Classes',
'Online Courses',
'Schools',
'Colleges',
'Skill Development',
],
'Home & Garden': [
'Furniture',
'Home Decor',
'Appliances',
'Garden Tools',
'Lighting',
'Kitchen Items',
],
'Sports & Fitness': [
'Gym Equipment',
'Sports Goods',
'Outdoor Sports',
'Fitness Classes',
'Sports Coaching',
'Yoga Classes',
],
};
const categoryOneMap = new Map();
for (const [mainCatName, subCats] of Object.entries(subcategories)) {
const mainCategory = createdCategories.find(c => c.name === mainCatName);
const mainCategory = createdCategories.find(
(c) => c.name === mainCatName,
);
if (mainCategory) {
for (const subCatName of subCats) {
let categoryOne = await categoryOneRepo.findOne({
where: { name: subCatName, parent: { id: mainCategory.id } }
let categoryOne = await categoryOneRepo.findOne({
where: { name: subCatName, parent: { id: mainCategory.id } },
});
if (!categoryOne) {
categoryOne = categoryOneRepo.create({
@@ -120,7 +210,7 @@ export default class LineAdSeeder implements Seeder {
const imageFiles = [
'1746748722095.png',
'1747127356626.jpg',
'1747127917692.png',
'1747127917692.png',
'1747127958198.jpg',
'1747127984078.jpg',
'1747128020849.png',
@@ -165,7 +255,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Real Estate',
categoryOne: 'Residential',
content: '3BHK flat for sale in Bandra West. Prime location, sea view, fully furnished. Ready to move. 1200 sq ft. Parking available.',
content:
'3BHK flat for sale in Bandra West. Prime location, sea view, fully furnished. Ready to move. 1200 sq ft. Parking available.',
contactOne: 9876543210,
contactTwo: 8765432109,
state: 'Maharashtra',
@@ -185,7 +276,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Real Estate',
categoryOne: 'Rental',
content: '2BHK apartment for rent in Koramangala. Well ventilated, near metro station. Rs 25,000/month. Family preferred.',
content:
'2BHK apartment for rent in Koramangala. Well ventilated, near metro station. Rs 25,000/month. Family preferred.',
contactOne: 9988776655,
contactTwo: undefined,
state: 'Karnataka',
@@ -207,7 +299,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Jobs',
categoryOne: 'IT Jobs',
content: 'Software Developer required. React, Node.js, 2+ years exp. Salary: 8-12 LPA. Work from home available. Immediate joining.',
content:
'Software Developer required. React, Node.js, 2+ years exp. Salary: 8-12 LPA. Work from home available. Immediate joining.',
contactOne: 7766554433,
contactTwo: 9988112233,
state: 'Delhi',
@@ -227,7 +320,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Jobs',
categoryOne: 'Sales Jobs',
content: 'Sales Executive needed for leading FMCG company. Field sales, Delhi NCR. Good communication skills. Attractive incentives.',
content:
'Sales Executive needed for leading FMCG company. Field sales, Delhi NCR. Good communication skills. Attractive incentives.',
contactOne: 8877665544,
contactTwo: undefined,
state: 'Haryana',
@@ -249,7 +343,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Automotive',
categoryOne: 'Cars',
content: 'Maruti Swift 2019 for sale. Single owner, well maintained. 45,000 km driven. All documents ready. Price negotiable.',
content:
'Maruti Swift 2019 for sale. Single owner, well maintained. 45,000 km driven. All documents ready. Price negotiable.',
contactOne: 9876543211,
contactTwo: 8765432110,
state: 'Tamil Nadu',
@@ -269,7 +364,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Automotive',
categoryOne: 'Bikes',
content: 'Honda Activa 5G, 2020 model. Excellent condition, serviced regularly. Single owner. Price: Rs 65,000 negotiable.',
content:
'Honda Activa 5G, 2020 model. Excellent condition, serviced regularly. Single owner. Price: Rs 65,000 negotiable.',
contactOne: 9876543212,
contactTwo: undefined,
state: 'West Bengal',
@@ -291,7 +387,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Services',
categoryOne: 'Home Services',
content: 'Professional AC repair & servicing. All brands. 24/7 service. Experienced technicians. Warranty provided. Call now!',
content:
'Professional AC repair & servicing. All brands. 24/7 service. Experienced technicians. Warranty provided. Call now!',
contactOne: 7788996655,
contactTwo: 9988776644,
state: 'Gujarat',
@@ -311,7 +408,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Services',
categoryOne: 'Professional Services',
content: 'CA Services - Income Tax, GST, Company Registration, Audit. 15+ years experience. Reasonable rates. Free consultation.',
content:
'CA Services - Income Tax, GST, Company Registration, Audit. 15+ years experience. Reasonable rates. Free consultation.',
contactOne: 9876543213,
contactTwo: undefined,
state: 'Rajasthan',
@@ -333,7 +431,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Electronics',
categoryOne: 'Mobile Phones',
content: 'iPhone 13 128GB for sale. 11 months old, excellent condition. Bill & box available. No scratches. Price: Rs 55,000.',
content:
'iPhone 13 128GB for sale. 11 months old, excellent condition. Bill & box available. No scratches. Price: Rs 55,000.',
contactOne: 8877665511,
contactTwo: 7766554422,
state: 'Punjab',
@@ -353,7 +452,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Electronics',
categoryOne: 'Laptops',
content: 'Dell Inspiron 15 - Intel i5, 8GB RAM, 512GB SSD. Perfect for work & study. 2 years warranty remaining.',
content:
'Dell Inspiron 15 - Intel i5, 8GB RAM, 512GB SSD. Perfect for work & study. 2 years warranty remaining.',
contactOne: 9988776633,
contactTwo: undefined,
state: 'Telangana',
@@ -374,8 +474,9 @@ export default class LineAdSeeder implements Seeder {
// Fashion - mix of with/without images
{
mainCategory: 'Fashion',
categoryOne: 'Women\'s Clothing',
content: 'Designer sarees collection. Wedding & party wear. Premium quality silk. Direct from manufacturer. Wholesale rates available.',
categoryOne: "Women's Clothing",
content:
'Designer sarees collection. Wedding & party wear. Premium quality silk. Direct from manufacturer. Wholesale rates available.',
contactOne: 7766554411,
contactTwo: 8877665522,
state: 'Uttar Pradesh',
@@ -394,8 +495,9 @@ export default class LineAdSeeder implements Seeder {
},
{
mainCategory: 'Fashion',
categoryOne: 'Men\'s Clothing',
content: 'Branded shirts & trousers. Formal & casual wear. All sizes available. Best quality at reasonable prices. Home delivery.',
categoryOne: "Men's Clothing",
content:
'Branded shirts & trousers. Formal & casual wear. All sizes available. Best quality at reasonable prices. Home delivery.',
contactOne: 9876543214,
contactTwo: undefined,
state: 'Kerala',
@@ -417,7 +519,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Education',
categoryOne: 'Tuitions',
content: 'Mathematics & Physics tuition for 11th & 12th. IIT JEE preparation. 10+ years experience. Online & offline classes.',
content:
'Mathematics & Physics tuition for 11th & 12th. IIT JEE preparation. 10+ years experience. Online & offline classes.',
contactOne: 8877665533,
contactTwo: 7766554444,
state: 'Madhya Pradesh',
@@ -437,7 +540,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Education',
categoryOne: 'Coaching Classes',
content: 'UPSC Civil Services coaching. Experienced faculty, study material provided. Demo class available. Limited seats.',
content:
'UPSC Civil Services coaching. Experienced faculty, study material provided. Demo class available. Limited seats.',
contactOne: 9876543215,
contactTwo: undefined,
state: 'Delhi',
@@ -459,7 +563,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Health & Beauty',
categoryOne: 'Beauty Products',
content: 'Skincare products - Natural & organic. Anti-aging creams, face wash, moisturizers. All skin types. Chemical-free.',
content:
'Skincare products - Natural & organic. Anti-aging creams, face wash, moisturizers. All skin types. Chemical-free.',
contactOne: 7788996644,
contactTwo: 9988776655,
state: 'Goa',
@@ -479,7 +584,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Health & Beauty',
categoryOne: 'Fitness',
content: 'Personal trainer available. Weight loss, muscle building, yoga. Home visits. Certified trainer with 5+ years experience.',
content:
'Personal trainer available. Weight loss, muscle building, yoga. Home visits. Certified trainer with 5+ years experience.',
contactOne: 8877665544,
contactTwo: undefined,
state: 'Odisha',
@@ -501,7 +607,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Home & Garden',
categoryOne: 'Furniture',
content: 'Wooden dining table set for sale. 6 seater, excellent condition. Teak wood, carved design. Must sell urgently.',
content:
'Wooden dining table set for sale. 6 seater, excellent condition. Teak wood, carved design. Must sell urgently.',
contactOne: 9876543216,
contactTwo: 8765432111,
state: 'Assam',
@@ -521,7 +628,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Home & Garden',
categoryOne: 'Appliances',
content: 'Samsung refrigerator 265L, double door. 3 years old, working perfectly. Moving out sale. Price negotiable.',
content:
'Samsung refrigerator 265L, double door. 3 years old, working perfectly. Moving out sale. Price negotiable.',
contactOne: 7766554433,
contactTwo: undefined,
state: 'Jharkhand',
@@ -543,7 +651,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Sports & Fitness',
categoryOne: 'Gym Equipment',
content: 'Home gym setup for sale. Dumbbells, bench, barbell set. Excellent condition. Complete package deal available.',
content:
'Home gym setup for sale. Dumbbells, bench, barbell set. Excellent condition. Complete package deal available.',
contactOne: 8877665555,
contactTwo: 7766554466,
state: 'Bihar',
@@ -563,7 +672,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Sports & Fitness',
categoryOne: 'Sports Coaching',
content: 'Cricket coaching for kids & adults. Professional coach, group & individual sessions. Ground available. Weekend batches.',
content:
'Cricket coaching for kids & adults. Professional coach, group & individual sessions. Ground available. Weekend batches.',
contactOne: 9876543217,
contactTwo: undefined,
state: 'Uttarakhand',
@@ -585,7 +695,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Food & Beverages',
categoryOne: 'Restaurants',
content: 'New restaurant opening! Best North Indian cuisine. Family restaurant, home delivery available. Grand opening discounts.',
content:
'New restaurant opening! Best North Indian cuisine. Family restaurant, home delivery available. Grand opening discounts.',
contactOne: 7788996666,
contactTwo: 8877665577,
state: 'Himachal Pradesh',
@@ -605,7 +716,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Food & Beverages',
categoryOne: 'Catering',
content: 'Wedding catering services. Multi-cuisine menu, experienced chefs. Packages for 50-500 guests. Free tasting session.',
content:
'Wedding catering services. Multi-cuisine menu, experienced chefs. Packages for 50-500 guests. Free tasting session.',
contactOne: 9876543218,
contactTwo: undefined,
state: 'Chhattisgarh',
@@ -627,7 +739,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Entertainment',
categoryOne: 'Events',
content: 'DJ services for parties & weddings. Latest sound system, lighting available. Experienced DJ, all types of music.',
content:
'DJ services for parties & weddings. Latest sound system, lighting available. Experienced DJ, all types of music.',
contactOne: 8877665588,
contactTwo: 7766554499,
state: 'Tripura',
@@ -647,7 +760,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Entertainment',
categoryOne: 'Music',
content: 'Guitar classes - Acoustic & electric. Beginner to advanced levels. Music theory included. Group & individual lessons.',
content:
'Guitar classes - Acoustic & electric. Beginner to advanced levels. Music theory included. Group & individual lessons.',
contactOne: 9876543219,
contactTwo: undefined,
state: 'Nagaland',
@@ -669,7 +783,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Services',
categoryOne: 'Legal Services',
content: 'Advocate services - Civil, criminal, family matters. High court practice. Consultation available. Reasonable fees.',
content:
'Advocate services - Civil, criminal, family matters. High court practice. Consultation available. Reasonable fees.',
contactOne: 7788996677,
contactTwo: undefined,
state: 'Manipur',
@@ -689,7 +804,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Electronics',
categoryOne: 'Gaming',
content: 'PS5 console for sale with 5 games. 1 year old, warranty remaining. Extra controller included. Genuine buyer contact.',
content:
'PS5 console for sale with 5 games. 1 year old, warranty remaining. Extra controller included. Genuine buyer contact.',
contactOne: 8877665599,
contactTwo: 9988776688,
state: 'Arunachal Pradesh',
@@ -709,7 +825,8 @@ export default class LineAdSeeder implements Seeder {
{
mainCategory: 'Jobs',
categoryOne: 'Part Time Jobs',
content: 'Data entry work from home. Flexible timing, weekly payment. Computer & internet required. Students welcome.',
content:
'Data entry work from home. Flexible timing, weekly payment. Computer & internet required. Students welcome.',
contactOne: 9876543220,
contactTwo: undefined,
state: 'Mizoram',
@@ -730,8 +847,12 @@ export default class LineAdSeeder implements Seeder {
// Create line ads
for (const adData of lineAdsData) {
const mainCategory = createdCategories.find(c => c.name === adData.mainCategory);
const categoryOne = categoryOneMap.get(`${adData.mainCategory}-${adData.categoryOne}`);
const mainCategory = createdCategories.find(
(c) => c.name === adData.mainCategory,
);
const categoryOne = categoryOneMap.get(
`${adData.mainCategory}-${adData.categoryOne}`,
);
// Create images if the ad has them
const adImages: Image[] = [];
@@ -804,10 +925,13 @@ export default class LineAdSeeder implements Seeder {
await lineAdRepo.save(lineAd);
const imageCount = adImages.length;
const imageText = imageCount > 0 ? `with ${imageCount} image(s)` : 'without images';
console.log(`Created line ad for ${adData.postedBy} in ${adData.city} (${imageText})`);
const imageText =
imageCount > 0 ? `with ${imageCount} image(s)` : 'without images';
console.log(
`Created line ad for ${adData.postedBy} in ${adData.city} (${imageText})`,
);
}
console.log('Line Ad seeder completed successfully!');
}
}
}

View File

@@ -32,7 +32,9 @@ export default class PosterAdSeeder implements Seeder {
// Check if poster ads already exist
const existingPosterAds = await posterAdRepo.count();
if (existingPosterAds > 0) {
console.log(`${existingPosterAds} poster ads already exist. Skipping poster ad seeder.`);
console.log(
`${existingPosterAds} poster ads already exist. Skipping poster ad seeder.`,
);
return;
}
@@ -61,7 +63,9 @@ export default class PosterAdSeeder implements Seeder {
const createdCategories: MainCategory[] = [];
for (const cat of categories) {
let mainCategory = await mainCategoryRepo.findOne({ where: { name: cat.name } });
let mainCategory = await mainCategoryRepo.findOne({
where: { name: cat.name },
});
if (!mainCategory) {
mainCategory = mainCategoryRepo.create({
name: cat.name,
@@ -77,21 +81,33 @@ export default class PosterAdSeeder implements Seeder {
// Create subcategories
const subcategories = {
Electronics: ['Mobile Phones', 'Laptops', 'TV & Audio', 'Cameras'],
Fashion: ['Men\'s Clothing', 'Women\'s Clothing', 'Accessories', 'Footwear'],
Fashion: [
"Men's Clothing",
"Women's Clothing",
'Accessories',
'Footwear',
],
'Real Estate': ['Residential', 'Commercial', 'Land', 'Rental'],
Automotive: ['Cars', 'Bikes', 'Auto Parts', 'Services'],
Entertainment: ['Movies', 'Music', 'Gaming', 'Events'],
'Health & Beauty': ['Healthcare', 'Beauty Products', 'Fitness', 'Wellness'],
'Health & Beauty': [
'Healthcare',
'Beauty Products',
'Fitness',
'Wellness',
],
'Food & Beverages': ['Restaurants', 'Cafes', 'Groceries', 'Catering'],
};
const categoryOneMap = new Map();
for (const [mainCatName, subCats] of Object.entries(subcategories)) {
const mainCategory = createdCategories.find(c => c.name === mainCatName);
const mainCategory = createdCategories.find(
(c) => c.name === mainCatName,
);
if (mainCategory) {
for (const subCatName of subCats) {
let categoryOne = await categoryOneRepo.findOne({
where: { name: subCatName, parent: { id: mainCategory.id } }
let categoryOne = await categoryOneRepo.findOne({
where: { name: subCatName, parent: { id: mainCategory.id } },
});
if (!categoryOne) {
categoryOne = categoryOneRepo.create({
@@ -110,7 +126,7 @@ export default class PosterAdSeeder implements Seeder {
const imageFiles = [
'1746748722095.png',
'1747127356626.jpg',
'1747127917692.png',
'1747127917692.png',
'1747127958198.jpg',
'1747127984078.jpg',
'1747128020849.png',
@@ -172,7 +188,7 @@ export default class PosterAdSeeder implements Seeder {
},
{
mainCategory: 'Fashion',
categoryOne: 'Women\'s Clothing',
categoryOne: "Women's Clothing",
imageFile: imageFiles[1],
proofImage: proofImages[1],
state: 'Karnataka',
@@ -398,8 +414,12 @@ export default class PosterAdSeeder implements Seeder {
// Create poster ads
for (const adData of posterAdsData) {
const mainCategory = createdCategories.find(c => c.name === adData.mainCategory);
const categoryOne = categoryOneMap.get(`${adData.mainCategory}-${adData.categoryOne}`);
const mainCategory = createdCategories.find(
(c) => c.name === adData.mainCategory,
);
const categoryOne = categoryOneMap.get(
`${adData.mainCategory}-${adData.categoryOne}`,
);
// Create poster image
const posterImage = imageRepo.create({
@@ -464,9 +484,11 @@ export default class PosterAdSeeder implements Seeder {
});
await posterAdRepo.save(posterAd);
console.log(`Created poster ad for ${adData.postedBy} in ${adData.city} (${adData.side}-${adData.position})`);
console.log(
`Created poster ad for ${adData.postedBy} in ${adData.city} (${adData.side}-${adData.position})`,
);
}
console.log('Poster Ad seeder completed successfully!');
}
}
}

View File

@@ -32,7 +32,9 @@ export default class VideoAdSeeder implements Seeder {
// Check if video ads already exist
const existingVideoAds = await videoAdRepo.count();
if (existingVideoAds > 0) {
console.log(`${existingVideoAds} video ads already exist. Skipping video ad seeder.`);
console.log(
`${existingVideoAds} video ads already exist. Skipping video ad seeder.`,
);
return;
}
@@ -41,13 +43,12 @@ export default class VideoAdSeeder implements Seeder {
relations: ['user'],
take: 1,
});
if (!customers || customers.length === 0) {
throw new Error('No customer found. Please run user seeder first.');
}
const customer = customers[0];
const customer = customers[0];
// Create main categories for video ads
const categories = [

View File

@@ -66,6 +66,14 @@ export class CreateLineAdDto {
@IsOptional()
contactTwo?: number;
@IsString()
@IsOptional()
backgroundColor?: string;
@IsString()
@IsOptional()
textColor?: string;
@IsArray()
dates: string[]; // e.g., ['2024-05-03','2024-05-04']

View File

@@ -24,27 +24,27 @@ import { AdPosition } from 'src/ad-position/entities/ad-position.entity';
export class LineAd extends BaseEntity {
@Generated('increment')
@Column()
sequenceNumber: number;
sequenceNumber: number;
@Column({ nullable: true }) // Order ID can be derived from sequenceNumber + date etc in future
orderId: number;
orderId: number;
// --- Category Relations ---
@ManyToOne(() => MainCategory, { eager: true, nullable: false })
mainCategory: MainCategory;
mainCategory: MainCategory;
@ManyToOne(() => CategoryOne, { eager: true, nullable: true })
categoryOne?: CategoryOne;
categoryOne?: CategoryOne;
@ManyToOne(() => CategoryTwo, { eager: true, nullable: true })
categoryTwo?: CategoryTwo;
categoryTwo?: CategoryTwo;
@ManyToOne(() => CategoryThree, { eager: true, nullable: true })
categoryThree?: CategoryThree;
categoryThree?: CategoryThree;
// --- Ad Content ---
@Column()
content: string;
content: string;
@OneToMany(() => Image, (image) => image.lineAd, {
cascade: true,
@@ -52,31 +52,37 @@ export class LineAd extends BaseEntity {
nullable: true,
onDelete: 'CASCADE',
})
images: Image[];
images: Image[];
@Column()
state: string;
state: string;
@Column({ nullable: true })
sid: number;
sid: number;
@Column()
city: string;
city: string;
@Column({ nullable: true })
cid: number;
cid: number;
@Column('simple-array')
dates: string[];
dates: string[];
@Column()
postedBy: string;
postedBy: string;
@Column({ type: 'bigint' })
contactOne: number;
contactOne: number;
@Column({ nullable: true, type: 'bigint' })
contactTwo: number;
contactTwo: number;
@Column({ nullable: true })
backgroundColor: string;
@Column({ nullable: true })
textColor: string;
// --- Payment ---
@OneToOne(() => Payment, (payment) => payment.lineAd, {
@@ -84,7 +90,7 @@ export class LineAd extends BaseEntity {
nullable: true,
})
@JoinColumn()
payment: Payment;
payment: Payment;
// --- Ad Workflow Status & Metadata ---
@Column({
@@ -92,22 +98,22 @@ export class LineAd extends BaseEntity {
enum: AdStatus,
default: AdStatus.DRAFT,
})
status: AdStatus;
status: AdStatus;
@OneToMany(() => AdComment, (comment) => comment.lineAd, {})
comments: AdComment[];
comments: AdComment[];
// --- User/Admin Info ---
@ManyToOne(() => Customer, (customer) => customer.lineAds, {
nullable: false,
})
@JoinColumn() // optional, helps clarity
@JoinColumn() // optional, helps clarity
customer: Customer;
// Soft delete
@Column({ default: true })
isActive: boolean;
isActive: boolean;
@OneToOne(() => AdPosition, (position) => position.lineAd)
@JoinColumn()

View File

@@ -120,15 +120,18 @@ export class LineAdService {
cityId?: number;
stateId?: number;
}) {
// Get today's date in local timezone
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
const todayISOString = today.getFullYear() + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0');
const where: FindOptionsWhere<LineAd> = {
status: AdStatus.PUBLISHED,
isActive: true,
};
if (categoryId) {
console.log(categoryId);
where.mainCategory = { id: categoryId };
}
if (cityId) {
@@ -137,37 +140,59 @@ export class LineAdService {
if (stateId) {
where.sid = stateId;
}
// Fetch all published and active ads
const lineAds = await this.lineAdRepo.find({
where,
relations: ['customer', 'customer.user', 'images', 'position'],
relations: ['customer', 'customer.user', 'images', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
});
// Filter in JS for today's date in the dates array
const filteredAds = lineAds.filter(
(ad) =>
Array.isArray(ad.dates) &&
ad.dates.some((dateStr) => {
const dateOnly = dateStr.split('T')[0];
return dateOnly === todayISOString;
}),
);
// Filter ads that should be displayed today
const filteredAds = lineAds.filter((ad) => {
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
return false;
}
return ad.dates.some((dateStr) => {
try {
// Handle both ISO string and date-only formats
const adDate = new Date(dateStr);
const adDateString = adDate.getFullYear() + '-' +
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
String(adDate.getDate()).padStart(2, '0');
return adDateString === todayISOString;
} catch (error) {
console.error('Error parsing date:', dateStr, error);
return false;
}
});
});
// Return cleaned up ad data
return filteredAds.map((ad) => {
const {
payment,
customer,
cid,
sid,
sequenceNumber,
orderId,
status,
comments,
dates,
isActive,
cid,
sid,
...serializedAd
} = ad;
return serializedAd;
return {
...serializedAd,
// Include essential customer info without sensitive data
customerName: customer?.user?.name || null,
// Include location for display
cityId: cid,
stateId: sid,
};
});
}

View File

@@ -28,7 +28,7 @@ async function bootstrap() {
});
} else {
app.enableCors({
origin: ['https://paisaads.in','http://paisaads.in/'],
origin: ['https://paisaads.in', 'http://paisaads.in/'],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true,
allowedHeaders: 'Content-Type,Authorization',

View File

@@ -170,15 +170,18 @@ export class PosterAdService {
cityId?: number;
stateId?: number;
}) {
// Get today's date in local timezone
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
const todayISOString = today.getFullYear() + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0');
const where: FindOptionsWhere<PosterAd> = {
status: AdStatus.PUBLISHED,
isActive: true,
};
if (categoryId) {
console.log(categoryId);
where.mainCategory = { id: categoryId };
}
if (cityId) {
@@ -187,38 +190,59 @@ export class PosterAdService {
if (stateId) {
where.sid = stateId;
}
// Fetch all published and active ads
const lineAds = await this.posterAdRepo.find({
// Fetch all published and active ads with all necessary relations
const posterAds = await this.posterAdRepo.find({
where,
relations: ['position'],
relations: ['customer', 'customer.user', 'image', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
});
// Filter in JS for today's date in the dates array
const filteredAds = lineAds.filter(
(ad) =>
Array.isArray(ad.dates) &&
ad.dates.some((dateStr) => {
const dateOnly = dateStr.split('T')[0];
return dateOnly === todayISOString;
}),
// ad,
);
// Filter ads that should be displayed today
const filteredAds = posterAds.filter((ad) => {
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
return false;
}
return ad.dates.some((dateStr) => {
try {
// Handle both ISO string and date-only formats
const adDate = new Date(dateStr);
const adDateString = adDate.getFullYear() + '-' +
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
String(adDate.getDate()).padStart(2, '0');
return adDateString === todayISOString;
} catch (error) {
console.error('Error parsing date:', dateStr, error);
return false;
}
});
});
// Return cleaned up ad data
return filteredAds.map((ad) => {
const {
payment,
customer,
cid,
sid,
sequenceNumber,
orderId,
status,
comments,
dates,
isActive,
cid,
sid,
...serializedAd
} = ad;
return serializedAd;
return {
...serializedAd,
// Include essential customer info without sensitive data
customerName: customer?.user?.name || null,
// Include location for display
cityId: cid,
stateId: sid,
};
});
}
@@ -259,7 +283,7 @@ export class PosterAdService {
// For CENTER_TOP and CENTER_BOTTOM sides, set position to 0 (will be treated as null)
const newPosition =
typeof position === 'object' && position !== null
? (position as any).value
? position.value
: position;
const positionValue =
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
@@ -504,7 +528,9 @@ export class PosterAdService {
if (filters?.userType) {
query.andWhere('ad.postedBy = :userType', { userType: filters.userType });
} else if (filters?.userTypes && filters.userTypes.length > 0) {
query.andWhere('ad.postedBy IN (:...userTypes)', { userTypes: filters.userTypes });
query.andWhere('ad.postedBy IN (:...userTypes)', {
userTypes: filters.userTypes,
});
}
// Location filters
@@ -523,16 +549,24 @@ export class PosterAdService {
// Category filters
if (filters?.mainCategoryId) {
query.andWhere('mainCategory.id = :mainCategoryId', { mainCategoryId: filters.mainCategoryId });
query.andWhere('mainCategory.id = :mainCategoryId', {
mainCategoryId: filters.mainCategoryId,
});
}
if (filters?.categoryOneId) {
query.andWhere('categoryOne.id = :categoryOneId', { categoryOneId: filters.categoryOneId });
query.andWhere('categoryOne.id = :categoryOneId', {
categoryOneId: filters.categoryOneId,
});
}
if (filters?.categoryTwoId) {
query.andWhere('categoryTwo.id = :categoryTwoId', { categoryTwoId: filters.categoryTwoId });
query.andWhere('categoryTwo.id = :categoryTwoId', {
categoryTwoId: filters.categoryTwoId,
});
}
if (filters?.categoryThreeId) {
query.andWhere('categoryThree.id = :categoryThreeId', { categoryThreeId: filters.categoryThreeId });
query.andWhere('categoryThree.id = :categoryThreeId', {
categoryThreeId: filters.categoryThreeId,
});
}
if (filters?.categoryId) {
query.andWhere(
@@ -543,7 +577,9 @@ export class PosterAdService {
// Customer filter
if (filters?.customerId) {
query.andWhere('customer.id = :customerId', { customerId: filters.customerId });
query.andWhere('customer.id = :customerId', {
customerId: filters.customerId,
});
}
// Apply sorting

View File

@@ -1,4 +1,11 @@
import { IsOptional, IsEnum, IsString, IsDateString, IsNumber, IsArray } from 'class-validator';
import {
IsOptional,
IsEnum,
IsString,
IsDateString,
IsNumber,
IsArray,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { AdStatus } from '../../common/enums/ad-status.enum';
@@ -49,38 +56,47 @@ export class AdvancedFilterDto {
originalValue: value,
valueType: typeof value,
valueIsArray: Array.isArray(value),
valueConstructor: value?.constructor?.name
valueConstructor: value?.constructor?.name,
});
// Handle string values (comma-separated)
if (typeof value === 'string') {
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
const result = value
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0);
console.log('DEBUG DTO Transform - adTypes result:', result);
return result;
}
// Handle array values
if (Array.isArray(value)) {
return value;
}
// Handle null/undefined
if (value == null) {
return undefined;
}
// Handle unexpected types - convert to array or return undefined
console.warn('DEBUG DTO Transform - adTypes unexpected type, converting:', value);
console.warn(
'DEBUG DTO Transform - adTypes unexpected type, converting:',
value,
);
if (typeof value === 'object' && value.length !== undefined) {
// Try to convert array-like objects to actual arrays
try {
return Array.from(value);
} catch (error) {
console.error('DEBUG DTO Transform - adTypes failed to convert array-like object:', error);
console.error(
'DEBUG DTO Transform - adTypes failed to convert array-like object:',
error,
);
return undefined;
}
}
// Single value - wrap in array
return [value];
})
@@ -109,38 +125,47 @@ export class AdvancedFilterDto {
originalValue: value,
valueType: typeof value,
valueIsArray: Array.isArray(value),
valueConstructor: value?.constructor?.name
valueConstructor: value?.constructor?.name,
});
// Handle string values (comma-separated)
if (typeof value === 'string') {
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
const result = value
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0);
console.log('DEBUG DTO Transform - userTypes result:', result);
return result;
}
// Handle array values
if (Array.isArray(value)) {
return value;
}
// Handle null/undefined
if (value == null) {
return undefined;
}
// Handle unexpected types - convert to array or return undefined
console.warn('DEBUG DTO Transform - userTypes unexpected type, converting:', value);
console.warn(
'DEBUG DTO Transform - userTypes unexpected type, converting:',
value,
);
if (typeof value === 'object' && value.length !== undefined) {
// Try to convert array-like objects to actual arrays
try {
return Array.from(value);
} catch (error) {
console.error('DEBUG DTO Transform - userTypes failed to convert array-like object:', error);
console.error(
'DEBUG DTO Transform - userTypes failed to convert array-like object:',
error,
);
return undefined;
}
}
// Single value - wrap in array
return [value];
})
@@ -172,38 +197,47 @@ export class AdvancedFilterDto {
originalValue: value,
valueType: typeof value,
valueIsArray: Array.isArray(value),
valueConstructor: value?.constructor?.name
valueConstructor: value?.constructor?.name,
});
// Handle string values (comma-separated)
if (typeof value === 'string') {
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
const result = value
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0);
console.log('DEBUG DTO Transform - statuses result:', result);
return result;
}
// Handle array values
if (Array.isArray(value)) {
return value;
}
// Handle null/undefined
if (value == null) {
return undefined;
}
// Handle unexpected types - convert to array or return undefined
console.warn('DEBUG DTO Transform - statuses unexpected type, converting:', value);
console.warn(
'DEBUG DTO Transform - statuses unexpected type, converting:',
value,
);
if (typeof value === 'object' && value.length !== undefined) {
// Try to convert array-like objects to actual arrays
try {
return Array.from(value);
} catch (error) {
console.error('DEBUG DTO Transform - statuses failed to convert array-like object:', error);
console.error(
'DEBUG DTO Transform - statuses failed to convert array-like object:',
error,
);
return undefined;
}
}
// Single value - wrap in array
return [value];
})
@@ -284,7 +318,8 @@ export class AdvancedFilterDto {
categoryThreeId?: string;
@ApiProperty({
description: 'Filter by any category ID (searches across all category levels)',
description:
'Filter by any category ID (searches across all category levels)',
example: 'uuid-any-category',
required: false,
})
@@ -400,4 +435,4 @@ export class AdvancedFilterResponse<T> {
this.hasNext = page < this.totalPages;
this.hasPrevious = page > 1;
}
}
}

View File

@@ -16,7 +16,10 @@ import { AdStatus } from 'src/common/enums/ad-status.enum';
import { Roles } from 'src/auth/decorator/roles.decorator';
import { Role } from 'src/common/enums/role.enum';
import { AdType } from 'src/common/enums/ad-type';
import { AdvancedFilterDto, AdvancedFilterResponse } from './dto/advanced-filter.dto';
import {
AdvancedFilterDto,
AdvancedFilterResponse,
} from './dto/advanced-filter.dto';
@ApiTags('Reports')
@Controller('reports')
@@ -68,12 +71,13 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get filtered ads with comprehensive filtering options',
description: 'Retrieve ads based on multiple filter criteria including date range, ad type, user type, status, location, and categories'
description:
'Retrieve ads based on multiple filter criteria including date range, ad type, user type, status, location, and categories',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved filtered ads',
type: AdvancedFilterResponse
type: AdvancedFilterResponse,
})
async getFilteredAds(@Query() filters: AdvancedFilterDto) {
return this.reportsService.getFilteredAds(filters);
@@ -83,11 +87,12 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get statistics for filtered ads',
description: 'Get comprehensive statistics including breakdowns by status, type, location, category, and user type'
description:
'Get comprehensive statistics including breakdowns by status, type, location, category, and user type',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved filtered ad statistics'
description: 'Successfully retrieved filtered ad statistics',
})
async getFilteredAdStats(@Query() filters: AdvancedFilterDto) {
return this.reportsService.getFilteredAdStats(filters);
@@ -97,11 +102,12 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get categories for dropdown population',
description: 'Retrieve all categories (main, subcategories) for use in filter dropdowns'
description:
'Retrieve all categories (main, subcategories) for use in filter dropdowns',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved categories'
description: 'Successfully retrieved categories',
})
async getCategoriesForFilters() {
return this.reportsService.getCategoriesForFilters();
@@ -111,11 +117,12 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get available user types for filtering',
description: 'Retrieve all unique user types (posted by) values for filter dropdown'
description:
'Retrieve all unique user types (posted by) values for filter dropdown',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved user types'
description: 'Successfully retrieved user types',
})
async getUserTypesForFilters() {
return this.reportsService.getUserTypesForFilters();
@@ -125,11 +132,11 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get locations for filtering',
description: 'Retrieve all unique states and cities for location filters'
description: 'Retrieve all unique states and cities for location filters',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved locations'
description: 'Successfully retrieved locations',
})
async getLocationsForFilters() {
return this.reportsService.getLocationsForFilters();
@@ -139,16 +146,16 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Export filtered ads data',
description: 'Export ads data based on filters to CSV format'
description: 'Export ads data based on filters to CSV format',
})
@ApiQuery({ name: 'format', enum: ['csv', 'excel'], required: false })
@ApiResponse({
status: 200,
description: 'Successfully exported filtered ads data'
description: 'Successfully exported filtered ads data',
})
async exportFilteredAds(
@Query() filters: AdvancedFilterDto,
@Query('format') format: 'csv' | 'excel' = 'csv'
@Query('format') format: 'csv' | 'excel' = 'csv',
) {
return this.reportsService.exportFilteredAds(filters, format);
}
@@ -161,20 +168,25 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get user registration reports',
description: 'Get user registration statistics with date range and period grouping'
description:
'Get user registration statistics with date range and period grouping',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
@ApiQuery({
name: 'period',
enum: ['daily', 'weekly', 'monthly'],
required: false,
})
async getUserRegistrationReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate),
period
period,
};
return this.reportsService.getUserRegistrationReport(dateRange);
}
@@ -183,7 +195,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get active vs inactive users',
description: 'Get breakdown of active vs inactive users with percentages'
description: 'Get breakdown of active vs inactive users with percentages',
})
async getActiveVsInactiveUsersReport() {
return this.reportsService.getActiveVsInactiveUsersReport();
@@ -193,20 +205,24 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get user login activity report',
description: 'Get user activity patterns based on profile updates'
description: 'Get user activity patterns based on profile updates',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
@ApiQuery({
name: 'period',
enum: ['daily', 'weekly', 'monthly'],
required: false,
})
async getUserLoginActivityReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate),
period
period,
};
return this.reportsService.getUserLoginActivityReport(dateRange);
}
@@ -215,7 +231,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get user views per listing category',
description: 'Get estimated user engagement by category'
description: 'Get estimated user engagement by category',
})
async getUserViewsByCategoryReport() {
return this.reportsService.getUserViewsByCategoryReport();
@@ -227,22 +243,27 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get admin activity report',
description: 'Get comprehensive admin activity breakdown with approvals, rejections, etc.'
description:
'Get comprehensive admin activity breakdown with approvals, rejections, etc.',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
@ApiQuery({
name: 'period',
enum: ['daily', 'weekly', 'monthly'],
required: false,
})
@ApiQuery({ name: 'adminId', required: false, type: String })
async getAdminActivityReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
@Query('adminId') adminId?: string
@Query('adminId') adminId?: string,
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate),
period
period,
};
return this.reportsService.getAdminActivityReport(dateRange, adminId);
}
@@ -251,17 +272,17 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get admin user-wise activity report',
description: 'Get activity breakdown for each admin user'
description: 'Get activity breakdown for each admin user',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
async getAdminUserWiseActivityReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string
@Query('endDate') endDate: string,
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate)
endDate: new Date(endDate),
};
return this.reportsService.getAdminUserWiseActivityReport(dateRange);
}
@@ -270,17 +291,17 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get admin activity by category',
description: 'Get admin actions breakdown by category'
description: 'Get admin actions breakdown by category',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
async getAdminActivityByCategoryReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string
@Query('endDate') endDate: string,
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate)
endDate: new Date(endDate),
};
return this.reportsService.getAdminActivityByCategoryReport(dateRange);
}
@@ -291,18 +312,22 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get comprehensive listing analytics',
description: 'Get detailed listing performance analytics including images, categories, approval times'
description:
'Get detailed listing performance analytics including images, categories, approval times',
})
@ApiQuery({ name: 'startDate', required: false, type: String })
@ApiQuery({ name: 'endDate', required: false, type: String })
async getListingAnalyticsReport(
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string
@Query('endDate') endDate?: string,
) {
const dateRange = startDate && endDate ? {
startDate: new Date(startDate),
endDate: new Date(endDate)
} : undefined;
const dateRange =
startDate && endDate
? {
startDate: new Date(startDate),
endDate: new Date(endDate),
}
: undefined;
return this.reportsService.getListingAnalyticsReport(dateRange);
}
@@ -310,7 +335,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get active listings count by category',
description: 'Get count of active published listings grouped by category'
description: 'Get count of active published listings grouped by category',
})
async getActiveListingsByCategory() {
return this.reportsService.getActiveListingsByCategory();
@@ -320,7 +345,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get approval time analytics',
description: 'Get detailed approval time statistics for all ads'
description: 'Get detailed approval time statistics for all ads',
})
async getApprovalTimeReport() {
return this.reportsService.getApprovalTimeReport();
@@ -330,7 +355,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get listings by user report',
description: 'Get listing count and details grouped by user who posted'
description: 'Get listing count and details grouped by user who posted',
})
async getListingsByUserReport() {
return this.reportsService.getListingsByUserReport();
@@ -342,20 +367,24 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get payment transaction report',
description: 'Get comprehensive payment and revenue analytics'
description: 'Get comprehensive payment and revenue analytics',
})
@ApiQuery({ name: 'startDate', required: true, type: String })
@ApiQuery({ name: 'endDate', required: true, type: String })
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
@ApiQuery({
name: 'period',
enum: ['daily', 'weekly', 'monthly'],
required: false,
})
async getPaymentTransactionReport(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
) {
const dateRange: ReportDateRange = {
startDate: new Date(startDate),
endDate: new Date(endDate),
period
period,
};
return this.reportsService.getPaymentTransactionReport(dateRange);
}
@@ -364,7 +393,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get revenue by product type',
description: 'Get revenue breakdown by Line Ads, Poster Ads, and Video Ads'
description: 'Get revenue breakdown by Line Ads, Poster Ads, and Video Ads',
})
async getRevenueByProduct() {
return this.reportsService.getRevenueByProduct();
@@ -374,7 +403,7 @@ export class ReportsController {
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
@ApiOperation({
summary: 'Get revenue by category',
description: 'Get revenue breakdown by ad categories'
description: 'Get revenue breakdown by ad categories',
})
async getRevenueByCategoryReport() {
return this.reportsService.getRevenueByCategoryReport();

View File

@@ -18,19 +18,19 @@ import { MainCategory } from 'src/category/entities/main-category.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
LineAd,
PosterAd,
VideoAd,
User,
Customer,
Admin,
Payment,
AdComment,
MainCategory
LineAd,
PosterAd,
VideoAd,
User,
Customer,
Admin,
Payment,
AdComment,
MainCategory,
]),
LineAdModule,
PosterAdModule,
VideoAdModule
VideoAdModule,
],
controllers: [ReportsController],
providers: [ReportsService],

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,22 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { IsNotEmpty, IsEmail, IsEnum, IsOptional } from 'class-validator';
import { IsNotEmpty, IsEmail, IsEnum, IsOptional, IsBoolean } from 'class-validator';
import { Role } from 'src/common/enums/role.enum';
export class UpdateUserDto {
@IsOptional()
@IsNotEmpty()
name: string;
name?: string;
@IsEmail()
@IsOptional()
email: string;
@IsEmail()
email?: string;
@IsOptional()
@IsBoolean()
email_verified?: boolean;
@IsOptional()
@IsBoolean()
phone_verified?: boolean;
}

View File

@@ -1,8 +1,9 @@
import { Role } from 'src/common/enums/role.enum';
import { BaseEntity } from 'src/common/types/base-entity';
import { Column, Entity, JoinColumn, OneToOne } from 'typeorm';
import { Column, Entity, JoinColumn, OneToOne, OneToMany } from 'typeorm';
import { Admin } from './admin.entity';
import { Customer } from './customer.entity';
import { Otp } from 'src/auth/entities/otp.entity';
@Entity('users')
export class User extends BaseEntity {
@@ -27,9 +28,18 @@ export class User extends BaseEntity {
@Column({ default: true })
isActive: boolean;
@Column({ default: true })
email_verified: boolean;
@Column({ default: false })
phone_verified: boolean;
@OneToOne(() => Admin, (admin) => admin.user, { nullable: true })
admin: Admin;
@OneToOne(() => Customer, (customer) => customer.user, { nullable: true })
customer: Customer;
@OneToMany(() => Otp, (otp) => otp.user, {nullable:true})
otps: Otp[];
}

View File

@@ -3,6 +3,8 @@ import {
ConflictException,
NotFoundException,
BadRequestException,
Inject,
forwardRef,
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@@ -54,6 +56,10 @@ export class UserService {
// Optionally create admin/customer profile if specified by role
if (user.role === Role.USER) {
await this.customerRepo.save(this.customerRepo.create({ user: newUser }));
// Send verification instructions (don't send actual OTPs during registration)
console.log(`User registered: ${newUser.email} / ${newUser.phone_number}`);
console.log('Please use /auth/send-verification-otp to verify your account');
} else {
await this.adminRepo.save(this.adminRepo.create({ user: newUser }));
}
@@ -101,6 +107,7 @@ export class UserService {
proof,
});
await manager.save(Customer, customer);
});
}

View File

@@ -100,4 +100,4 @@ export class VideoAd extends BaseEntity {
@OneToOne(() => AdPosition, (position) => position.videoAd)
@JoinColumn()
position: AdPosition;
}
}

View File

@@ -128,7 +128,7 @@ export class VideoAdController {
@Param('id') id: string,
@Body() updateVideoAdDto: UpdateVideoAdDto,
) {
return this.videoAdService.updateAdByUser(user.sub, id, updateVideoAdDto);
return this.videoAdService.updateAdByAdmin(user.sub, id, updateVideoAdDto);
}
@Delete(':id')

View File

@@ -14,7 +14,7 @@ import { AdPositionModule } from 'src/ad-position/ad-position.module';
CategoryModule,
ImageModule,
UserModule,
AdPositionModule
AdPositionModule,
],
controllers: [VideoAdController],
providers: [VideoAdService],

View File

@@ -166,9 +166,12 @@ export class VideoAdService {
stateId?: number;
pageType: PageType;
}) {
// Get today's date in local timezone
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
const todayISOString = today.getFullYear() + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0');
const where: FindOptionsWhere<VideoAd> = {
status: AdStatus.PUBLISHED,
isActive: true,
@@ -176,8 +179,8 @@ export class VideoAdService {
pageType: pageType,
},
};
if (categoryId) {
console.log(categoryId);
where.mainCategory = { id: categoryId };
}
if (cityId) {
@@ -186,37 +189,59 @@ export class VideoAdService {
if (stateId) {
where.sid = stateId;
}
// Fetch all published and active ads
const lineAds = await this.videoAdRepo.find({
// Fetch all published and active ads with all necessary relations
const videoAds = await this.videoAdRepo.find({
where,
relations: ['position'],
relations: ['customer', 'customer.user', 'image', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
});
// Filter in JS for today's date in the dates array
const filteredAds = lineAds.filter(
(ad) =>
Array.isArray(ad.dates) &&
ad.dates.some((dateStr) => {
const dateOnly = dateStr.split('T')[0];
return dateOnly === todayISOString;
}),
);
// Filter ads that should be displayed today
const filteredAds = videoAds.filter((ad) => {
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
return false;
}
return ad.dates.some((dateStr) => {
try {
// Handle both ISO string and date-only formats
const adDate = new Date(dateStr);
const adDateString = adDate.getFullYear() + '-' +
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
String(adDate.getDate()).padStart(2, '0');
return adDateString === todayISOString;
} catch (error) {
console.error('Error parsing date:', dateStr, error);
return false;
}
});
});
// Return cleaned up ad data
return filteredAds.map((ad) => {
const {
payment,
customer,
cid,
sid,
sequenceNumber,
orderId,
status,
comments,
dates,
isActive,
cid,
sid,
...serializedAd
} = ad;
return serializedAd;
return {
...serializedAd,
// Include essential customer info without sensitive data
customerName: customer?.user?.name || null,
// Include location for display
cityId: cid,
stateId: sid,
};
});
}
@@ -526,7 +551,9 @@ export class VideoAdService {
if (filters?.userType) {
query.andWhere('ad.postedBy = :userType', { userType: filters.userType });
} else if (filters?.userTypes && filters.userTypes.length > 0) {
query.andWhere('ad.postedBy IN (:...userTypes)', { userTypes: filters.userTypes });
query.andWhere('ad.postedBy IN (:...userTypes)', {
userTypes: filters.userTypes,
});
}
// Location filters
@@ -545,16 +572,24 @@ export class VideoAdService {
// Category filters
if (filters?.mainCategoryId) {
query.andWhere('mainCategory.id = :mainCategoryId', { mainCategoryId: filters.mainCategoryId });
query.andWhere('mainCategory.id = :mainCategoryId', {
mainCategoryId: filters.mainCategoryId,
});
}
if (filters?.categoryOneId) {
query.andWhere('categoryOne.id = :categoryOneId', { categoryOneId: filters.categoryOneId });
query.andWhere('categoryOne.id = :categoryOneId', {
categoryOneId: filters.categoryOneId,
});
}
if (filters?.categoryTwoId) {
query.andWhere('categoryTwo.id = :categoryTwoId', { categoryTwoId: filters.categoryTwoId });
query.andWhere('categoryTwo.id = :categoryTwoId', {
categoryTwoId: filters.categoryTwoId,
});
}
if (filters?.categoryThreeId) {
query.andWhere('categoryThree.id = :categoryThreeId', { categoryThreeId: filters.categoryThreeId });
query.andWhere('categoryThree.id = :categoryThreeId', {
categoryThreeId: filters.categoryThreeId,
});
}
if (filters?.categoryId) {
query.andWhere(
@@ -565,7 +600,9 @@ export class VideoAdService {
// Customer filter
if (filters?.customerId) {
query.andWhere('customer.id = :customerId', { customerId: filters.customerId });
query.andWhere('customer.id = :customerId', {
customerId: filters.customerId,
});
}
// Apply sorting