package controller; import sugoi.db.Cache; import sugoi.Web; import sugoi.mail.Mail; import Common; using Lambda; using tools.DateTool; class Cron extends Controller { public function doDefault() {} /** * CLI only en prod */ function canRun() { if (App.current.user != null && App.current.user.isAdmin()) { return true; } else if (App.config.DEBUG) { return true; } else { if (Web.isModNeko) { Sys.print("only CLI."); return false; } else { return true; } } } public function doMinute() { print("Cron.doMinute is called"); if (!canRun()) return; app.event(MinutelyCron); sendEmailsfromBuffer(); } /** * Hourly Cron * * this function can be locally tested with `neko index.n cron/hour > cron.log` */ public function doHour() { app.event(HourlyCron); distribNotif(4, db.User.UserFlags.HasEmailNotif4h); // 4h before distribNotif(24, db.User.UserFlags.HasEmailNotif24h); // 24h before distribNotif(0, db.User.UserFlags.HasEmailNotifOuverture); // on command open distribValidationNotif(); // sendOrdersByProductWhenOrdersClose(); } public function doDaily() { if (!canRun()) return; app.event(DailyCron); // ERRORS MONITORING var n = Date.now(); var yest24h = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0); var yest0h = DateTools.delta(yest24h, -1000 * 60 * 60 * 24); var errors = sugoi.db.Error.manager.search($date < yest24h && $date > yest0h); if (errors.length > 0) { var report = new StringBuf(); report.add("

" + App.config.NAME + " : ERRORS

"); for (e in errors) { report.add("
" + e.error + " at URL " + e.url + " ( user : " + (e.user != null ? e.user.toString() : "none") + ", IP : " + e.ip
					+ ")

"); } var m = new Mail(); m.setSender(App.config.get("default_email"), t._("Cagette.net")); m.addRecipient(App.config.get("webmaster_email")); m.setSubject(App.config.NAME + " Errors"); m.setHtmlBody(app.processTemplate("mail/message.mtt", {text: report.toString()})); App.sendMail(m); } // DEMO CONTRATS deletion after 7 days ( see controller.Group.doCreate() ) db.Contract.manager.delete($name == "Contrat AMAP Maraîcher Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7)); db.Contract.manager.delete($name == "Contrat Poulet Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7)); // Old Messages cleaning db.Message.manager.delete($date < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 6)); // DB cleaning : I dont know how, but some people have empty string emails... /*for ( u in db.User.manager.search($email == "", true)){ u.email = Std.random(9999) + "@cagette.net"; u.update(); } for ( u in db.User.manager.search($email2 == "", true)){ u.email2 = null; u.update(); }*/ } /** * Send email notifications to users before a distribution * @param hour * @param flag */ function distribNotif(hour:Int, flag:db.User.UserFlags) { // trouve les distrib qui commencent dans le nombre d'heures demandé // on recherche celles qui commencent jusqu'à une heure avant pour ne pas en rater var from = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * (hour - 1)); var to = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * hour); // if (App.config.DEBUG) from = DateTools.delta(from, 1000.0 * 60 * 60 * 24 * -30); // dans le cas HasEmailNotifOuverture la date à prendre est le orderStartDate // et non pas date qui est la date de la distribution var distribs; if (db.User.UserFlags.HasEmailNotifOuverture == flag) distribs = db.Distribution.manager.search($orderStartDate >= from && $orderStartDate <= to, false); else distribs = db.Distribution.manager.search($date >= from && $date <= to, false); // Sys.print("distribNotif "+hour+" from "+from+" to "+to+"
\n"); // on s'arrete immédiatement si aucune distibution trouvée if (distribs.length == 0) return; // cherche plus tard si on a pas une "grappe" de distrib /*while (true) { var extraDistribs ; if ( db.User.UserFlags.HasEmailNotifOuverture != flag ) extraDistribs = db.Distribution.manager.search( $date >= to && $date = to && $orderStartDate 0) { //on fait un tour de plus avec une heure plus tard to = DateTools.delta(h, 1000.0 * 60 * 60); }else { //plus de distribs break; } }*/ // 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 var cacheId = Date.now().toString().substr(0, 10) + Std.string(flag); var dist:Array = sugoi.db.Cache.get(cacheId); if (dist != null) { for (d in Lambda.array(distribs)) { if (Lambda.exists(dist, function(x) return x == d.id)) { // Comment this line in case of local test distribs.remove(d); } } } else { dist = []; } // toutes les distribs trouvées ont deja été traitées if (distribs.length == 0) return; // stocke cache for (d in distribs) dist.push(d.id); Cache.set(cacheId, dist, 24 * 60 * 60); // We have now the distribs we want to notify about. var distribsByContractId = new Map(); for (d in distribs) { if (d == null || d.contract == null) continue; distribsByContractId.set(d.contract.id, d); } // Boucle sur les distributions pour gerer le cas de plusieurs distributions le même jour sur le même contrat var orders = []; for (d in distribs) { if (d == null || d.contract == null) continue; // get orders for both type of contracts for (x in d.contract.getOrders(d)) orders.push(x); } /* * Group orders by users-group to receive separate emails by groups for the same user. * Map key is $userId-$groupId */ var users = new Map, vendors:Array }>(); for (o in orders) { var x = users.get(o.user.id + "-" + o.product.contract.amap.id); if (x == null) x = { user: o.user, distrib: null, products: [], vendors: [] }; x.distrib = distribsByContractId.get(o.product.contract.id); x.products.push(o); users.set(o.user.id + "-" + o.product.contract.amap.id, x); // trace (o.userId+"-"+o.product.contract.amap.id, x);Sys.print("
\n"); // Prévenir également le deuxième user en cas des commandes alternées if (o.user2 != null) { var x = users.get(o.user2.id + "-" + o.product.contract.amap.id); if (x == null) x = { user: o.user2, distrib: null, products: [], vendors: [] }; x.distrib = distribsByContractId.get(o.product.contract.id); x.products.push(o); users.set(o.user2.id + "-" + o.product.contract.amap.id, x); // trace (o.user2.id+"-"+o.product.contract.amap.id, x);Sys.print("
\n"); } } // remove zero qt orders for (k in users.keys()) { var x = users.get(k); var total = 0.0; for (o in x.products) total += o.quantity; if (total == 0.0) users.remove(k); } // Dans le cas de l'ouverture de commande, ce sont tous les users qu'il faut intégrer if (db.User.UserFlags.HasEmailNotifOuverture == flag) { for (d in distribs) { var memberList = d.contract.amap.getMembers(); for (u in memberList) { var x = users.get(u.id + "-" + d.contract.amap.id); if (x == null) x = { user: u, distrib: null, products: [], vendors: [] }; x.distrib = distribsByContractId.get(d.contract.id); x.vendors.push(d.contract.vendor); users.set(u.id + "-" + d.contract.amap.id, x); // print(u.id+"-"+d.contract.amap.id, x); } } } for (u in users) { if (u.user.flags.has(flag)) { if (u.user.email != null) { var group = u.distrib.contract.amap; this.t = sugoi.i18n.Locale.init(u.user.lang); // switch to the user language var text; if (db.User.UserFlags.HasEmailNotifOuverture == flag) { // order opening notif text = t._("Opening of orders for the delivery of ::date::", {date: view.hDate(u.distrib.date)}); text += "
"; text += t._("The following suppliers are involved :"); text += "
    "; for (v in u.vendors) { text += "
  • " + v + "
  • "; } text += "
"; } else { // Distribution notif to the users var d = u.distrib; text = t._("Do not forget the delivery on ::day:: from ::from:: to ::to::
", {day: view.dDate(d.date), from: view.hHour(d.date), to: view.hHour(d.end)}); text += t._("Your products to collect :") + "
    "; for (p in u.products) { text += "
  • " + p.quantity + " x " + p.product.getName(); // Gerer le cas des contrats en alternance if (p.user2 != null) { text += " " + t._("alternated with") + " "; if (u.user == p.user) text += p.user2.getCoupleName(); else text += p.user.getCoupleName(); } text += "
  • "; } text += "
"; } if (u.distrib.isDistributor(u.user)) { text += t._("Warning: you are in charge of the delivery ! Do not forget to print the attendance sheet."); } try { var m = new Mail(); m.setSender(App.config.get("default_email"), t._("Cagette.net")); if (group.contact != null) m.setReplyTo(group.contact.email, group.name); m.addRecipient(u.user.email, u.user.getName()); if (u.user.email2 != null) m.addRecipient(u.user.email2); m.setSubject(group.name + " : " + t._("Distribution on ::date::", {date: app.view.hDate(u.distrib.date)})); m.setHtmlBody(app.processTemplate("mail/message.mtt", {text: text, group: group})); App.sendMail(m, u.distrib.contract.amap); } catch (e:Dynamic) { app.logError(e); // email could be invalid } } } } } /** * Check if there is a multi-distrib to validate. * * Autovalidate it after 10 days */ function distribValidationNotif() { var now = Date.now(); var from = now.setHourMinute(now.getHours(), 0); var to = now.setHourMinute(now.getHours() + 1, 0); var explain = t._("

This step is important in order to:

"); explain += t._("
  • Update orders if delivered quantities are different from ordered quantities
  • "); explain += t._("
  • Confirm the reception of payments (checks, cash, transfers) in order to mark orders as 'paid'
"); /* * warn administrator if a distribution just ended */ var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), false); for (d in Lambda.array(ds)) { if (d.contract.type != db.Contract.TYPE_VARORDER) { ds.remove(d); } else if (!d.contract.amap.hasPayments()) { ds.remove(d); } } var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds); var view = App.current.view; for (d in ds) { // var subj = "["+d.contract.amap.name+"] " + t._("Validation of the ::date:: distribution",{date:view.hDate(d.date)}); var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)}); var url = "http://" + App.config.HOST + "/distribution/validate/" + d.date.toString().substr(0, 10) + "/" + d.place.id; var html = t._("

Your distribution just finished, don't forget to validate it

"); html += explain; html += t._("

Click here to validate the distribution (You must be connected to your group Cagette)", {distriburl: url}); App.quickMail(d.contract.amap.contact.email, subj, html); } /* * warn administrator if a distribution ended 3 days ago */ var from = now.setHourMinute(now.getHours(), 0).deltaDays(-3); var to = now.setHourMinute(now.getHours() + 1, 0).deltaDays(-3); // warn administrator if a distribution just ended var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), false); for (d in Lambda.array(ds)) { if (d.contract.type != db.Contract.TYPE_VARORDER) { ds.remove(d); } else if (!d.contract.amap.hasPayments()) { ds.remove(d); } } var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds); for (d in ds) { // var subj = d.contract.amap.name + t._(": Validation of the delivery of the ") + App.current.view.hDate(d.date); var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)}); var url = "http://" + App.config.HOST + "/distribution/validate/" + d.date.toString().substr(0, 10) + "/" + d.place.id; var html = t._("

Reminder: you have a delivery to validate.

"); html += explain; html += t._("

Click here to validate the delivery (You must be connected to your Cagette group)", {distriburl: url}); App.quickMail(d.contract.amap.contact.email, subj, html); } /* * Autovalidate unvalidated distributions after 10 days */ var from = now.setHourMinute(now.getHours(), 0).deltaDays(0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT); var to = now.setHourMinute(now.getHours() + 1, 0).deltaDays(0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT); print('AUTOVALIDATION'); print('Find distributions from $from to $to'); var ds = db.Distribution.manager.search(!$validated && ($end >= from) && ($end < to), true); for (d in Lambda.array(ds)) { if (d.contract.type != db.Contract.TYPE_VARORDER) { ds.remove(d); } else if (!d.contract.amap.hasPayments()) { ds.remove(d); } } for (d in ds) { print(d.toString()); service.PaymentService.validateDistribution(d); } // email var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds); for (d in ds) { // var subj = d.contract.amap.name + t._(": Validation of the distribution of the ") + App.current.view.hDate(d.date); var subj = t._("[::group::] Validation of the ::date:: distribution", {group: d.contract.amap.name, date: view.hDate(d.date)}); var html = t._("

As you did not validate it manually after 10 days,
the delivery of the ::deliveryDate:: has been validated automatically

", { deliveryDate: App.current.view.hDate(d.date) }); App.quickMail(d.contract.amap.contact.email, subj, html); } } /** * Send emails from buffer. * * Warning, if the cron is executed each minute, * you should consider the right amount of emails to send each minute in order to avoid overlaping and getting in concurrency problems. * (like "SELECT * FROM BufferedMail WHERE sdate IS NULL ORDER BY cdate DESC LIMIT 100 FOR UPDATE Lock wait timeout exceeded; try restarting transaction") */ function sendEmailsfromBuffer() { App.log("Send Emails from Buffer"); // send for (e in sugoi.db.BufferedMail.manager.search($sdate == null, {limit: 50, orderBy: -cdate}, false)) { e.lock(); if (e.isSent()) continue; App.log('Send Email id=#${e.id} - title=${e.title}'); e.finallySend(); Sys.sleep(0.1); } // delete old emails var threeMonthsAgo = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 3); sugoi.db.BufferedMail.manager.delete($cdate < threeMonthsAgo); // emails that cannot be sent for (e in sugoi.db.BufferedMail.manager.search($tries > 100, {limit: 50, orderBy: -cdate}, true)) { if (e.sender.email != App.config.get("default_email")) { var str = t._("Sorry, the email entitled ::title:: could not be sent.", {title: e.title}); App.quickMail(e.sender.email, t._("Email not sent"), str); } e.delete(); } } /** * Email product report when orders close **/ function sendOrdersByProductWhenOrdersClose() { var range = tools.DateTool.getLastHourRange(); // Sys.println("Time is "+Date.now()+"
"); // Sys.println('Find all distributions that have closed in the last hour from ${range.from} to ${range.to} \n
'); for (d in db.Distribution.manager.search($orderEndDate >= range.from && $orderEndDate < range.to, false)) { service.OrderService.sendOrdersByProductReport(d); } } function print(text) { Sys.println(text + "
"); } }