aboutsummaryrefslogtreecommitdiffstats
path: root/trunk/etherpad/src/etherpad/billing/team_billing.js
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/etherpad/src/etherpad/billing/team_billing.js')
-rw-r--r--trunk/etherpad/src/etherpad/billing/team_billing.js422
1 files changed, 422 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/billing/team_billing.js b/trunk/etherpad/src/etherpad/billing/team_billing.js
new file mode 100644
index 0000000..ae8ae8a
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/billing/team_billing.js
@@ -0,0 +1,422 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("exceptionutils");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.billing");
+import("etherpad.globals");
+import("etherpad.log");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.store.checkout");
+import("etherpad.utils.renderTemplateAsString");
+
+jimport("java.lang.System.out.println");
+
+function recurringBillingNotifyUrl() {
+ return "";
+}
+
+function _billing() {
+ if (! appjet.cache.billing) {
+ appjet.cache.billing = {};
+ }
+ return appjet.cache.billing;
+}
+
+function _lpad(str, width, padDigit) {
+ str = String(str);
+ padDigit = (padDigit === undefined ? ' ' : padDigit);
+ var count = width - str.length;
+ var prepend = []
+ for (var i = 0; i < count; ++i) {
+ prepend.push(padDigit);
+ }
+ return prepend.join("")+str;
+}
+
+// utility functions
+
+function _dayToDateTime(date) {
+ return [date.getFullYear(), _lpad(date.getMonth()+1, 2, '0'), _lpad(date.getDate(), 2, '0')].join("-");
+}
+
+function _createInvoice(subscription) {
+ var maxUsers = getMaxUsers(subscription.customer);
+ var invoice = inTransaction(function() {
+ var invoiceId = billing.createInvoice();
+ billing.updateInvoice(
+ invoiceId,
+ {purchase: subscription.id,
+ amt: billing.dollarsToCents(calculateSubscriptionCost(maxUsers, subscription.coupon)),
+ users: maxUsers});
+ return billing.getInvoice(invoiceId);
+ });
+ if (invoice) {
+ resetMaxUsers(subscription.customer)
+ }
+ return invoice;
+}
+
+function getExpiredSubscriptions(date) {
+ return sqlobj.selectMulti('billing_purchase',
+ {type: 'subscription',
+ status: 'active',
+ paidThrough: ['<', _dayToDateTime(date)]});
+}
+
+function getAllSubscriptions() {
+ return sqlobj.selectMulti('billing_purchase', {type: 'subscription', status: 'active'});
+}
+
+function getSubscriptionForCustomer(customerId) {
+ return sqlobj.selectSingle('billing_purchase',
+ {type: 'subscription',
+ customer: customerId});
+}
+
+function getOrCreateInvoice(subscription) {
+ return inTransaction(function() {
+ var existingInvoice =
+ sqlobj.selectSingle('billing_invoice',
+ {purchase: subscription.id, status: 'pending'});
+ if (existingInvoice) {
+ return existingInvoice;
+ } else {
+ return _createInvoice(subscription);
+ }
+ });
+}
+
+function getLatestPendingInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'pending'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function getLatestPaidInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'paid'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function pendingTransactions(customer) {
+ return billing.getPendingTransactionsForCustomer(customer);
+}
+
+function checkPendingTransactions(transactions) {
+ // XXX: do nothing for now.
+ return transactions.length > 0;
+}
+
+function getRecurringBillingTransactionId(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId}).transaction;
+}
+
+function getRecurringBillingInfo(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId});
+}
+
+function clearRecurringBillingInfo(customerId) {
+ return sqlobj.deleteRows('billing_payment_info', {customer: customerId});
+}
+
+function setRecurringBillingInfo(customerId, fullName, email, paymentSummary, expiration, transactionId) {
+ var info = {
+ fullname: fullName,
+ email: email,
+ paymentsummary: paymentSummary,
+ expiration: expiration,
+ transaction: transactionId
+ }
+ inTransaction(function() {
+ if (sqlobj.selectSingle('billing_payment_info', {customer: customerId})) {
+ sqlobj.update('billing_payment_info', {customer: customerId}, info);
+ } else {
+ info.customer = customerId;
+ sqlobj.insert('billing_payment_info', info);
+ }
+ });
+}
+
+function createSubscription(customerId, couponCode) {
+ domainCacheClear(customerId);
+ return inTransaction(function() {
+ return billing.createSubscription(customerId, 'ONDEMAND', 0, couponCode);
+ });
+}
+
+function updateSubscriptionCouponCode(subscriptionId, couponCode) {
+ billing.updatePurchase(subscriptionId, {coupon: couponCode || ""});
+}
+
+function subscriptionChargeFailure(subscription, invoice, failureMessage) {
+ billing.updatePurchase(subscription.id,
+ {error: failureMessage, status: 'inactive'});
+ sendFailureEmail(subscription, invoice);
+}
+
+function subscriptionChargeSuccess(subscription, invoice) {
+ sendReceiptEmail(subscription, invoice);
+}
+
+function errorFieldsToMessage(errorCodes) {
+ var prefix = "Your payment information was rejected. Please verify your ";
+ var errorList = (errorCodes.permanentErrors ? errorCodes.permanentErrors : errorCodes.userErrors);
+
+ return prefix +
+ errorList.map(function(field) {
+ return checkout.billingCartFieldMap[field].d;
+ }).join(", ")+
+ "."
+}
+
+function getAllInvoices(customer) {
+ var purchase = getSubscriptionForCustomer(customer);
+ if (! purchase) {
+ return [];
+ }
+ return billing.getInvoicesForPurchase(purchase.id);
+}
+
+// scheduled charges
+
+function attemptCharge(invoice, subscription) {
+ var billingInfo = getRecurringBillingInfo(subscription.customer);
+ if (! billingInfo) {
+ subscriptionChargeFailure(subscription, invoice, "No billing information on file.");
+ return false;
+ }
+
+ var result =
+ billing.asyncRecurringPurchase(
+ invoice.id,
+ subscription.id,
+ billingInfo.transaction,
+ billingInfo.paymentsummary,
+ billing.centsToDollars(invoice.amt),
+ 1, // 1 month only for now
+ recurringBillingNotifyUrl);
+ if (result.status == 'success') {
+ subscriptionChargeSuccess(subscription, invoice);
+ return true;
+ } else {
+ subscriptionChargeFailure(subscription, invoice, errorFieldsToMessage(result.errorField));
+ return false;
+ }
+}
+
+function processSubscription(subscription) {
+ try {
+ var hasPendingTransactions = inTransaction(function() {
+ var transactions = pendingTransactions(subscription.customer);
+ if (checkPendingTransactions(transactions)) {
+ billing.log({type: 'pending-transactions-delay', subscription: subscription, transactions: transactions});
+ // there are actual pending transactions. wait until tomorrow.
+ return true;
+ } else {
+ return false;
+ }
+ });
+ if (hasPendingTransactions) {
+ return;
+ }
+ var invoice = getOrCreateInvoice(subscription);
+
+ return attemptCharge(invoice, subscription);
+ } catch (e) {
+ log.logException(e);
+ billing.log({message: "Thrown error",
+ exception: exceptionutils.getStackTracePlain(e),
+ subscription: subscription});
+ subscriptionChargeFailure(subscription, "Permanent failure. Please confirm your billing information.");
+ } finally {
+ domainCacheClear(subscription.customer);
+ }
+}
+
+function processAllSubscriptions() {
+ var subs = getExpiredSubscriptions(new Date);
+ println("processing "+subs.length+" subscriptions.");
+ subs.forEach(processSubscription);
+}
+
+function _scheduleNextDailyUpdate() {
+ // Run at 2:22am every day
+ var now = +(new Date);
+ var tomorrow = new Date(now + 1000*60*60*24);
+ tomorrow.setHours(2);
+ tomorrow.setMinutes(22);
+ tomorrow.setMilliseconds(222);
+ log.info("Scheduling next daily billing update for: "+tomorrow.toString());
+ var delay = +tomorrow - (+(new Date));
+ execution.scheduleTask('billing', "billingDailyUpdate", delay, []);
+}
+
+serverhandlers.tasks.billingDailyUpdate = function() {
+ return; // do nothing, there's no more billing.
+ // if (! globals.isProduction()) { return; }
+ // try {
+ // processAllSubscriptions();
+ // } finally {
+ // _scheduleNextDailyUpdate();
+ // }
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("billing", 1);
+ _scheduleNextDailyUpdate();
+}
+
+// pricing
+
+function getMaxUsers(customer) {
+ return pro_quotas.getAccountUsageCount(customer);
+}
+
+function resetMaxUsers(customer) {
+ pro_quotas.resetAccountUsageCount(customer);
+}
+
+var COST_PER_USER = 8;
+
+function getCouponValue(couponCode) {
+ if (couponCode && couponCode.length == 8) {
+ return sqlobj.selectSingle('checkout_pro_referral', {id: couponCode});
+ }
+}
+
+function calculateSubscriptionCost(users, couponId) {
+ if (users <= globals.PRO_FREE_ACCOUNTS) {
+ return 0;
+ }
+ var coupon = getCouponValue(couponId);
+ var pctDiscount = (coupon ? coupon.pctDiscount : 0);
+ var freeUsers = (coupon ? coupon.freeUsers : 0);
+
+ var cost = (users - freeUsers) * COST_PER_USER;
+ cost = cost * (100-pctDiscount)/100;
+
+ return Math.max(0, cost);
+}
+
+// currentDomainsCache
+
+function _cache() {
+ if (! appjet.cache.currentDomainsCache) {
+ appjet.cache.currentDomainsCache = {};
+ }
+ return appjet.cache.currentDomainsCache;
+}
+
+function domainCacheClear(domain) {
+ delete _cache()[domain];
+}
+
+function _domainCacheGetOrUpdate(domain, f) {
+ if (domain in _cache()) {
+ return _cache()[domain];
+ }
+
+ _cache()[domain] = f();
+ return _cache()[domain];
+}
+
+// external API helpers
+
+function _getPaidThroughDate(domainId) {
+ return _domainCacheGetOrUpdate(domainId, function() {
+ var subscription = getSubscriptionForCustomer(domainId);
+ if (! subscription) {
+ return null;
+ } else {
+ return subscription.paidThrough;
+ }
+ });
+}
+
+// external API
+
+var GRACE_PERIOD_DAYS = 10;
+
+var CURRENT = 0;
+var PAST_DUE = 1;
+var SUSPENDED = 2;
+var NO_BILLING_INFO = 3;
+
+function getDomainStatus(domainId) {
+ var paidThrough = _getPaidThroughDate(domainId);
+
+ if (paidThrough == null) {
+ return NO_BILLING_INFO;
+ }
+ if (paidThrough.getTime() > new Date(Date.now()-86400*1000)) {
+ return CURRENT;
+ }
+ // less than GRACE_PERIOD_DAYS have passed since paidThrough date
+ if (paidThrough.getTime() > Date.now() - GRACE_PERIOD_DAYS*86400*1000) {
+ return PAST_DUE;
+ }
+ return SUSPENDED;
+}
+
+function getDomainDueDate(domainId) {
+ return _getPaidThroughDate(domainId);
+}
+
+function getDomainSuspensionDate(domainId) {
+ return new Date(_getPaidThroughDate(domainId).getTime() + GRACE_PERIOD_DAYS*86400*1000);
+}
+
+// emails
+
+function sendReceiptEmail(subscription, invoice) {
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var coupon = getCouponValue(subscription.coupon);
+ var emailText = renderTemplateAsString('email/pro_payment_receipt.ejs', {
+ fullName: paymentInfo.fullname,
+ paymentSummary: paymentInfo.paymentsummary,
+ expiration: checkout.formatExpiration(paymentInfo.expiration),
+ invoiceNumber: invoice.id,
+ numUsers: invoice.users,
+ cost: billing.centsToDollars(invoice.amt),
+ dollars: checkout.dollars,
+ coupon: coupon,
+ globals: globals
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Receipt for "+paymentInfo.fullname,
+ {}, emailText);
+}
+
+function sendFailureEmail(subscription, invoice, failureMessage) {
+ var domain = subscription.customer;
+ var subDomain = domains.getDomainRecord(domain).subDomain;
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var emailText = renderTemplateAsString('email/pro_payment_failure.ejs', {
+ fullName: paymentInfo.fullname,
+ billingError: failureMessage,
+ balance: "US $"+checkout.dollars(billing.centsToDollars(invoice.amt)),
+ suspensionDate: checkout.formatDate(new Date(subscription.paidThrough.getTime()+GRACE_PERIOD_DAYS*86400*1000)),
+ billingAdminLink: "https://"+subDomain+".pad.spline.inf.fu-berlin.de/ep/admin/billing/"
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Payment Failure for "+paymentInfo.fullname,
+ {}, emailText);
+} \ No newline at end of file