text: /**
   * 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, appjet.config['etherpad.email.fromAddr'], "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+"."+appjet.config['fromDomain']+"/ep/admin/billing/"
    });
    var address = paymentInfo.email;
    checkout.salesEmail(address, appjet.config['etherpad.email.fromAddr'], "EtherPad: Payment Failure for "+paymentInfo.fullname,
                        {}, emailText);
  }
  