~etherpad/etherpad/ubuntu-lucid-backport

« back to all changes in this revision

Viewing changes to etherpad/src/etherpad/billing/team_billing.js

  • Committer: James Page
  • Date: 2011-04-13 08:00:43 UTC
  • Revision ID: james.page@canonical.com-20110413080043-eee2nq7y1v7cv2mp
* Refactoring to use native Ubuntu Java libraries. 
* debian/control:
  - use openjdk instead of sun's java
  - update maintainer
* debian/etherpad.init.orig, debian/etherpad.upstart:
  - move the init script out of the way
  - create a basic upstart script
  - note that the open office document conversion daemon was dropped
    from the upstart configuration; if this behavior is desired, please
    create a separate upstart job for it
* debian/rules:
  - just use basic dh_installinit, as it will pick up the new upstart job
* New release
* Changed maintainer to Packaging
* Fixed installation scripts
* Initial Release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * Copyright 2009 Google Inc.
 
3
 * 
 
4
 * Licensed under the Apache License, Version 2.0 (the "License");
 
5
 * you may not use this file except in compliance with the License.
 
6
 * You may obtain a copy of the License at
 
7
 * 
 
8
 *      http://www.apache.org/licenses/LICENSE-2.0
 
9
 * 
 
10
 * Unless required by applicable law or agreed to in writing, software
 
11
 * distributed under the License is distributed on an "AS-IS" BASIS,
 
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
13
 * See the License for the specific language governing permissions and
 
14
 * limitations under the License.
 
15
 */
 
16
 
 
17
import("execution");
 
18
import("exceptionutils");
 
19
import("sqlbase.sqlobj");
 
20
import("sqlbase.sqlcommon.inTransaction");
 
21
 
 
22
import("etherpad.billing.billing");
 
23
import("etherpad.globals");
 
24
import("etherpad.log");
 
25
import("etherpad.pro.domains");
 
26
import("etherpad.pro.pro_quotas");
 
27
import("etherpad.store.checkout");
 
28
import("etherpad.utils.renderTemplateAsString");
 
29
 
 
30
jimport("java.lang.System.out.println");
 
31
 
 
32
function recurringBillingNotifyUrl() {
 
33
  return "";
 
34
}
 
35
 
 
36
function _billing() {
 
37
  if (! appjet.cache.billing) {
 
38
    appjet.cache.billing = {};
 
39
  }
 
40
  return appjet.cache.billing;
 
41
}
 
42
 
 
43
function _lpad(str, width, padDigit) {
 
44
  str = String(str);
 
45
  padDigit = (padDigit === undefined ? ' ' : padDigit);
 
46
  var count = width - str.length;
 
47
  var prepend = []
 
48
  for (var i = 0; i < count; ++i) {
 
49
    prepend.push(padDigit);
 
50
  }
 
51
  return prepend.join("")+str;
 
52
}
 
53
 
 
54
// utility functions
 
55
 
 
56
function _dayToDateTime(date) {
 
57
  return [date.getFullYear(), _lpad(date.getMonth()+1, 2, '0'), _lpad(date.getDate(), 2, '0')].join("-");
 
58
}
 
59
 
 
60
function _createInvoice(subscription) {
 
61
  var maxUsers = getMaxUsers(subscription.customer);
 
62
  var invoice = inTransaction(function() {
 
63
    var invoiceId = billing.createInvoice();
 
64
    billing.updateInvoice(
 
65
      invoiceId, 
 
66
      {purchase: subscription.id, 
 
67
       amt: billing.dollarsToCents(calculateSubscriptionCost(maxUsers, subscription.coupon)),
 
68
       users: maxUsers});
 
69
    return billing.getInvoice(invoiceId);
 
70
  });  
 
71
  if (invoice) {
 
72
    resetMaxUsers(subscription.customer)
 
73
  }
 
74
  return invoice;
 
75
}
 
76
 
 
77
function getExpiredSubscriptions(date) {
 
78
  return sqlobj.selectMulti('billing_purchase',
 
79
                            {type: 'subscription', 
 
80
                             status: 'active',
 
81
                             paidThrough: ['<', _dayToDateTime(date)]});  
 
82
}
 
83
 
 
84
function getAllSubscriptions() {
 
85
  return sqlobj.selectMulti('billing_purchase', {type: 'subscription', status: 'active'});
 
86
}
 
87
 
 
88
function getSubscriptionForCustomer(customerId) {
 
89
  return sqlobj.selectSingle('billing_purchase',
 
90
                             {type: 'subscription',
 
91
                              customer: customerId});
 
92
}
 
