/** * 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("email.sendEmail"); import("fastJSON"); import("funhtml.*"); import("jsutils.*"); import("sqlbase.sqlcommon.inTransaction"); import("stringutils.*"); import("etherpad.billing.billing"); import("etherpad.billing.fields"); import("etherpad.billing.team_billing"); import("etherpad.control.pro.admin.pro_admin_control"); import("etherpad.globals"); import("etherpad.helpers"); import("etherpad.pro.domains"); import("etherpad.pro.pro_accounts"); import("etherpad.pro.pro_utils"); import("etherpad.sessions"); import("etherpad.store.checkout"); import("etherpad.utils.*"); import("static.js.billing_shared.{billing=>billingJS}"); var billingButtonName = "Confirm" function _cart() { var s = sessions.getSession(); if (! s.proBillingCart) { s.proBillingCart = {}; } return s.proBillingCart; } function _billingForm() { return renderTemplateAsString('store/eepnet-checkout/billing-info.ejs', { cart: _cart(), billingButtonName: billingButtonName, billingFinalPhrase: "", helpers: helpers, errorIfInvalid: _errorIfInvalid, billing: billingJS, obfuscateCC: checkout.obfuscateCC, dollars: checkout.dollars, countryList: fields.countryList, usaStateList: fields.usaStateList, getFullSuperdomainHost: pro_utils.getFullSuperdomainHost, showCouponCode: true, }); } function _plural(num) { return (num == 1 ? "" : "s"); } function _billingSummary(domainId, subscription) { var paymentInfo = team_billing.getRecurringBillingInfo(domainId); if (! paymentInfo) { return; } var latestInvoice = team_billing.getLatestPaidInvoice(subscription.id); var usersSoFar = team_billing.getMaxUsers(domainId); var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription.coupon); var lastPaymentString = (latestInvoice ? "US $"+checkout.dollars(billing.centsToDollars(latestInvoice.amt))+ " ("+latestInvoice.users+" account"+_plural(latestInvoice.users)+")"+ ", on "+checkout.formatDate(latestInvoice.time) : "None"); var coupon = false; if (subscription.coupon) { println("has a coupon: "+subscription.coupon); var cval = team_billing.getCouponValue(subscription.coupon); coupon = []; if (cval.freeUsers) { coupon.push(cval.freeUsers+" free user"+(cval.freeUsers == 1 ? "" : "s")); } if (cval.pctDiscount) { coupon.push(cval.pctDiscount+"% savings"); } coupon = coupon.join(", "); } return { fullName: paymentInfo.fullname, paymentSummary: paymentInfo.paymentsummary + (paymentInfo.expiration ? ", expires "+checkout.formatExpiration(paymentInfo.expiration) : ""), lastPayment: lastPaymentString, nextPayment: checkout.formatDate(subscription.paidThrough), maxUsers: usersSoFar, estimatedPayment: "US $"+checkout.dollars(costSoFar), coupon: coupon } } function _statusMessage() { if (_cart().statusMessage) { return toHTML(P({style: "color: green;"}, _cart().statusMessage)); } else { return ''; } } function renderMainPage(doEdit) { var cart = _cart(); var domainId = domains.getRequestDomainId(); var subscription = team_billing.getSubscriptionForCustomer(domainId); var pendingInvoice = team_billing.getLatestPendingInvoice(domainId) var usersSoFar = team_billing.getMaxUsers(domainId); var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription && subscription.coupon); checkout.guessBillingNames(cart, pro_accounts.getSessionProAccount().fullName); if (! cart.billingReferralCode) { if (subscription && subscription.coupon) { cart.billingReferralCode = subscription.coupon; } } var summary = _billingSummary(domainId, subscription); if (! summary) { doEdit = true; } pro_admin_control.renderAdminPage('manage-billing', { billingForm: _billingForm, doEdit: doEdit, paymentInfo: summary, getFullSuperdomainHost: pro_utils.getFullSuperdomainHost, firstCharge: checkout.formatDate(subscription ? subscription.paidThrough : dateutils.nextMonth(new Date)), billingButtonName: billingButtonName, errorDiv: _errorDiv, showBackButton: (summary != undefined), statusMessage: _statusMessage, isBehind: (subscription ? subscription.paidThrough < Date.now() - 86400*1000 : false), amountDue: "US $"+checkout.dollars(billing.centsToDollars(pendingInvoice ? pendingInvoice.amt : costSoFar*100)), cart: _cart() }); delete _cart().errorId; delete _cart().errorMsg; delete _cart().statusMessage; } function render_main() { renderMainPage(false); } function render_edit() { renderMainPage(true); } function _errorDiv() { var m = _cart().errorMsg; if (m) { return DIV({className: 'errormsg', id: 'errormsg'}, m); } else { return ''; } } function _validationError(id, errorMessage) { var cart = _cart(); cart.errorMsg = errorMessage; cart.errorId = {}; if (id instanceof Array) { id.forEach(function(k) { cart.errorId[k] = true; }); } else { cart.errorId[id] = true; } response.redirect('/ep/admin/billing/edit'); } function _errorIfInvalid(id) { var cart = _cart(); if (cart.errorId && cart.errorId[id]) { return 'error'; } else { return ''; } } function paypalNotifyUrl() { return request.scheme+"://"+pro_utils.getFullSuperdomainHost()+"/ep/store/paypalnotify"; } function _paymentSummary(payInfo) { return payInfo.cardType + " ending in " + payInfo.cardNumber.substr(-4); } function _expiration(payInfo) { return payInfo.cardExpiration; } function _attemptAuthorization(success_f) { var cart = _cart(); var domain = domains.getRequestDomainRecord(); var domainId = domain.id; var domainName = domain.subDomain; var payInfo = checkout.generatePayInfo(cart); var proAccount = pro_accounts.getSessionProAccount(); var fullName = cart.billingFirstName+" "+cart.billingLastName; var email = proAccount.email; // PCI rules require that we not store the CVV longer than necessary to complete the transaction var savedCvv = payInfo.cardCvv; delete payInfo.cardCvv; checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), domain: domain, payInfo: payInfo})); payInfo.cardCvv = savedCvv; var result = billing.authorizePurchase(payInfo, paypalNotifyUrl()); if (result.status == 'success') { billing.log({type: 'new-subscription', name: fullName, domainId: domainId, domainName: domainName}); success_f(result); } else if (result.status == 'pending') { _validationError('', "Your authorization is pending. When it clears, your account will be activated. "+ "You may choose to pay by different means now, or wait until your authorization clears."); } else if (result.status == 'failure') { var paypalResult = result.debug; billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult}); checkout.validateErrorFields(_validationError, "There seems to be an error in your billing information."+ " Please verify and correct your ", result.errorField.userErrors); checkout.validateErrorFields(_validationError, "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. \n\n"+ fastJSON.stringify(cart)); _validationError('', "An unknown error occurred. We're looking into it!") } } function _processNewSubscription() { _attemptAuthorization(function(result) { var domain = domains.getRequestDomainRecord(); var domainId = domain.id; var domainName = domain.subDomain; var cart = _cart(); var payInfo = checkout.generatePayInfo(cart); var proAccount = pro_accounts.getSessionProAccount(); var fullName = cart.billingFirstName+" "+cart.billingLastName; var email = proAccount.email; inTransaction(function() { var subscriptionId = team_billing.createSubscription(domainId, cart.billingReferralCode); team_billing.setRecurringBillingInfo( domainId, fullName, email, _paymentSummary(payInfo), _expiration(payInfo), result.purchaseInfo.paypalId); }); if (globals.isProduction()) { sendEmail('sales@pad.spline.inf.fu-berlin.de', 'sales@pad.spline.inf.fu-berlin.de', "EtherPad: New paid pro account for "+fullName, {}, "This is an automatic notification.\n\n"+fullName+" ("+email+") successfully set up "+ "a billing profile for domain: "+domainName+"."); } }); } function _updateExistingSubscription(subscription) { var cart = _cart(); _attemptAuthorization(function(result) { inTransaction(function() { var cart = _cart(); var domain = domains.getRequestDomainId(); var payInfo = checkout.generatePayInfo(cart); var proAccount = pro_accounts.getSessionProAccount(); var fullName = cart.billingFirstName+" "+cart.billingLastName; var email = proAccount.email; var subscriptionId = subscription.id; team_billing.setRecurringBillingInfo( domain, fullName, email, _paymentSummary(payInfo), _expiration(payInfo), result.purchaseInfo.paypalId); }); }); if (subscription.paidThrough < new Date) { // if they're behind, do the purchase! if (team_billing.processSubscription(subscription)) { cart.statusMessage = "Your payment was successful, and your account is now up to date! You will receive a receipt by email." } else { cart.statusMessage = "Your payment failed; you will receive further instructions by email."; } } } function _processBillingInfo() { var cart = _cart(); var domain = domains.getRequestDomainId(); var subscription = team_billing.getSubscriptionForCustomer(domain); if (! subscription) { _processNewSubscription(); response.redirect('/ep/admin/billing/'); } else { team_billing.updateSubscriptionCouponCode(subscription.id, cart.billingReferralCode); if (cart.billingCCNumber.length > 0) { _updateExistingSubscription(subscription); } response.redirect('/ep/admin/billing') } } function _processPaypalPurchase() { var domain = domains.getRequestDomainId(); billing.log({type: "paypal-attempt", domain: domain, message: "Someone tried to use paypal to pay for on-demand."+ " They got an error message. If this happens a lot, we should implement paypal."}) java.lang.Thread.sleep(5000); _validationError('billingPurchaseType', "There was an error contacting PayPal. Please try another payment type.") } function _processInvoicePurchase() { var output = [ "Name: "+cart.billingFirstName+" "+cart.billingLastName, "\nAddress: ", cart.billingAddressLine1+(cart.billingAddressLine2.length > 0 ? "\n"+cart.billingAddressLine2 : ""), cart.billingCity + ", " + (cart.billingState.length > 0 ? cart.billingState : cart.billingProvince), cart.billingZipCode.length > 0 ? cart.billingZipCode : cart.billingPostalCode, cart.billingCountry, "\nEmail: ", pro_accounts.getSessionProAccount().email ].join("\n"); var recipient = (globals.isProduction() ? 'sales@pad.spline.inf.fu-berlin.de' : 'jd@appjet.com'); sendEmail( recipient, 'sales@pad.spline.inf.fu-berlin.de', 'Invoice payment request - '+pro_utils.getProRequestSubdomain(), {}, "Hi there,\n\nA pro user tried to pay by invoice. Their information follows."+ "\n\nThanks!\n\n"+output); _validationError('', "Your information has been sent to our sales department; a salesperson will contact you shortly regarding your invoice request.") } function render_apply() { var cart = _cart(); eachProperty(request.params, function(k, v) { if (startsWith(k, "billing")) { if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; } cart[k] = toHTML(v); } }); if (! request.params.backbutton) { var allPaymentFields = ["billingCCNumber", "billingExpirationMonth", "billingExpirationYear", "billingCSC", "billingAddressLine1", "billingAddressLine2", "billingCity", "billingState", "billingZipCode", "billingProvince", "billingPostalCode"]; var allBlank = true; allPaymentFields.forEach(function(field) { if (cart[field].length > 0) { allBlank = false; }}); if (! allBlank) { checkout.validateBillingCart(_validationError, cart); } } else { response.redirect("/ep/admin/billing/"); } var couponCode = cart.billingReferralCode; if (couponCode.length != 0 && (couponCode.length != 8 || ! team_billing.getCouponValue(couponCode))) { _validationError('billingReferralCode', 'Invalid referral code entered. Please verify your code and try again.'); } if (cart.billingPurchaseType == 'paypal') { _processPaypalPurchase(); } else if (cart.billingPurchaseType == 'invoice') { _processInvoicePurchase(); } _processBillingInfo(); } function handlePaypalNotify() { // XXX: handle delayed paypal authorization } function render_invoices() { if (request.params.id) { var purchaseId = team_billing.getSubscriptionForCustomer(domains.getRequestDomainId()).id; var invoice = billing.getInvoice(request.params.id); if (invoice.purchase != purchaseId) { response.redirect(request.path); } var transaction; var adjustments = billing.getAdjustments(invoice.id); if (adjustments.length == 1) { transaction = billing.getTransaction(adjustments[0].transaction); } pro_admin_control.renderAdminPage('single-invoice', { formatDate: checkout.formatDate, dollars: checkout.dollars, centsToDollars: billing.centsToDollars, invoice: invoice, transaction: transaction }); } else { var invoices = team_billing.getAllInvoices(domains.getRequestDomainId()); pro_admin_control.renderAdminPage('billing-invoices', { invoices: invoices, formatDate: checkout.formatDate, dollars: checkout.dollars, centsToDollars: billing.centsToDollars }); } }