391 lines
20 KiB
JavaScript
391 lines
20 KiB
JavaScript
|
// Contrôleurs gérant l'abonnement de l'utilisateur mais aussi les questionnaires auxquels il a accès
|
||
|
|
||
|
const { Op, QueryTypes } = require("sequelize");// pour certaines requêtes sql
|
||
|
const jwt = require("jsonwebtoken");
|
||
|
|
||
|
const config = require("../config/main");
|
||
|
const configQuestionnaires = require("../config/questionnaires");
|
||
|
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
|
||
|
|
||
|
const tool = require("../tools/main");
|
||
|
const toolFile = require("../tools/file");
|
||
|
const toolMail = require("../tools/mail");
|
||
|
|
||
|
const txt = require("../lang/"+config.adminLang+"/subscription");
|
||
|
const txtQuestionnaire= require("../lang/"+config.adminLang+"/questionnaire");
|
||
|
const txtQuestionnaireAccess= require("../lang/"+config.adminLang+"/questionnaireaccess");
|
||
|
const txtGeneral= require("../lang/"+config.adminLang+"/general");
|
||
|
|
||
|
const answerCtrl = require("./answer");
|
||
|
const questionnaireCtrl = require("./questionnaire");
|
||
|
const userCtrl = require("./user");
|
||
|
|
||
|
// Clic sur lien de désabonnement aux alertes email
|
||
|
exports.unsubscribeLink = async (req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const userDatas= await userCtrl.checkTokenUser(req.params.token);
|
||
|
await db["User"].update({ newsletterOk: false }, { where: { id : userDatas.User.id }, limit:1 });
|
||
|
await db["Subscription"].update({ noticeOk: false }, { where: { UserId : userDatas.User.id }, limit:1 });
|
||
|
userCtrl.creaUserJson(userDatas.User.id);
|
||
|
res.status(200).json({ message: txt.unsubscriptionOk });
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CRONS
|
||
|
|
||
|
// + une autre notification le jour "J" ??? vérifier si l'utilisateur actif les dernières 48H et laisser les autres tranquilles ?
|
||
|
exports.notifyExpirationFreeAccount= async(req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const dateExpirationMin=new Date(new Date().getTime()-(config.freeAccountTimingInDays-config.freeAccountExpirationNotificationInDays+1)*24*3600*1000);
|
||
|
const dateExpirationMax=new Date(new Date().getTime()-(config.freeAccountTimingInDays-config.freeAccountExpirationNotificationInDays)*24*3600*1000);
|
||
|
console.log("je cherche les utilisateurs freemium dont l'abonnement a été créé entre le "+dateExpirationMin+" et le "+dateExpirationMax+".");
|
||
|
const users=await db["Subscription"].findAll(
|
||
|
{
|
||
|
where:
|
||
|
{
|
||
|
[Op.and]:
|
||
|
[
|
||
|
{ numberOfDays: config.freeAccountTimingInDays },
|
||
|
{ createdAt: { [Op.gte]: dateExpirationMin } },
|
||
|
{ createdAt: { [Op.lt]: dateExpirationMax } }
|
||
|
]
|
||
|
},
|
||
|
attributes: ["UserId"]
|
||
|
});
|
||
|
const sendNotification = async (user) =>
|
||
|
{
|
||
|
let userInfos=await userCtrl.searchUserById(user.UserId);
|
||
|
if(userInfos && userInfos.User.status=="user")
|
||
|
{
|
||
|
//const token=jwt.sign({ userId: userInfos.User.id }, config.tokenPrivateKey, { expiresIn: config.tokenLoginLinkTimeInHours });
|
||
|
const mapMail =
|
||
|
{
|
||
|
SITE_NAME: config.siteName,
|
||
|
USER_NAME: userInfos.User.name,
|
||
|
LINK_URL : config.siteUrl+"/"+configTpl.updateAccountPage+"#abo"
|
||
|
};
|
||
|
const mailDatas=
|
||
|
{
|
||
|
mailSubject: txt.mailEndFreeTimeSubject,
|
||
|
mailPreheader: txt.mailEndFreeTimeSubject,
|
||
|
mailTitle: txt.mailEndFreeTimeSubject,
|
||
|
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.updateAccountPage+"#abo",
|
||
|
mailHeaderLinkTxt: txt.mailEndFreeTimeLinkTxt,
|
||
|
mailMainContent: tool.replaceAll(txt.mailEndFreeTimeBodyHTML, mapMail),
|
||
|
linksCTA: [{ url:config.siteUrl+"/"+configTpl.updateAccountPage+"#abo", txt:txt.mailEndFreeTimeLinkTxt }],
|
||
|
mailRecipientAddress: userInfos.User.email
|
||
|
}
|
||
|
await toolMail.sendMail(userInfos.User.smtp, userInfos.User.email, txt.mailEndFreeTimeSubject, tool.replaceAll(txt.mailEndFreeTimeBodyTxt, mapMail), "", mailDatas);
|
||
|
}
|
||
|
}
|
||
|
for(let i in users)
|
||
|
sendNotification(users[i]);
|
||
|
if(res.message)
|
||
|
res.message+="\n"+users.length+txt.mailEndFreeTimeMessage;
|
||
|
else
|
||
|
res.message=users.length+txt.mailEndFreeTimeMessage;
|
||
|
res.status(200).json(true);
|
||
|
console.log(users.length+txt.mailEndFreeTimeMessage);
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.notifyExpirationAccount= async(req, res, next) =>
|
||
|
{ // utiliser la vue puis des recherches en sql natif ou ajouter un modèle : https://stackoverflow.com/questions/48407329/cant-able-to-create-views-in-mysql-using-sequelize-orm
|
||
|
try
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const getUsers= async (nbDays) =>
|
||
|
{
|
||
|
const dateExpirationMin=new Date(new Date().getTime()+nbDays*24*3600*1000);
|
||
|
const dateExpirationMax=new Date(new Date().getTime()+(nbDays+1)*24*3600*1000);
|
||
|
const users=await db["Subscription"].findAll(
|
||
|
{
|
||
|
where:
|
||
|
{
|
||
|
[Op.and]:
|
||
|
[
|
||
|
{ numberOfDays: { [Op.gt]: config.freeAccountTimingInDays } },
|
||
|
db.sequelize.where(db.sequelize.fn('ADDDATE', db.sequelize.col('createdAt'), db.sequelize.literal('INTERVAL `numberOfDays` DAY')), { [Op.gte]: dateExpirationMin }),
|
||
|
db.sequelize.where(db.sequelize.fn('ADDDATE', db.sequelize.col('createdAt'), db.sequelize.literal('INTERVAL `numberOfDays` DAY')), { [Op.lt]: dateExpirationMax })
|
||
|
]
|
||
|
},
|
||
|
attributes: ["UserId"],
|
||
|
});
|
||
|
return users;
|
||
|
}
|
||
|
const sendNotification= async (user, mail) => // utiliser la paramètre "mail" pour adapter le texte du mail !
|
||
|
{
|
||
|
let userInfos=await searchUserById(user.UserId);
|
||
|
if(userInfos && userInfos.User.status=="user")
|
||
|
{
|
||
|
//const token=jwt.sign({ userId: userInfos.User.id }, config.tokenPrivateKey, { expiresIn: config.tokenLoginLinkTimeInHours });
|
||
|
const mapMail =
|
||
|
{
|
||
|
SITE_NAME: config.siteName,
|
||
|
USER_NAME: userInfos.User.name,
|
||
|
LINK_URL : config.siteUrl+"/"+configTpl.updateAccountPage+"#abo"
|
||
|
};
|
||
|
const mailDatas=
|
||
|
{
|
||
|
mailSubject: txt.mailExpirationSubject,
|
||
|
mailPreheader: txt.mailExpirationSubject,
|
||
|
mailTitle: txt.mailExpirationSubject,
|
||
|
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.updateAccountPage+"#abo",
|
||
|
mailHeaderLinkTxt: txt.mailExpirationLinkTxt,
|
||
|
mailMainContent: tool.replaceAll(txt.mailExpirationBodyHTML, mapMail),
|
||
|
linksCTA: [{ url:config.siteUrl+"/"+configTpl.updateAccountPage+"#abo", txt:txt.mailExpirationLinkTxt }],
|
||
|
mailRecipientAddress: userInfos.User.email
|
||
|
}
|
||
|
await toolMail.sendMail(userInfos.User.smtp, userInfos.User.email, txt.mailExpirationSubject, tool.replaceAll(txt.mailExpirationBodyTxt, mapMail), "", mailDatas);
|
||
|
}
|
||
|
}
|
||
|
// première salve :
|
||
|
let users1=await getUsers(config.accountExpirationFirstNotificationInDays);
|
||
|
for(i in users1)
|
||
|
sendNotification(users1[i], "first");
|
||
|
// deuxième relance :
|
||
|
let users2=await getUsers(config.accountExpirationSecondNotificationInDays);
|
||
|
for(i in users2)
|
||
|
sendNotification(users2[i], "second");
|
||
|
if(res.message)
|
||
|
res.message+="\n"+tool.replaceAll(txt.mailExpirationMessage, { FIRST:users1.length , SECOND:users2.length });
|
||
|
else
|
||
|
res.message=tool.replaceAll(txt.mailExpirationMessage, { FIRST:users1.length , SECOND:users2.length });
|
||
|
res.status(200).json(true);
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CRON donnant accès à l'utilisateur à nouveau quiz suivant son choix
|
||
|
exports.addNewQuestionnaireUsers = async(req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
// Utilisateurs dont l'abonnement est toujours actif et souhaitant recevoir un nouveau quiz le jour de l'appel de cette méthode.
|
||
|
// Le tout en heure locale et en ignorant ceux qui ont déjà été servis ce jour.
|
||
|
const subscriptionsOk = await db.sequelize.query("SELECT `Subscriptions`.`id` as SubscriptionId, `UserId`, `name`, `email`, `smtp`, `language`, `noticeOk`, `receiptDays`, ADDDATE(UTC_TIMESTAMP, INTERVAL `timeDifference` MINUTE) AS localDate FROM `Subscriptions` INNER JOIN `Users` ON `Subscriptions`.`UserId`=`Users`.`id` WHERE `status`='user' AND ADDDATE(`Subscriptions`.`createdAt`, `numberOfDays`) > UTC_TIMESTAMP HAVING HOUR(localDate) > "+config.hourGiveNewQuestionnaireBegin+" AND HOUR(localDate) < "+config.hourGiveNewQuestionnaireEnd+" AND LOCATE(DAYOFWEEK(localDate),receiptDays)!=0 AND SubscriptionId NOT IN (SELECT DISTINCT `SubscriptionId` FROM `Pauses` WHERE ADDDATE(`startingAt`, INTERVAL `timeDifference` MINUTE) <= localDate AND ADDDATE(`endingAT`, INTERVAL `timeDifference` MINUTE) > localDate) AND `UserId` NOT IN (SELECT DISTINCT `UserId` FROM `QuestionnaireAccesses` WHERE DATEDIFF(NOW(),`createdAt`) < 1 AND `selfCreatedOk` = false) LIMIT "+config.numberNewQuestionnaireAtSameTime, { type: QueryTypes.SELECT });
|
||
|
if(subscriptionsOk.length===0)
|
||
|
{
|
||
|
res.status(200).json({ message: txt.allSubscriptionProcessed });
|
||
|
//console.log(txt.allSubscriptionProcessed);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
await toolMail.sendMail(0, config.adminEmail, "Abonnements à traiter", subscriptionsOk.length+" abonnements à traiter.", "<p>"+subscriptionsOk.length+" abonnements à traiter.</p>");
|
||
|
let newQuestionnaire, access, questionnaire, token;
|
||
|
for (let i in subscriptionsOk)
|
||
|
{
|
||
|
newQuestionnaire=await db.sequelize.query("SELECT `Questionnaires`.`id`, `title`, `slug`, `introduction`, `url`, `anchor`,`estimatedTime` FROM `Questionnaires` INNER JOIN `Links` ON `Links`.`QuestionnaireId`=`Questionnaires`.`id` WHERE `isPublished`=1 AND `language`='"+subscriptionsOk[i].language+"' AND `Questionnaires`.`id` NOT IN (SELECT `QuestionnaireId` FROM `QuestionnaireAccesses` WHERE `UserId`="+subscriptionsOk[i].UserId+") ORDER BY `id` LIMIT 1", { type: QueryTypes.SELECT });
|
||
|
if(newQuestionnaire.length!==0)
|
||
|
{
|
||
|
access=await db["QuestionnaireAccess"].create({ QuestionnaireId: newQuestionnaire[0].id, UserId: subscriptionsOk[i].UserId, selfCreatedOk: false });
|
||
|
if(access)
|
||
|
{
|
||
|
answerCtrl.creaUserQuestionnairesWithoutAnswerJson(subscriptionsOk[i].UserId);
|
||
|
if(subscriptionsOk[i].noticeOk)
|
||
|
{
|
||
|
token=jwt.sign({ userId: subscriptionsOk[i].UserId }, config.tokenPrivateKey, { expiresIn: config.tokenUnsubscribeLinkTimeInDays });
|
||
|
const mapMail =
|
||
|
{
|
||
|
USER_NAME: subscriptionsOk[i].name,
|
||
|
QUESTIONNAIRE_URL: config.siteUrl+"/"+configQuestionnaires.dirWebQuestionnaire+"/"+newQuestionnaire[0].slug+".html#questionnaire",
|
||
|
UNSUBSCRIBE_URL: config.siteUrl+"/"+configTpl.stopMailPage+token
|
||
|
};
|
||
|
const mailDatas=
|
||
|
{
|
||
|
mailSubject: newQuestionnaire[0].title,
|
||
|
mailPreheader: newQuestionnaire[0].title,
|
||
|
mailTitle: newQuestionnaire[0].title,
|
||
|
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.stopMailPage+token,
|
||
|
mailHeaderLinkTxt: txt.mailStopMailLinkTxt,
|
||
|
mailMainContent: newQuestionnaire[0].introduction+"<p><b>"+txtQuestionnaire.estimatedTime+"</b> "+txtQuestionnaire.estimatedTimeOption[newQuestionnaire[0].estimatedTime]+".</p>",
|
||
|
linksCTA: [{ url:newQuestionnaire[0].url, txt:newQuestionnaire[0].anchor }, { url:config.siteUrl+"/"+configQuestionnaires.dirWebQuestionnaire+"/"+newQuestionnaire[0].slug+".html#questionnaire", txt:txtQuestionnaire.btnShowQuestionnaire }],
|
||
|
mailRecipientAddress: subscriptionsOk[i].email
|
||
|
}
|
||
|
toolMail.sendMail(subscriptionsOk[i].smtp, subscriptionsOk[i].email, newQuestionnaire[0].title, tool.replaceAll(txt.mailNewQuestionnaireBodyTxt, mapMail), "", mailDatas);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(res.alerte)
|
||
|
res.alerte+="\n"+txt.noNewQuestionnaireForUser+subscriptionsOk[i].email;
|
||
|
else
|
||
|
res.alerte=txt.noNewQuestionnaireForUser+subscriptionsOk[i].UserId;
|
||
|
await toolMail.sendMail(0, config.adminEmail, "Abonné sans nouveau quiz", txt.noNewQuestionnaireForUser+subscriptionsOk[i].email, "<p>"+txt.noNewQuestionnaireForUser+subscriptionsOk[i].email+"</p>");
|
||
|
}
|
||
|
}
|
||
|
res.status(200).json(subscriptionsOk);
|
||
|
console.log(subscriptionsOk);
|
||
|
}
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// FONCTIONS UTILITAIRES
|
||
|
|
||
|
// Retourne un booléen suivant si l'utilisateur a accès ou non à un questionnaire
|
||
|
const checkQuestionnaireAccess = async (UserId, QuestionnaireId) =>
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const checkQuestionnaireAccess=await db["QuestionnaireAccess"].findOne({ where: { UserId : UserId, QuestionnaireId : QuestionnaireId }, attributes: ["createdAt"] });
|
||
|
if(checkQuestionnaireAccess)
|
||
|
return true;
|
||
|
else
|
||
|
return false;
|
||
|
}
|
||
|
exports.checkQuestionnaireAccess = checkQuestionnaireAccess;
|
||
|
|
||
|
// Combien d'abonnements ont été enregistrés ces dernières 24 H ? depuis le début ? combien sont en premium ?
|
||
|
// Revoir : chercher les dates de paiement WP pour avoir stats 24 H ?
|
||
|
const getStatsSubscriptions = async () =>
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const getSubscriptions24H = await db.sequelize.query("SELECT `id` FROM `Subscriptions` WHERE `createdAt` > ADDDATE(NOW(), -1)", { type: QueryTypes.SELECT });
|
||
|
const getSubscriptionsTot = await db.sequelize.query("SELECT `id` FROM `Subscriptions`", { type: QueryTypes.SELECT });
|
||
|
const getSubscriptionsPremium = await db.sequelize.query("SELECT `id` FROM `Subscriptions` WHERE `numberOfDays` > :nb", { replacements: { nb: config.freeAccountTimingInDays }, type: QueryTypes.SELECT });
|
||
|
if(getSubscriptions24H && getSubscriptionsTot && getSubscriptionsPremium)
|
||
|
{
|
||
|
const stats =
|
||
|
{
|
||
|
nbSubscriptions24H : getSubscriptions24H.length,
|
||
|
nbSubscriptionsTot : getSubscriptionsTot.length,
|
||
|
nbSubscriptionsPremium : getSubscriptionsPremium.length
|
||
|
}
|
||
|
return stats;
|
||
|
}
|
||
|
else
|
||
|
return false;
|
||
|
}
|
||
|
exports.getStatsSubscriptions = getStatsSubscriptions;
|
||
|
|
||
|
/* Abandonné pour l'instant
|
||
|
*
|
||
|
// Recherche par mots-clés parmis les questionnaires auxquels l'utilisateur a accès !! où ceux auxquels il a répondu ? est-ce que cela a un intérêt ?
|
||
|
// Seul un certain nombre de résultats est renvoyé (pagination)
|
||
|
exports.searchQuestionnaires = async (req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
let search=tool.trimIfNotNull(req.body.searchQuestionnaires);
|
||
|
if(search === null || search === "" || search.length < config.minSearchQuestionnaires)
|
||
|
res.status(400).json(txtQuestionnaireAccess.searchIsNotLongEnough);
|
||
|
else
|
||
|
{
|
||
|
const db = require("../models/index");
|
||
|
const getQuestionnaires=await db.sequelize.query("SELECT `Questionnaires`.`id` FROM `Questionnaires` INNER JOIN `QuestionnaireAccesses` WHERE `Questionnaires`.`id`=`QuestionnaireAccesses`.`QuestionnaireId` AND `QuestionnaireAccesses`.`UserId`=:id AND `Questionnaires`.`isPublished` = 1 AND (`Questionnaires`.`title` LIKE :search OR `Questionnaires`.`introduction` LIKE :search)", { replacements: { id: req.connectedUser.User.id, search: "%"+search+"%" }, type: QueryTypes.SELECT });
|
||
|
let begin=0, end, output="";
|
||
|
if(!tool.isEmpty(req.body.begin))
|
||
|
begin=req.body.begin;
|
||
|
end=begin+configTpl.maxQuestionnairesByPage-1;// commence à 0 !
|
||
|
if(req.body.output!==undefined)
|
||
|
output=req.body.output;
|
||
|
datas=await questionnaireCtrl.getListingsQuestionnairesOuput(getQuestionnaires, begin, end, output);
|
||
|
res.status(200).json(datas);
|
||
|
}
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Retourne un questionnaire au hasard parmi ceux auxquels l'utilisateur a accès
|
||
|
exports.getOneRandomQuestionnaire = async(req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
const questionnaires=await getUserQuestionnaires(req.params.id, false);
|
||
|
if(!questionnaires || questionnaires.ids.length===0)
|
||
|
res.status(404).json({ txtCode: "notFound" });
|
||
|
else
|
||
|
{
|
||
|
const randomInt=tool.getRandomInt(0, questionnaires.ids.length);
|
||
|
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(questionnaires.ids[randomInt]);
|
||
|
if(questionnaire)
|
||
|
res.status(200).json(questionnaire);
|
||
|
else
|
||
|
throw { message : txtQuestionnaireAccess.notFound+questionnaires.ids[randomInt], status : 404 };
|
||
|
}
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Retourne la liste des questionnaires auxquels un utilisateur a accès.
|
||
|
// Ils sont listés par ordre de fraîcheur, les + récents étant en début de liste
|
||
|
// Si un questionnaire de début et un nombre de questionnaires sont fournis, l'ensemble des informations du questionnaire sont retournées
|
||
|
// Sinon, seulement les ids.
|
||
|
exports.getQuestionnairesByUser = async(req, res, next) =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
let datas;
|
||
|
if(req.params.begin && req.params.nb)
|
||
|
datas=await getUserQuestionnaires(req.params.id, true, req.params.begin, req.params.nb);
|
||
|
else
|
||
|
datas=await getUserQuestionnaires(req.params.id, false);
|
||
|
if(datas)
|
||
|
res.status(200).json(datas);
|
||
|
else
|
||
|
res.status(404).json(txtQuestionnaire.notFound);
|
||
|
next();
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
next(e);
|
||
|
}
|
||
|
}
|
||
|
*
|
||
|
// Création du fichier des identifiants des questionnaires d'un utilisateur
|
||
|
// Les premiers étant les derniers
|
||
|
const creaUserQuestionnairesJson = async (UserId) =>
|
||
|
{
|
||
|
UserId=tool.trimIfNotNull(UserId);
|
||
|
if(UserId===null)
|
||
|
return false;
|
||
|
const db = require("../models/index");
|
||
|
const userQuestionnaires=await db.sequelize.query("SELECT `QuestionnaireId` FROM `QuestionnaireAccesses` WHERE `UserId`=:id ORDER BY `createdAt` DESC", { replacements: { id: UserId }, type: QueryTypes.SELECT });
|
||
|
if(userQuestionnaires)
|
||
|
{
|
||
|
const questionnairesId=[];// les ids suffisent et allègent le fichier
|
||
|
for(i in userQuestionnaires)
|
||
|
questionnairesId.push(userQuestionnaires[i].QuestionnaireId);
|
||
|
await toolFile.createJSON(config.dirCacheUsersQuestionnaires, UserId, { ids: questionnairesId });
|
||
|
return { ids: questionnairesId };
|
||
|
}
|
||
|
else
|
||
|
return false;
|
||
|
}
|
||
|
exports.creaUserQuestionnairesJson = creaUserQuestionnairesJson;
|
||
|
* */
|