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("dateutils.*");
  import("fastJSON");
  import("jsutils.eachProperty");
  import("netutils.urlPost");
  import("sqlbase.sqlbase");
  import("sqlbase.sqlcommon");
  import("sqlbase.sqlobj");
  import("stringutils.{md5,repeat}");
  
  import("etherpad.log.{custom=>eplog}");
  
  
  jimport("java.lang.System.out.println");
  
  function clearKeys(obj, keys) {
    var newObj = {};
    eachProperty(obj, function(k, v) {
      var isCopied = false;
      keys.forEach(function(key) {
        if (k == key.name &&
            key.valueTest(v)) {
          newObj[k] = key.valueReplace(v);
          isCopied = true;
        }
      });
      if (! isCopied) {
        if (typeof(obj[k]) == 'object') {
          newObj[k] = clearKeys(v, keys);
        } else {
          newObj[k] = v;
        }
      }
    });
    return newObj;
  }
  
  function replaceWithX(s) {
    return repeat("X", s.length);
  }
  
  function log(obj) {  
    eplog('billing', clearKeys(obj, [
      {name: "ACCT", 
       valueTest: function(s) { return /^\d{15,16}$/.test(s) }, 
       valueReplace: replaceWithX},
      {name: "CVV2", 
       valueTest: function(s) { return /^\d{3,4}$/.test(s) },
       valueReplace: replaceWithX}]));
  }
  
  var _USER = function() { return appjet.config['etherpad.paypal.user'] || "zamfir_1239051855_biz_api1.gmail.com"; }
  var _PWD = function() { return appjet.config['etherpad.paypal.pwd'] || "1239051867"; }
  var _SIGNATURE = function() { return appjet.config['etherpad.paypal.signature'] || "AQU0e5vuZCvSg-XJploSa.sGUDlpAwAy5fz.FhtfOQ25Qa9sFLDt7Bmp"; }
  var _RECEIVER = function() { return appjet.config['etherpad.paypal.receiver'] || "zamfir_1239051855_biz@gmail.com"; }
  var _paypalApiUrl = function() { return appjet.config['etherpad.paypal.apiUrl'] || "https://api-3t.sandbox.paypal.com/nvp"; }
  var _paypalWebUrl = function() { return appjet.config['etherpad.paypal.webUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr"; }
  function paypalPurchaseUrl(token) {
    return (appjet.config['etherpad.paypal.purchaseUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=")+token;
  }
  
  function getPurchase(id) {
    return sqlobj.selectSingle('billing_purchase', {id: id});
  }
  
  function getPurchaseForCustomer(customerId) {
    return sqlobj.selectSingle('billing_purchase', {customer: customerId});
  }
  
  function updatePurchase(id, fields) {
    sqlobj.updateSingle('billing_purchase', {id: id}, fields);
  }
  
  function getInvoicesForPurchase(purchaseId) {
    return sqlobj.selectMulti('billing_invoice', {purchase: purchaseId});
  }
  
  function getInvoice(id) {
    return sqlobj.selectSingle('billing_invoice', {id: id});
  }
  
  function createInvoice() {
    return _newInvoice();
  }
  
  function updateInvoice(id, fields) {
    sqlobj.updateSingle('billing_invoice', {id: id}, fields)
  }
  
  function getTransaction(id) {
    return sqlobj.selectSingle('billing_transaction', {id: id});
  }
  function getTransactionByExternalId(txnId) {
    return sqlobj.selectSingle('billing_transaction', {txnId: txnId});
  }
  
  function getTransactionsForCustomer(customerId) {
    return sqlobj.selectMulti('billing_transaction', {customer: customerId});
  }
  
  function getPendingTransactionsForCustomer(customerId) {
    return sqlobj.selectMulti('billing_transaction', {customer: customerId, status: 'pending'});
  }
  
  function _updateTransaction(id, fields) {
    return sqlobj.updateSingle('billing_transaction', {id: id}, fields);
  }
  
  function getAdjustments(invoiceId) {
    return sqlobj.selectMulti('billing_adjustment', {invoice: invoiceId});
  }
  
  function createSubscription(customer, product, dollars, couponCode) {
    var purchaseId = _newPurchase(customer, product, dollarsToCents(dollars), couponCode);
    _purchaseActive(purchaseId);
    updatePurchase(purchaseId, {type: 'subscription', paidThrough: nextMonth(noon(new Date))});
    return purchaseId;
  }
  
  function _newPurchase(customer, product, cents, couponCode) {
    var purchaseId = sqlobj.insert_return_id('billing_purchase', {
      customer: customer,
      product: product,
      cost: cents,
      coupon: couponCode,
      status: 'inactive'
    }, "id");
    return purchaseId;  
  }
  
  function _newInvoice() {
    var invoiceId = sqlobj.insert('billing_invoice', {
      time: new Date(),
      purchase: -1,
      amt: 0,
      status: 'pending'
    }, "id");
    return invoiceId;
  }
  
  function _newTransaction(customer, cents) {
    var transactionId = sqlobj.insert('billing_transaction', {
      customer: customer,
      time: new Date(),
      amt: cents,
      status: 'new'
    }, "id");
    return transactionId;
  }
  
  function _newAdjustment(transaction, invoice, cents) {
    sqlobj.insert('billing_adjustment', {
      transaction: transaction,
      invoice: invoice,
      time: new Date(),
      amt: cents
    });
  }
  
  function _transactionSuccess(transaction, txnId, payInfo) {
    _updateTransaction(transaction, {
      status: 'success', txnId: txnId, time: new Date(), payInfo: payInfo
    });
  }
  
  function _transactionFailure(transaction, txnId) {
    _updateTransaction(transaction, {
      status: 'failure', txnId: txnId, time: new Date()
    });
  }
  
  function _transactionPending(transaction, txnId) {
    _updateTransaction(transaction, {
      status: 'pending', txnId: txnId, time: new Date()
    });
  }
  
  function _invoicePaid(invoice) {
    updateInvoice(invoice, {status: 'paid'});
  }
  
  function _purchaseActive(purchase) {
    updatePurchase(purchase, {status: 'active'});
  }
  
  function _purchaseExtend(purchase, monthCount) {
    var expiration = getPurchase(purchase).paidThrough;
    for (var i = monthCount; i > 0; i--) {
      expiration = nextMonth(expiration);
    }
    // paying your invoice always makes you current.
    if (expiration < new Date) {
      expiration = nextMonth(new Date);
    }
    updatePurchase(purchase, {paidThrough: expiration});
  }
  
  function _doPost(url, body) {
    try {
      var ret = urlPost(url, body);
    } catch (e) {
      if (e.javaException) {
        net.appjet.oui.exceptionlog.apply(e.javaException);
      }
      return { error: e };
    }
    return { value: ret };
  }
  
  function _doPaypalNvpPost(properties0) {
    return {
      status: 'failure',
      errorMessage: "Billing has been discontinued. No new services may be purchased."
    }
    // var properties = {
    //   USER: _USER(),
    //   PWD: _PWD(),
    //   SIGNATURE: _SIGNATURE(),
    //   VERSION: "56.0"
    // }    
    // eachProperty(properties0, function(k, v) {
    //   if (v !== undefined) {
    //     properties[k] = v;      
    //   }
    // })
    // log({'type': 'api call', 'value': properties});
    // var ret = _doPost(_paypalApiUrl(), properties);
    // if (ret.error) {
    //   return {
    //     status: 'failure',
    //     exception: ret.error.javaException || ret.error,
    //     errorMessage: ret.error.message
    //   }
    // }
    // ret = ret.value;
    // var paypalResponse = {};
    // ret.content.split("&").forEach(function(x) {
    //     var parts = x.split("=");
    //     paypalResponse[decodeURIComponent(parts[0])] = 
    //       decodeURIComponent(parts[1]);
    //   })
    // 
    // var res = paypalResponse;
    // log(res)
    // if (res.ACK == "Success" || res.ACK == "SuccessWithWarning") {
    //   return {
    //     status: 'success',
    //     response: res
    //   }
    // } else {
    //   errors = [];
    //   for (var i = 0; res['L_LONGMESSAGE'+i]; ++i) {
    //     errors.push(res['L_LONGMESSAGE'+i]);
    //   }
    //   return {
    //     status: 'failure',
    //     errorMessage: errors.join(", "),
    //     errorMessages: errors,
    //     response: res
    //   }
    // }  
  }
  
  // status -> 'completion', 'bad', 'redundant', 'possible_fraud'
  function handlePaypalNotification() {
    var content = (typeof(request.content) == 'string' ? request.content : undefined);
    if (! content) {
      return new BillingResult('bad', "no content");
    }
    log({'type': 'paypal-notification', 'content': content});
    var params = {};
    content.split("&").forEach(function(x) {
      var parts = x.split("=");
      params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
    });
    var txnId = params.txn_id;
    var properties = [];
    for(var i in params) {
      properties.push(i+" -> "+params[i]);
    }
    var debugString = properties.join(", ");
    log({'type': 'parsed-paypal-notification', 'value': debugString});
    var transaction = getTransactionByExternalId(txnId);
    log({'type': 'notification-transaction', 'value': (transaction || {})});
    if (_RECEIVER() != params.receiver_email) {
      return new BillingResult('possible_fraud', debugString);
    }
    if (params.payment_status == "Completed" && transaction &&
        (transaction.status == 'pending' || transaction.status == 'new')) {
      var ret = _doPost(_paypalWebUrl(), "cmd=_notify-validate&"+content);
      if (ret.error || ret.value.content != "VERIFIED") {
        return new BillingResult('possible_fraud', debugString);
      }
      var invoice = getInvoice(params.invoice);
      if (invoice.amt != dollarsToCents(params.mc_gross)) {
        return new BillingResult('possible_fraud', debugString);
      }
      
      sqlcommon.inTransaction(function () {
        _transactionSuccess(transaction.id, txnId, "via eCheck");
        _invoicePaid(invoice.id);
        _purchaseActive(invoice.purchase);      
      });
      var purchase = getPurchase(invoice.purchase);
      return new BillingResult('completion', debugString, null,
        new PurchaseInfo(params.custom,
                         invoice.id,
                         transaction.id,
                         params.txn_id,
                         purchase.id,
                         centsToDollars(invoice.amt),
                         purchase.couponCode,
                         purchase.time,
                         undefined));
    } else {
      return new BillingResult('redundant', debugString);
    }
  }
  
  function _expressCheckoutCustom(invoiceId, transactionId) {
    return md5("zimki_sucks"+invoiceId+transactionId);
  }
  
  function PurchaseInfo(custom, invoiceId, transactionId, paypalId, purchaseId, dollars, couponCode, time, token, description) {
    this.__defineGetter__("custom", function() { return custom });
    this.__defineGetter__("invoiceId", function() { return invoiceId });
    this.__defineGetter__("transactionId", function() { return transactionId });
    this.__defineGetter__("paypalId", function() { return paypalId });
    this.__defineGetter__("purchaseId", function() { return purchaseId });
    this.__defineGetter__("cost", function() { return dollars });
    this.__defineGetter__("couponCode", function() { return couponCode });
    this.__defineGetter__("time", function() { return time });
    this.__defineGetter__("token", function() { return token });
    this.__defineGetter__("description", function() { return description });
  }
  
  function PayerInfo(paypalResult) {
    this.__defineGetter__("payerId", function() { return paypalResult.response.PAYERID });
    this.__defineGetter__("email", function() { return paypalResult.response.EMAIL });
    this.__defineGetter__("businessName", function() { return paypalResult.response.BUSINESS });
    this.__defineGetter__("nameSalutation", function() { return paypalResult.response.SALUTATION });
    this.__defineGetter__("nameFirst", function() { return paypalResult.response.FIRSTNAME });
    this.__defineGetter__("nameMiddle", function() { return paypalResult.response.MIDDLENAME });
    this.__defineGetter__("nameLast", function() { return paypalResult.response.LASTNAME });  
  }
  
  function BillingResult(status, debug, errorField, purchaseInfo, payerInfo) {
    this.__defineGetter__("status", function() { return status });
    this.__defineGetter__("debug", function() { return debug });
    this.__defineGetter__("errorField", function() { return errorField });
    this.__defineGetter__("purchaseInfo", function() { return purchaseInfo });
    this.__defineGetter__("payerInfo", function() { return payerInfo });
  }
  
  function dollarsToCents(dollars) {
    return Math.round(Number(dollars)*100);
  }
  
  function centsToDollars(cents) {
    return Math.round(Number(cents)) / 100;
  }
  
  function verifyDollars(dollars) {
    return Math.round(Number(dollars)*100)/100;
  }
  
  function beginExpressPurchase(invoiceId, customerId, productId, dollars, couponCode, successUrl, failureUrl, notifyUrl, authorizeOnly) {
    var cents = dollarsToCents(dollars);
    var time = new Date();
    var purchaseId;
    var transactionid;
    if (! authorizeOnly) {
      try {
        sqlcommon.inTransaction(function() {
          purchaseId = _newPurchase(customerId, productId, cents, couponCode);
          updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
          transactionId = _newTransaction(customerId, cents);
          _newAdjustment(transactionId, invoiceId, cents);
        });
      } catch (e) {
        if (e instanceof BillingResult) { return e; }
        throw e;
      }    
    }
    
    var paypalResult = 
      _setExpressCheckout(invoiceId, transactionId, cents,
                          successUrl, failureUrl, notifyUrl, authorizeOnly);
    
    if (paypalResult.status == 'success') {
      var token = paypalResult.response.TOKEN;
      return new BillingResult('success', paypalResult, null, new PurchaseInfo(
        _expressCheckoutCustom(invoiceId, transactionId),
        invoiceId,
        transactionId,
        undefined,
        purchaseId,
        verifyDollars(dollars),
        couponCode,
        time,
        token));
    } else {
      return new BillingResult('failure', paypalResult);
    }
  }
  
  function _setExpressCheckout(invoiceId, transactionId, cents, successUrl, failureUrl, notifyUrl, authorizeOnly) {
    var properties = {
      INVNUM: invoiceId,
      
      METHOD: 'SetExpressCheckout',
      CUSTOM: 
        _expressCheckoutCustom(invoiceId, transactionId),
      MAXAMT: centsToDollars(cents),
      RETURNURL: successUrl,
      CANCELURL: failureUrl,
      NOTIFYURL: notifyUrl,
      NOSHIPPING: 1,
      PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
      
      AMT: centsToDollars(cents)
    }
    
    return _doPaypalNvpPost(properties);
  }
  
  function continueExpressPurchase(purchaseInfo, authorizeOnly) {
    var paypalResult = _getExpressCheckoutDetails(purchaseInfo.token, authorizeOnly)
    if (paypalResult.status == 'success') {
      if (! authorizeOnly) {
        if (paypalResult.response.INVNUM != purchaseInfo.invoiceId) {
          return new BillingResult('failure', "invoice id mismatch");
        }      
      }
      if (paypalResult.response.CUSTOM !=
          _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)) {
        return new BillingResult('failure', "custom mismatch");
      }
      return new BillingResult('success', paypalResult, null, null, new PayerInfo(paypalResult));
    } else {
      return new BillingResult('failure', paypalResult);
    }
  }
  
  function _getExpressCheckoutDetails(token, authorizeOnly) {
    var properties = {
      METHOD: 'GetExpresscheckoutDetails',
      TOKEN: token,
    }
    
    return _doPaypalNvpPost(properties);
  }
  
  function completeExpressPurchase(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
    var paypalResult = _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly);
    
    if (paypalResult.status == 'success') {
      if (paypalResult.response.PAYMENTSTATUS == 'Completed') {
        if (! authorizeOnly) {
          sqlcommon.inTransaction(function() {
            _transactionSuccess(purchaseInfo.transactionId, 
                                paypalResult.response.TRANSACTIONID, "via PayPal");
            _invoicePaid(purchaseInfo.invoiceId);
            _purchaseActive(purchaseInfo.purchaseId);
          });        
        }
        return new BillingResult('success', paypalResult);
      } else if (paypalResult.response.PAYMENTSTATUS == 'Pending') {
        if (! authorizeOnly) {
          sqlcommon.inTransaction(function() {
            _transactionPending(purchaseInfo.transactionId,
                                paypalResult.response.TRANSACTIONID);        
          });        
        }
        return new BillingResult('pending', paypalResult);
      }
    } else {
      if (! authorizeOnly) {
        sqlcommon.inTransaction(function() {
          _transactionFailure(purchaseInfo.transactionId,
                              (paypalResult.response ?
                               paypalResult.response.TRANSACTIONID || "" :
                               ""));
        });
      }
      return new BillingResult('failure', paypalResult);
    }
  }
  
  function _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
    var properties = {
      METHOD: 'DoExpressCheckoutPayment',
      TOKEN: purchaseInfo.token,
      PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
      
      NOTIFYURL: notifyUrl,
      
      PAYERID: payerInfo.payerId,
      
      AMT: verifyDollars(purchaseInfo.cost), // dollars
      INVNUM: purchaseInfo.invoiceId,
      CUSTOM:
        _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)
    }
    
    return _doPaypalNvpPost(properties);
  }
  
  // which field has error? and, is it not user-correctable?
  var _directErrorCodes = {
    '10502': ['cardExpiration'],
    '10504': ['cardCvv'],
    '10505': ['addressStreet', true],
    '10508': ['cardExpiration'], 
    '10510': ['cardType'],
    '10512': ['nameFirst'],
    '10513': ['nameLast'],
    '10519': ['cardNumber'],
    '10521': ['cardNumber'],
    '10527': ['cardNumber'],
    '10534': ['cardNumber', true],
    '10535': ['cardNumber'],
    '10536': ['invoiceId', true],
    '10537': ['addressCountry', true],
    '10540': ['addressStreet', true],
    '10541': ['cardNumber', true],
    '10554': ['address', true],
    '10555': ['address', true],
    '10556': ['address', true],
    '10561': ['address'],
    '10562': ['cardExpiration'],
    '10563': ['cardExpiration'],
    '10565': ['addressCountry'],
    '10566': ['cardType'],
    '10571': ['cardCvv'],
    '10701': ['address'],
    '10702': ['addressStreet'],
    '10703': ['addressStreet2'],
    '10704': ['addressCity'],
    '10705': ['addressState'],
    '10706': ['addressZip'],
    '10707': ['addressCountry'],
    '10708': ['address'],
    '10709': ['addressStreet'],
    '10710': ['addressCity'],
    '10711': ['addressState'],
    '10712': ['addressZip'],
    '10713': ['addressCountry'],
    '10714': ['address'],
    '10715': ['addressState'],
    '10716': ['addressZip'],
    '10717': ['addressZip'],
    '10718': ['addressCity,addressState'],
    '10748': ['cardCvv'],
    '10752': ['card'],
    '10756': ['address,card'],
    '10759': ['cardNumber'],
    '10762': ['cardCvv'],
    '11611': function(response) {
      var avsCode = response.AVSCODE;
      var cvv2Match = response.CVV2MATCH;
      var errorFields = [];
      switch (avsCode) {
        case 'N': case 'C': case 'A': case 'B':
        case 'R': case 'S': case 'U': case 'G':
        case 'I': case 'E':
          errorFields.push('address');
      }
      switch (cvv2Match) {
        case 'N':
          errorFields.push('cardCvv');
      }
      return [errorFields.join(",")];
    },
    '15004': ['cardCvv'],
    '15005': ['cardNumber'],
    '15006': ['cardNumber'],
    '15007': ['cardNumber']
  }
  
  function authorizePurchase(payinfo, notifyUrl) {
    return directPurchase(undefined, undefined, undefined, 1, undefined, payinfo, notifyUrl, true);
  }
  
  function directPurchase(invoiceId, customerId, productId, dollars, couponCode, payinfo, notifyUrl, authorizeOnly) {
    var time = new Date();
    var cents = dollarsToCents(dollars);
    
    var purchaseId, transactionId;
    
    if (! authorizeOnly) {
      try {
        sqlcommon.inTransaction(function() {
          purchaseId = _newPurchase(customerId, productId, cents, couponCode);
          updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
          transactionId = _newTransaction(customerId, cents);
          _newAdjustment(transactionId, invoiceId, cents);
        });
      } catch (e) {
        if (e instanceof BillingResult) { return e; }
        if (e.javaException || e.rhinoException) {
          throw e.javaException || e.rhinoException;
        }
        throw e;
      }    
    }
    
    var paypalResult = _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly);
    
    if (paypalResult.status == 'success') {
      if (! authorizeOnly) {
        sqlcommon.inTransaction(function() {
          _transactionSuccess(transactionId, 
                              paypalResult.response.TRANSACTIONID,
                              payinfo.cardType+" ending in "+payinfo.cardNumber.substr(-4));
          _invoicePaid(invoiceId);
          _purchaseActive(purchaseId);      
        });      
      }
      return new BillingResult('success', paypalResult, null, new PurchaseInfo(
        undefined,
        invoiceId,
        transactionId,
        paypalResult.response.TRANSACTIONID,
        purchaseId,
        verifyDollars(dollars),
        couponCode,
        time,
        undefined));
    } else {
      if (! authorizeOnly) {
        sqlcommon.inTransaction(function() {
          _transactionFailure(transactionId, 
                              (paypalResult.response ?
                               paypalResult.response.TRANSACTIONID || "":
                               ""));      
        });      
      }
      return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
    }
  }
  
  function _getErrorCodes(paypalResponse) {
    var errorCodes = {userErrors: [], permanentErrors: []};
    if (! paypalResponse) {
      return undefined;
    }
    for (var i = 0; paypalResponse['L_ERRORCODE'+i]; ++i) {
      var code = paypalResponse['L_ERRORCODE'+i];
      var errorField = _directErrorCodes[code];
      if (typeof(errorField) == 'function') {
        errorField = errorField(paypalResponse);
      }
      if (errorField && errorField[1]) {
        Array.prototype.push.apply(errorCodes.permanentErrors, errorField[0].split(","));
      } else if (errorField) {
        Array.prototype.push.apply(errorCodes.userErrors, errorField[0].split(","));
      }
    }
    return errorCodes;
  }
  
  function _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly) {
    var properties = {
      INVNUM: invoiceId,
      
      METHOD: 'DoDirectPayment',
      PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
      IPADDRESS: request.clientAddr,
      NOTIFYURL: notifyUrl,
      
      CREDITCARDTYPE: payinfo.cardType,
      ACCT: payinfo.cardNumber,
      EXPDATE: payinfo.cardExpiration,
      CVV2: payinfo.cardCvv,
      
      SALUTATION: payinfo.nameSalutation,
      FIRSTNAME: payinfo.nameFirst,
      MIDDLENAME: payinfo.nameMiddle,
      LASTNAME: payinfo.nameLast,
      SUFFIX: payinfo.nameSuffix,
      
      STREET: payinfo.addressStreet,
      STREET2: payinfo.addressStreet2,
      CITY: payinfo.addressCity,
      STATE: payinfo.addressState,
      COUNTRYCODE: payinfo.addressCountry,
      ZIP: payinfo.addressZip,
      
      AMT: centsToDollars(cents)
    }
    
    return _doPaypalNvpPost(properties);
  }
  
  // function directAuthorization(payInfo, dollars, notifyUrl) {
  //   var paypalResult = _doDirectPurchase(undefined, dollarsToCents(dollars), payInfo, notifyUrl, true);
  //   if (paypalResult.status == 'success') {
  //     return new BillingResult('success', paypalResult, null, new PurchaseInfo(
  //       undefined,
  //       undefined,
  //       paypalResult.response.TRANSACTIONID,
  //       undefined,
  //       verifyDollars(dollars),
  //       undefined,
  //       undefined,
  //       undefined));
  //   } else {
  //     return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
  //   }
  // }
  
  function asyncRecurringPurchase(invoiceId, purchaseId, oldTransactionId, paymentInfo, dollars, monthCount, notifyUrl) {
    var time = new Date();
    var cents = dollarsToCents(dollars);
    
    var purchase, transactionId;
    
    try {
      sqlcommon.inTransaction(function() {
        // purchaseId = _newPurchase(customerId, productId, cents, couponCode);
        purchase = getPurchase(purchaseId);
        updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
        transactionId = _newTransaction(purchase.customer, cents);
        _newAdjustment(transactionId, invoiceId, cents);
      });
    } catch (e) {
      if (e instanceof BillingResult) { return e; }
      if (e.rhinoException) {
        throw e.rhinoException;
      }
      throw e;
    }
    
    // do transaction using previous transaction as template
    var paypalResult;
    if (cents == 0) {
      // can't actually charge nothing, so fake it.
      paypalResult = { status: 'success', response: { TRANSACTIONID: null }}
    } else {
      paypalResult = _doReferenceTransaction(invoiceId, cents, oldTransactionId, notifyUrl);
    }
    
    if (paypalResult.status == 'success') {
      sqlcommon.inTransaction(function() {
        _transactionSuccess(transactionId, 
                            paypalResult.response.TRANSACTIONID,
                            paymentInfo);
        _invoicePaid(invoiceId);
        _purchaseActive(purchaseId);
        _purchaseExtend(purchaseId, monthCount);
      });
      return new BillingResult('success', paypalResult, null, new PurchaseInfo(
        undefined,
        invoiceId,
        transactionId,
        paypalResult.response.TRANSACTIONID,
        purchaseId,
        verifyDollars(dollars),
        undefined,
        time,
        undefined));
    } else {
      sqlcommon.inTransaction(function() {
        _transactionFailure(transactionId, 
                            (paypalResult.response ?
                             paypalResult.response.TRANSACTIONID || "":
                             ""));      
      });
      return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
    }
  }
  
  function _doReferenceTransaction(invoiceId, cents, transactionId, notifyUrl) {
    var properties = {
      METHOD: 'DoReferenceTransaction',
      PAYMENTACTION: 'Sale',
      
      REFERENCEID: transactionId,
      AMT: centsToDollars(cents),
      INVNUM: invoiceId
    }
    
    return _doPaypalNvpPost(properties);
  }
  