//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
}