dmdb/core/Cronjob.cs

427 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using dezentrale.model;
namespace dezentrale.core
{
//! \short Functionality for automatic processing that can be run in a scheduled way
//! \brief A huge port of the dezentrale-members software is designed to help with
//! previously manual tasks by defining an automated manner / refresh user
//! data according to repeated actions, e.g. membership payments.
public class Cronjob
{
//! \brief Perform a Run on one Member. This will perform all necessary actions
//! at this point.
//! \param m Member to check
//! \param smContact Mail contact entry for the Schatzmeister account, in order to send notification mails for some events
public static void Run(Member m, ConfigEMail smContact)
{
Console.WriteLine($"Processing member {m.Number:D3} ({m.Nickname})...");
CheckGreeting(m);
CheckSpawning(m);
// CheckReducedFeeValidity(m);
CheckBalance_PreDegrade(m, smContact);
BalanceDegrade(m);
CheckBalance_PostDegrade(m, smContact);
}
//! \short Helper function for Run(m, sm). Determines sm automatically.
//! \param m Member to check
public static void Run(Member m)
{
Run(m, Program.config.Schatzmeister);
}
//! \short Perform a cronjob run on a List of members.
//! \brief Perform a cronjob run on a List of members. This may be a
//! partial List (e.g. by checking member entries from GUI)sm
//! \param partialList List of members to process. If null, the program will process the complete member list.
public static void Run(List<Member> partialList = null)
{
if (partialList == null || partialList == Program.members.Entries)
{
foreach (Member m in Program.members.Entries) Run(m, Program.config.Schatzmeister);
//GenerateReport();
if (!Program.config.LastCronjobRun.Equals(ProgramStartTime))
{
try { new MemberReport(true).Send(Program.config.Schatzmeister); }
catch (Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to Schatzmeister: {ex.Message}"); }
try { new MemberReport(false).Send(Program.config.Schriftfuehrer); }
catch (Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to schriftfuehrer: {ex.Message}"); }
Console.WriteLine("Cronjob.Run(): Member Reports sent.");
}
Program.config.LastCronjobRun = ProgramStartTime;
try
{
XmlData.SaveToFile(Program.ConfigFile, Program.config);
Console.WriteLine("Cronjob.Run(): Saved config.");
}
catch (Exception ex)
{
Console.WriteLine($"Cronjob.Run(): Error while saving config: {ex.Message}");
}
}
else
{
foreach (Member m in partialList) Run(m, Program.config.Schatzmeister);
}
}
//! \short Send greetings E-Mail to new member, if conditions are met.
//! \note Also sends a mail to vorstand mailinglist
//! \param m Member to check
private static void CheckGreeting(Member m)
{
if (m.Status == Member.eStatus.Uninitialized && m.EMail.Length > 0)
{
try
{
//Send greetings mail,//send mail to vorstand
m.StartLogEvent("Sending greetings", LogEvent.eEventType.Greetings, "automatic");
try
{
FormMail tmp = FormMail.GenerateNewMemberWelcome().ReplaceReflect(m);
LogSubEvent lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to user failed");
} catch (Exception ex) { throw ex; }
try
{
FormMail tmp = FormMail.GenerateNewMemberVorstand().ReplaceReflect(Program.config.VS).ReplaceReflect(m);
LogSubEvent lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to vorstand failed");
} catch(Exception ex) { throw ex; }
}
catch (Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = $"Failed:\n{ex.Message}",
});
Console.WriteLine($"Cannot send mail: {ex.Message}");
m.SaveToFile();
return;
}
m.GreetedDate = DateTime.Now;
m.Status = Member.eStatus.Greeted;
m.SaveToFile();
}
}
//! \short Check if a previously greeted member is ready to become active.
//! \note Also sends a mail to vorstand mailinglist
private static void CheckSpawning(Member m)
{
if(m.Status != Member.eStatus.Greeted)
{
//Console.WriteLine("status != GREETED");
return;
}
{
TimeSpan ts = DateTime.Now.Subtract(m.GreetedDate);
if (ts.TotalDays < 7)
{
Console.WriteLine("TotalDays < 7");
return;
}
if (m.SpawnDate > DateTime.Now)
{
Console.WriteLine("spawnDate > now");
return;
}
if ((m.AccountBalance < (Int64) m.PaymentAmount) && (m.PaymentsTotal < m.PaymentAmount))
{
Console.WriteLine("payments < paymentAmount");
return;
}
if (m.AccountBalance < 0)
{
Console.WriteLine("accountbalance < 0");
return;
}
Console.WriteLine("Sending activation mail");
m.StartLogEvent("Sending activation mail", LogEvent.eEventType.Activation, "automatic");
//Send greetings mail, send mail to vorstand
try
{
FormMail
tmp = FormMail.GenerateMemberAccountActivated().ReplaceReflect(Program.config).ReplaceReflect(m);
LogSubEvent
lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to user failed");
tmp = FormMail.GenerateMemberAccountVorstand().ReplaceReflect(Program.config).ReplaceReflect(Program.config.VS).ReplaceReflect(m);
lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to vorstand failed");
}
catch (Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = $"Failed:\n{ex.Message}",
});
Console.WriteLine($"Cannot send mail: {ex.Message}");
m.FinishLogEvent();
m.SaveToFile();
return;
}
m.Status = Member.eStatus.Active;
m.SaveToFile();
}
}
private static void CheckReducedFeeValidity(Member m)
{
TimeSpan ts = ProgramStartTime.Subtract(m.ReducedFeeValid);
TimeSpan tsLastMail = ProgramStartTime.Subtract(m.LastCronjobReducedFeeMail);
if (ts.TotalDays > 0 && ts.TotalDays > 6) //remind if late and max. once a week
{
m.StartLogEvent($"Reminder to hand in reduced fee prove", LogEvent.eEventType.EMail, "automatic");
LogSubEvent lse;
try
{
lse = FormMail.GenerateReducedFeeReminder().Send(m);
m.LastCronjobReducedFeeMail = ProgramStartTime;
} catch(Exception ex)
{
lse = new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "CheckReducedFeeValidity",
Details = $"Cannot send reminder mail: {ex.Message}"
};
}
m.CurrentLog.SubEvents.Add(lse);
m.SaveToFile();
}
}
//! \brief This shall degrade the balance by a membership fee per month.
//! Calls to this function within the same month will not degrade
//! balance. Subsequent calls at the end of the month, and then
//! at the start of the next month, will degrade balance.
//! \param m Member to check
private static void BalanceDegrade(Member m)
{
if (m.Status == Member.eStatus.Greeted && (DateTime.Now < m.SpawnDate)) return;
if (m.Status == Member.eStatus.Uninitialized || m.Status == Member.eStatus.Disabled || m.Status == Member.eStatus.Deleted) return;
uint thisMonth = (uint) DateTime.Now.Month;
uint thisYear = (uint) DateTime.Now.Year;
uint months = 0;
DateTime lastDegrade = m.LastBalanceDegrade;
if (lastDegrade < m.SpawnDate) lastDegrade = m.SpawnDate;
while (lastDegrade.Month != thisMonth) { lastDegrade = lastDegrade.AddMonths(1); months++; }
while (lastDegrade.Year < thisYear) { lastDegrade = lastDegrade.AddYears(1); months += 12; }
if (months > 0)
{
//We need to cap the negative balance here to -2 monthly fees.
if (m.AccountBalance > -2 * (Int64)m.PaymentAmount)
{
m.StartLogEvent($"Membership fee: {months} Months * {m.PaymentAmount} EURcents = {months * m.PaymentAmount} EURcents", LogEvent.eEventType.MembershipFee, "automatic");
m.AccountBalance -= (Int64)(((Int64)months) * (Int64)m.PaymentAmount);
m.LastBalanceDegrade = DateTime.Now;
} else
{
m.StartLogEvent($"Skipping membership fee for {months} Months ({months * m.PaymentAmount} EURcents) as balance is already <= 2 monthly fees!", LogEvent.eEventType.MembershipFee, "automatic");
m.LastBalanceDegrade = DateTime.Now;
}
m.SaveToFile();
}
}
//! \brief During program execution, we want to keep a constant
//! timestamp for cronjob activity, to be able to compare for
//! multiple runs (e.g. member list) and not to store it every
//! time to disk
private static DateTime ProgramStartTime { get; set; } = DateTime.Now;
//! \short Necessary actions prior to BalanceDegrade.
private static void CheckBalance_PreDegrade(Member m, ConfigEMail smContact = null)
{
}
private static void CheckBalance_PostDegrade(Member m, ConfigEMail smContact = null)
{
if (m.Status != Member.eStatus.Active || m.PaymentAmount == 0) return;
TimeSpan tsLastMail = ProgramStartTime.Subtract(m.LastCronjobBalanceMail);
bool skipInsufficientNotify = ((DateTime.Now.Day < 8) || (tsLastMail.TotalDays < 14));
if(m.Number == 37)
{
//Sixtus detected
Console.WriteLine("Sixtus detected");
}
int currentDebtLevel = 0; //no payments missed
//Account balance is capped to -2 monthly fees
//if (m.AccountBalance < -2 * (int)m.PaymentAmount) currentDeptLevel = -3;
if (m.AccountBalance <= -2 * (int)m.PaymentAmount) currentDebtLevel = -2;
else if (m.AccountBalance < 0) currentDebtLevel = -1;
else if (m.AccountBalance > 200 * 100) currentDebtLevel = 1;
bool debtLevelDecrease = (m.DebtLevel > currentDebtLevel);
bool debtLevelIncrease = (m.DebtLevel < currentDebtLevel);
if(!skipInsufficientNotify)
{
if (debtLevelDecrease) m.DebtLevel--;
else m.DebtLevel = currentDebtLevel;
//We don't set skipInsufficientNotify here as we don't want to not-warn
//if it's still negative
}
switch(m.DebtLevel)
{
case 1:
{
if (debtLevelIncrease)
{
//Member has given much money
m.StartLogEvent($"Excess amount of payments", LogEvent.eEventType.EMail, "automatic");
if (smContact != null)
{
FormMail above200 = FormMail.GenerateBalanceAbove200NotifySM().ReplaceReflect(m);
above200.To = smContact.ToString();
try
{
m.CurrentLog.SubEvents.Add(above200.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "(balance too high) Can't send Mail to Schatzmeister",
Details = ex.Message,
});
Console.WriteLine($"(balance too high) Can't send Mail to Schatzmeiste: {ex.Message}");
}
}
else
Console.WriteLine("ERROR: CheckBalance_PostDegrade: sm = null");
}
} break;
case 0:
{
//all is fine.
if (debtLevelDecrease || debtLevelIncrease)
{
m.StartLogEvent($"Debt level changed", LogEvent.eEventType.DataChange, "automatic");
}
} break;
case -1:
{
if (!skipInsufficientNotify)
{
m.StartLogEvent($"Insufficient amount of payments #1", LogEvent.eEventType.EMail, "automatic");
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify1().Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #1 notification: {ex.Message}");
}
}
} break;
case -2:
{
if (!skipInsufficientNotify)
{
if(debtLevelDecrease)
{
m.StartLogEvent($"Insufficient amount of payments #2", LogEvent.eEventType.EMail, "automatic");
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify2().Send(m));
} catch (Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 notification: {ex.Message}");
}
FormMail below2sm = FormMail.GenerateBalanceNegativeNotify2SM().ReplaceReflect(m);
below2sm.To = smContact.ToString();
try
{
m.CurrentLog.SubEvents.Add(below2sm.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 SM notification: {ex.Message}");
}
} else
{
m.StartLogEvent($"Deactivation (insufficient payment)", LogEvent.eEventType.Deactivation, "automatic");
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivation().ReplaceReflect(Program.config.VS).Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error, Topic = "Email BalanceNegativeMemberDeactivation", Details = ex.Message,
});
}
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivationVS().ReplaceReflect(Program.config.VS).Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error, Topic = "Email BalanceNegativeMemberDeactivationVS", Details = ex.Message,
});
}
m.Status = Member.eStatus.Disabled;
}
}
} break;
default:
break;
}
//If there are data changes to the member, there's a log and we need to store everything
if (m.CurrentLog != null)
{
m.LastCronjobBalanceMail = DateTime.Now;
m.SaveToFile();
}
}
}
}