diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js')
-rw-r--r-- | trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js b/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js new file mode 100644 index 0000000..ddd4973 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js @@ -0,0 +1,757 @@ +/** + * 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("email.sendEmail"); +import("fastJSON"); +import("funhtml.*"); +import("jsutils.*"); +import("sqlbase.sqlobj"); +import("stringutils"); +import("sync"); + +import("etherpad.billing.billing"); +import("etherpad.billing.fields"); +import("etherpad.globals"); +import("etherpad.globals.*"); +import("etherpad.helpers"); +import("etherpad.licensing"); +import("etherpad.pro.pro_utils"); +import("etherpad.sessions.{getSession,getTrackingId,getSessionId}"); +import("etherpad.store.checkout"); +import("etherpad.store.eepnet_checkout"); +import("etherpad.utils.*"); + +import("static.js.billing_shared.{billing=>billingJS}"); + +jimport("java.lang.System.out.println"); + +//---------------------------------------------------------------- + +var STORE_URL = '/ep/store/eepnet-checkout/'; + +var _pageSequence = [ + ['purchase', "Number of Users", true], + ['support-contract', "Support Contract", true], + ['license-info', "License Information", true], + ['billing-info', "Billing Information", true], + ['confirmation', "Confirmation", false] +]; + +var _specialPages = { + 'receipt': ['receipt', "Receipt", false] +} + +//---------------------------------------------------------------- + +function _cart() { + return getSession().eepnetCart; +} + +function _currentPageSegment() { + return request.path.split('/')[4]; +} + +function _currentPageId() { + return _applyToCurrentPageSequenceEntry(function(ps) { return ps[0]; }); +} + +function _applyToCurrentPageSequenceEntry(f) { + for (var i = 0; i < _pageSequence.length; i++) { + if (_pageSequence[i][0] == _currentPageSegment()) { + return f(_pageSequence[i], i, true); + } + } + if (_specialPages[_currentPageSegment()]) { + return f(_specialPages[_currentPageSegment()], -1, false); + } + return undefined; +} + +function _currentPageIndex() { + return _applyToCurrentPageSequenceEntry(function(ps, i) { return i; }); +} + +function _currentPageTitle() { + return _applyToCurrentPageSequenceEntry(function(ps) { return ps[1]; }); +} + +function _currentPageShowCart() { + return _applyToCurrentPageSequenceEntry(function(ps) { return ps[2]; }); +} + +function _currentPageInFlow() { + return _applyToCurrentPageSequenceEntry(function(ps, i, isSpecial) { return isSpecial }); +} + +function _pageId(d) { + return _applyToCurrentPageSequenceEntry(function(ps, i) { + if (_pageSequence[i+d]) { + return _pageSequence[i+d][0]; + } + }); +} + +function _nextPageId() { return _pageId(+1); } +function _prevPageId() { return _pageId(-1); } + +function _advancePage() { + response.redirect(_pathTo(_nextPageId())); +} + +function _pathTo(id) { + return STORE_URL+id; +} + +// anything starting with 'billing' is also ok. +function _isAutomaticallySetParam(p) { + var _automaticallySetParams = arrayToSet([ + 'numUsers', 'couponCode', 'supportContract', + 'email', 'ownerName', 'orgName', 'licenseAgreement' + ]); + + return _automaticallySetParams[p] || stringutils.startsWith(p, "billing"); +} + +function _lastSubmittedPage() { + var cart = _cart(); + return isNaN(cart.lastSubmittedPage) ? -1 : Number(cart.lastSubmittedPage); +} + +function _shallowSafeCopy(obj) { + return billing.clearKeys(obj, [ + {name: 'billingCCNumber', + valueTest: function(s) { return /^\d{15,16}$/.test(s) }, + valueReplace: billing.replaceWithX }, + {name: 'billingCSC', + valueTest: function(s) { return /^\d{3,4}$/.test(s) }, + valueReplace: billing.replaceWithX }]); +} + +function onRequest() { + billing.log({ + 'type': "billing-request", + 'date': +(new Date), + 'method': request.method, + 'path': request.path, + 'query': request.query, + 'host': request.host, + 'scheme': request.scheme, + 'params': _shallowSafeCopy(request.params), + 'cart': _shallowSafeCopy(_cart()) + }); + if (request.path == STORE_URL+"paypalnotify") { + _handlePaypalNotification(); + } + if (request.path == STORE_URL+"paypalredirect") { + _handlePayPalRedirect(); + } + var cart = _cart(); + if (!cart || request.params.clearcart) { + getSession().eepnetCart = { + lastSubmittedPage: -1, + invoiceId: billing.createInvoice() + }; + if (request.params.clearcart) { + response.redirect(request.path); + } + if (_currentPageId() != 'purchase') { + response.redirect(_pathTo('purchase')); + } + cart = _cart(); + } + if (request.params.invoice) { + cart.billingPurchaseType = 'invoice'; + } + if (cart.purchaseComplete && _currentPageId() != 'receipt') { + cart.showStartOverMessage = true; + response.redirect(_pathTo('receipt')); + } + // somehow user got too far? + if (_currentPageIndex() > _lastSubmittedPage() + 1) { + response.redirect(_pathTo(_pageSequence[_lastSubmittedPage()+1][0])); + } + if (request.isGet) { + // see if this is a standard cart-page get + if (_currentPageId()) { + _renderCartPage(); + return true; + } + } + if (request.isPost) { + // add params to cart + eachProperty(request.params, function(k,v) { + if (! _isAutomaticallySetParam(k)) { return; } + if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; } + cart[k] = stringutils.toHTML(v); + }); + if (_currentPageId() == 'license-info' && ! request.params.licenseAgreement) { + delete cart.licenseAgreement; + } + if (_currentPageIndex() > cart.lastSubmittedPage) { + cart.lastSubmittedPage = _currentPageIndex(); + } + } + if (request.params.backbutton) { + _updateCosts(); + response.redirect(_pathTo(_prevPageId())); + } + return false; // commence auto-dispatch +} + +function _getCoupon(code) { + return sqlobj.selectSingle('checkout_referral', {id: code}); +} + +function _supportCost() { + var cart = _cart(); + return Math.max(eepnet_checkout.SUPPORT_MIN_COST, eepnet_checkout.SUPPORT_COST_PCT/100*cart.baseCost); +} + +function _discountedSupportCost() { + var cart = _cart(); + if ('couponSupportPctDiscount' in cart) { + return _supportCost() - + (cart.couponSupportPctDiscount ? + cart.couponSupportPctDiscount/100 * _supportCost() : + 0); + } +} + +function _updateCosts() { + var cart = _cart(); + + if (cart.numUsers) { + cart.numUsers = Number(cart.numUsers); + + cart.baseCost = cart.numUsers * eepnet_checkout.COST_PER_USER; + + if (cart.supportContract == "true") { + cart.supportCost = _supportCost(); + } else { + delete cart.supportCost; + } + + var coupon = _getCoupon(cart.couponCode); + if (coupon) { + for (i in coupon) { + cart["coupon"+stringutils.makeTitle(i)] = coupon[i]; + } + cart.coupon = coupon; + } else { + for (i in cart.coupon) { + delete cart["coupon"+stringutils.makeTitle(i)]; + } + delete cart.coupon; + } + + if (cart.couponProductPctDiscount) { + cart.productReferralDiscount = + cart.couponProductPctDiscount/100 * cart.baseCost; + } else { + delete cart.productReferralDiscount; + } + if (cart.couponSupportPctDiscount) { + cart.supportReferralDiscount = + cart.couponSupportPctDiscount/100 * (cart.supportCost || 0); + } else { + delete cart.supportReferralDiscount; + } + cart.subTotal = + cart.baseCost - (cart.productReferralDiscount || 0) + + (cart.supportCost || 0) - (cart.supportReferralDiscount || 0); + + if (cart.couponTotalPctDiscount) { + cart.totalReferralDiscount = + cart.couponTotalPctDiscount/100 * cart.subTotal; + } else { + delete cart.totalReferralDiscount; + } + + if (cart.couponFreeUsersCount || cart.couponFreeUsersPct) { + cart.freeUserCount = + Math.round(cart.couponFreeUsersCount + + cart.couponFreeUsersPct/100 * cart.numUsers); + } else { + delete cart.freeUserCount; + } + cart.userCount = Number(cart.numUsers) + Number(cart.freeUserCount || 0); + + cart.total = + cart.subTotal - (cart.totalReferralDiscount || 0); + } +} + +//---------------------------------------------------------------- +// template helper functions +//---------------------------------------------------------------- + +function _cartDebug() { + if (globals.isProduction()) { + return ''; + } + + var d = DIV({style: 'font-family: monospace; font-size: 1em; border: 1px solid #ccc; padding: 1em; margin: 1em;'}); + d.push(H3({style: "font-size: 1.5em; font-weight: bold;"}, "Debug Info:")); + var t = TABLE({border: 1, cellspacing: 0, cellpadding: 4}); + keys(_cart()).sort().forEach(function(k) { + var v = _cart()[k]; + if (typeof(v) == 'object' && v != null) { + v = v.toSource(); + } + t.push(TR(TD({style: 'padding: 2px 6px;', align: 'right'}, k), + TD({style: 'padding: 2px 6px;', align: 'left'}, v))); + }); + d.push(t); + return d; +} + +var billingButtonName = "Review Order"; + +function _templateContext(extra) { + var cart = _cart(); + + var pageId = _currentPageId(); + + var ret = { + cart: cart, + costPerUser: eepnet_checkout.COST_PER_USER, + supportCostPct: eepnet_checkout.SUPPORT_COST_PCT, + supportMinCost: eepnet_checkout.SUPPORT_MIN_COST, + errorIfInvalid: _errorIfInvalid, + dollars: checkout.dollars, + countryList: fields.countryList, + usaStateList: fields.usaStateList, + obfuscateCC: checkout.obfuscateCC, + helpers: helpers, + inFlow: _currentPageInFlow(), + displayCart: _displayCart, + displaySummary: _displaySummary, + pathTo: _pathTo, + billing: billingJS, + handlePayPalRedirect: _handlePayPalRedirect, + supportCost: _supportCost, + discountedSupportCost: _discountedSupportCost, + billingButtonName: billingButtonName, + billingFinalPhrase: "<p>You will not be charged until you review"+ + " and confirm your order on the next page.</p>", + getFullSuperdomainHost: pro_utils.getFullSuperdomainHost, + showCouponCode: false + }; + eachProperty(extra, function(k, v) { + ret[k] = v; + }); + return ret; +} + +function _displayCart(cartid, editable) { + return renderTemplateAsString('store/eepnet-checkout/cart.ejs', _templateContext({ + shoppingcartid: cartid || "shoppingcart", + editable: editable + })); +} + +function _displaySummary(editable) { + return renderTemplateAsString('store/eepnet-checkout/summary.ejs', _templateContext({ + editable: editable + })); +} + +function _renderCartPage() { + var cart = _cart(); + + var pageId = _currentPageId(); + var title = _currentPageTitle(); + + function _getContent() { + return renderTemplateAsString('store/eepnet-checkout/'+pageId+'.ejs', _templateContext()); + } + + renderFramed('store/eepnet-checkout/checkout-template.ejs', { + cartDebug: _cartDebug, + errorDiv: _errorDiv, + pageId: pageId, + getContent: _getContent, + title: title, + inFlow: _currentPageInFlow(), + displayCart: _displayCart, + showCart: _currentPageShowCart(), + cart: cart, + billingButtonName: billingButtonName + }); + + // clear errors + delete cart.errorMsg; + delete cart.errorId; +} + +function _errorDiv() { + var m = _cart().errorMsg; + if (m) { + return DIV({className: 'errormsg', id: 'errormsg'}, m); + } else { + return ''; + } +} + +function _errorIfInvalid(id) { + var e = _cart().errorId + if (e && e[id]) { + return 'error'; + } else { + return ''; + } +} + +function _validationError(id, msg, pageId) { + var cart = _cart(); + cart.errorMsg = msg; + cart.errorId = {}; + if (id instanceof Array) { + id.forEach(function(k) { + cart.errorId[k] = true; + }); + } else { + cart.errorId[id] = true; + } + if (pageId) { + response.redirect(_pathTo(pageId)); + } + response.redirect(request.path); +} + +//-------------------------------------------------------------------------------- +// main +//-------------------------------------------------------------------------------- + +function render_main() { + response.redirect(STORE_URL+'purchase'); +} + +//-------------------------------------------------------------------------------- +// cart +//-------------------------------------------------------------------------------- + +function render_purchase_post() { + var cart = _cart(); + + // validate numUsers and couponCode + if (! checkout.isOnlyDigits(cart.numUsers)) { + _validationError("numUsers", "Please enter a valid number of users."); + } + if (Number(cart.numUsers) < 1) { + _validationError("numUsers", "Please specify at least one user."); + } + + if (cart.couponCode && (cart.couponCode.length != 8 || ! _getCoupon(cart.couponCode))) { + _validationError("couponCode", "That coupon code does not appear to be valid."); + } + + _updateCosts(); + _advancePage(); +} + +//-------------------------------------------------------------------------------- +// support-contract +//-------------------------------------------------------------------------------- + +function render_support_contract_post() { + var cart = _cart(); + + if (cart.supportContract != "true" && cart.supportContract != "false") { + _validationError("supportContract", "Please select one of the options."); + } + + _updateCosts(); + _advancePage(); +} + +//-------------------------------------------------------------------------------- +// license-info +//-------------------------------------------------------------------------------- + +function render_license_info_post() { + var cart = _cart(); + + if (!isValidEmail(cart.email)) { + _validationError("email", "That email address does not look valid."); + } + if (!cart.ownerName) { + _validationError("ownerName", "Please enter a license owner name."); + } + if (!cart.orgName) { + _validationError("orgName", "Please enter an organization name."); + } + if (!cart.licenseAgreement) { + _validationError("licenseAgreement", "You must agree to the terms of the license to purchase EtherPad PNE."); + } + + if ((! cart.billingFirstName) && ! (cart.billingLastName)) { + var nameParts = cart.ownerName.split(/\s+/); + if (nameParts.length == 1) { + cart.billingFirstName = nameParts[0]; + } else { + cart.billingLastName = nameParts[nameParts.length-1]; + cart.billingFirstName = nameParts.slice(0, nameParts.length-1).join(' '); + } + } + + _updateCosts(); + _advancePage(); +} + +//-------------------------------------------------------------------------------- +// billing-info +//-------------------------------------------------------------------------------- + +function render_billing_info_post() { + var cart = _cart(); + + checkout.validateBillingCart(_validationError, cart); + if (cart.billingPurchaseType == 'paypal') { + _beginPaypalPurchase(); + } + + _updateCosts(); + _advancePage(); +} + +function _absoluteUrl(id) { + return request.scheme+"://"+request.host+_pathTo(id); +} + +function _beginPaypalPurchase() { + _updateCosts(); + + var cart = _cart(); + + var purchase = _generatePurchaseRecord(); + var result = + billing.beginExpressPurchase(cart.invoiceId, cart.customerId, + "EEPNET", cart.total || 0.01, cart.couponCode || "", + _absoluteUrl('paypalredirect?status=ok'), + _absoluteUrl('paypalredirect?status=fail'), + _absoluteUrl('paypalnotify')); + if (result.status != 'success') { + _validationError("billingPurchaseType", + "PayPal purchase not available at the moment. "+ + "Please try again later, or try using a different payment option."); + } + cart.paypalPurchaseInfo = result.purchaseInfo; + response.redirect(billing.paypalPurchaseUrl(result.purchaseInfo.token)); +} + +//-------------------------------------------------------------------------------- +// confirmation +//-------------------------------------------------------------------------------- + +function _handlePaypalNotification() { + var ret = billing.handlePaypalNotification(); + if (ret.status == 'completion') { + var purchaseInfo = ret.purchaseInfo; + var eepnetPurchase = eepnet_checkout.getPurchaseByInvoiceId(purchaseInfo.invoiceId); + var fakeCart = { + ownerName: eepnetPurchase.owner, + orgName: eepnetPurchase.organization, + email: eepnetPurchase.emails, + customerId: eepnetPurchase.id, + userCount: eepnetPurchase.numUsers, + receiptEmail: eepnetPurchase.receiptEmail, + } + eepnet_checkout.generateLicenseKey(fakeCart); + eepnet_checkout.sendReceiptEmail(fakeCart); + eepnet_checkout.sendLicenseEmail(fakeCart); + billing.log({type: 'purchase-complete', dollars: purchaseInfo.cost}); + } +} + +function _handlePayPalRedirect() { + var cart = _cart(); + + if (request.params.status == 'ok' && cart.paypalPurchaseInfo) { + var result = billing.continueExpressPurchase(cart.paypalPurchaseInfo); + if (result.status == 'success') { + cart.paypalPayerInfo = result.payerInfo; + response.redirect(_pathTo('confirmation')); + } else { + _validationError("billingPurchaseType", + "There was an error processing your payment through PayPal. "+ + "Please try again later, or use a different payment option.", + 'billing-info'); + } + } else { + _validationError("billingPurchaseType", + "PayPal payment didn't go through. "+ + "Please try again later, or use a different payment option.", + 'billing-info'); + } +} + +function _recordPurchase(p) { + return sqlobj.insert("checkout_purchase", p); +} + +function _generatePurchaseRecord() { + var cart = _cart(); + + if (! cart.invoiceId) { + throw Error("No invoice id!"); + } + + var purchase = { + invoiceId: cart.invoiceId, + email: cart.email, + firstName: cart.billingFirstName, + lastName: cart.billingLastName, + owner: cart.ownerName || "", + organization: cart.orgName || "", + addressLine1: cart.billingAddressLine1 || "", + addressLine2: cart.billingAddressLine2 || "", + city: cart.billingCity || "", + state: cart.billingState || "", + zip: cart.billingZipCode || "", + referral: cart.couponCode, + cents: cart.total*100, // cents here. + numUsers: cart.userCount, + purchaseType: cart.billingPurchaseType, + } + cart.customerId = _recordPurchase(purchase); + return purchase; +} + +function _performCreditCardPurchase() { + var cart = _cart(); + var purchase = _generatePurchaseRecord(); + var payInfo = checkout.generatePayInfo(cart); + + // log everything but the CVV, which we're not allowed to store + // any longer than it takes to process this transaction. + var savedCvv = payInfo.cardCvv; + delete payInfo.cardCvv; + checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), purchase: purchase, customerId: cart.customerId, payInfo: payInfo})); + payInfo.cardCvv = savedCvv; + + var result = + billing.directPurchase(cart.invoiceId, cart.customerId, + "EEPNET", cart.total || 0.01, + cart.couponCode || "", + payInfo, _absoluteUrl('paypalnotify')); + + if (result.status == 'success') { + cart.status = 'success'; + cart.purchaseComplete = true; + eepnet_checkout.generateLicenseKey(cart); + eepnet_checkout.sendReceiptEmail(cart); + eepnet_checkout.sendLicenseEmail(cart); + billing.log({type: 'purchase-complete', dollars: cart.total, + email: cart.email, user: cart.ownerName, + org: cart.organization}); + // TODO: generate key and include in receipt page, and add to purchase table. + } else if (result.status == 'pending') { + cart.status = 'pending'; + cart.purchaseComplete = true; + eepnet_checkout.sendReceiptEmail(cart); + // save the receipt email text to resend later. + eepnet_checkout.updatePurchaseWithReceipt(cart.customerId, + eepnet_checkout.receiptEmailText(cart)); + } else if (result.status == 'failure') { + var paypalResult = result.debug; + billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult}); + if (result.errorField.permanentErrors[0] == 'invoiceId') { + // repeat invoice id. damnit, this is bad. + sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'DUPLICATE INVOICE WARNING!', {}, + "Hey,\n\nThis is a billing system error. The EEPNET checkout tried to make a "+ + "purchase with PayPal and got a duplicate invoice error on invoice ID "+cart.invoiceId+ + ".\n\nUnless you're expecting this (or recently ran a selenium test, or have reason to "+ + "believe this isn't an exceptional condition, please look into this "+ + "and get back to the user ASAP!\n\n"+fastJSON.stringify(cart)); + _validationError('', "Your payment was processed, but we cannot proceed. "+ + "You will hear from us shortly via email. (If you don't hear from us "+ + "within 24 hours, please email <a href='mailto:sales@pad.spline.inf.fu-berlin.de'>"+ + "sales@pad.spline.inf.fu-berlin.de</a>.)"); + } + checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "There seems to be an error in your billing information."+ + " Please verify and correct your ", + result.errorField.userErrors); + checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "The bank declined your billing information. Please try a different ", + result.errorField.permanentErrors); + _validationError('', "A temporary error has prevented processing of your payment. Please try again later."); + } else { + billing.log({'type': 'FATAL', value: "Unknown error: "+result.status+" - debug: "+result.debug}); + sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'UNKNOWN ERROR WARNING!', {}, + "Hey,\n\nThis is a billing system error. Some unknown error occurred. "+ + "This shouldn't ever happen. Probably good to let J.D. know. <grin>\n\n"+ + fastJSON.stringify(cart)); + _validationError('', "An unknown error occurred. We're looking into it!") + } +} + +function _completePaypalPurchase() { + var cart = _cart(); + var purchaseInfo = cart.paypalPurchaseInfo; + var payerInfo = cart.paypalPayerInfo; + + var result = billing.completeExpressPurchase(purchaseInfo, payerInfo, _absoluteUrl('paypalnotify')); + if (result.status == 'success') { + cart.status = 'success'; + cart.purchaseComplete = true; + eepnet_checkout.generateLicenseKey(cart); + eepnet_checkout.sendReceiptEmail(cart); + eepnet_checkout.sendLicenseEmail(cart); + billing.log({type: 'purchase-complete', dollars: cart.total, + email: cart.email, user: cart.ownerName, + org: cart.organization}); + + } else if (result.status == 'pending') { + cart.status = 'pending'; + cart.purchaseComplete = true; + eepnet_checkout.sendReceiptEmail(cart); + // save the receipt email text to resend later. + eepnet_checkout.updatePurchaseWithReceipt(cart.customerId, + eepnet_checkout.receiptEmailText(cart)); + } else { + billing.log({'type': 'FATAL', value: "Paypal failed.", cart: cart, paypal: paypalResult}); + _validationError("billingPurchaseType", + "There was an error processing your payment through PayPal. "+ + "Please try again later, or use a different payment option.", + 'billing-info'); + } +} + +function _showReceipt() { + response.redirect(_pathTo('receipt')); +} + +function render_confirmation_post() { + var cart = _cart(); + + _updateCosts(); // no fishy business, please. + + if (cart.billingPurchaseType == 'creditcard') { + _performCreditCardPurchase(); + _showReceipt(); + } else if (cart.billingPurchaseType == 'paypal') { + _completePaypalPurchase(); + _showReceipt(); + } +} + +//-------------------------------------------------------------------------------- +// receipt +//-------------------------------------------------------------------------------- + +function render_receipt_post() { + response.redirect(request.path); +} |