diff --git a/src/features/campaign/campaign.controller.ts b/src/features/campaign/campaign.controller.ts index 2d5324cb4..6b25bed66 100644 --- a/src/features/campaign/campaign.controller.ts +++ b/src/features/campaign/campaign.controller.ts @@ -109,4 +109,5 @@ export class CampaignController { await this.campaignService.sendNewsLetter(body); } } + } diff --git a/src/features/children/children.controller.ts b/src/features/children/children.controller.ts index b8e316225..c3ff83554 100644 --- a/src/features/children/children.controller.ts +++ b/src/features/children/children.controller.ts @@ -14,6 +14,7 @@ import { UploadedFiles, Delete, Res, + BadRequestException, } from '@nestjs/common'; import { ApiHeader, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { ChildrenService } from './children.service'; @@ -63,7 +64,7 @@ import { } from 'src/utils/helpers'; import axios from 'axios'; import { AllUserEntity } from 'src/entities/user.entity'; -import { checkIfFileOrDirectoryExists, moveFile } from 'src/utils/file'; +import { checkIfDirectoryExists, getCurrentFilenames, moveFile, renameFile } from 'src/utils/file'; import fs from 'fs'; import { CampaignService } from '../campaign/campaign.service'; import { File } from '@web-std/file'; @@ -87,7 +88,7 @@ export class ChildrenController { private locationService: LocationService, private downloadService: DownloadService, private campaignService: CampaignService, - ) {} + ) { } @Get(`preregister/:childFlaskId`) @ApiOperation({ description: 'Get child preregister' }) async getChildPreregister( @@ -285,35 +286,6 @@ export class ChildrenController { } } - @Delete(`preregister/:id`) - @ApiOperation({ description: 'Delete a pre register' }) - async deletePreRegister(@Req() req: Request, @Param('id') id: string) { - const panelFlaskUserId = req.headers['panelFlaskUserId']; - const panelFlaskTypeId = req.headers['panelFlaskTypeId']; - if ( - !isAuthenticated(panelFlaskUserId, panelFlaskTypeId) || - !( - panelFlaskTypeId === FlaskUserTypesEnum.SUPER_ADMIN || - panelFlaskTypeId === FlaskUserTypesEnum.ADMIN - ) - ) { - throw new ForbiddenException('You Are not the Super admin'); - } - - try { - const preRegister = await this.childrenService.getChildPreRegisterById( - id, - ); - - if (preRegister.status === PreRegisterStatusEnum.CONFIRMED) { - throw new ForbiddenException('This Child has been confirmed'); - } - await this.childrenService.deletePreRegister(preRegister.id); - } catch (e) { - throw new ServerError(e.message, e.status); - } - } - @Post(`preregister`) @ApiOperation({ description: 'Create children pre register' }) @UseInterceptors( @@ -326,7 +298,7 @@ export class ChildrenController { ), ) @UsePipes(new ValidationPipe()) - async preRegisterAdd( + async preRegisterAdminCreate( @Req() req: Request, @UploadedFiles() files: { @@ -350,66 +322,194 @@ export class ChildrenController { throw new ForbiddenException('You Are not the Authorized!'); } - if (!files) { - throw new ServerError('No files were uploaded!'); + if (!files || !files.awakeFile || !files.sleptFile) { + throw new BadRequestException('No files were uploaded!'); } // for local purposes - organized folders and files if (process.env.NODE_ENV === 'development') { - const newChildFolder = `../../Docs/children${ - Number(body.sex) === SexEnum.MALE ? '/boys/' : '/girls/' - }organized/${capitalizeFirstLetter(body.sayNameEn)}-${body.sayNameFa}`; - - const originalAwakeGirl = `../../Docs/children/girls/${ - files.awakeFile[0].filename.split('-s-')[0] - }.png`; - const originalAwakeBoy = `../../Docs/children/boys/${ - files.awakeFile[0].filename.split('-s-')[0] - }.png`; - const originalSleptGirl = `../../Docs/children/girls/${ - files.sleptFile[0].filename.split('-s-')[0] - }.png`; - const originalSleptBoy = `../../Docs/children/boys/${ - files.sleptFile[0].filename.split('-s-')[0] - }.png`; + const preRegistersByName = await this.childrenService.getPreChildrenByName(body.sayNameFa) + + if (preRegistersByName && preRegistersByName.length > 0) { + throw new BadRequestException('Can not have similar names!'); + } + + const originalAwakeGirl = `../../Docs/children/girls/${files.awakeFile[0].filename.split('-s-')[0] + }.png`; + const originalAwakeBoy = `../../Docs/children/boys/${files.awakeFile[0].filename.split('-s-')[0] + }.png`; + const originalSleptGirl = `../../Docs/children/girls/${files.sleptFile[0].filename.split('-s-')[0] + }.png`; + const originalSleptBoy = `../../Docs/children/boys/${files.sleptFile[0].filename.split('-s-')[0] + }.png`; + const newAwakeName = `awake-${body.sayNameEn.toLowerCase()}.png`; const newSleepName = `sleep-${body.sayNameEn.toLowerCase()}.png`; - if ( - checkIfFileOrDirectoryExists(originalAwakeGirl) || - checkIfFileOrDirectoryExists(originalAwakeBoy) - ) { - if (!checkIfFileOrDirectoryExists(newChildFolder)) { - fs.mkdirSync(newChildFolder); + try { + + let preRegister: ChildrenPreRegisterEntity + if ( + checkIfDirectoryExists(originalAwakeGirl) || + checkIfDirectoryExists(originalAwakeBoy) + ) { + preRegister = await this.childrenService.createPreRegisterChild( + files.awakeFile[0].filename, + files.sleptFile[0].filename, + { fa: body.sayNameFa, en: body.sayNameEn }, + body.sex, + ); + + const newChildFolder = `../../Docs/children${Number(body.sex) === SexEnum.MALE ? '/boys/' : '/girls/' + }organized/${capitalizeFirstLetter(body.sayNameEn)}_${preRegister.id}`; + + if (!checkIfDirectoryExists(newChildFolder)) { + console.log('Creating the child organized folder ...'); + fs.mkdirSync(newChildFolder); + if (!checkIfDirectoryExists(newChildFolder)) { + throw new ServerError('could not find the folder'); + } + } + if (Number(body.sex) === SexEnum.MALE) { + moveFile(originalAwakeBoy, `${newChildFolder}/${newAwakeName}`); + moveFile(originalSleptBoy, `${newChildFolder}/${newSleepName}`); + } + if (Number(body.sex) === SexEnum.FEMALE) { + moveFile(originalAwakeGirl, `${newChildFolder}/${newAwakeName}`); + moveFile(originalSleptGirl, `${newChildFolder}/${newSleepName}`); + } + } else { + throw new ServerError('could not find the file'); } - if (Number(body.sex) === SexEnum.MALE) { - moveFile(originalAwakeBoy, `${newChildFolder}/${newAwakeName}`); - moveFile(originalSleptBoy, `${newChildFolder}/${newSleepName}`); + return preRegister; + + + } catch (e) { + throw new ServerError(e.msg); + } + } + } + + @Delete(`preregister/:id`) + @ApiOperation({ description: 'Delete a pre register' }) + async deletePreRegister(@Req() req: Request, @Param('id') id: string) { + const panelFlaskUserId = req.headers['panelFlaskUserId']; + const panelFlaskTypeId = req.headers['panelFlaskTypeId']; + if ( + !isAuthenticated(panelFlaskUserId, panelFlaskTypeId) || + !( + panelFlaskTypeId === FlaskUserTypesEnum.SUPER_ADMIN || + panelFlaskTypeId === FlaskUserTypesEnum.ADMIN + ) + ) { + throw new ForbiddenException('You Are not the Super admin'); + } + + try { + const preRegister = await this.childrenService.getChildPreRegisterById( + id, + ); + + if (preRegister.status === PreRegisterStatusEnum.CONFIRMED) { + throw new ForbiddenException('This Child has been confirmed'); + } + await this.childrenService.deletePreRegister(preRegister.id); + } catch (e) { + throw new ServerError(e.message, e.status); + } + } + + @Get(`complete-delete`) + @ApiOperation({ + description: 'After delete we need to move back the avatars to their folders', + }) + @UsePipes(new ValidationPipe()) + async preRegisterCompleteDelete(@Req() req: Request) { + const panelFlaskUserId = req.headers['panelFlaskUserId']; + const panelFlaskTypeId = req.headers['panelFlaskTypeId']; + if ( + !isAuthenticated(panelFlaskUserId, panelFlaskTypeId) || + !( + panelFlaskTypeId === FlaskUserTypesEnum.SUPER_ADMIN || + panelFlaskTypeId === FlaskUserTypesEnum.ADMIN + ) + ) { + throw new ForbiddenException('You Are not the Authorized!'); + } + // for local purposes - organized folders and files + if (process.env.NODE_ENV === 'development') { + try { + const token = + config().dataCache.fetchPanelAuthentication(panelFlaskUserId).token; + const configs = { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: token, + contentType: false, + flaskId: panelFlaskUserId, + 'X-TAKE': 0, + 'X-LIMIT': 100, + }, + }; + const result1 = await axios.get( + `https://nest.saydao.org/api/dao/children/preregister/all/${PreRegisterStatusEnum.PRE_REGISTERED}`, + configs, + ); + const result2 = await axios.get( + `https://nest.saydao.org/api/dao/children/preregister/all/${PreRegisterStatusEnum.CONFIRMED}`, + configs, + ); + const result3 = await axios.get( + `https://nest.saydao.org/api/dao/children/preregister/all/${PreRegisterStatusEnum.NOT_REGISTERED}`, + configs, + ); + const allPreRegisters = result1.data.data.concat(result2.data.data).concat(result3.data.data); + + // remove the path which has a preregister id. return the paths left in array -> those which were deleted, ... + function removeItemOnce(arr: any[], value: string) { + const index = arr.indexOf(value); + if (index > -1) { + arr.splice(index, 1); + } + return arr; } - if (Number(body.sex) === SexEnum.FEMALE) { - moveFile(originalAwakeGirl, `${newChildFolder}/${newAwakeName}`); - moveFile(originalSleptGirl, `${newChildFolder}/${newSleepName}`); + let path: string; + const boysFiles = fs.readdirSync(`../../Docs/children/boys/organized`); + const girlsFiles = fs.readdirSync(`../../Docs/children/girls/organized`); + const filesDir = boysFiles.concat(girlsFiles); + + for (const p of allPreRegisters) { + path = filesDir.find( + (d) => d.split(`_`)[1] === p.id + ); + if (!path) { + // This one is deleted and we need to restore the child folder + continue + } else { + removeItemOnce(filesDir, path) + } } - } else { - throw new ServerError('could not find the file'); + + if (girlsFiles.length + boysFiles.length !== (boysFiles.concat(girlsFiles)).length) { + throw new ServerError('The arrays are different'); + } + + return { "FoldersToBeManaged": filesDir }; + } catch (e) { + console.log(e); + throw new ServerError(e.message); } } - return await this.childrenService.createPreRegisterChild( - files.awakeFile[0].filename, - files.sleptFile[0].filename, - { fa: body.sayNameFa, en: body.sayNameEn }, - body.sex, - ); } @ApiOperation({ description: 'NGO / SW add a child by updating pre register', }) - @Patch(`preregister/prepare`) + @Patch(`preregister/assign`) @UsePipes(new ValidationPipe()) @UseInterceptors(FileInterceptor('voiceFile', voiceStorage)) - async preRegisterPrepare( + async preRegisterAssignChild( @Req() req: Request, @UploadedFile() voiceFile, @Body(ValidateChildPipe) body: PreparePreRegisterChildDto, @@ -521,7 +621,7 @@ export class ChildrenController { if (!nestSocialWorker || !ngo || !contributor) { throw new ServerError('This social worker has not contributed yet'); } - return await this.childrenService.preRegisterPrepare( + return await this.childrenService.preRegisterAssignChild( candidate.id, { phoneNumber: body.phoneNumber, @@ -862,8 +962,8 @@ export class ChildrenController { const found = names.filter((n) => lang === 'en' ? n.en.toUpperCase() === newName.toUpperCase() || - (n.en && - n.en.slice(-3).toUpperCase() === newName.slice(-3).toUpperCase()) + (n.en && + n.en.slice(-3).toUpperCase() === newName.slice(-3).toUpperCase()) : n.fa === newName || (n.fa && n.fa.slice(-3) === newName.slice(-3)), ); @@ -979,13 +1079,25 @@ export class ChildrenController { return await this.childrenService.getChildNeedsSummery(token, childId); } + @Get(`no-need`) + @ApiOperation({ description: 'Get children with no need' }) + async childrenWithNoNeed(@Req() req: Request) { + const panelFlaskUserId = req.headers['panelFlaskUserId']; + const panelFlaskTypeId = req.headers['panelFlaskTypeId']; + if (!isAuthenticated(panelFlaskUserId, panelFlaskTypeId)) { + throw new ForbiddenException('You Are not authorized'); + } + + return await this.campaignService.childrenWithNoNeed(); + } + @Get('/network') getAvailableContributions(@Req() req: Request) { const dappFlaskUserId = req.headers['dappFlaskUserId']; if (!isAuthenticated(dappFlaskUserId, FlaskUserTypesEnum.FAMILY)) { throw new ForbiddenException('You Are not authorized'); } - return this.childrenService.gtTheNetwork(); + return this.childrenService.getTheNetwork(); } @Get('avatars/images/:fileName') diff --git a/src/features/children/children.service.ts b/src/features/children/children.service.ts index b5ee6c4af..0332eb08f 100644 --- a/src/features/children/children.service.ts +++ b/src/features/children/children.service.ts @@ -43,7 +43,7 @@ export class ChildrenService { private childrenRepository: Repository, @InjectRepository(Child, 'flaskPostgres') private flaskChildRepository: Repository, - ) {} + ) { } async countChildren(ngoIds: number[]) { return this.flaskChildRepository @@ -164,7 +164,7 @@ export class ChildrenService { ); } - preRegisterPrepare( + preRegisterAssignChild( theId: string, childDetails: PreRegisterChildPrepareParams, location: LocationEntity, @@ -344,6 +344,8 @@ export class ChildrenService { }); } + + async getFlaskChildren( options: PaginateQuery, body: { @@ -365,8 +367,8 @@ export class ChildrenService { body.isConfirmed === ChildConfirmation.CONFIRMED ? [true] : ChildConfirmation.NOT_CONFIRMED - ? [false] - : ChildConfirmation.BOTH && [true, false], + ? [false] + : ChildConfirmation.BOTH && [true, false], }) .andWhere('child.id_social_worker IN (:...socialWorkerIds)', { socialWorkerIds: [...socialWorkerIds], @@ -376,11 +378,11 @@ export class ChildrenService { body.statuses[0] >= 0 ? [...body.statuses] : [ - ChildExistence.DEAD, - ChildExistence.AlivePresent, - ChildExistence.AliveGone, - ChildExistence.TempGone, - ], + ChildExistence.DEAD, + ChildExistence.AlivePresent, + ChildExistence.AliveGone, + ChildExistence.TempGone, + ], }) .andWhere('child.id_ngo NOT IN (:...testNgoIds)', { @@ -429,6 +431,19 @@ export class ChildrenService { .getMany(); } + async getPreChildrenByName(sayNameEn: string): Promise { + return await this.preRegisterChildrenRepository + .createQueryBuilder('child') + // .where('child.status != :status', { + // status: PreRegisterStatusEnum.CONFIRMED, + // }) + .where("child.sayName -> 'en' = :sayName", { + sayName: sayNameEn, + }) + .select(['child.sayName']) + .getMany(); + } + getChildPreRegisterById(id: string): Promise { const child = this.preRegisterChildrenRepository.findOne({ where: { @@ -472,7 +487,7 @@ export class ChildrenService { .getMany(); } - async gtTheNetwork(): Promise { + async getTheNetwork(): Promise { return this.flaskChildRepository .createQueryBuilder('child') .leftJoinAndMapOne( diff --git a/src/features/midjourney/midjourney.controller.ts b/src/features/midjourney/midjourney.controller.ts index 14beee5b7..2bffafa48 100644 --- a/src/features/midjourney/midjourney.controller.ts +++ b/src/features/midjourney/midjourney.controller.ts @@ -24,7 +24,7 @@ import { SUPER_ADMIN_ID_PANEL, } from 'src/types/interfaces/interface'; import { WalletExceptionFilter } from 'src/filters/wallet-exception.filter'; -import { checkIfFileOrDirectoryExists } from 'src/utils/file'; +import { checkIfDirectoryExists } from 'src/utils/file'; import fs from 'fs'; import path from 'path'; @@ -36,7 +36,7 @@ export class MidjourneyController { private readonly downloadService: DownloadService, private readonly needService: NeedService, private readonly familyService: FamilyService, - ) {} + ) { } @Get(`db/all`) @ApiSecurity('flask-access-token') @@ -264,7 +264,7 @@ export class MidjourneyController { } const path = `../midjourney-bot/main/need-images/need-${id}`; - if (checkIfFileOrDirectoryExists(path)) { + if (checkIfDirectoryExists(path)) { await rimraf(path); list.push(path); } else { diff --git a/src/features/midjourney/midjourney.service.ts b/src/features/midjourney/midjourney.service.ts index 4de98c7a9..0d818420a 100644 --- a/src/features/midjourney/midjourney.service.ts +++ b/src/features/midjourney/midjourney.service.ts @@ -5,7 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { NeedEntity } from 'src/entities/need.entity'; import fs from 'fs'; import config from 'src/config'; -import { checkIfFileOrDirectoryExists, deleteFile } from 'src/utils/file'; +import { checkIfDirectoryExists, deleteFile } from 'src/utils/file'; import { NeedService } from '../need/need.service'; import { FamilyService } from '../family/family.service'; import { SAYPlatformRoles } from 'src/types/interfaces/interface'; @@ -27,7 +27,7 @@ export class MidjourneyService { private readonly midjourneyRepository: Repository, private readonly familyService: FamilyService, private readonly needService: NeedService, - ) {} + ) { } async getAllImages(): Promise { return this.midjourneyRepository.find(); @@ -77,7 +77,7 @@ export class MidjourneyService { } }); config().dataCache.storeMidjourney(list); - if (checkIfFileOrDirectoryExists('../midjourney-bot/midjourney.json')) { + if (checkIfDirectoryExists('../midjourney-bot/midjourney.json')) { deleteFile('../midjourney-bot/midjourney.json'); } fs.appendFile( diff --git a/src/features/schedule/schedule.service.ts b/src/features/schedule/schedule.service.ts index 5a5a2d378..19f5b1bae 100644 --- a/src/features/schedule/schedule.service.ts +++ b/src/features/schedule/schedule.service.ts @@ -154,7 +154,7 @@ export class ScheduleService { name: 'Reminders At 08:30 on Saturday.', timeZone: 'Asia/Tehran', }) - async handleReminderMailCron() { + async handleChildNoNeedReminderMailCron() { if (process.env.NODE_ENV === 'production') { this.logger.debug('Sending Reminder to Social workers'); await this.campaignService.sendSwChildNoNeedReminder(); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 1d0ec3e1b..364d273ae 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -14,8 +14,6 @@ export async function updateFlaskCacheAuthentication(req, logger: Logger) { const requestDappFlaskId = Number(req.headers['flaskdappid']); const requestPanelFlaskId = Number(req.headers['flaskid']); - console.log(requestDappFlaskId); - console.log(accessToken); if (!accessToken || (!requestPanelFlaskId && !requestDappFlaskId)) { throw new ForbiddenException('Access Token and the ID is required!'); } @@ -103,6 +101,8 @@ export async function updateFlaskCacheAuthentication(req, logger: Logger) { accessToken, Number(requestPanelFlaskId), ); + console.log('here2'); + if (!socialWorker) { throw new ForbiddenException('You Do not have Access!'); } diff --git a/src/utils/file.ts b/src/utils/file.ts index 3f024ff0e..1f78d274f 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -9,10 +9,23 @@ import { promisify } from 'util'; * * @returns {boolean} */ -export const checkIfFileOrDirectoryExists = (path: string): boolean => { +export const checkIfDirectoryExists = (path: string): boolean => { + console.log(`Checking if folder exists: ${path}`); return fs.existsSync(path); }; +export const renameFile = (currPath: string, newPath: string) => { + fs.rename(currPath, newPath, function (err) { + if (err) { + console.log(err) + } else { + console.log("Successfully renamed the directory.") + } + }) +}; + + + /** * Gets file data from a given path via a promise interface. * @@ -44,7 +57,7 @@ export const createFile = async ( fileName: string, data: string | NodeJS.ArrayBufferView, ): Promise => { - if (!checkIfFileOrDirectoryExists(path)) { + if (!checkIfDirectoryExists(path)) { fs.mkdirSync(path); } console.log(`${path}/${fileName}`); @@ -69,3 +82,30 @@ export const deleteFile = async (path: string): Promise => { export const moveFile = async (oldPath: string, newPath: string) => { fs.rename(oldPath, newPath, () => console.log('Moved a file...')); }; + + +// Function to get current filenames +// in directory +export const getCurrentFilenames = () => { + console.log("\nCurrent filenames:"); + fs.readdirSync(__dirname).forEach(file => { + console.log(file); + }); + console.log("\n"); +} + + +export function getAllFilesFromFolder(dir: string) { + let results = []; + if (checkIfDirectoryExists(dir)) { + fs.readdirSync(dir).forEach(function (file) { + file = dir + '/' + file; + const stat = fs.statSync(file); + if (stat && stat.isDirectory()) { + results = results.concat(getAllFilesFromFolder(file)); + } else results.push(file); + }); + } + + return results; +} \ No newline at end of file diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 2f39c643c..84f540d3c 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -18,7 +18,7 @@ import { AnnouncementEnum, } from 'src/types/interfaces/interface'; import fs from 'fs'; -import { checkIfFileOrDirectoryExists } from './file'; +import { checkIfDirectoryExists } from './file'; export const Q1_LOWER_COEFFICIENT = 0.75; export const Q1_TO_Q2_COEFFICIENT = 1; @@ -32,20 +32,6 @@ export function sleep(time) { console.log('Sleeping...'); return new Promise((resolve) => setTimeout(resolve, time)); } -export function getAllFilesFromFolder(dir: string) { - let results = []; - if (checkIfFileOrDirectoryExists(dir)) { - fs.readdirSync(dir).forEach(function (file) { - file = dir + '/' + file; - const stat = fs.statSync(file); - if (stat && stat.isDirectory()) { - results = results.concat(getAllFilesFromFolder(file)); - } else results.push(file); - }); - } - - return results; -} // {id: 44 } export function removeSpecialDuplicates(array: any[]) { @@ -213,28 +199,28 @@ export function persianMonthStringFarsi(value: Date) { return pm === 'Farvardin' ? 'فروردین' : pm === 'Ordibehesht' - ? 'اردیبهست' - : pm === 'Khordad' - ? 'خرداد' - : pm === 'Tir' - ? 'تیر' - : pm === 'Mordad' - ? 'مرداد' - : pm === 'Shahrivar' - ? 'شهریور' - : pm === 'Mehr' - ? 'مهر' - : pm === 'Aban' - ? 'آبان' - : pm === 'Azar' - ? 'آذر' - : pm === 'Dey' - ? 'دی' - : pm === 'Bahman' - ? 'بهمن' - : pm === 'Esfand' - ? 'اسفند' - : null; + ? 'اردیبهست' + : pm === 'Khordad' + ? 'خرداد' + : pm === 'Tir' + ? 'تیر' + : pm === 'Mordad' + ? 'مرداد' + : pm === 'Shahrivar' + ? 'شهریور' + : pm === 'Mehr' + ? 'مهر' + : pm === 'Aban' + ? 'آبان' + : pm === 'Azar' + ? 'آذر' + : pm === 'Dey' + ? 'دی' + : pm === 'Bahman' + ? 'بهمن' + : pm === 'Esfand' + ? 'اسفند' + : null; } export function persianDay(value: Date) { @@ -561,7 +547,7 @@ export function ticketNotifications( !myView || (latestView.flaskUserId !== myView.flaskUserId && Date.parse(myView.viewed.toUTCString()) < - Date.parse(latestView.viewed.toUTCString())) + Date.parse(latestView.viewed.toUTCString())) ); }); @@ -572,7 +558,7 @@ export function isUnpayable(need: Need) { return ( need.unavailable_from && timeDifference(new Date(), need.unavailable_from).hh < - PRODUCT_UNPAYABLE_PERIOD + PRODUCT_UNPAYABLE_PERIOD ); }