You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

489 lines
16 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. package controller;
  2. import sugoi.db.Cache;
  3. import sugoi.Web;
  4. import sugoi.mail.Mail;
  5. import Common;
  6. using Lambda;
  7. using tools.DateTool;
  8. class Cron extends Controller {
  9. public function doDefault() {}
  10. /**
  11. * CLI only en prod
  12. */
  13. function canRun() {
  14. if (App.current.user != null && App.current.user.isAdmin()) {
  15. return true;
  16. } else if (App.config.DEBUG) {
  17. return true;
  18. } else {
  19. if (Web.isModNeko) {
  20. Sys.print("only CLI.");
  21. return false;
  22. } else {
  23. return true;
  24. }
  25. }
  26. }
  27. public function doMinute() {
  28. print("Cron.doMinute is called");
  29. if (!canRun())
  30. return;
  31. app.event(MinutelyCron);
  32. sendEmailsfromBuffer();
  33. }
  34. /**
  35. * Hourly Cron
  36. *
  37. * this function can be locally tested with `neko index.n cron/hour > cron.log`
  38. */
  39. public function doHour() {
  40. app.event(HourlyCron);
  41. distribNotif(4, db.User.UserFlags.HasEmailNotif4h); // 4h before
  42. distribNotif(24, db.User.UserFlags.HasEmailNotif24h); // 24h before
  43. distribNotif(0, db.User.UserFlags.HasEmailNotifOuverture); // on command open
  44. distribValidationNotif();
  45. // sendOrdersByProductWhenOrdersClose();
  46. }
  47. public function doDaily() {
  48. if (!canRun())
  49. return;
  50. app.event(DailyCron);
  51. // ERRORS MONITORING
  52. var n = Date.now();
  53. var yest24h = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
  54. var yest0h = DateTools.delta(yest24h, -1000 * 60 * 60 * 24);
  55. var errors = sugoi.db.Error.manager.search($date < yest24h && $date > yest0h);
  56. if (errors.length > 0) {
  57. var report = new StringBuf();
  58. report.add("<h1>" + App.config.NAME + " : ERRORS</h1>");
  59. for (e in errors) {
  60. report.add("<div><pre>" + e.error + " at URL " + e.url + " ( user : " + (e.user != null ? e.user.toString() : "none") + ", IP : " + e.ip
  61. + ")</pre></div><hr/>");
  62. }
  63. var m = new Mail();
  64. m.setSender(App.config.get("default_email"), t._("Cagette.net"));
  65. m.addRecipient(App.config.get("webmaster_email"));
  66. m.setSubject(App.config.NAME + " Errors");
  67. m.setHtmlBody(app.processTemplate("mail/message.mtt", {text: report.toString()}));
  68. App.sendMail(m);
  69. }
  70. // DEMO CONTRATS deletion after 7 days ( see controller.Group.doCreate() )
  71. db.Contract.manager.delete($name == "Contrat AMAP Maraîcher Exemple"
  72. && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
  73. db.Contract.manager.delete($name == "Contrat Poulet Exemple"
  74. && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
  75. // Old Messages cleaning
  76. db.Message.manager.delete($date < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 6));
  77. // DB cleaning : I dont know how, but some people have empty string emails...
  78. /*for ( u in db.User.manager.search($email == "", true)){
  79. u.email = Std.random(9999) + "@cagette.net";
  80. u.update();
  81. }
  82. for ( u in db.User.manager.search($email2 == "", true)){
  83. u.email2 = null;
  84. u.update();
  85. }*/
  86. }
  87. /**
  88. * Send email notifications to users before a distribution
  89. * @param hour
  90. * @param flag
  91. */
  92. function distribNotif(hour:Int, flag:db.User.UserFlags) {
  93. // trouve les distrib qui commencent dans le nombre d'heures demandé
  94. // on recherche celles qui commencent jusqu'à une heure avant pour ne pas en rater
  95. var from = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * (hour - 1));
  96. var to = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * hour);
  97. // if (App.config.DEBUG) from = DateTools.delta(from, 1000.0 * 60 * 60 * 24 * -30);
  98. // dans le cas HasEmailNotifOuverture la date à prendre est le orderStartDate
  99. // et non pas date qui est la date de la distribution
  100. var distribs;
  101. if (db.User.UserFlags.HasEmailNotifOuverture == flag)
  102. distribs = db.Distribution.manager.search($orderStartDate >= from && $orderStartDate <= to, false);
  103. else
  104. distribs = db.Distribution.manager.search($date >= from && $date <= to, false);
  105. // Sys.print("distribNotif "+hour+" from "+from+" to "+to+"<br/>\n");
  106. // on s'arrete immédiatement si aucune distibution trouvée
  107. if (distribs.length == 0)
  108. return;
  109. // cherche plus tard si on a pas une "grappe" de distrib
  110. /*while (true) {
  111. var extraDistribs ;
  112. if ( db.User.UserFlags.HasEmailNotifOuverture != flag )
  113. extraDistribs = db.Distribution.manager.search( $date >= to && $date <DateTools.delta(to,1000.0*60*60) , false);
  114. else
  115. extraDistribs = db.Distribution.manager.search( $orderStartDate >= to && $orderStartDate <DateTools.delta(to,1000.0*60*60) , false);
  116. for ( e in extraDistribs) distribs.add(e);
  117. if (extraDistribs.length > 0) {
  118. //on fait un tour de plus avec une heure plus tard
  119. to = DateTools.delta(h, 1000.0 * 60 * 60);
  120. }else {
  121. //plus de distribs
  122. break;
  123. }
  124. }*/
  125. // on vérifie dans le cache du jour que ces distrib n'ont pas deja été traitées lors d'un cron précédent
  126. var cacheId = Date.now().toString().substr(0, 10) + Std.string(flag);
  127. var dist:Array<Int> = sugoi.db.Cache.get(cacheId);
  128. if (dist != null) {
  129. for (d in Lambda.array(distribs)) {
  130. if (Lambda.exists(dist, function(x) return x == d.id)) {
  131. // Comment this line in case of local test
  132. distribs.remove(d);
  133. }
  134. }
  135. } else {
  136. dist = [];
  137. }
  138. // toutes les distribs trouvées ont deja été traitées
  139. if (distribs.length == 0)
  140. return;
  141. // stocke cache
  142. for (d in distribs)
  143. dist.push(d.id);
  144. Cache.set(cacheId, dist, 24 * 60 * 60);
  145. // We have now the distribs we want to notify about.
  146. var distribsByContractId = new Map<Int, db.Distribution>();
  147. for (d in distribs) {
  148. if (d == null || d.contract == null)
  149. continue;
  150. distribsByContractId.set(d.contract.id, d);
  151. }
  152. // Boucle sur les distributions pour gerer le cas de plusieurs distributions le même jour sur le même contrat
  153. var orders = [];
  154. for (d in distribs) {
  155. if (d == null || d.contract == null)
  156. continue;
  157. // get orders for both type of contracts
  158. for (x in d.contract.getOrders(d))
  159. orders.push(x);
  160. }
  161. /*
  162. * Group orders by users-group to receive separate emails by groups for the same user.
  163. * Map key is $userId-$groupId
  164. */
  165. var users = new Map<String, {
  166. user:db.User,
  167. distrib:db.Distribution,
  168. products:Array<db.UserContract>,
  169. vendors:Array<db.Vendor>
  170. }>();
  171. for (o in orders) {
  172. var x = users.get(o.user.id + "-" + o.product.contract.amap.id);
  173. if (x == null)
  174. x = {
  175. user: o.user,
  176. distrib: null,
  177. products: [],
  178. vendors: []
  179. };
  180. x.distrib = distribsByContractId.get(o.product.contract.id);
  181. x.products.push(o);
  182. users.set(o.user.id + "-" + o.product.contract.amap.id, x);
  183. // trace (o.userId+"-"+o.product.contract.amap.id, x);Sys.print("<br/>\n");
  184. // Prévenir également le deuxième user en cas des commandes alternées
  185. if (o.user2 != null) {
  186. var x = users.get(o.user2.id + "-" + o.product.contract.amap.id);
  187. if (x == null)
  188. x = {
  189. user: o.user2,
  190. distrib: null,
  191. products: [],
  192. vendors: []
  193. };
  194. x.distrib = distribsByContractId.get(o.product.contract.id);
  195. x.products.push(o);
  196. users.set(o.user2.id + "-" + o.product.contract.amap.id, x);
  197. // trace (o.user2.id+"-"+o.product.contract.amap.id, x);Sys.print("<br/>\n");
  198. }
  199. }
  200. // remove zero qt orders
  201. for (k in users.keys()) {
  202. var x = users.get(k);
  203. var total = 0.0;
  204. for (o in x.products)
  205. total += o.quantity;
  206. if (total == 0.0)
  207. users.remove(k);
  208. }
  209. // Dans le cas de l'ouverture de commande, ce sont tous les users qu'il faut intégrer
  210. if (db.User.UserFlags.HasEmailNotifOuverture == flag) {
  211. for (d in distribs) {
  212. var memberList = d.contract.amap.getMembers();
  213. for (u in memberList) {
  214. var x = users.get(u.id + "-" + d.contract.amap.id);
  215. if (x == null)
  216. x = {
  217. user: u,
  218. distrib: null,
  219. products: [],
  220. vendors: []
  221. };
  222. x.distrib = distribsByContractId.get(d.contract.id);
  223. x.vendors.push(d.contract.vendor);
  224. users.set(u.id + "-" + d.contract.amap.id, x);
  225. // print(u.id+"-"+d.contract.amap.id, x);
  226. }
  227. }
  228. }
  229. for (u in users) {
  230. if (u.user.flags.has(flag)) {
  231. if (u.user.email != null) {
  232. var group = u.distrib.contract.amap;
  233. this.t = sugoi.i18n.Locale.init(u.user.lang); // switch to the user language
  234. var text;
  235. if (db.User.UserFlags.HasEmailNotifOuverture == flag) {
  236. // order opening notif
  237. text = t._("Opening of orders for the delivery of <b>::date::</b>", {date: view.hDate(u.distrib.date)});
  238. text += "<br/>";
  239. text += t._("The following suppliers are involved :");
  240. text += "<br/><ul>";
  241. for (v in u.vendors) {
  242. text += "<li>" + v + "</li>";
  243. }
  244. text += "</ul>";
  245. } else {
  246. // Distribution notif to the users
  247. var d = u.distrib;
  248. text = t._("Do not forget the delivery on <b>::day::</b> from ::from:: to ::to::<br/>",
  249. {day: view.dDate(d.date), from: view.hHour(d.date), to: view.hHour(d.end)});
  250. text += t._("Your products to collect :") + "<br/><ul>";
  251. for (p in u.products) {
  252. text += "<li>" + p.quantity + " x " + p.product.getName();
  253. // Gerer le cas des contrats en alternance
  254. if (p.user2 != null) {
  255. text += " " + t._("alternated with") + " ";
  256. if (u.user == p.user)
  257. text += p.user2.getCoupleName();
  258. else
  259. text += p.user.getCoupleName();
  260. }
  261. text += "</li>";
  262. }
  263. text += "</ul>";
  264. }
  265. if (u.distrib.isDistributor(u.user)) {
  266. text += t._("<b>Warning: you are in charge of the delivery ! Do not forget to print the attendance sheet.</b>");
  267. }
  268. try {
  269. var m = new Mail();
  270. m.setSender(App.config.get("default_email"), t._("Cagette.net"));
  271. if (group.contact != null)
  272. m.setReplyTo(group.contact.email, group.name);
  273. m.addRecipient(u.user.email, u.user.getName());
  274. if (u.user.email2 != null)
  275. m.addRecipient(u.user.email2);
  276. m.setSubject(group.name + " : " + t._("Distribution on ::date::", {date: app.view.hDate(u.distrib.date)}));
  277. m.setHtmlBody(app.processTemplate("mail/message.mtt", {text: text, group: group}));
  278. App.sendMail(m, u.distrib.contract.amap);
  279. } catch (e:Dynamic) {
  280. app.logError(e); // email could be invalid
  281. }
  282. }
  283. }
  284. }
  285. }
  286. /**
  287. * Check if there is a multi-distrib to validate.
  288. *
  289. * Autovalidate it after 10 days
  290. */
  291. function distribValidationNotif() {
  292. var now = Date.now();
  293. var from = now.setHourMinute(now.getHours(), 0);
  294. var to = now.setHourMinute(now.getHours() + 1, 0);
  295. var explain = t._("<p>This step is important in order to:</p>");
  296. explain += t._("<ul><li>Update orders if delivered quantities are different from ordered quantities</li>");
  297. explain += t._("<li>Confirm the reception of payments (checks, cash, transfers) in order to mark orders as 'paid'</li></ul>");
  298. /*
  299. * warn administrator if a distribution just ended
  300. */
  301. var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), false);
  302. for (d in Lambda.array(ds)) {
  303. if (d.contract.type != db.Contract.TYPE_VARORDER) {
  304. ds.remove(d);
  305. } else if (!d.contract.amap.hasPayments()) {
  306. ds.remove(d);
  307. }
  308. }
  309. var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
  310. var view = App.current.view;
  311. for (d in ds) {
  312. // var subj = "["+d.contract.amap.name+"] " + t._("Validation of the ::date:: distribution",{date:view.hDate(d.date)});
  313. var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)});
  314. var url = "http://" + App.config.HOST + "/distribution/validate/" + d.date.toString().substr(0, 10) + "/" + d.place.id;
  315. var html = t._("<p>Your distribution just finished, don't forget to <b>validate</b> it</p>");
  316. html += explain;
  317. html += t._("<p><a href='::distriburl::'>Click here to validate the distribution</a> (You must be connected to your group Cagette)",
  318. {distriburl: url});
  319. App.quickMail(d.contract.amap.contact.email, subj, html);
  320. }
  321. /*
  322. * warn administrator if a distribution ended 3 days ago
  323. */
  324. var from = now.setHourMinute(now.getHours(), 0).deltaDays(-3);
  325. var to = now.setHourMinute(now.getHours() + 1, 0).deltaDays(-3);
  326. // warn administrator if a distribution just ended
  327. var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), false);
  328. for (d in Lambda.array(ds)) {
  329. if (d.contract.type != db.Contract.TYPE_VARORDER) {
  330. ds.remove(d);
  331. } else if (!d.contract.amap.hasPayments()) {
  332. ds.remove(d);
  333. }
  334. }
  335. var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
  336. for (d in ds) {
  337. // var subj = d.contract.amap.name + t._(": Validation of the delivery of the ") + App.current.view.hDate(d.date);
  338. var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)});
  339. var url = "http://" + App.config.HOST + "/distribution/validate/" + d.date.toString().substr(0, 10) + "/" + d.place.id;
  340. var html = t._("<p>Reminder: you have a delivery to validate.</p>");
  341. html += explain;
  342. html += t._("<p><a href='::distriburl::'>Click here to validate the delivery</a> (You must be connected to your Cagette group)",
  343. {distriburl: url});
  344. App.quickMail(d.contract.amap.contact.email, subj, html);
  345. }
  346. /*
  347. * Autovalidate unvalidated distributions after 10 days
  348. */
  349. var from = now.setHourMinute(now.getHours(), 0).deltaDays(0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT);
  350. var to = now.setHourMinute(now.getHours() + 1, 0).deltaDays(0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT);
  351. print('AUTOVALIDATION');
  352. print('Find distributions from $from to $to');
  353. var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), true);
  354. for (d in Lambda.array(ds)) {
  355. if (d.contract.type != db.Contract.TYPE_VARORDER) {
  356. ds.remove(d);
  357. } else if (!d.contract.amap.hasPayments()) {
  358. ds.remove(d);
  359. }
  360. }
  361. for (d in ds) {
  362. print(d.toString());
  363. service.PaymentService.validateDistribution(d);
  364. }
  365. // email
  366. var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
  367. for (d in ds) {
  368. // var subj = d.contract.amap.name + t._(": Validation of the distribution of the ") + App.current.view.hDate(d.date);
  369. var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)});
  370. var html = t._("<p>As you did not validate it manually after 10 days, <br/>the delivery of the ::deliveryDate:: has been validated automatically</p>",
  371. {
  372. deliveryDate: App.current.view.hDate(d.date)
  373. });
  374. App.quickMail(d.contract.amap.contact.email, subj, html);
  375. }
  376. }
  377. /**
  378. * Send emails from buffer.
  379. *
  380. * Warning, if the cron is executed each minute,
  381. * you should consider the right amount of emails to send each minute in order to avoid overlaping and getting in concurrency problems.
  382. * (like "SELECT * FROM BufferedMail WHERE sdate IS NULL ORDER BY cdate DESC LIMIT 100 FOR UPDATE Lock wait timeout exceeded; try restarting transaction")
  383. */
  384. function sendEmailsfromBuffer() {
  385. App.log("Send Emails from Buffer");
  386. // send
  387. for (e in sugoi.db.BufferedMail.manager.search($sdate == null, {limit: 50, orderBy: -cdate}, false)) {
  388. e.lock();
  389. if (e.isSent())
  390. continue;
  391. App.log('Send Email id=#${e.id} - title=${e.title}');
  392. e.finallySend();
  393. Sys.sleep(0.1);
  394. }
  395. // delete old emails
  396. var threeMonthsAgo = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 3);
  397. sugoi.db.BufferedMail.manager.delete($cdate < threeMonthsAgo);
  398. // emails that cannot be sent
  399. for (e in sugoi.db.BufferedMail.manager.search($tries > 100, {limit: 50, orderBy: -cdate}, true)) {
  400. if (e.sender.email != App.config.get("default_email")) {
  401. var str = t._("Sorry, the email entitled <b>::title::</b> could not be sent.", {title: e.title});
  402. App.quickMail(e.sender.email, t._("Email not sent"), str);
  403. }
  404. e.delete();
  405. }
  406. }
  407. /**
  408. * Email product report when orders close
  409. **/
  410. function sendOrdersByProductWhenOrdersClose() {
  411. var range = tools.DateTool.getLastHourRange();
  412. // Sys.println("Time is "+Date.now()+"<br/>");
  413. // Sys.println('Find all distributions that have closed in the last hour from ${range.from} to ${range.to} \n<br/>');
  414. for (d in db.Distribution.manager.search($orderEndDate >= range.from && $orderEndDate < range.to, false)) {
  415. service.OrderService.sendOrdersByProductReport(d);
  416. }
  417. }
  418. function print(text) {
  419. Sys.println(text + "<br/>");
  420. }
  421. }