diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/billing/billing.js')
-rw-r--r-- | trunk/etherpad/src/etherpad/billing/billing.js | 800 |
1 files changed, 800 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/billing/billing.js b/trunk/etherpad/src/etherpad/billing/billing.js new file mode 100644 index 0000000..444c233 --- /dev/null +++ b/trunk/etherpad/src/etherpad/billing/billing.js @@ -0,0 +1,800 @@ +/** + * 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('billing_purchase', { + customer: customer, + product: product, + cost: cents, + coupon: couponCode, + status: 'inactive' + }); + return purchaseId; +} + +function _newInvoice() { + var invoiceId = sqlobj.insert('billing_invoice', { + time: new Date(), + purchase: -1, + amt: 0, + status: 'pending' + }); + return invoiceId; +} + +function _newTransaction(customer, cents) { + var transactionId = sqlobj.insert('billing_transaction', { + customer: customer, + time: new Date(), + amt: cents, + status: 'new' + }); + 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); +}
\ No newline at end of file |