/** * 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); }