move invoice mailing to gacksbilling.py, lookup contact names when no billing contact...
[raven.git] / apps / gacks / gacksbilling.py
1 import datetime
2 from email.mime.text import MIMEText
3 import hashlib
4 import logging
5 from subprocess import Popen, PIPE
6 import time
7 import gacksaccount
8 import gacksinvoice
9 import gackspolicy
10 from gacksconfig import GacksConfig
11 from gacksinvoice import GacksInvoiceManager, STATE_INVOICED, STATE_PENDING, STATE_AGGREGATED, KIND_CYCLE_CHARGE, KIND_SLOT_CHARGE, KIND_CYCLE_AGGREGATE
12 from gackstime import GacksTime
13
14 glo_logger_name = "gacksapi"
15
16 def set_logger_name(x):
17     global glo_logger_name
18     glo_logger_name = x
19
20 class GacksBilling:
21    def __init__(self, accounts, invoices, policies, resources):
22        self.accounts = accounts
23        self.invoices = invoices
24        self.policies = policies
25        self.resources = resources
26        self.config = GacksConfig()
27
28    def mylogger(self):
29        return logging.getLogger(glo_logger_name)
30
31    def get_handlers(self):
32        handlers = []
33        for resource in self.resources.resources:
34            handler = resource.get_handler_obj()
35            if handler is not None:
36                handlers.append(handler)
37
38        return handlers
39
40    def get_default_billing_emails(self, account_name):
41        emails = []
42        for handler in self.get_handlers():
43            persons = handler.getPersons(account_name)
44            for person in persons:
45                email = person['email']
46                if email not in emails:
47                    emails.append(email)
48        return emails
49
50    def bill_membership(self):
51        self.mylogger().info("doing bill_membship")
52
53        accounts = self.accounts.get_accounts()
54        for acct in accounts:
55             policy = self.policies.get_policy(acct.level, None)
56             if policy is None:
57                 continue
58
59             self.invoices.bill_membership(acct, policy)
60
61    def auto_renew(self):
62        self.mylogger().info("doing auto-renew")
63
64        time_threshold = time.time() - 48*60*60*10
65        inv = self.invoices.get_invoice_prime({"account_id": "_GRP_",
66                                               "start_date": time_threshold,
67                                               "kind_id": [KIND_CYCLE_CHARGE, KIND_SLOT_CHARGE, KIND_CYCLE_AGGREGATE]},
68                                              lookup_objects=True)
69
70        handlers = self.get_handlers()
71
72        for charge in inv.charges:
73            account_name = charge.account_name
74            for handler in handlers:
75                handler.renewSlice(account_name, 30)
76
77    def terminate_or_renew_service(self):
78        self.mylogger().info("doing terminate_or_renew_service")
79
80        accounts = self.accounts.get_accounts()
81        for acct in accounts:
82             if acct.serviceStartDate <= 0:
83                 # no service start date, it's probably an old account, leave it alone
84                 continue
85
86             policy = self.policies.get_policy(acct.level, None)
87
88             if not policy:
89                 self.mylogger().warning("couldn't find policy for %s" % acct.name)
90                 continue
91
92             if policy.term <= 0:
93                 self.mylogger().info("term is zero or less for %s" % acct.name)
94                 continue
95
96             # compute when the service term should have ended
97             endDate = GacksTime.month_delta(acct.serviceStartDate, policy.term)
98
99             # if it has ended, then we need to either renew it or terminate it
100             if (endDate < time.time()):
101                 if acct.autoRenew:
102                     self.mylogger().info("renewing service on %s from %s to %s" % (acct.name, time.ctime(acct.serviceStartDate), time.ctime(endDate)))
103                     acct.serviceStartDate = endDate
104                     acct.commit()
105                 else:
106                     self.mylogger().info("terminating service on %s. Expired on %s" % (acct.name, time.ctime(endDate)))
107                     acct.level = "default"
108                     acct.commit()
109             else:
110                 self.mylogger().info("service on %s is still active (until %s)" % (acct.name, time.ctime(endDate)))
111
112    def generate_invoice_email(self, account, end_date=None, days=7):
113         if end_date==None:
114             now = datetime.datetime.now()
115             if now.weekday()!=6:
116                 # if it's not sunday, then find the last sunday
117                 last_sunday = now - datetime.timedelta(days=now.weekday()+1)
118             else:
119                 last_sunday = now
120             end_date = time.mktime(last_sunday.timetuple())
121
122         # round it to the nearest day
123         end_date = time.mktime(datetime.datetime.fromtimestamp(end_date).date().timetuple())
124
125         # include all time until the last minute of that day
126         end_date = end_date + 24*60*60 - 1
127
128         # add one second, since start date is a >= relation
129         start_date = end_date - days * 24*60*60 + 1
130
131         start_date_str = str(datetime.date.fromtimestamp(start_date))
132         end_date_str = str(datetime.date.fromtimestamp(end_date))
133
134         filter = {"account": account.name, "start_date": start_date, "end_date": end_date, "state": STATE_INVOICED}
135         invoice = self.invoices.get_invoice_prime(filter)
136         summary = invoice.get_summary()
137
138         gacks_secret = self.config.get("gacks", "account_secret")
139         account_token=hashlib.sha1(gacks_secret+account.name).hexdigest()[:8]
140
141         has_activity = (summary["charges"] > 0) or (summary["credits"] > 0)
142
143         text = ""
144         text += "This email confirms that your latest billing statement for the account %s is now available on Vicci.org. " % account.name
145         text += "This statement covers the period from %s to %s.\n\n" % (start_date_str, end_date_str)
146         text += "Unique best effort machines used: %d\n" % summary["be_machines"]
147         text += "Total est effort core-hours used: %0.2f\n" % summary["be_core_hours"]
148         text += "Unique reservation machines used: %d\n" % summary["resv_machines"]
149         text += "Total charges: $ %0.2f\n" % summary["charges"]
150         text += "Total credits: $ %0.2f\n\n" % summary["credits"]
151         text += "Billing contacts for this account are: " + account.billingContacts + ". "
152         text += "Please see the Invoice page on the Vicci web site for detailed information:\n\n"
153         text += "https://vicci.org/db/gacks/invoices.php?account_name=%s&start_date=%d&end_date=%d&state=3&account_token=%s\n\n" % (account.name, start_date, end_date, account_token)
154         text += "Sincerely,\n"
155         text += "Vicci.org\n\n"
156         return (text, has_activity)
157
158    def mail_invoice(self, account, end_date=None, days=7, email_list=[], send_blank=False):
159         (invoice_email, has_activity) = self.generate_invoice_email(account, end_date, days)
160
161         if (not has_activity) and (not send_blank):
162             return False
163
164         for email in email_list:
165             msg = MIMEText(invoice_email)
166             msg["From"] = "billing@vicci.org"
167             msg["To"] = email
168             msg["Subject"] = "Vicci Invoice"
169             p = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE)
170             p.communicate(msg.as_string())
171
172         # return True if invoices mailed
173         return True
174
175    def is_billable_account(self, name):
176        if name.startswith("system/"):
177            return False
178
179        if name.startswith("libvert_"):
180            return False
181
182        if name.startswith("user/"):
183            return False
184
185        if name in ["blankgroup",
186                    "pl_sirius",
187                    "princeton_codnsdemux", "princeton_comon", "princeton_coredirect", "princeton_slicestat", "princeton_vcoblitz"]:
188            return False
189
190        return True
191
192    def mail_invoices(self, filter={}, email_list=None):
193         what_i_did = {}
194         what_i_did["_filter"] = filter
195
196         accounts = self.accounts.get_accounts(filter)
197         for account in accounts:
198             if not self.is_billable_account(account.name):
199                 continue
200
201             if not account.billingContacts:
202                 account.billingContacts = ", ".join(self.get_default_billing_emails(account.name))
203                 account.commit()
204
205             if account.billingContacts:
206                 if email_list == None:
207                     this_email_list = [x.strip() for x in account.billingContacts.split(",")]
208                 else:
209                     this_email_list = email_list
210
211                 if (self.mail_invoice(account, email_list = this_email_list)):
212                     what_i_did[account.name] = email_list
213
214         return what_i_did
215
216    def do_nightly(self):
217         self.mylogger().info("doing do_nightly")
218
219         # terminate any accounts that it is time to terminate
220         self.terminate_or_renew_service()
221
222         # do monthly service fees
223         self.bill_membership()
224
225         # throw away any STATE_AGGREGATED charges older than 48 hours
226         self.invoices.prune_charges(STATE_AGGREGATED, 48*60*60)
227
228         # apply pending charges to nightly invoices
229         self.invoices.apply_invoices()
230
231         # auto-renew slices
232         self.auto_renew()
233
234