The Tip Report sample application shows how to create a tip report for sellers who pool tips for their staff.
Applies to: Customers API | GraphQL | Labor API | Orders API | Payments API | Team API
Capabilities: Payments | Commerce | Customers | Staff
The Tip Report sample application shows earnings from a tip pool for each team member at a location in a given time period. It provides detail about the orders completed for each server and the tip amount for each order. The Python sample application also includes a script that populates your Square account Sandbox environment with a set of team members, labor shifts, orders, payments, and customers.
This scenario shows how to download, install, and run the sample. The second part of the scenario provides a detailed look at the Python code in the application.
Did you know?
If a seller is subscribed to Square for Restaurants Plus, tip pooling and cash tip tracking (attribution) is available within the application. The Square API supports tip pool reporting only.
The following sections present call flow details in the order that the application calls the Square API. The flow starts when you select the Run Report button on the Tip Report application.
The application composes a set of objects comprised of fields from TeamMember, Payment, Shift, and calculated tips. The objects are stored in the team_member_shift_dict
, a dictionary declared in the run_tip_report
function. The report details in the output come from this dictionary.
The run_tip_report
function starts the application call flow and returns the tip report to the client by gathering completed shifts, the team members who worked the shifts, and the payments completed during the reporting period.
A shift is eligible to earn tips if the associated job title is tip enabled. The Shift object carries the shift['wage']['tip_eligible']
Boolean field. If the value is true
, the team member can earn tips on the shift.
def run_tip_report(
location_id, *,
team_member_ids,
start_date,
end_date) -> dict:
tip_report_start = time.time()
team_member_total_hours = 0
# Get a list of all shifts that were worked during the start and end date
shifts_by_date_range = get_shifts_by_date_range(
location_id=location_id,
start_date=start_date,
end_date=end_date)
print(shifts_by_date_range)
# Team_Member_shift_dict - this dictionary will be our source of information
# for the rest of the app
# key: team member id
# value: {shifts: []}
team_member_shift_dict = {}
for shift in shifts_by_date_range['shifts']:
team_member_id = shift['team_member_id']
# Check if the shift was for a tippable job.
# only include them in this list if the job is tippable
if (shift['wage']['tip_eligible'] is True):
# Add up shift time of all team members
team_member_total_hours += get_shift_length(
shift['start_at'], shift['end_at'])
# Only create the shift dictionary for selected team members in the
# frontend
if (team_member_id in team_member_ids):
team_member_shift_dict.setdefault(
team_member_id, {}).setdefault('shifts', []).append(shift)
# Go and get the team member name and put it into our dictionary object
team_member_shift_dict = get_team_member_info(team_member_shift_dict)
print('Time to get Team Member Data: ', time.time() - tip_report_start)
team_member_shift_dict = get_payment_data(location_id,
start_date,
end_date,
team_member_shift_dict,
total_hours_worked=team_member_total_hours)
print('Total Time to run tip report: ', time.time() - tip_report_start)
return team_member_shift_dict
The Labor API can provide all completed shifts for all team members across all locations. The following query filters by the desired location and date range. Only shifts for that location and started within the report date range are returned.
def get_shifts_by_date_range(
location_id,
start_date,
end_date) -> dict:
body = {
"query": {
"filter": {
"location_ids": [
location_id
],
"start": {
"start_at": start_date,
"end_at": end_date
}
}
}
}
result = square_client.labor.search_shifts(body)
if result.is_success():
return result.body
else:
return result.errors
The dictionary returned by this function doesn't yet have any tip amounts.
You can use API Explorer to experiment with the SearchShift endpoint using your own Square account data.
After gathering the shifts, the report gets the team members who worked those shifts.
This helper function uses BulkUpdateTeamMembers for its ability to accept a set of team member IDs, update those team members with values in the request body, and return full team objects. Because the function doesn't request updates — 'team_member': {}
, the request is a no-op but does return the set of team members from the request list.
def get_team_member_info(
team_member_shift_dict
) -> dict:
team_ids = list(team_member_shift_dict.keys())
body = {'team_members': {}}
# Prepare body for request
for id in team_ids:
body['team_members'][id] = {
'team_member': {}
}
# The team api cannot list members with a supply of ids
# however we can bulk update without changing data to get what we want
team_members_result = square_client.team.bulk_update_team_members(body)
if team_members_result.is_success():
team_members = team_members_result.body['team_members']
for key, _ in team_members.items():
# return team member data
team_member = team_members[key]['team_member']
data = {
"id": key,
'given_name': team_member['given_name']
if 'given_name' in team_member else "",
'family_name': team_member['family_name']
if 'family_name' in team_member else "",
'tips': 0,
"shifts": team_member_shift_dict[key]['shifts']
}
team_member_shift_dict[key] = data
return team_member_shift_dict
After this code runs, the team_member_shift_dict
dictionary holds a set of team members and the shifts that they worked in the reporting period. The team member tip amount fields are still zero. The amounts are filled in the next step.
The Tip Report calculates the size of the tip pool by reading all payments at the location for the reporting period. To do this, the report calls the ListPayment endpoint with the report date range and location ID.
The resulting list is iterated to get the tip pool and record the order IDs in the team/shifts dictionary. If a payment doesn't have a tip or the payment isn't COMPLETE
, it's skipped.
After the payment loop is complete, the tip pool and the total team hours worked are provided to the credit_tip_to_team_member
function.
def get_payment_data(location_id,
start_date,
end_date,
team_member_shift_dict,
total_hours_worked) -> dict:
payment_data_start = time.time()
result = square_client.payments.list_payments(
begin_time=start_date,
end_time=end_date,
location_id=location_id
)
team_tip_pool = 0
if result.is_success():
# Loop through payments and tally total tips for the given time frame
for payment in result.body['payments']:
if 'tip_money' in payment and payment['status'] == 'COMPLETED':
team_tip_pool += payment['tip_money']['amount']
if payment['team_member_id'] in team_member_shift_dict:
team_member_id = payment['team_member_id']
team_member_shift_dict[team_member_id]['shifts'][0].setdefault(
'orders', []).append(payment['order_id'])
credit_tip_to_team_member(
team_member_shift_dict, team_tip_pool, total_hours_worked, )
elif result.is_error():
payment_data = dict()
for error in result.errors:
payment_data['errors'] = error['category'] + " " \
+ error['code'] + " " + \
error['detail']
print('Time to get tips', time.time() - payment_data_start)
return team_member_shift_dict
Did you know?
A cash payment can also record tip money. This lets the report account for tips made in cash when a buyer paid their bill in cash.
The tip amount per team member is calculated by dividing the tip pool by the total team work hours to get a tip amount per hour worked. The per hour amount is multiplied by the hours worked by a team member to get their total tips.
For example, if the team combined for a total of 150 hours and the tip pool size is $2,000 (money amounts are stored as pennies - 200,000 pennies), then the pennies per hour would be 1,333 or $13.33 dollars per hour. If a server worked 8 hours during the reporting period, they would earn $106 in tips.
def credit_tip_to_team_member(
team_member_shift_dict,
team_tip_pool,
total_hours_worked,
) -> dict:
# number of pennies earned per hour
pennies_per_hour = int(team_tip_pool / total_hours_worked)
# Loop through our team members
for key, value in team_member_shift_dict.items():
# Loop through the team members shifts
team_member_shift_total = 0
for shift in value['shifts']:
team_member_shift_total += get_shift_length(
shift['start_at'], shift['end_at'])
# do the tip math
# Increment the tip for the tippable team member
team_member_shift_dict[key]['tips'] = pennies_per_hour * \
team_member_shift_total
team_member_shift_dict[key]['hours_worked'] = team_member_shift_total
return team_member_shift_dict
Note
The tip calculation shown in this example might not be correct for your seller. You should update the logic in the function for your calculation. Be sure to write your tip calculations into team_member_shift_dict[key]['tips']
so that the client page reports your results.
The following functions implement key technical details referenced in the Tip Report application code.
The application client presents a list of seller locations to choose from by calling the backend locations
endpoint, which calls ListLocations. The name and ID of each location are returned to the client to be loaded in a selector.
class Locations(Resource):
def get(self):
result = square_client.locations.list_locations()
return [{"name": location['name'], "id": location['id']}\
for location in result.body['locations']]
The application client calls this endpoint to fill a list of team members to select from.
class Team(Resource):
def get(self):
# Populate our Team member data
args = request.args
location_id = args['location_id']
result = square_client.team.search_team_members(
body={
"query": {
"filter": {"is_owner": False, "status": "ACTIVE",
"location_ids": [location_id]}
}
})
return list(
map(
lambda team_member: {
"name": f"{team_member['given_name']}\
{team_member['family_name']}",
"id": team_member['id']
},
result.body['team_members']
)
)
The per-team-member tip calculation requires the total number of hours worked across all of a team member's shifts in the reporting period. This helper function returns the hours between the start and end of a shift.
def get_shift_length(start_time_str, end_time_str):
# Convert strings to datetime objects
start_time = datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M:%SZ')
end_time = datetime.strptime(end_time_str, '%Y-%m-%dT%H:%M:%SZ')
# Calculate the time difference
shift_length = end_time - start_time
# Extract the hour difference
return int(shift_length.total_seconds() / 3600)
Important
In production, a team member usually takes one or more breaks in the course of a shift. Every shift object has a breaks
field, which is a list of all breaks taken by the team member during the shift. Each break has a start and end time. You should iterate on the break list and subtract break time from total shift time before returning the shift length.
Order details include fields from an Order
object and other fields from a Customer
object.
The Tip Report could have made a single BatchRetrieveOrders call but it would have also had to make a RetrieveCustomer call for every customer. Instead, the application uses a GraphQL query to return a set of orders with related customer fields based on the order IDs found in the payments retrieved earlier. This single GraphQL query replaces several Orders API and Customers API REST calls and results in a significant performance improvement.
def fetch_graphql(ids):
# GraphQL server endpoint
url = 'https://connect.squareupsandbox.com/public/graphql'\
if os.environ[
'FLASK_ENV'] == 'development' else\
'https://connect.squareup.com/public/graphql'
# GraphQL query
query = '''
query ($merchantId: ID!, $ids: [ID!]){
orders(
filter: {
merchantId: { equalToAnyOf: [$merchantId] }
id: { equalToAnyOf: $ids }
}
) {
nodes {
id
customer {
id
givenName
familyName
}
ticketName
location {
id
}
totalMoney {
amount
}
totalTip {
amount
}
}
}
}
'''
# Create the request headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {}'\
.format(os.environ['SQUARE_SANDBOX_ACCESS_TOKEN'])
}
merchant_id = requests.get(
'https://connect.squareupsandbox.com/v2/merchants/me',\
headers=headers).json()['merchant']['id']
# Create the request payload
data = {
'query': query,
'variables': {
'merchantId': merchant_id,
'ids': ids,
}
}
graphql_start = time.time()
# Send the POST request to the server
response = requests.post(url, headers=headers, json=data)
print('Time to get graphql', time.time() - graphql_start)
# Parse the response as JSON
result = response.json()
return result['data']['orders']['nodes']
- You have a Square account and a Square API based application. For production payment processing and order completion, your account must first be activated at squareup.com/activation.
- For applications that use OAuth, you have an OAuth access token that grants
PAYMENTS_READ
,CUSTOMERS_READ
,EMPLOYEES_READ
,TIMECARDS_READ
, andORDERS_READ
permissions. To test while following along in this scenario, you can use a personal access token. - Tips are recorded with each payment.
- The seller is using the Team application in the Seller Dashboard to manage staff.
- The seller is using Shifts Free or the Labor API to record shifts.
- The seller has defined one or more job titles as being tip-eligible.
This scenario shows how to use Payments, Orders, Customers, Staff, and Labor APIs for use with sellers who aren't subscribing to Staff Plus but who need tip pool reporting. The Tip Report sample uses read operations on these APIs to get data. You should explore their full capability by reading the following topics: