//Root is used to show the login page of the app //when a user browses to the page (usually just the domain minus any path), the user is checked for a session //if a session exists, the app attempts to auto-login the user //otherwise a user is shown the log in prompt //this also handles the "first run" of the app in which no users exist yet...it forces creation of the "super admin" func Root(w http.ResponseWriter, r *http.Request) { //check that session store was initialized correctly if err := sessionutils.CheckSession(); err != nil { notificationPage(w, "panel-danger", sessionInitError, err, "btn-default", "/", "Go Back") return } //check that stripe private key and statement desecriptor were read correctly if err := card.CheckStripe(); err != nil { notificationPage(w, "panel-danger", sessionInitError, err, "btn-default", "/", "Go Back") return } //check if the admin user exists //redirect user to create admin if it does not exist err := users.DoesAdminExist(r) if err == users.ErrAdminDoesNotExist { http.Redirect(w, r, "/setup/", http.StatusFound) return } else if err != nil { notificationPage(w, "panel-danger", adminInitError, err, "btn-default", "/", "Go Back") return } //check if user is already signed in //if user is already logged in, redirect to /main/ page session := sessionutils.Get(r) if session.IsNew == false { uId := session.Values["user_id"].(int64) c := appengine.NewContext(r) u, err := users.Find(c, uId) if err != nil { sessionutils.Destroy(w, r) notificationPage(w, "panel-danger", "Autologin Error", "There was an issue looking up your user account. Please go back and try logging in.", "btn-default", "/", "Go Back") return } //user data was found //check if user is allowed access if users.AllowedAccess(u) == false { sessionutils.Destroy(w, r) notificationPage(w, "panel-danger", "Autologin Error", "You are not allowed access. Please contact an administrator.", "btn-default", "/", "Go Back") } //user account is found an allowed access //redirect user http.Redirect(w, r, "/main/", http.StatusFound) return } //load the login page templates.Load(w, "root", nil) return }
//Login verifies a username and password combo //this makes sure the user exists, that the password is correct, and that the user is active //if user is allowed access, their data is saved to the session and they are redirected into the app func Login(w http.ResponseWriter, r *http.Request) { //get form values username := r.FormValue("username") password := r.FormValue("password") //get user data c := appengine.NewContext(r) id, data, err := exists(c, username) if err == ErrUserDoesNotExist { notificationPage(w, "panel-danger", "Cannot Log In", "The username you provided does not exist.", "btn-default", "/", "Try Again") return } //is user allowed access if AllowedAccess(data) == false { notificationPage(w, "panel-danger", "Cannot Log In", "You are not allowed access. Please contact an administrator.", "btn-default", "/", "Go Back") return } //validate password _, err = pwds.Verify(password, data.Password) if err != nil { notificationPage(w, "panel-danger", "Cannot Log In", "The password you provided is invalid.", "btn-default", "/", "Try Again") return } //user validated //save session data session := sessionutils.Get(r) if session.IsNew == false { sessionutils.Destroy(w, r) session = sessionutils.Get(r) } sessionutils.AddValue(session, "username", username) sessionutils.AddValue(session, "user_id", id) sessionutils.Save(session, w, r) //show user main page http.Redirect(w, r, "/main/", http.StatusFound) return }
//Auth checks if a user is logged in and is allowed access to the app //this is done on every page load and every endpoint func Auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //get user data from session session := sessionutils.Get(r) //session data does not exist yet //this is a new session //redirect user to log in page if session.IsNew { http.Redirect(w, r, "/", http.StatusFound) return } //check if user id is in session //it *should* be! //otherwise show user a notice and force user to log in again userId, ok := session.Values["user_id"].(int64) if ok == false { sessionutils.Destroy(w, r) notificationPage(w, "panel-danger", "Session Expired", "Your session has expired. Please log back in or contact an administrator if this problem persists.", "btn-default", "/", "Log In") return } //look up user in memcache and/or datastore c := appengine.NewContext(r) data, err := users.Find(c, userId) if err != nil { sessionutils.Destroy(w, r) notificationPage(w, "panel-danger", "Application Error", "The app encountered an error in the middleware while trying to authenticate you as a legitimate user. Please try logging in again or contact an administrator.", "btn-default", "/", "Log In") return } //check if user is allowed access to the app //this is a setting the app's administrators can toggle for each user if users.AllowedAccess(data) == false { sessionutils.Destroy(w, r) http.Redirect(w, r, "/", http.StatusFound) return } //user is allowed access //extend expiration of session cookie to allow user to stay "logged in" sessionutils.ExtendExpiration(session, w, r) //move to next middleware or handler next.ServeHTTP(w, r) }) }
//AddCards checks if the user is allowed to add credit cards to the app func AddCards(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //get session data session := sessionutils.Get(r) //look up user data c := appengine.NewContext(r) userId := session.Values["user_id"].(int64) data, err := users.Find(c, userId) if err != nil { output.Error(err, "An error occurred in the middleware.", w, r) return } //check if user can add cards if data.AddCards == false { output.Error(ErrNotAuthorized, "You do not have permission to add new cards.", w, r) return } //move to next middleware or handler next.ServeHTTP(w, r) }) }
//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 }
//CreateAdmin saves the initial super-admin for the app //this user is used to log in and create new users //this user is created when the app is first deployed and used // done this way b/c we don't want to set a default password/username in the code func CreateAdmin(w http.ResponseWriter, r *http.Request) { //make sure the admin user doesnt already exist err := DoesAdminExist(r) if err == nil { notificationPage(w, "panel-danger", "Error", "The admin user already exists.", "btn-default", "/", "Go Back") return } //get form values pass1 := r.FormValue("password1") pass2 := r.FormValue("password2") //make sure they match if doStringsMatch(pass1, pass2) == false { notificationPage(w, "panel-danger", "Error", "The passwords id not match.", "btn-default", "/setup/", "Try Again") return } //make sure the password is long enough if len(pass1) < minPwdLength { notificationPage(w, "panel-danger", "Error", "The password you provided is too short. It must me at least "+strconv.FormatInt(minPwdLength, 10)+" characters.", "btn-default", "/setup/", "Try Again") return } //hash the password hashedPwd := pwds.Create(pass1) //create the user u := User{ Username: adminUsername, Password: hashedPwd, AddCards: true, RemoveCards: true, ChargeCards: true, ViewReports: true, Administrator: true, Active: true, Created: timestamps.ISO8601(), } //save to datastore c := appengine.NewContext(r) incompleteKey := createNewUserKey(c) completeKey, err := saveUser(c, incompleteKey, u) if err != nil { fmt.Fprint(w, err) return } //save user to session //this is how we authenticate users that are already signed in //the user is automatically logged in as the administrator user session := sessionutils.Get(r) if session.IsNew == false { notificationPage(w, "panel-danger", "Error", "An error occured while saving the admin user. Please clear your cookies and restart your browser.", "btn-default", "/setup/", "Try Again") return } sessionutils.AddValue(session, "username", adminUsername) sessionutils.AddValue(session, "user_id", completeKey.IntID()) sessionutils.Save(session, w, r) //show user main page http.Redirect(w, r, "/main/", http.StatusFound) 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 }
//Main loads the main UI of the app //this is the page the user sees once they are logged in //this ui is a single page app and holds almost all the functionality of the app //the user only sees the parts of the ui they have access to...the rest is removed via go's contemplating //we also check if this page was loaded with a bunch of extra data in the url...this would be used to perform the api-like semi-automated charging of the card // if a link to the page has a "customer_id" form value, this will automatically find the customer's card data and show it in the panel // if "amount", "invoice", and/or "po" form values are given, these will also automatically be filled into the charge panel's form // if "customer_id" is not given, no auto filling will occur of any fields // "amount" must be in cents // card is not automatically charged, user still has to click "charge" button func Main(w http.ResponseWriter, r *http.Request) { //placeholder for sending data back to template var tempData autoloader //get logged in user data //catch instances where session is not working and redirect user to log in page //use the user's data to show/hide certain parts of the ui per the users access rights session := sessionutils.Get(r) if session.IsNew == true { notificationPage(w, "panel-danger", "Cannot Load Page", "Your session has expired or there is an error. Please try logging in again or contact an administrator.", "btn-default", "/", "Log In") return } userId := session.Values["user_id"].(int64) c := appengine.NewContext(r) user, err := users.Find(c, userId) if err != nil { notificationPage(w, "panel-danger", "Cannot Load Page", err, "btn-default", "/", "Try Again") return } //check for url form values for autofilling charge panel //if data in url does not exist, just load the page with user data only custId := r.FormValue("customer_id") if len(custId) == 0 { tempData.UserData = user templates.Load(w, "main", tempData) return } //data in url does exist //look up card data by customer id //get the card data to show in the panel so user can visually confirm they are charging the correct card //if an error occurs, just load the page normally custData, err := card.FindByCustId(c, custId) if err != nil { tempData.Error = "The form could not be autofilled because the customer ID you provided could not be found. The ID is either incorrect or the customer's credit card has not been added yet." tempData.UserData = user templates.Load(w, "main", tempData) return } tempData.CardData = custData tempData.UserData = user //if amount was given, it is in cents //display it in html input as dollars amountUrl := r.FormValue("amount") amountFloat, _ := strconv.ParseFloat(amountUrl, 64) amountDollars := amountFloat / 100 tempData.Amount = amountDollars //check for other form values and build template tempData.Invoice = r.FormValue("invoice") tempData.Po = r.FormValue("po") //load the page with the card data templates.Load(w, "main", tempData) 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 }
//Report gets the data for charges and refunds by the defined filters (date range and customer) and builds the reports page //the reports show up in a different page so they are easily printable and more easily inspected //date range is inclusive of start and end days func Report(w http.ResponseWriter, r *http.Request) { //get form valuess datastoreId := r.FormValue("customer-id") startString := r.FormValue("start-date") endString := r.FormValue("end-date") hoursToUTC := r.FormValue("timezone") //get report data form stripe //make sure inputs are given if len(startString) == 0 { output.Error(ErrMissingInput, "You must supply a 'start-date'.", w, r) return } if len(endString) == 0 { output.Error(ErrMissingInput, "You must supply a 'end-date'.", w, r) return } if len(hoursToUTC) == 0 { output.Error(ErrMissingInput, "You must supply a 'timezone'.", w, r) return } //get timezone offset //adjust for the local timezone the user is in so that the date range is correct //hoursToUTC is a number generated by JS (-4 for EST) tzOffset := calcTzOffset(hoursToUTC) //get datetimes from provided strings startDt, err := time.Parse("2006-01-02 -0700", startString+" "+tzOffset) if err != nil { output.Error(err, "Could not convert start date to a time.Time datetime.", w, r) return } endDt, err := time.Parse("2006-01-02 -0700", endString+" "+tzOffset) if err != nil { output.Error(err, "Could not convert end date to a time.Time datetime.", w, r) return } //get end of day datetime //need to get 23:59:59 so we include the whole day endDt = endDt.Add((24*60-1)*time.Minute + (59 * time.Second)) //get unix timestamps //stripe only accepts timestamps for filtering charges startUnix := startDt.Unix() endUnix := endDt.Unix() //init stripe c := appengine.NewContext(r) sc := createAppengineStripeClient(c) //retrieve data from stripe //date is a range inclusive of the days the user chose //limit of 100 is the max per stripe params := &stripe.ChargeListParams{} params.Filters.AddFilter("created", "gte", strconv.FormatInt(startUnix, 10)) params.Filters.AddFilter("created", "lte", strconv.FormatInt(endUnix, 10)) params.Filters.AddFilter("limit", "", "100") //check if we need to filter by a specific customer //look up stripe customer id by the datastore id if len(datastoreId) != 0 { datastoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64) custData, err := findByDatastoreId(c, datastoreIdInt) if err != nil { output.Error(err, "An error occured and this report could not be generated.", w, r) return } params.Filters.AddFilter("customer", "", custData.StripeCustomerToken) } //get results //loop through each charge and extract charge data //add up total amount of all charges charges := sc.Charges.List(params) data := make([]chargeutils.Data, 0, 10) var amountTotal uint64 = 0 var numCharges uint16 = 0 for charges.Next() { //get each charges data chg := charges.Charge() d := chargeutils.ExtractData(chg) //make sure this charge was captured //do not count charges that failed if d.Captured == false { continue } data = append(data, d) //increment totals amountTotal += d.AmountCents numCharges++ } //convert total amount to dollars amountTotalDollars := strconv.FormatFloat((float64(amountTotal) / 100), 'f', 2, 64) //retrieve refunds eventParams := &stripe.EventListParams{} eventParams.Filters.AddFilter("created", "gte", strconv.FormatInt(startUnix, 10)) eventParams.Filters.AddFilter("created", "lte", strconv.FormatInt(endUnix, 10)) eventParams.Filters.AddFilter("limit", "", "100") eventParams.Filters.AddFilter("type", "", "charge.refunded") events := sc.Events.List(eventParams) refunds := chargeutils.ExtractRefunds(events) //get logged in user's data //for determining if receipt/refund buttons need to be hidden or shown based on user's access rights session := sessionutils.Get(r) userId := session.Values["user_id"].(int64) userdata, _ := users.Find(c, userId) //store data for building template result := reportData{ UserData: userdata, StartDate: startDt, EndDate: endDt, Charges: data, Refunds: refunds, TotalAmount: amountTotalDollars, NumCharges: numCharges, } //build template to display report //separate page in gui templates.Load(w, "report", result) return }