93
 
 
94
function getOrCreateInvoice(subscription) {
 
95
  return inTransaction(function() {
 
96
    var existingInvoice = 
 
97
      sqlobj.selectSingle('billing_invoice',
 
98
                          {purchase: subscription.id, status: 'pending'});
 
99
    if (existingInvoice) {
 
100
      return existingInvoice;
 
101
    } else {
 
102
      return _createInvoice(subscription);
 
103
    }
 
104
  });
 
105
}
 
106
 
 
107
function getLatestPendingInvoice(subscriptionId) {
 
108
  return sqlobj.selectMulti('billing_invoice',
 
109
                            {purchase: subscriptionId, status: 'pending'},
 
110
                            {orderBy: '-time', limit: 1})[0];  
 
111
}
 
112
 
 
113
function getLatestPaidInvoice(subscriptionId) {
 
114
  return sqlobj.selectMulti('billing_invoice',
 
115
                            {purchase: subscriptionId, status: 'paid'},
 
116
                            {orderBy: '-time', limit: 1})[0];
 
117
}
 
118
 
 
119
function pendingTransactions(customer) {
 
120
  return billing.getPendingTransactionsForCustomer(customer);
 
121
}
 
122
 
 
123
function checkPendingTransactions(transactions) {
 
124
  // XXX: do nothing for now.
 
125
  return transactions.length > 0;
 
126
}
 
127
 
 
128
function getRecurringBillingTransactionId(customerId) {
 
129
  return sqlobj.selectSingle('billing_payment_info', {customer: customerId}).transaction;
 
130
}
 
131
 
 
132
function getRecurringBillingInfo(customerId) {
 
133
  return sqlobj.selectSingle('billing_payment_info', {customer: customerId});
 
134
}
 
135
 
 
136
function clearRecurringBillingInfo(customerId) {
 
137
  return sqlobj.deleteRows('billing_payment_info', {customer: customerId});
 
138
}
 
139
 
 
140
function setRecurringBillingInfo(customerId, fullName, email, paymentSummary, expiration, transactionId) {
 
141
  var info = {
 
142
    fullname: fullName,
 
143
    email: email,
 
144
    paymentsummary: paymentSummary,
 
145
    expiration: expiration,
 
146
    transaction: transactionId
 
147
  }
 
148
  inTransaction(function() {
 
149
    if (sqlobj.selectSingle('billing_payment_info', {customer: customerId})) {
 
150
      sqlobj.update('billing_payment_info', {customer: customerId}, info);
 
151
    } else {
 
152
      info.customer = customerId;
 
153
      sqlobj.insert('billing_payment_info', info);
 
154
    }
 
155
  });
 
156
}
 
157
 
 
158
function createSubscription(customerId, couponCode) {
 
159
  domainCacheClear(customerId);
 
160
  return inTransaction(function() {
 
161
    return billing.createSubscription(customerId, 'ONDEMAND', 0, couponCode);
 
162
  });
 
163
}
 
164
 
 
165
function updateSubscriptionCouponCode(subscriptionId, couponCode) {
 
166
  billing.updatePurchase(subscriptionId, {coupon: couponCode || ""});
 
167
}
 
168
 
 
169
function subscriptionChargeFailure(subscription, invoice, failureMessage) {
 
170
  billing.updatePurchase(subscription.id,
 
171
                         {error: failureMessage, status: 'inactive'});
 
172
  sendFailureEmail(subscription, invoice);
 
173
}
 
174
 
 
175
function subscriptionChargeSuccess(subscription, invoice) {
 
176
  sendReceiptEmail(subscription, invoice);
 
177
}
 
178
 
 
179
function errorFieldsToMessage(errorCodes) {
 
180
  var prefix = "Your payment information was rejected. Please verify your ";
 
181
  var errorList = (errorCodes.permanentErrors ? errorCodes.permanentErrors : errorCodes.userErrors);
 
182
 
 
183
  return prefix + 
 
184
    errorList.map(function(field) { 
 
185
      return checkout.billingCartFieldMap[field].d;
 
186
    }).join(", ")+
 
187
    "."
 
188
}
 
189
 
 
190
function getAllInvoices(customer) {
 
191
  var purchase = getSubscriptionForCustomer(customer);
 
192
  if (! purchase) {
 
193
    return [];
 
194
  }
 
195
  return billing.getInvoicesForPurchase(purchase.id);
 
196
}
 
197
 
 
198
// scheduled charges
 
