2
* Copyright 2009 Google Inc.
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
8
* http://www.apache.org/licenses/LICENSE-2.0
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.
18
import("exceptionutils");
19
import("sqlbase.sqlobj");
20
import("sqlbase.sqlcommon.inTransaction");
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");
30
jimport("java.lang.System.out.println");
32
function recurringBillingNotifyUrl() {
37
if (! appjet.cache.billing) {
38
appjet.cache.billing = {};
40
return appjet.cache.billing;
43
function _lpad(str, width, padDigit) {
45
padDigit = (padDigit === undefined ? ' ' : padDigit);
46
var count = width - str.length;
48
for (var i = 0; i < count; ++i) {
49
prepend.push(padDigit);
51
return prepend.join("")+str;
56
function _dayToDateTime(date) {
57
return [date.getFullYear(), _lpad(date.getMonth()+1, 2, '0'), _lpad(date.getDate(), 2, '0')].join("-");
60
function _createInvoice(subscription) {
61
var maxUsers = getMaxUsers(subscription.customer);
62
var invoice = inTransaction(function() {
63
var invoiceId = billing.createInvoice();
64
billing.updateInvoice(
66
{purchase: subscription.id,
67
amt: billing.dollarsToCents(calculateSubscriptionCost(maxUsers, subscription.coupon)),
69
return billing.getInvoice(invoiceId);
72
resetMaxUsers(subscription.customer)
77
function getExpiredSubscriptions(date) {
78
return sqlobj.selectMulti('billing_purchase',
79
{type: 'subscription',
81
paidThrough: ['<', _dayToDateTime(date)]});
84
function getAllSubscriptions() {
85
return sqlobj.selectMulti('billing_purchase', {type: 'subscription', status: 'active'});
88
function getSubscriptionForCustomer(customerId) {
89
return sqlobj.selectSingle('billing_purchase',
90
{type: 'subscription',
91
customer: customerId});
94
function getOrCreateInvoice(subscription) {
95
return inTransaction(function() {
97
sqlobj.selectSingle('billing_invoice',
98
{purchase: subscription.id, status: 'pending'});
99
if (existingInvoice) {
100
return existingInvoice;
102
return _createInvoice(subscription);
107
function getLatestPendingInvoice(subscriptionId) {
108
return sqlobj.selectMulti('billing_invoice',
109
{purchase: subscriptionId, status: 'pending'},
110
{orderBy: '-time', limit: 1})[0];
113
function getLatestPaidInvoice(subscriptionId) {
114
return sqlobj.selectMulti('billing_invoice',
115
{purchase: subscriptionId, status: 'paid'},
116
{orderBy: '-time', limit: 1})[0];
119
function pendingTransactions(customer) {
120
return billing.getPendingTransactionsForCustomer(customer);
123
function checkPendingTransactions(transactions) {
124
// XXX: do nothing for now.
125
return transactions.length > 0;
128
function getRecurringBillingTransactionId(customerId) {
129
return sqlobj.selectSingle('billing_payment_info', {customer: customerId}).transaction;
132
function getRecurringBillingInfo(customerId) {
133
return sqlobj.selectSingle('billing_payment_info', {customer: customerId});
136
function clearRecurringBillingInfo(customerId) {
137
return sqlobj.deleteRows('billing_payment_info', {customer: customerId});
140
function setRecurringBillingInfo(customerId, fullName, email, paymentSummary, expiration, transactionId) {
144
paymentsummary: paymentSummary,
145
expiration: expiration,
146
transaction: transactionId
148
inTransaction(function() {
149
if (sqlobj.selectSingle('billing_payment_info', {customer: customerId})) {
150
sqlobj.update('billing_payment_info', {customer: customerId}, info);
152
info.customer = customerId;
153
sqlobj.insert('billing_payment_info', info);
158
function createSubscription(customerId, couponCode) {
159
domainCacheClear(customerId);
160
return inTransaction(function() {
161
return billing.createSubscription(customerId, 'ONDEMAND', 0, couponCode);
165
function updateSubscriptionCouponCode(subscriptionId, couponCode) {
166
billing.updatePurchase(subscriptionId, {coupon: couponCode || ""});
169
function subscriptionChargeFailure(subscription, invoice, failureMessage) {
170
billing.updatePurchase(subscription.id,
171
{error: failureMessage, status: 'inactive'});
172
sendFailureEmail(subscription, invoice);
175
function subscriptionChargeSuccess(subscription, invoice) {
176
sendReceiptEmail(subscription, invoice);
179
function errorFieldsToMessage(errorCodes) {
180
var prefix = "Your payment information was rejected. Please verify your ";
181
var errorList = (errorCodes.permanentErrors ? errorCodes.permanentErrors : errorCodes.userErrors);
184
errorList.map(function(field) {
185
return checkout.billingCartFieldMap[field].d;
190
function getAllInvoices(customer) {
191
var purchase = getSubscriptionForCustomer(customer);
195
return billing.getInvoicesForPurchase(purchase.id);
200
function attemptCharge(invoice, subscription) {
201
var billingInfo = getRecurringBillingInfo(subscription.customer);
203
subscriptionChargeFailure(subscription, invoice, "No billing information on file.");
208
billing.asyncRecurringPurchase(
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);
220
subscriptionChargeFailure(subscription, invoice, errorFieldsToMessage(result.errorField));
225
function processSubscription(subscription) {
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.
237
if (hasPendingTransactions) {
240
var invoice = getOrCreateInvoice(subscription);
242
return attemptCharge(invoice, subscription);
245
billing.log({message: "Thrown error",
246
exception: exceptionutils.getStackTracePlain(e),
247
subscription: subscription});
248
subscriptionChargeFailure(subscription, "Permanent failure. Please confirm your billing information.");
250
domainCacheClear(subscription.customer);
254
function processAllSubscriptions() {
255
var subs = getExpiredSubscriptions(new Date);
256
println("processing "+subs.length+" subscriptions.");
257
subs.forEach(processSubscription);
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, []);
272
serverhandlers.tasks.billingDailyUpdate = function() {
273
return; // do nothing, there's no more billing.
274
// if (! globals.isProduction()) { return; }
276
// processAllSubscriptions();
278
// _scheduleNextDailyUpdate();
282
function onStartup() {
283
execution.initTaskThreadPool("billing", 1);
284
_scheduleNextDailyUpdate();
289
function getMaxUsers(customer) {
290
return pro_quotas.getAccountUsageCount(customer);
293
function resetMaxUsers(customer) {
294
pro_quotas.resetAccountUsageCount(customer);
297
var COST_PER_USER = 8;
299
function getCouponValue(couponCode) {
300
if (couponCode && couponCode.length == 8) {
301
return sqlobj.selectSingle('checkout_pro_referral', {id: couponCode});
305
function calculateSubscriptionCost(users, couponId) {
306
if (users <= globals.PRO_FREE_ACCOUNTS) {
309
var coupon = getCouponValue(couponId);
310
var pctDiscount = (coupon ? coupon.pctDiscount : 0);
311
var freeUsers = (coupon ? coupon.freeUsers : 0);
313
var cost = (users - freeUsers) * COST_PER_USER;
314
cost = cost * (100-pctDiscount)/100;
316
return Math.max(0, cost);
319
// currentDomainsCache
322
if (! appjet.cache.currentDomainsCache) {
323
appjet.cache.currentDomainsCache = {};
325
return appjet.cache.currentDomainsCache;
328
function domainCacheClear(domain) {
329
delete _cache()[domain];
332
function _domainCacheGetOrUpdate(domain, f) {
333
if (domain in _cache()) {
334
return _cache()[domain];
337
_cache()[domain] = f();
338
return _cache()[domain];
341
// external API helpers
343
function _getPaidThroughDate(domainId) {
344
return _domainCacheGetOrUpdate(domainId, function() {
345
var subscription = getSubscriptionForCustomer(domainId);
346
if (! subscription) {
349
return subscription.paidThrough;
356
var GRACE_PERIOD_DAYS = 10;
361
var NO_BILLING_INFO = 3;
363
function getDomainStatus(domainId) {
364
var paidThrough = _getPaidThroughDate(domainId);
366
if (paidThrough == null) {
367
return NO_BILLING_INFO;
369
if (paidThrough.getTime() > new Date(Date.now()-86400*1000)) {
372
// less than GRACE_PERIOD_DAYS have passed since paidThrough date
373
if (paidThrough.getTime() > Date.now() - GRACE_PERIOD_DAYS*86400*1000) {
379
function getDomainDueDate(domainId) {
380
return _getPaidThroughDate(domainId);
383
function getDomainSuspensionDate(domainId) {
384
return new Date(_getPaidThroughDate(domainId).getTime() + GRACE_PERIOD_DAYS*86400*1000);
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,
403
var address = paymentInfo.email;
404
checkout.salesEmail(address, "sales@etherpad.com", "EtherPad: Receipt for "+paymentInfo.fullname,
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/"
419
var address = paymentInfo.email;
420
checkout.salesEmail(address, "sales@etherpad.com", "EtherPad: Payment Failure for "+paymentInfo.fullname,
b'\\ No newline at end of file'