diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/billing/team_billing.js')
-rw-r--r-- | trunk/etherpad/src/etherpad/billing/team_billing.js | 422 |
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 |