199
 
 
200
function attemptCharge(invoice, subscription) {
 
201
  var billingInfo = getRecurringBillingInfo(subscription.customer);
 
202
  if (! billingInfo) {
 
203
    subscriptionChargeFailure(subscription, invoice, "No billing information on file.");
 
204
    return false;
 
205
  }
 
206
  
 
207
  var result = 
 
208
    billing.asyncRecurringPurchase(
 
209
      invoice.id, 
 
210
      subscription.id, 
 
211
      billingInfo.transaction,
 
212
      billingInfo.paymentsummary,
 
213
      billing.centsToDollars(invoice.amt),
 
214
      1, // 1 month only for now
 
215
      recurringBillingNotifyUrl);
 
216
  if (result.status == 'success') {
 
217
    subscriptionChargeSuccess(subscription, invoice);
 
218
    return true;
 
219
  } else {
 
220
    subscriptionChargeFailure(subscription, invoice, errorFieldsToMessage(result.errorField));
 
221
    return false;
 
222
  }
 
223
}
 
224
 
 
225
function processSubscription(subscription) {
 
226
  try {
 
227
    var hasPendingTransactions = inTransaction(function() {
 
228
      var transactions = pendingTransactions(subscription.customer);
 
229
      if (checkPendingTransactions(transactions)) {
 
230
        billing.log({type: 'pending-transactions-delay', subscription: subscription, transactions: transactions});
 
231
        // there are actual pending transactions. wait until tomorrow.
 
232
        return true;
 
233
      } else {
 
234
        return false;
 
235
      }
 
236
    });
 
237
    if (hasPendingTransactions) {
 
238
      return;
 
239
    }
 
240
    var invoice = getOrCreateInvoice(subscription);
 
241
    
 
242
    return attemptCharge(invoice, subscription);
 
243
  } catch (e) {
 
244
    log.logException(e);
 
245
    billing.log({message: "Thrown error", 
 
246
                 exception: exceptionutils.getStackTracePlain(e),
 
247
                 subscription: subscription});
 
248
    subscriptionChargeFailure(subscription, "Permanent failure. Please confirm your billing information.");
 
249
  } finally {
 
250
    domainCacheClear(subscription.customer);
 
251
  }
 
252
}
 
253
 
 
254
function processAllSubscriptions() {
 
255
  var subs = getExpiredSubscriptions(new Date);
 
256
  println("processing "+subs.length+" subscriptions.");
 
257
  subs.forEach(processSubscription);      
 
258
}
 
259
 
 
260
function _scheduleNextDailyUpdate() {
 
261
  // Run at 2:22am every day
 
262
  var now = +(new Date);
 
263
  var tomorrow = new Date(now + 1000*60*60*24);
 
264
  tomorrow.setHours(2);
 
265
  tomorrow.setMinutes(22);
 
266
  tomorrow.setMilliseconds(222);
 
267
  log.info("Scheduling next daily billing update for: "+tomorrow.toString());
 
268
  var delay = +tomorrow - (+(new Date));
 
269
  execution.scheduleTask('billing', "billingDailyUpdate", delay, []);
 
270
}
 
271
 
 
272
serverhandlers.tasks.billingDailyUpdate = function() {
 
273
  return; // do nothing, there's no more billing.
 
274
  // if (! globals.isProduction()) { return; }
 
275
  // try {
 
276
  //   processAllSubscriptions();
 
277
  // } finally {
 
278
  //   _scheduleNextDailyUpdate();
 
279
  // }
 
280
}
 
281
 
 
282
function onStartup() {
 
283
  execution.initTaskThreadPool("billing", 1);
 
284
  _scheduleNextDailyUpdate();
 
285
}
 
286
 
 
287
// pricing
 
288
 
 
289
function getMaxUsers(customer) {
 
290
  return pro_quotas.getAccountUsageCount(customer);
 
291
}
 
292
 
 
293
function resetMaxUsers(customer) {
 
294
  pro_quotas.resetAccountUsageCount(customer);
 
295
}
 
296
 
 
297
var COST_PER_USER = 8;
 
298
 
 
299
function getCouponValue(couponCode) {
 
300
  if (couponCode && couponCode.length == 8) {
 
301
    return sqlobj.selectSingle('checkout_pro_referral', {id: couponCode});
 
302
  }
 
303
}
 
304
 
 
305
function calculateSubscriptionCost(users, couponId) {
 
306
  if (users <= globals.PRO_FREE_ACCOUNTS) {
 
307
    return 0;
 
308
  }
 
309
  var coupon = getCouponValue(couponId);
 
310
  var pctDiscount = (coupon ? coupon.pctDiscount : 0);
 
311
  var freeUsers = (coupon ? coupon.freeUsers : 0);
 
312
  
 
313
  var cost = (users - freeUsers) * COST_PER_USER;
 
314
  cost = cost * (100-pctDiscount)/100;
 
315
  
 
316
  return Math.max(0, cost);
 
317
}
 
318
 
 
319
// currentDomainsCache
 
