//FindByCustId retrieves a card's information by the unique id from a CRM system //this id was provided when a card was saved //this func is used when making api style request to semi-automate the charging of a card. //first memcache is checked for the data, then the datastore func FindByCustId(c context.Context, customerId string) (CustomerDatastore, error) { //check for card in memcache var r CustomerDatastore _, err := memcache.Gob.Get(c, customerId, &r) if err == nil { return r, nil } //card data not found in memcache //look up data in datastore //save card to memcache after it is found if err == memcache.ErrCacheMiss { //only getting the fields we need to show data in the charge card panel data, err := datastoreFindOne(c, "CustomerId =", customerId, []string{"CustomerName", "Cardholder", "CardLast4", "CardExpiration"}) if err != nil { return data, err } //save to memcache //ignore errors since we already got the data memcacheutils.Save(c, customerId, data) //done return data, nil } else { return CustomerDatastore{}, err } return CustomerDatastore{}, err }
//findByDatastoreId retrieves a card's information by its datastore id //this returns all the info on a card that is needed to build the ui //first memcache is checked for the data, then the datastore func findByDatastoreId(c context.Context, datastoreId int64) (CustomerDatastore, error) { //check for card in memcache var r CustomerDatastore datastoreIdStr := strconv.FormatInt(datastoreId, 10) _, err := memcache.Gob.Get(c, datastoreIdStr, &r) if err == nil { return r, nil } //card data not found in memcache //look up data in datastore //save card to memcache after it is found if err == memcache.ErrCacheMiss { key := getCustomerKeyFromId(c, datastoreId) data, err := datastoreFindOne(c, "__key__ =", key, []string{"CustomerId", "CustomerName", "Cardholder", "CardLast4", "CardExpiration", "StripeCustomerToken"}) if err != nil { return data, err } //save to memcache //ignore errors since we already got the data memcacheutils.Save(c, datastoreIdStr, data) //done return data, nil } else { return CustomerDatastore{}, err } return CustomerDatastore{}, err }
//getCompanyInfo actually retrienves the information from memcache or the datastore //putting this into a separate func cleans up code elsewhere func getCompanyInfo(r *http.Request) (companyInfo, error) { //check memcache c := appengine.NewContext(r) var result companyInfo _, err := memcache.Gob.Get(c, memcacheKeyName, &result) if err == nil { return result, nil } //data not found in memcache //get from datastore if err == memcache.ErrCacheMiss { key := datastore.NewKey(c, datastoreKind, datastoreKey, 0, nil) //get data err := datastore.Get(c, key, &result) if err == datastore.ErrNoSuchEntity { return result, ErrCompanyDataDoesNotExist } else if err != nil { return result, err } //save to memcache //ignore errors since we already got the data memcacheutils.Save(c, memcacheKeyName, result) //return data return result, nil } else if err != nil { return companyInfo{}, err } return companyInfo{}, err }
//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 }
//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 }
//saveUser does the actual saving of a user to the datastore //separate function to clean up code func saveUser(c context.Context, key *datastore.Key, user User) (*datastore.Key, error) { //save to datastore completeKey, err := datastore.Put(c, key, &user) if err != nil { return completeKey, err } //save user to memcache mKey := strconv.FormatInt(completeKey.IntID(), 10) err = memcacheutils.Save(c, mKey, user) if err != nil { return completeKey, err } //done return completeKey, nil }
//save does the actual saving of a card to the datastore //separate function to clean up code func save(c context.Context, key *datastore.Key, customer CustomerDatastore) (*datastore.Key, error) { //save customer completeKey, err := datastore.Put(c, key, &customer) if err != nil { return key, err } //save customer to memcache //have to generate a memcache key b/c memcache keys must be strings mKey := strconv.FormatInt(completeKey.IntID(), 10) err = memcacheutils.Save(c, mKey, customer) if err != nil { return completeKey, err } //done return completeKey, nil }
//Find gets the data for a given user id //this returns all the info on a user //first memcache is checked for the data, then the datastore func Find(c context.Context, userId int64) (User, error) { //check for card in memcache var r User userIdStr := strconv.FormatInt(userId, 10) _, err := memcache.Gob.Get(c, userIdStr, &r) if err == nil { return r, nil } //user data not found in memcache //look up data in datastore //save to memcache after it is found if err == memcache.ErrCacheMiss { key := getUserKeyFromId(c, userId) q := datastore.NewQuery(datastoreKind).Filter("__key__ =", key).Limit(1) result := make([]User, 0, 1) _, err := q.GetAll(c, &result) if err != nil { return User{}, err } //return error if no result exists if len(result) == 0 { return User{}, ErrUserDoesNotExist } //one result userData := result[0] //save to memcache //ignore errors since we already got the data memcacheutils.Save(c, userIdStr, userData) //done return userData, nil } else { return User{}, err } return User{}, err }
//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 }
//Show builds an html page that display a receipt //this is a very boring, plain test, monospaced font page designed for easy printing and reading //the receipt is generated from the charge id //the data for the charge may be in memcache or will have to be retrieved from stripe func Show(w http.ResponseWriter, r *http.Request) { //get charge id from form value chargeId := r.FormValue("chg_id") //try looking up charge data in memcache var chg *stripe.Charge c := appengine.NewContext(r) _, err := memcache.Gob.Get(c, chargeId, &chg) //charge not found in memcache //look up charge data from stripe if err == memcache.ErrCacheMiss { //init stripe c := appengine.NewContext(r) stripe.SetBackend(stripe.APIBackend, nil) stripe.SetHTTPClient(urlfetch.Client(c)) //get charge data chg, err = charge.Get(chargeId, nil) if err != nil { fmt.Fprint(w, "An error occured and the receipt cannot be displayed.\n") fmt.Fprint(w, err) return } //save to memcache //just in case we want to view the receipt again memcacheutils.Save(c, chg.ID, chg) } //extract charge data d := chargeutils.ExtractData(chg) //get company info info, err := getCompanyInfo(r) name, street, suite, city, state, postal, country, phone := "", "", "", "", "", "", "", "" if err == ErrCompanyDataDoesNotExist { name = "**Company info has not been set yet.**" street = "**Please contact an administrator to fix this.**" } else { name = info.CompanyName street = info.Street suite = info.Suite city = info.City state = info.State postal = info.PostalCode country = info.Country phone = info.PhoneNum } //display receipt output := templateData{ CompanyName: name, Street: street, Suite: suite, City: city, State: state, Postal: postal, Country: country, PhoneNum: phone, Customer: d.Customer, Cardholder: d.Cardholder, CardBrand: d.CardBrand, LastFour: d.LastFour, Expiration: d.Expiration, Captured: d.CapturedStr, Timestamp: d.Timestamp, Amount: d.AmountDollars, Invoice: d.Invoice, Po: d.Po, } templates.Load(w, "receipt", output) return }