pyc-website
main website for pyc inc.
git clone https://9o.is/git/pyc-website.git
commit bdfeeefd3324991020e55c80ce99925f381dfd07 parent 134c354adbfcad9ad50ae98d890fff0f5f0a04c5 Author: Jul <jul@9o.is> Date: Thu, 26 Jun 2014 11:35:01 -0400 Implemented purchase limit field, twilio. Still not tested Diffstat:
12 files changed, 294 insertions(+), 203 deletions(-)
diff --git a/src/main/resources/props/default.props b/src/main/resources/props/default.props @@ -1,4 +1,3 @@ -<<<<<<< HEAD mail.smtp.host=smtp.gmail.com mail.smtp.user= mail.smtp.pass= @@ -12,3 +11,7 @@ mongo.default.pwd= idverify.access.key= idverify.secret.key= idverify.bucket= + +twilio.sid= +twilio.token= +twilio.phone= diff --git a/src/main/scala/inc/pyc/config/Site.scala b/src/main/scala/inc/pyc/config/Site.scala @@ -31,6 +31,7 @@ case class MenuLoc(menu: Menu) { object Emails { val idVerification = "idverification@"+Site.domain val atmBusiness = "atmbusiness@"+Site.domain + val technical = "technical@"+Site.domain } object Site extends Locs { @@ -58,15 +59,6 @@ object Site extends Locs { val settings = MenuLoc(Menu.i("Settings") / "settings" >> SettingsGroup >> RequireLoggedIn) val password = MenuLoc(Menu.i("Password") / "settings" / "password" >> SettingsGroup >> RequireLoggedIn) val emailResetToken = MenuLoc(buildEmailResetTokenMenu) - - val idVerification = MenuLoc(Menu.i("ID Verification") / "settings" / "verification" >> SettingsGroup >> RequireLoggedIn >> - TemplateBox(() => User.currentUser.map { user => - val page = - if (user.isPycVerified) "verified" - else "verification" - - Templates("settings" :: page :: Nil) openOr xml.NodeSeq.Empty - })) val purchaseLimit = MenuLoc(Menu.i("Purchase Limit") / "settings" / "purchase_limit" >> SettingsGroup >> RequireLoggedIn) @@ -92,7 +84,6 @@ object Site extends Locs { forgotPassword.menu, settings.menu, password.menu, - idVerification.menu, purchaseLimit.menu, emailResetToken.menu, Menu.i("Error") / "error" >> Hidden, diff --git a/src/main/scala/inc/pyc/lib/IdVerification.scala b/src/main/scala/inc/pyc/lib/IdVerification.scala @@ -113,7 +113,7 @@ object IdVerificationHelper { |Identity Check Request! |Full Name: ${user.fname} ${user.lname} |Id: ${user.id.get} - |Status: ${user.idVerification.get} + |Limit: ${user.purchaseLimit.get} |Email: ${user.email.get} |Username: ${user.username.get} """ diff --git a/src/main/scala/inc/pyc/lib/Twilio.scala b/src/main/scala/inc/pyc/lib/Twilio.scala @@ -0,0 +1,41 @@ +package inc.pyc +package lib + +import config._ +import dispatch._, Defaults._ +import net.liftweb.util.{Mailer, Props} +import com.ning.http.client.Response +import net.liftmodules.mongoauth.MongoAuth + +object Twilio { + + lazy val host = :/("api.twilio.com").secure / "2010-04-01" / "Accounts" / sid as_!(sid,token) + lazy val sid = Props.get("twilio.sid", "") + lazy val token = Props.get("twilio.token", "") + lazy val phone = Props.get("twilio.phone", "") + + def sms(to: String, body: String)(implicit countrycode: String = "+1"): Boolean = { + val r: Future[Response] = + Http(host / "SMS" / "Messages.json" << Map("From" -> phone, "To" -> to, "Body" -> body)) + + val success = r().getStatusCode() == 200 + + if(!success) notifyByEmail(r().getStatusText()) + success + } + + /** + * Notify us of the new applicant by email, + * so we can begin reviewing immediately. + */ + def notifyByEmail(msg: String): Unit = { + import Mailer._ + + sendMail( + From(MongoAuth.systemFancyEmail), + Subject("Technical: Twilio SMS Failed"), + To(Emails.technical), + PlainMailBodyType(msg) + ) + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/model/User.scala b/src/main/scala/inc/pyc/model/User.scala @@ -4,21 +4,19 @@ package model import lib._ import field._ import lib.RogueMetaRecord - import org.bson.types.ObjectId import org.joda.time.DateTime - import net.liftweb._ import common._ import http.{StringField => _, BooleanField => _, _} import mongodb.record.field._ import record.field._ - import net.liftmodules.mongoauth._ import net.liftmodules.mongoauth.field._ import net.liftmodules.mongoauth.model._ +import java.util.regex.Pattern -class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] with UserVerification[User] { +class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] with USAUserVerification[User] { def meta = User def userIdAsString: String = id.toString @@ -34,8 +32,22 @@ class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] with Use valMaxLen(64, "First Name must be 64 characters or less") _ :: super.validations } + + object postal extends PostalCodeField(this, usa) + + object phone extends StringField(this, 10) { + override def validations = + valRegex(Pattern.compile("[0-9]{10}"), "Phone number must be 10 digits long") _ :: + super.validations + } + + object phoneverified extends BooleanField(this, false) def whenCreated: DateTime = new DateTime(id.get.getTime) + + private def usa = new CountryField(this) { + override def defaultValue = Countries.USA + } } object User extends User with ProtoAuthUserMeta[User] with RogueMetaRecord[User] with Loggable { diff --git a/src/main/scala/inc/pyc/model/field/IdVerificationField.scala b/src/main/scala/inc/pyc/model/field/IdVerificationField.scala @@ -1,79 +0,0 @@ -package inc.pyc -package model -package field - -import net.liftweb._ -import http._ -import util.Helpers._ -import record._ -import field._ -import json.JsonAST._ -import common._ - -object IdVerification extends Enumeration { - - val - Unverified, // not verified - PYC_Pending, // application submitted but not verified yet - PYC_Verified, // verified by us - Third_Party_Request, // requested to be verified by 3rd party - Third_Party_Pending, // user info was submitted to 3rd party; waiting for reply - Third_Party_Verified // verified by third party - = Verification - - def Verification = new Verification - - class Verification extends Val { - override def toString = super.toString.replace("_", " ") - } - - def options: List[(String, String)] = - IdVerification.values.map(i => (i.toString, i.toString)).toList - - def select(s: String) = tryo(IdVerification.withName(s)) -} - -/** - * ID Verification field for records. - */ -class IdVerificationField[OwnerType <: Record[OwnerType]](rec: OwnerType) extends EnumField(rec, IdVerification) { - override def setFromJValue(jvalue: JValue): Box[IdVerificationField.this.MyType] = { - val stringToInt = jvalue transform { - case JString(s) => - val value: Box[Int] = IdVerification.select(s).map(_.id) - value.map(JInt(_)).openOr(JNothing) - case _ => JNothing - } - super.setFromJValue(stringToInt) - } -} - -/** - * Add this to a user to keep track of the user's real ID verification stage. - */ -trait UserVerification[A <: Record[A]] { - this: A => - /** The record's real id verification stage. */ - object idVerification extends IdVerificationField[A](this) - - /** - * If user is verified by us. - */ - def isPycVerified: Boolean = - idVerification.get == IdVerification.PYC_Verified || - waiting3rdPartyVerification || - is3rdPartyVerified - - /** - * If user is waiting for verification from 3rd party company. - */ - def waiting3rdPartyVerification: Boolean = - idVerification.get == IdVerification.Third_Party_Request || - idVerification.get == IdVerification.Third_Party_Pending - - /** - * If user is verified by 3rd party company. - */ - def is3rdPartyVerified: Boolean = - idVerification.get == IdVerification.Third_Party_Verified -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/model/field/PurchaseLimitField.scala b/src/main/scala/inc/pyc/model/field/PurchaseLimitField.scala @@ -0,0 +1,74 @@ +package inc.pyc +package model +package field + +import net.liftweb._ +import http._ +import util.Helpers._ +import record._ +import field._ +import json.JsonAST._ +import common._ + +trait PurchaseLimit extends Enumeration + +/** + * Purchase limit in the United States by dollars. + * + * $500 - not verified + * $1000 - verified phone and postal code + * $3000 - submitted identification + * $3000 - verified identification + * Unlimited - request to be checked by 3rd party + * Unlimited - verified by third party + * + * For more information, visit + * http://www.fincen.gov/financial_institutions/msb/materials/en/bank_reference.html + */ +object USAPurchaseLimit extends PurchaseLimit { + + val D500 = Value("$500") + val D1000 = Value("$1,000") + val D3000_Pending = Value("$3,000 Pending") + val D3000 = Value("$3,000") + val Unlimited_Pending = Value("Unlimited Pending") + val Unlimited = Value("Unlimited") + + def options: List[(String, String)] = + USAPurchaseLimit.values.map(i => (i.toString, i.toString)).toList + + def select(s: String) = tryo(USAPurchaseLimit.withName(s)) +} + +/** + * Purchase Limit field for records. + */ +class USAPurchaseLimitField[OwnerType <: Record[OwnerType]](rec: OwnerType) extends EnumField(rec, USAPurchaseLimit) { + override def setFromJValue(jvalue: JValue): Box[USAPurchaseLimitField.this.MyType] = { + val stringToInt = jvalue transform { + case JString(s) => + val value: Box[Int] = USAPurchaseLimit.select(s).map(_.id) + value.map(JInt(_)).openOr(JNothing) + case _ => JNothing + } + super.setFromJValue(stringToInt) + } +} + +/** + * Add this to a user to keep track of the user's real ID verification stage. + */ +trait USAUserVerification[A <: Record[A]] { + this: A => + + /** The record's purchase limit. */ + object purchaseLimit extends USAPurchaseLimitField[A](this) + + /* + * Friendly functions to check limits + */ + def limit500: Boolean = purchaseLimit.get == USAPurchaseLimit.D500 + def limit1000: Boolean = purchaseLimit.get == USAPurchaseLimit.D1000 || purchaseLimit.get == USAPurchaseLimit.D3000_Pending + def limit3000: Boolean = purchaseLimit.get == USAPurchaseLimit.D3000 || purchaseLimit.get == USAPurchaseLimit.Unlimited_Pending + def unlimited: Boolean = purchaseLimit.get == USAPurchaseLimit.Unlimited +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/snippet/UserSnip.scala b/src/main/scala/inc/pyc/snippet/UserSnip.scala @@ -311,104 +311,108 @@ class UserSettingsEmail extends AngularCurrentUser { } } -object IdVerification extends CurrentUser { - def is3rdPartyVerified(ns: NodeSeq): NodeSeq = serve { - user => - if(user.is3rdPartyVerified) ns else NodeSeq.Empty - } - - def waiting3rdPartyVerification(ns: NodeSeq): NodeSeq = serve { - user => - if(user.waiting3rdPartyVerification && - !user.is3rdPartyVerified) ns - else NodeSeq.Empty - } - - def isPycVerified(ns: NodeSeq): NodeSeq = serve { - user => - if(user.isPycVerified && - !user.waiting3rdPartyVerification && - !user.is3rdPartyVerified) ns - else NodeSeq.Empty - } -} -class IdVerification extends AngularCurrentUser { +class PhoneVerification extends AngularCurrentUser { + + lazy val smscode = (100000 + new scala.util.Random().nextInt(900000)).toString def roundTrips: List[RoundTripInfo] = List( - "submit" -> submit _, - "thirdPartyVerify" -> thirdPartyVerify _, + "postal" -> postal _, + "sendsms" -> sendsms _, + "verifyphone" -> verifyphone _, "init" -> init _) - def submit(model: JValue): JValue = serve { + + def postal(model: JValue): JValue = serve { + user => + for { + JString(postal) <- model + } yield { + user.postal(postal) + validateAndUpdate() + } + } + + def sendsms(model: JValue): JValue = serve { user => for { - JString(dob) <- model \ "dob" - JString(ssn) <- model \ "ssn" - JString(address) <- model \ "address" - JString(city) <- model \ "city" - JString(state) <- model \ "state" + JString(phone) <- model \ "phone" } yield { + val msg = s"Hi ${user.fname.get}, enter $smscode to"+ + " verify your PYC account. This is a 1-time message." - val output = - s"Name: ${user.fname.get} ${user.lname.get}\n"+ - s"dob: $dob\n"+ - s"ssn: $ssn\n"+ - s"address: $address\n"+ - s"city: $city\n"+ - s"state: $state" - - IdVerificationFiles.set(IdVerificationFiles.get :+ - new InMemFileParamHolder("", "text/plain", "info", output.getBytes)) - - import IdVerificationHelper._ - val service = s3(createS3Folder(user)) - - val statusCodes = IdVerificationFiles.get.map { fp => - service.createFile(fp.fileName, fp.file, fp.mimeType) - } map { - f => - import dispatch._ - f().getStatusCode() - } - - IdVerificationFiles(Nil) - - if(statusCodes.forall(_ == 200)) { - user.idVerification(field.IdVerification.PYC_Pending) - user.update - notifyIdentityRequest(user) - NgAlert.success( - <i class="fa-fw fa fa-thumbs-o-up"></i> ++ - <span>We have received your application:</span> ++ - <p>We will notify you as soon as we are done reviewing your information.</p> - ) + if(Twilio.sms(phone, msg)) { + user.phone(phone) + validateAndUpdate() } else { NgAlert.danger( - <i class="fa-fw fa fa-thumbs-o-down"></i> ++ - <span>File could not be uploaded. Please try again.</span>, Nil - ) + <i class="fa-fw fa fa-thumbs-o-down"></i> ++ + <span>We apologize for the incovenience, but we're experiencing</span> ++ + <span> technical difficulties. Please try again later.</span>, Nil) } + } } - def thirdPartyVerify(model: JValue): JValue = serve { + def verifyphone(model: JValue): JValue = serve { user => - user.idVerification(field.IdVerification.Third_Party_Request) - user.update - IdVerificationHelper.notifyIdentityRequest(user, thirdParty = true) + for { + JString(smscode) <- model \ "smscode" + } yield + + if(this.smscode == smscode) { + user.purchaseLimit(USAPurchaseLimit.D1000) + user.phoneverified(true) + validateAndUpdate() + } else { + NgAlert.danger( + <i class="fa-fw fa fa-thumbs-o-down"></i> ++ + <span>The private code is incorrect. Try again</span>, Nil) + } - NgAlert.success( - <i class="fa-fw fa fa-thumbs-o-up"></i> ++ - <span>Application will be verified by third party company</span> ++ - <p>Thank you for your patience.</p> - ) } def init(model: JValue): JValue = serve { user => - ("idverification" -> user.idVerification.get.toString) ~ - ("fname" -> user.fname.get) ~ - ("lname" -> user.lname.get) + ("postal" -> user.postal.get) ~ + ("phone" -> user.phone.get) + } +} + + +class IdVerification extends AngularCurrentUser { + + def roundTrips: List[RoundTripInfo] = List( + "submit" -> submit _) + + def submit(model: JValue): JValue = serve { + user => + + import IdVerificationHelper._ + val service = s3(createS3Folder(user)) + + val statusCodes = IdVerificationFiles.get.map { fp => + service.createFile(fp.fileName, fp.file, fp.mimeType) + } map { + f => + import dispatch._ + f().getStatusCode() + } + + IdVerificationFiles(Nil) + + if (statusCodes.forall(_ == 200)) { + user.purchaseLimit(USAPurchaseLimit.D3000_Pending) + user.update + notifyIdentityRequest(user) + NgAlert.success( + <i class="fa-fw fa fa-thumbs-o-up"></i> ++ + <span>We have received your application:</span> ++ + <p>We will notify you as soon as we are done reviewing your information.</p>) + } else { + NgAlert.danger( + <i class="fa-fw fa fa-thumbs-o-down"></i> ++ + <span>File could not be uploaded. Please try again.</span>, Nil) + } } } \ No newline at end of file diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -185,6 +185,55 @@ app.controller('PasswordChangeCtrl', ['$scope', '$controller', '$rootScope', fun }]); +app.controller('PhoneVerificationAlert', ['$scope', '$controller', function($scope, $controller) { + $controller('AlertCtrl', {$scope: $scope}); + + $scope.$on('alertPhoneVerification', function(event, alert) { + $scope.addAlert(alert); + }); +}]); + +app.controller('PhoneVerificationCtrl', ['$scope', '$controller', '$rootScope', 'WizardHandler', function($scope, $controller, $rootScope, WizardHandler) { + $controller('AutoUpdateFormCtrl', {$scope: $scope, $controller: $controller, $rootScope: $rootScope}); + $scope.init('PhoneVerification', 'init'); + + $scope.updatePostal = function() { + $scope.update('PhoneVerification', 'postal'); + }; + + $scope.sentsms = false; + + $scope.sendsms = function() { + var success = function() { + $scope.sentsms = true; + $rootScope.$broadcast('alertPhoneVerification', { + msg_type: "success", + msg: "SMS Private Code has been sent. Input the private code to verify.", + timeout: 4000 + }); + }; + + var failure = function(alert) { + $rootScope.$broadcast('alertPhoneVerification', alert); + }; + + $scope.submit('PhoneVerification', 'sendsms', success, failure); + }; + + $scope.verifyphone = function () { + var success = function() { + WizardHandler.wizard().next(); + }; + + var failure = function(alert) { + $rootScope.$broadcast('alertPhoneVerification', alert); + }; + + $scope.submit('PhoneVerification', 'verifyphone', success, failure); + }; +}]); + + app.controller('IdVerificationAlert', ['$scope', '$controller', function($scope, $controller) { $controller('AlertCtrl', {$scope: $scope}); @@ -194,8 +243,7 @@ app.controller('IdVerificationAlert', ['$scope', '$controller', function($scope, }]); app.controller('IdVerificationCtrl', ['$scope', '$controller', '$rootScope', '$fileUploader', function($scope, $controller, $rootScope, $fileUploader) { - $controller('LoadedFormCtrl', {$scope: $scope, $controller: $controller}); - $scope.init('IdVerification', 'init'); + $controller('FormCtrl', {$scope: $scope, $controller: $controller}); $scope.uploader = $fileUploader.create({ scope: $scope, @@ -214,20 +262,20 @@ app.controller('IdVerificationCtrl', ['$scope', '$controller', '$rootScope', '$f return item.size < 5243000; }); - // 3 files max are allowed - $scope.uploader.queueLimit = 3; + // 1 file max are allowed + $scope.uploader.queueLimit = 1; - // 2 files minimum required before submitting + // 1 file minimum required before submitting $scope.filesRequired = function() { - return $scope.uploader.queue.length < 2; + return $scope.uploader.queue.length < 1; }; $scope.uploader.bind('whenaddingfilefailed', function () { - $rootScope.$broadcast('alertIdVerification', {type: "danger", msg: "Your file could not be added."}); + $rootScope.$broadcast('alertIdVerification', {msg_type: "danger", msg: "Your file could not be added."}); }); $scope.uploader.bind('error', function () { - $rootScope.$broadcast('alertIdVerification', {type: "danger", msg: "Your files could not be uploaded. Please try again."}); + $rootScope.$broadcast('alertIdVerification', {msg_type: "danger", msg: "Your file could not be uploaded. Please try again."}); }); $scope.uploader.bind('success', function () { @@ -249,19 +297,6 @@ app.controller('IdVerificationCtrl', ['$scope', '$controller', '$rootScope', '$f $scope.loading = true; $scope.uploader.uploadAll(); }; - - $scope.thirdPartyVerify = function() { - var success = function(alert) { - $rootScope.$broadcast('alertDialog', alert); - document.location.reload(); - }; - - var failure = function(alert) { - $rootScope.$broadcast('alertDialog', alert); - }; - - $scope.submit('IdVerification', 'thirdPartyVerify', success, failure); - }; }]); app.controller('UserSettingsCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { diff --git a/src/main/webapp/app/controllers/Forms.js b/src/main/webapp/app/controllers/Forms.js @@ -109,7 +109,7 @@ angular.module("Forms", ['ngAlert']) */ $scope.update = function(className, funcName, successFunc, failureFunc) { // if values are different, update - if($scope.diff(funcName)) { + if($scope.diff(funcName) && $scope.form[funcName].$valid === true) { $scope[funcName+"_loading"] = true; window[className][funcName]($scope.model[funcName]).then(function(alert) { $scope.$apply(function() { diff --git a/src/main/webapp/app/controllers/ngAlert.js b/src/main/webapp/app/controllers/ngAlert.js @@ -13,7 +13,7 @@ * } */ angular.module("ngAlert", ['ui.bootstrap']) - .controller('AlertCtrl', ['$scope', function($scope) { + .controller('AlertCtrl', ['$scope', '$timeout', function($scope, $timeout) { $scope.alerts = []; $scope.$on('alertDialog', function(event, alert) { @@ -27,6 +27,10 @@ angular.module("ngAlert", ['ui.bootstrap']) $scope.addAlert = function(alert) { $scope.alerts = []; $scope.alerts.push({type: alert.msg_type, msg: alert.msg}); + + if(alert.hasOwnProperty('timeout')) { + $timeout($scope.clearAlerts(), alert.timeout); + } }; $scope.closeAlert = function(index) { diff --git a/src/main/webapp/templates-hidden/parts/phone-verification-form.html b/src/main/webapp/templates-hidden/parts/phone-verification-form.html @@ -1,17 +1,18 @@ <div data-lift="PhoneVerification" ng-controller="PhoneVerificationCtrl" ng-cloak> - <form name="form" class="smart-form client-form padding-20-sides" ng-submit="save()" novalidate> + <form name="form" class="smart-form client-form padding-20-sides" novalidate> <fieldset> <section> <label>Zip Code</label> <label class="input" ng-class="{{ stateSuccessError('postal') }}"> - <input name="postal" ng-model="model.postal" ui-mask="99999" placeholder="xxxxx" type="text" class="input" required> + <i ng-show="postal_loading" class="icon-append fa fa-spinner fa-spin"></i> + <input name="postal" ng-model="model.postal" ui-mask="99999" placeholder="xxxxx" type="text" ng-blur="updatePostal()" required> <span ng-show="form.postal.$invalid && form.postal.$dirty" class="text-danger small"> Invalid postal code </span> </label> </section> - <section> + <section ng-show="form.postal.$valid"> <label>Phone Number <small>with text messaging</small></label> <label class="input" ng-class="{{ stateSuccessError('phone') }}"> <i class="icon-append fa fa-phone"></i> @@ -22,7 +23,7 @@ </label> </section> - <section> + <section ng-show="sentsms"> <label class="input" ng-class="{{ stateSuccessError('smscode') }}"> <label>SMS Code</label> <input name="smscode" ng-model="model.smscode" ui-mask="999-999" placeholder="xxx-xxx" type="text" required> @@ -31,8 +32,12 @@ </fieldset> <footer> - <button type="submit" class="btn btn-primary">Send SMS Code</button> - <button wz-next type="submit" class="btn btn-primary">Verify Number</button> + <div ng-controller="PhoneVerificationAlert" ng-cloak> + <span data-lift="embed?what=/templates-hidden/parts/alert"></span> + </div> + + <button ng-show="form.postal.$valid && !sentsms" ng-disable="form.phone.$invalid" ng-click="sendsms()" class="btn btn-primary">Send SMS Code</button> + <button ng-show="sentsms" ng-click="verifyphone()" class="btn btn-primary">Verify Number</button> </footer> </form> </div> \ No newline at end of file