320
 
 
321
function _cache() {
 
322
  if (! appjet.cache.currentDomainsCache) {
 
323
    appjet.cache.currentDomainsCache = {};
 
324
  }
 
325
  return appjet.cache.currentDomainsCache;
 
326
}
 
327
 
 
328
function domainCacheClear(domain) {
 
329
  delete _cache()[domain];
 
330
}
 
331
 
 
332
function _domainCacheGetOrUpdate(domain, f) {
 
333
  if (domain in _cache()) {
 
334
    return _cache()[domain];
 
335
  }
 
336
  
 
337
  _cache()[domain] = f();
 
338
  return _cache()[domain];
 
339
}
 
340
 
 
341
// external API helpers 
 
342
 
 
343
function _getPaidThroughDate(domainId) {
 
344
  return _domainCacheGetOrUpdate(domainId, function() {
 
345
    var subscription = getSubscriptionForCustomer(domainId);
 
346
    if (! subscription) {
 
347
      return null;
 
348
    } else {
 
349
      return subscription.paidThrough;
 
350
    }
 
351
  });  
 
352
}
 
353
 
 
354
// external API
 
355
 
 
356
var GRACE_PERIOD_DAYS = 10;
 
357
 
 
358
var CURRENT = 0;
 
359
var PAST_DUE = 1;
 
360
var SUSPENDED = 2;
 
361
var NO_BILLING_INFO = 3;
 
362
 
 
363
function getDomainStatus(domainId) {
 
364
  var paidThrough = _getPaidThroughDate(domainId);
 
365
  
 
366
  if (paidThrough == null) {
 
367
    return NO_BILLING_INFO;
 
368
  }
 
369
  if (paidThrough.getTime() > new Date(Date.now()-86400*1000)) {
 
370
    return CURRENT;
 
371
  }
 
372
  // less than GRACE_PERIOD_DAYS have passed since paidThrough date
 
373
  if (paidThrough.getTime() > Date.now() - GRACE_PERIOD_DAYS*86400*1000) {
 
374
    return PAST_DUE;
 
375
  }
 
376
  return SUSPENDED;
 
377
}
 
378
 
 
379
function getDomainDueDate(domainId) {
 
380
  return _getPaidThroughDate(domainId);
 
381
}
 
382
 
 
383
function getDomainSuspensionDate(domainId) {
 
384
  return new Date(_getPaidThroughDate(domainId).getTime() + GRACE_PERIOD_DAYS*86400*1000);
 
385
}
 
386
 
 
387
// emails
 
388
 
 
389
function sendReceiptEmail(subscription, invoice) {
 
390
  var paymentInfo = getRecurringBillingInfo(subscription.customer);
 
391
  var coupon = getCouponValue(subscription.coupon);
 
392
  var emailText = renderTemplateAsString('email/pro_payment_receipt.ejs', {
 
393
    fullName: paymentInfo.fullname,
 
394
    paymentSummary: paymentInfo.paymentsummary,
 
395
    expiration: checkout.formatExpiration(paymentInfo.expiration),
 
396
    invoiceNumber: invoice.id,
 
397
    numUsers: invoice.users,
 
398
    cost: billing.centsToDollars(invoice.amt),
 
399
    dollars: checkout.dollars,
 
400
    coupon: coupon,
 
401
    globals: globals
 
402
  });
 
403
  var address = paymentInfo.email;
 
404
  checkout.salesEmail(address, "sales@etherpad.com", "EtherPad: Receipt for "+paymentInfo.fullname,
 
405
                      {}, emailText);
 
406
}
 
407
 
 
408
function sendFailureEmail(subscription, invoice, failureMessage) {
 
409
  var domain = subscription.customer;
 
410
  var subDomain = domains.getDomainRecord(domain).subDomain;
 
411
  var paymentInfo = getRecurringBillingInfo(subscription.customer);
 
412
  var emailText = renderTemplateAsString('email/pro_payment_failure.ejs', {
 
413
    fullName: paymentInfo.fullname,
 
414
    billingError: failureMessage,
 
415
    balance: "US $"+checkout.dollars(billing.centsToDollars(invoice.amt)),
 
416
    suspensionDate: checkout.formatDate(new Date(subscription.paidThrough.getTime()+GRACE_PERIOD_DAYS*86400*1000)),
 
417
    billingAdminLink: "https://"+subDomain+".etherpad.com/ep/admin/billing/"
 
418
  });
 
419
  var address = paymentInfo.email;
 
420
  checkout.salesEmail(address, "sales@etherpad.com", "EtherPad: Payment Failure for "+paymentInfo.fullname,
 
421
                      {}, emailText);
 
422
}
 
 
b'\\ No newline at end of file'