//GetAll retrieves the list of all users in the datastore //the data is pulled from memcache or the datastore //the data is returned as a json to populate select menus in the gui func GetAll(w http.ResponseWriter, r *http.Request) { //check if list of users is in memcache result := make([]userList, 0, 5) c := appengine.NewContext(r) _, err := memcache.Gob.Get(c, listOfUsersKey, &result) if err == nil { output.Success("userList-cached", result, w) return } //list of cards not found in memcache //get list from datastore //only need to get username and entity key to cut down on datastore usage //save the list to memcache for faster retrieval next time if err == memcache.ErrCacheMiss { q := datastore.NewQuery(datastoreKind).Order("Username").Project("Username") users := make([]User, 0, 5) keys, err := q.GetAll(c, &users) if err != nil { output.Error(err, "Error retrieving list of users from datastore.", w, r) return } //build result //format data to show just datastore id and username //creates a map of structs idsAndNames := make([]userList, 0, 5) for i, r := range users { x := userList{ Username: r.Username, Id: keys[i].IntID(), } idsAndNames = append(idsAndNames, x) } //save the list of users to memcache //ignore errors since we still retrieved the data memcacheutils.Save(c, listOfUsersKey, idsAndNames) //return data to clinet output.Success("userList", idsAndNames, w) return } else if err != nil { output.Error(err, "Unknown error retrieving list of users.", w, r) return } return }
//GetAll retrieves the list of all cards in the datastore (datastore id and customer name only) //the data is pulled from memcache or the datastore //the data is returned as json to populate the datalist in the html ui func GetAll(w http.ResponseWriter, r *http.Request) { //check if list of cards is in memcache c := appengine.NewContext(r) result := make([]CardList, 0, 50) _, err := memcache.Gob.Get(c, listOfCardsKey, &result) if err == nil { output.Success("cardlist-cached", result, w) return } //list of cards not found in memcache //get list from datastore //only need to get entity keys and customer names: cuts down on datastore usage //save the list to memcache for faster retrieval next time if err == memcache.ErrCacheMiss { q := datastore.NewQuery(datastoreKind).Order("CustomerName").Project("CustomerName") cards := make([]CustomerDatastore, 0, 50) keys, err := q.GetAll(c, &cards) if err != nil { output.Error(err, "Error retrieving list of cards from datastore.", w, r) return } //build result //format data to show just datastore id and customer name //creates a map of structs idAndNames := make([]CardList, 0, 50) for i, r := range cards { x := CardList{r.CustomerName, keys[i].IntID()} idAndNames = append(idAndNames, x) } //save list of cards to memcache //ignore errors since we already got the data memcacheutils.Save(c, listOfCardsKey, idAndNames) //return data to client output.Success("cardList-datastore", idAndNames, w) return } else if err != nil { output.Error(err, "Unknown error retrieving list of cards.", w, r) return } return }
//ChangePwd is used to change a user's password func ChangePwd(w http.ResponseWriter, r *http.Request) { //gather inputs userId := r.FormValue("userId") userIdInt, _ := strconv.ParseInt(userId, 10, 64) password1 := r.FormValue("pass1") password2 := r.FormValue("pass2") //make sure passwords match if doStringsMatch(password1, password2) == false { output.Error(ErrPasswordsDoNotMatch, "The passwords you provided to not match.", w, r) return } //make sure password is long enough if len(password1) < minPwdLength { output.Error(ErrPasswordTooShort, "The password you provided is too short. It must be at least "+strconv.FormatInt(minPwdLength, 10)+" characters.", w, r) return } //hash the password hashedPwd := pwds.Create(password1) //get user data c := appengine.NewContext(r) userData, err := Find(c, userIdInt) if err != nil { output.Error(err, "Error while retreiving user data to update user's password.", w, r) return } //set new password userData.Password = hashedPwd //clear memcache for this userID & username err = memcacheutils.Delete(c, userId) err1 := memcacheutils.Delete(c, userData.Username) if err != nil { output.Error(err, "Error clearing cache for user id.", w, r) return } else if err1 != nil { output.Error(err1, "Error clearing cache for username.", w, r) return } //generate full datastore key for user fullKey := getUserKeyFromId(c, userIdInt) //save user _, err = saveUser(c, fullKey, userData) if err != nil { output.Error(err, "Error saving user to database after password change.", w, r) return } //done output.Success("userChangePassword", nil, w) return }
//Remove removes a card from the datastore, memcache, and stripe func Remove(w http.ResponseWriter, r *http.Request) { //get form values datastoreId := r.FormValue("customerId") datastoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64) //make sure an id was given if len(datastoreId) == 0 { output.Error(ErrMissingInput, "A customer's datastore ID must be given but was missing. This value is different from your \"Customer ID\" and should have been submitted automatically.", w, r) return } //init stripe c := appengine.NewContext(r) sc := createAppengineStripeClient(c) //delete customer on stripe custData, err := findByDatastoreId(c, datastoreIdInt) if err != nil { output.Error(err, "An error occured while trying to look up customer's Stripe information.", w, r) } stripeCustId := custData.StripeCustomerToken sc.Customers.Del(stripeCustId) //delete custome from datastore completeKey := getCustomerKeyFromId(c, datastoreIdInt) err = datastore.Delete(c, completeKey) if err != nil { output.Error(err, "There was an error while trying to delete this customer. Please try again.", w, r) return } //delete customer from memcache //delete list of cards in memcache since this list is now stale //all memcache.Delete operations are listed first so error handling doesn't return if one fails...each call does not depend on another so this is safe //obviously, if the card is not in the cache it cannot be removed err1 := memcache.Delete(c, datastoreId) err2 := memcache.Delete(c, custData.CustomerId) err3 := memcache.Delete(c, listOfCardsKey) if err1 != nil && err1 != memcache.ErrCacheMiss { output.Error(err1, "There was an error flushing this card's data from the cache (by datastore id). Please contact an administrator and have them flush the cache manually.", w, r) return } if err2 != nil && err2 != memcache.ErrCacheMiss { output.Error(err2, "There was an error flushing this card's data from the cache (by customer id). Please contact an administrator and have them flush the cache manually.", w, r) return } if err3 != nil && err3 != memcache.ErrCacheMiss { output.Error(err3, "There was an error flushing the cached list of cards.", w, r) return } //customer removed //return to client output.Success("removeCustomer", nil, w) return }
//GetCompanyInfo is used when viewing the data in the gui or on a receipt func GetCompanyInfo(w http.ResponseWriter, r *http.Request) { //get info info, err := getCompanyInfo(r) if err != nil { output.Error(err, "", w, r) return } output.Success("dataFound", info, w) return }
//SaveCompanyInfo saves new or updates existing company info in the datastore func SaveCompanyInfo(w http.ResponseWriter, r *http.Request) { //get form values name := strings.TrimSpace(r.FormValue("name")) street := strings.TrimSpace(r.FormValue("street")) suite := strings.TrimSpace(r.FormValue("suite")) city := strings.TrimSpace(r.FormValue("city")) state := strings.TrimSpace(r.FormValue("state")) postal := strings.TrimSpace(r.FormValue("postal")) country := strings.TrimSpace(r.FormValue("country")) phone := strings.TrimSpace(r.FormValue("phone")) //look up data for this company //may return a blank struct if this data does not exist yet data, err := getCompanyInfo(r) if err != nil && err != ErrCompanyDataDoesNotExist { output.Error(err, "", w, r) return } //context c := appengine.NewContext(r) //generate entity key //keyname is hard coded so only one entity exists key := datastore.NewKey(c, datastoreKind, datastoreKey, 0, nil) //build entity to save //or update existing entity data.CompanyName = name data.Street = street data.Suite = suite data.City = city data.State = strings.ToUpper(state) data.PostalCode = postal data.Country = strings.ToUpper(country) data.PhoneNum = phone //save company info _, err = datastore.Put(c, key, &data) if err != nil { output.Error(err, "", w, r) return } //save company into to memcache //ignoring errors since we can always get data from the datastore memcacheutils.Save(c, memcacheKeyName, data) //done output.Success("dataSaved", data, w) return }
//GetOne retrieves the full data for one user //this is used to fill in the edit user modal in the gui func GetOne(w http.ResponseWriter, r *http.Request) { //get user id from form value userId := r.FormValue("userId") userIdInt, _ := strconv.ParseInt(userId, 10, 64) //get user data c := appengine.NewContext(r) data, err := Find(c, userIdInt) if err != nil { output.Error(err, "Cannot look up user data.", w, r) return } //return user data output.Success("findUser", data, w) return }
//GetOne retrieves the full data for one card from the datastore //this is used to fill in the "charge card" panel with identifying info on the card so the user car verify they are charging the correct card func GetOne(w http.ResponseWriter, r *http.Request) { //get form value datastoreId := r.FormValue("customerId") datstoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64) //get customer card data c := appengine.NewContext(r) data, err := findByDatastoreId(c, datstoreIdInt) if err != nil { output.Error(err, "Could not find this customer's data.", w, r) return } //return data to client output.Success("cardFound", data, w) return }
//REFUND A CHARGE func Refund(w http.ResponseWriter, r *http.Request) { //get form values chargeId := r.FormValue("chargeId") amount := r.FormValue("amount") reason := r.FormValue("reason") //make sure inputs were given if len(chargeId) == 0 { output.Error(ErrMissingInput, "A charge ID was not provided. This is a serious error. Please contact an administrator.", w, r) return } if len(amount) == 0 { output.Error(ErrMissingInput, "No amount was given to refund.", w, r) return } //convert refund amount to cents //stripe requires cents amountCents, err := getAmountAsIntCents(amount) if err != nil { output.Error(err, "An error occured while converting the amount to charge into cents. Please try again or contact an administrator.", w, r) return } //get username of logged in user //for tracking who processed this refund session := sessionutils.Get(r) username := session.Values["username"].(string) //build refund params := &stripe.RefundParams{ Charge: chargeId, Amount: amountCents, } //add metadata to refund //same field name as when creating a charge params.AddMeta("charged_by", username) //get reason code for refund //these are defined by stripe if reason == "duplicate" { params.Reason = refund.RefundDuplicate } else if reason == "requested_by_customer" { params.Reason = refund.RefundRequestedByCustomer } //init stripe c := appengine.NewContext(r) sc := createAppengineStripeClient(c) //create refund with stripe _, err = sc.Refunds.New(params) if err != nil { stripeErr := err.(*stripe.Error) stripeErrMsg := stripeErr.Msg output.Error(ErrStripe, stripeErrMsg, w, r) return } //done output.Success("refund-done", nil, w) return }
//UpdatePermissions is used to save changes to a user's permissions (access rights) //super-admin "administrator" account cannot be edited...this user always has full permissions //you can not edit your own permissions so you don't lock yourself out of the app func UpdatePermissions(w http.ResponseWriter, r *http.Request) { //gather form values userId := r.FormValue("userId") userIdInt, _ := strconv.ParseInt(userId, 10, 64) addCards, _ := strconv.ParseBool(r.FormValue("addCards")) removeCards, _ := strconv.ParseBool(r.FormValue("removeCards")) chargeCards, _ := strconv.ParseBool(r.FormValue("chargeCards")) viewReports, _ := strconv.ParseBool(r.FormValue("reports")) isAdmin, _ := strconv.ParseBool(r.FormValue("admin")) isActive, _ := strconv.ParseBool(r.FormValue("active")) //check if the logged in user is an admin //user updating another user's permission must be an admin //failsafe/second check since non-admins would not see the settings panel anyway session := sessionutils.Get(r) if session.IsNew { output.Error(ErrSessionMismatch, "An error occured. Please log out and log back in.", w, r) return } //get user data to update c := appengine.NewContext(r) userData, err := Find(c, userIdInt) if err != nil { output.Error(err, "We could not retrieve this user's information. This user could not be updates.", w, r) return } //check if the logged in user is trying to update their own permissions //you cannot edit your own permissions no matter what if session.Values["username"].(string) == userData.Username { output.Error(ErrCannotUpdateSelf, "You cannot edit your own permissions. Please contact another administrator.", w, r) return } //check if user is editing the super admin user if userData.Username == adminUsername { output.Error(ErrCannotUpdateSuperAdmin, "You cannot update the 'administrator' user. The account is locked.", w, r) return } //update the user userData.AddCards = addCards userData.RemoveCards = removeCards userData.ChargeCards = chargeCards userData.ViewReports = viewReports userData.Administrator = isAdmin userData.Active = isActive //clear memcache err = memcacheutils.Delete(c, userId) err1 := memcacheutils.Delete(c, userData.Username) if err != nil { output.Error(err, "Error clearing cache for user id.", w, r) return } else if err1 != nil { output.Error(err1, "Error clearing cache for username.", w, r) return } //generate complete key for user completeKey := getUserKeyFromId(c, userIdInt) //resave user //saves to datastore and memcache //save user _, err = saveUser(c, completeKey, userData) if err != nil { output.Error(err, "Error saving user to database after updating permission.", w, r) return } //done output.Success("userUpdatePermissins", nil, w) return }
//Add saves a new user to the app func Add(w http.ResponseWriter, r *http.Request) { //get form values username := r.FormValue("username") password1 := r.FormValue("password1") password2 := r.FormValue("password2") addCards, _ := strconv.ParseBool(r.FormValue("addCards")) removeCards, _ := strconv.ParseBool(r.FormValue("removeCards")) chargeCards, _ := strconv.ParseBool(r.FormValue("chargeCards")) viewReports, _ := strconv.ParseBool(r.FormValue("reports")) isAdmin, _ := strconv.ParseBool(r.FormValue("admin")) isActive, _ := strconv.ParseBool(r.FormValue("active")) //check if this user already exists c := appengine.NewContext(r) _, _, err := exists(c, username) if err == nil { //user already exists //notify client output.Error(ErrUserAlreadyExists, "This username already exists. Please choose a different username.", w, r) return } //make sure passwords match if doStringsMatch(password1, password2) == false { output.Error(ErrPasswordsDoNotMatch, "The passwords you provided to not match.", w, r) return } //make sure password is long enough if len(password1) < minPwdLength { output.Error(ErrPasswordTooShort, "The password you provided is too short. It must be at least "+strconv.FormatInt(minPwdLength, 10)+" characters.", w, r) return } //hash the password hashedPwd := pwds.Create(password1) //create the user u := User{ Username: username, Password: hashedPwd, AddCards: addCards, RemoveCards: removeCards, ChargeCards: chargeCards, ViewReports: viewReports, Administrator: isAdmin, Active: isActive, Created: timestamps.ISO8601(), } //save to datastore incompleteKey := createNewUserKey(c) _, err = saveUser(c, incompleteKey, u) if err != nil { fmt.Fprint(w, err) return } //clear list of users saved in memcache since a new user was added memcacheutils.Delete(c, listOfUsersKey) //respond to client with success message output.Success("addNewUser", nil, w) return }
//Charge charges a credit card func Charge(w http.ResponseWriter, r *http.Request) { //get form values datastoreId := r.FormValue("datastoreId") customerName := r.FormValue("customerName") amount := r.FormValue("amount") invoice := r.FormValue("invoice") poNum := r.FormValue("po") //validation if len(datastoreId) == 0 { output.Error(ErrMissingInput, "A customer ID should have been submitted automatically but was not. Please contact an administrator.", w, r) return } if len(amount) == 0 { output.Error(ErrMissingInput, "No amount was provided. You cannot charge a card nothing!", w, r) return } //get amount as cents amountCents, err := getAmountAsIntCents(amount) if err != nil { output.Error(err, "An error occured while converting the amount to charge into cents. Please try again or contact an administrator.", w, r) return } //check if amount is greater than the minimum charge //min charge may be greater than 0 because of transactions costs //for example, stripe takes 30 cents...it does not make sense to charge a card for < 30 cents if amountCents < minCharge { output.Error(ErrChargeAmountTooLow, "You must charge at least "+strconv.FormatInt(minCharge, 10)+" cents.", w, r) return } //create context //need to adjust deadline in case stripe takes longer than 5 seconds //default timeout for a urlfetch is 5 seconds //sometimes charging a card through stripe api takes longer //calls seems to take roughly 2 seconds normally with a few near 5 seconds (old deadline) //the call might still complete via stripe but appengine will return to the gui that it failed //10 secodns is a bit over generous but covers even really strange senarios c := appengine.NewContext(r) c, _ = context.WithTimeout(c, 10*time.Second) //look up stripe customer id from datastore datastoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64) custData, err := findByDatastoreId(c, datastoreIdInt) if err != nil { output.Error(err, "An error occured while looking up the customer's Stripe information.", w, r) return } //make sure customer name matches //just another catch in case of strange errors and mismatched data if customerName != custData.CustomerName { output.Error(err, "The customer name did not match the data for the customer ID. Please log out and try again.", w, r) return } //get username of logged in user //used for tracking who processed a charge //for audits and reports session := sessionutils.Get(r) username := session.Values["username"].(string) //init stripe sc := createAppengineStripeClient(c) //build charge object chargeParams := &stripe.ChargeParams{ Customer: custData.StripeCustomerToken, Amount: amountCents, Currency: currency, Desc: "Charge for invoice: " + invoice + ", purchase order: " + poNum + ".", Statement: formatStatementDescriptor(), } //add metadata to charge //used for reports and receipts chargeParams.AddMeta("customer_name", customerName) chargeParams.AddMeta("datastore_id", datastoreId) chargeParams.AddMeta("customer_id", custData.CustomerId) chargeParams.AddMeta("invoice_num", invoice) chargeParams.AddMeta("po_num", poNum) chargeParams.AddMeta("charged_by", username) //process the charge chg, err := sc.Charges.New(chargeParams) //handle errors //*url.Error can be thrown if urlfetch reaches timeout (request took too long to complete) //*stripe.Error is a error with the stripe api and should return a human readable error message if err != nil { errorMsg := "" switch err.(type) { default: errorMsg = "There was an error processing this charge. Please check the Report to see if this charge was successful." break case *url.Error: errorMsg = "Charging this card timed out. The charge may have succeeded anyway. Please check the Report to see if this charge was successful." break case *stripe.Error: stripeErr := err.(*stripe.Error) errorMsg = stripeErr.Msg } output.Error(ErrStripe, errorMsg, w, r) return } //charge successful //save charge to memcache //less data to get from stripe if receipt is needed //errors are ignores since if we can't save this data to memcache we can always get it from the datastore/stripe memcacheutils.Save(c, chg.ID, chg) //save count of card types //used for negotiating rates with Stripe and just extra info saveChargeDetails(c, chg) //build struct to output a success message to the client out := chargeSuccessful{ CustomerName: customerName, Cardholder: custData.Cardholder, CardExpiration: custData.CardExpiration, CardLast4: custData.CardLast4, Amount: amount, Invoice: invoice, Po: poNum, Datetime: timestamps.ISO8601(), ChargeId: chg.ID, } output.Success("cardCharged", out, w) return }
//Add adds a new card the the app engine datastore //this is done by validating the provided inputs, sending the card token to stripe, and saving the data to the datastore //the card token was generaged client side by the stipe-js // this is done so the card number and security code is never sent to the server // the server has no way of "touching" the card number for security //when the card token is sent to stripe, stripe generates a customer token which we store and use to process payments func Add(w http.ResponseWriter, r *http.Request) { //get form values customerId := r.FormValue("customerId") //a unique key, not the datastore id or stripe customer id customerName := r.FormValue("customerName") //user provided, could be company name/client name/may be same as cardholder cardholder := r.FormValue("cardholder") //name on card as it appears cardToken := r.FormValue("cardToken") //from stripejs cardExp := r.FormValue("cardExp") //from stripejs, not from html input cardLast4 := r.FormValue("cardLast4") //from stripejs, not from html input //make sure all form values were given if len(customerName) == 0 { output.Error(ErrMissingCustomerName, "You did not provide the customer's name.", w, r) return } if len(cardholder) == 0 { output.Error(ErrMissingCustomerName, "You did not provide the cardholer's name.", w, r) return } if len(cardToken) == 0 { output.Error(ErrMissingCardToken, "A serious error occured; the card token is missing. Please refresh the page and try again.", w, r) return } if len(cardExp) == 0 { output.Error(ErrMissingExpiration, "The card's expiration date is missing from Stripe. Please refresh the page and try again.", w, r) return } if len(cardLast4) == 0 { output.Error(ErrMissingLast4, "The card's last four digits are missing from Stripe. Please refresh the page and try again.", w, r) return } //init context c := appengine.NewContext(r) //if customerId was given, make sure it is unique //this id should be unique in the user's company's crm //the customerId is used to autofill the charge card panel when performing the api-like semi-automated charges if len(customerId) != 0 { _, err := FindByCustId(c, customerId) if err == nil { //customer already exists output.Error(ErrCustIdAlreadyExists, "This customer ID is already in use. Please double check your records or remove the customer with this customer ID first.", w, r) return } else if err != ErrCustomerNotFound { output.Error(err, "An error occured while verifying this customer ID does not already exist. Please try again or leave the customer ID blank.", w, r) return } } //init stripe sc := createAppengineStripeClient(c) //create the customer on stripe //assigns the card via the cardToken to this customer //this card is used when making charges to this customer custParams := &stripe.CustomerParams{Desc: customerName} custParams.SetSource(cardToken) cust, err := sc.Customers.New(custParams) if err != nil { stripeErr := err.(*stripe.Error) stripeErrMsg := stripeErr.Msg output.Error(ErrStripe, stripeErrMsg, w, r) return } //get username of logged in user //used for tracking who added a card, just for diagnostics session := sessionutils.Get(r) username := session.Values["username"].(string) //save customer & card data to datastore newCustKey := createNewCustomerKey(c) newCustomer := CustomerDatastore{ CustomerId: customerId, CustomerName: customerName, Cardholder: cardholder, CardExpiration: cardExp, CardLast4: cardLast4, StripeCustomerToken: cust.ID, DatetimeCreated: timestamps.ISO8601(), AddedByUser: username, } _, err = save(c, newCustKey, newCustomer) if err != nil { output.Error(err, "There was an error while saving this customer. Please try again.", w, r) return } //customer saved //return to client output.Success("createCustomer", nil, w) //resave list of cards in memcache //since a card was added, memcache is stale //clients will retreive new list when refreshing page/app memcacheutils.Delete(c, listOfCardsKey) return }