Courier Invoice

API Overview

The purpose of this API is to give our clients direct access to their data so that they can better serve their clients. This API implements the project written by Maurits van der Schee released under the MIT License and hosted on GitHub at version 2.

F.A.Q.


Functions And Code Examples
PHP >= 7.0

countryFromAbbr scheduleFrequency call buildURI testResponse responseError webhookHandler invoiceCron

End Points

All non-numeric resources are collated utf8mb4_unicode_ci. The end points, resource names, and write permissions for the Courier Invoice API are as follows:

config contract_locations contract_runs dispatchers drivers clients o_clients schedule_override tickets invoices webhooks

configInformation relating to processing and displaying tickets and invoices.

Resource Data Type Read Only Null Default Description
Return To Top
config_index int(11)(AI) TRUE NO PRIMARY KEY index used for updating via API
user_index int(11) TRUE NO Foreign Key Constraint. References client_index of client 0 on the clients end point.
LogoFileName varchar(11) TRUE NO logo.* File upload script renames files to "logo" with the appropriate file extension.
CurrencySymbol varchar(8) FALSE NO ¤
WeightsMeasures tinyint(1) FALSE NO 0 0 = Imperial, 1 = Metric
InternationalAddressing tinyint(1) FALSE NO 0 0 = Hide country input and display, 1 = Show country input and display
TimeZone varchar(42) FALSE NO UTC Timezone string ex: America/North_Dakota/New_Salem
Supported Timezones
diPrice decimal(6,2) FALSE NO 0.00
OneHour float FALSE NO 1
TwoHour float FALSE NO 1
ThreeHour float FALSE NO 1
FourHour float FALSE NO 1
DeadRun float FALSE NO 0
DedicatedRunRate float FALSE NO 1
MaximumFee decimal(6,2) FALSE NO 0 A value of 0 (zero) indicates no maximum.
Geocoders text FALSE YES NULL
BaseTicketFee decimal(6,2) FALSE YES NULL
RangeIncrement tinyint(3) FALSE YES NULL
PriceIncrement float FALSE YES NULL
MaxRange decimal(7,2) FALSE YES NULL
RangeCenter varchar(23) FALSE YES NULL Latitude and Longitude of delivery range center. Ex: 41.2522201,-95.9822628

contract_locationsInformation relating to the locations for contract/repeating runs.

Resource Data Type Read Only Null Default Description
Return To Top
cloc_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
ID int(11) FALSE NO None This values should be confirmed unique before submission.
Client varchar(45) FALSE NO None
Department varchar(45) FALSE YES NULL
Contact varchar(45) FALSE YES NULL
Telephone varchar(20) FALSE YES NULL
Address1 varchar(45) FALSE NO None
Address2 varchar(45) FALSE NO None
Country varchar(2) FALSE NO None Country abbreviation generated with this function
Deleted tinyint(1) FALSE NO 0

contract_runsDetails for contract/repeating runs.

Resource Data Type Read Only Null Default Description
Return To Top
crun_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
RunNumber int(11) FALSE NO None This value should be confirmed unique before submission.
BillTo int(11) FALSE NO None References clients
PickUp int(11) FALSE NO None References contract_locations
DropOff int(11) FALSE NO None References contract_locations
pickup_id int(11) FALSE NO 1 Foreign Key Constraint. References cloc_index on the contract_locations endpoint. This value is returned when a location is succfully entered via the API, an array of values is returned if locations are batch entered.
dropoff_id int(11) FALSE NO 1 Foreign Key Constraint. References cloc_index on the contract_locations endpoint. This value is returned when a location is succfully entered via the API, an array of values is returned if locations are batch entered.
RoundTrip tinyint(1) FALSE NO 0 bool
pTime time FALSE YES NULL
dTime time FALSE YES NULL
d2Time time FALSE YES NULL
DispatchedTo int(11) FALSE NO 0 References drivers
Schedule varchar(20) FALSE NO None Comma-separated list of 2 character scheduling codes translated by this function
StartDate date FALSE NO None
LastCompleted date FALSE NO 0000-00-00 Used for scheduling
DryIce tinyint(1) FALSE NO 0 bool
diWeight decimal(6,3) FALSE NO 0.000
Notes text FALSE YES NULL
PriceOverride tinyint(1) FALSE NO 0 bool
TicketPrice decimal(6,2) FALSE NO None

dispatchersInformation relating to dispatchers.

Resource Data Type Read Only Null Default Description
Return To Top
dispatch_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
DispatchID int(11) FALSE NO None This values should be confirmed unique before submission.
FirstName varchar(20) FALSE NO Default Name
LastName varchar(20) FALSE YES NULL
EmailAddress varchar(254) FALSE YES NULL
Password varchar(255) FALSE NO None Hash only. No raw passwords should be stored in the database
LoggedIn tinyint(1) FALSE NO 0 bool
LastSeen date FALSE YES NULL
Deleted tinyint(1) FALSE NO 0 bool

driversInformation relating to drivers.

Resource Data Type Read Only Null Default Description
Return To Top
driver_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
DriverID int(11) FALSE NO None This value should be confirmed unique before submission.
FirstName varchar(20) FALSE NO Default Name
LastName varchar(20) FALSE YES NULL
EmailAddress varchar(254) FALSE YES NULL
Password varchar(255) FALSE NO None Hash only. No raw passwords should stored in the database
LoggedIn tinyint(1) FALSE NO 0 bool
LastSeen date FALSE YES NULL
CanDispatch tinyint(1) FALSE NO 0 Describes to whom the driver can dispatch: 0 = None, 1 = Self, 2 = Any
Deleted tinyint(1) FALSE NO 0 bool

clientsInformation relating to repeat clients.

Resource Data Type Read Only Null Default Description
Return To Top
client_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
RepeatClient tinyint(1) FALSE NO 1 bool: 0 = Non-Repeat Client, 1 = Repeat Client
ClientID int(11) FALSE NO None This value should be confirmed unique before submission.
ClientName varchar(45) FALSE NO Default Name
Department varchar(45) FALSE YES NULL
ShippingAddress1 varchar(45) FALSE NO None
ShippingAddress2 varchar(45) FALSE NO None
ShippingCountry varchar(2) FALSE NO None Country abbreviation generated with this function
BillingName varchar(45) FALSE Yes NULL
BillingAddress1 varchar(45) FALSE Yes NULL
BillingAddress2 varchar(45) FALSE Yes NULL
BillingCountry varchar(2) FALSE Yes NULL Country abbreviation generated with this function
Same tinyint(1) FALSE No 0 bool. Indicates if shipping and billing name and address are the same.
Telephone varchar(20) FALSE YES NULL
EmailAddress varchar(254) FALSE YES NULL
Attention varchar(45) FALSE YES NULL
ContractDiscount decimal(5,2) FALSE NO 0.00
GeneralDiscount decimal(5,2) FALSE NO 0.00
Organization int(11) FALSE NO 0 References the ID field of the o_clients end point
org_id int(11) FLASE NO 1 Foreign Key Constraint. References o_client_index on the o_clients endpoint. This value is returned when an organization is succfully entered via the API, an array of values is returned if organizations are batch entered.
Deleted tinyint(1) FALSE NO 0 bool
Password varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database
AdminPassword varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database

o_clientsInformation on clients grouped together as an organization.

Resource Data Type Read Only Null Default Description
Return To Top
o_client_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
id int(11) FALSE NO None This value should be confirmed unique before submission.
Name varchar(40) FALSE NO Default Name
Login varchar(40) FALSE NO default
Password varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database
ListBy tinyint(1) FALSE NO 0 0 = Street Address, 1 = Department
Deleted tinyint(1) FALSE NO 0 bool

schedule_overrideInformation relating to exceptions to contract/repeating run scheduling.

Resource Data Type Read Only Null Default Description
Return To Top
s_o_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
ID int(11) FALSE NO None This value should be confirmed unique before submission.
Cancel tinyint(1) FALSE NO 1 1 = cancel All runs on one day, 2 = cancel All runs over a date range, 3 = cancel One run on one day, 4 = cancel One run over a date range, 5 = Reschedule
StartDate date FALSE NO None
EndDate date FALSE NO None
RunNumber int(11) FALSE NO None References contract_runs
run_id int(11) FALSE NO 1 Foreign Key Constraint. References crun_index on the contract_runs endpoint. This value is returned when a run is succfully entered via the API, an array of values is returned if runs are batch entered.
pTime time FALSE NO 00:00:00 New Pick Up time if Cancel = 5
dTime time FALSE NO 00:00:00 New Drop Off time if Cancel = 5
d2Time time FALSE YES NULL New Return Time if Cancel = 5
DriverID int(11) FALSE NO 0 References drivers

ticketsInformation relating to tickets.

Resource Data Type Read Only Null Default Description
Return To Top
ticket_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
TicketNumber bigint(20) FALSE NO 0 This value should be confirmed unique before submission.
RunNumber int(11) FALSE NO 0 References contract_runs
Rescheduled int(11) FALSE NO 0 References the id field of schedule_override
BillTo int(11) FALSE NO 0 References clients.
RepeatClient tinyint(1) FALSE NO 1 bool: Refers to the value of the RepeatClient field in the clients table.
RequestedBy varchar(45) FALSE YES NULL
pClient varchar(45) FALSE NO Pick Up Client Pick up location name
pDepartment varchar(45) FALSE YES NULL Pick up location department
pContact varchar(45) FALSE YES NULL Pick up location contact
pTelephone varchar(20) FALSE YES NULL Pick up location contact telephone
pAddress1 varchar(45) FALSE NO - Pick up building number, street, room number
pAddress2 varchar(45) FALSE NO - Picket up city/town, state/province, zip/post code
pCountry varchar(2) FALSE NO - Pick up country abbreviation generated with this function
dClient varchar(45) FALSE NO Delivery Client Delivery location name
dDepartment varchar(45) FALSE YES NULL Delivery location department
dContact varchar(45) FALSE YES NULL Delivery location contact
dTelephone varchar(20) FALSE YES NULL Delivery location contact telephone
dAddress1 varchar(45) FALSE NO - Delivery building number, street, room number
dAddress2 varchar(45) FALSE NO - Delivery city/town, state/province, zip/post code
dCountry varchar(2) FALSE NO - Delivery country abbreviation generated with this function
dryIce tinyint(1) FALSE NO 0 bool: 0 = No Dry Ice, 1 = Dry Ice
diWeight decimal(6,3) FALSE NO 0.00 Total weight of dry ice included with the delivery
diPrice decimal(6,2) FALSE NO 0.00 Total charge for dry ice included with the delivery
TicketBase decimal(6,2) FALSE NO 0.00 Unmodified ticket price
Charge tinyint(1) FALSE NO 5 0 = Canceled, 1 = 1 hour, 2 = 2 hour, 3 = 3 hour, 4 = 4 hour, 5 = Routine, 6 = Round Trip, 7 = Dedicated Run, 8 = Dead Run, 9 = Credit
Contract tinyint(1) FALSE NO 0 bool: 0 = On Call, 1 = Contract
Multiplier float FALSE NO 1
RunPrice decimal(6,2) FALSE NO 0.00 Ticket price after modification by Charge value.
TicketPrice decimal(6,2) FALSE NO 0.00 Final ticket price after Charge modification and dry ice addition.
Notes text FALSE YES NULL
Telephone varchar(20) FALSE YES NULL Telephone number of billed client
EmailConfirm tinyint(1) FALSE NO 0 0 = none, 1 = On Pick Up, 2 = On Delivery, 3 = On Pick Up and On Delivery, 4 = On Return, 5 = On Pick Up and On Return, 6 = On Delivery and On Return, 7 = At Each Step
EmailAddress varchar(255) FALSE YES NULL
pSigReq tinyint(1) FALSE NO 0 Request signature on pick up.
pSigPrint varchar(45) FALSE YES NULL
pSig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
pSigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in pSig without a dot.
See image_type_to_extension
dSigReq tinyint(1) FALSE NO 0 Request signature on delivery.
dSigPrint varchar(45) FALSE YES NULL
dSig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
dSigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in dSig without a dot.
See image_type_to_extension
d2SigReq tinyint(1) FALSE NO 0 Request signature on return.
d2SigPrint varchar(45) FALSE YES NULL
d2Sig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
d2SigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in d2Sig without a dot.
See image_type_to_extension
NotForDispatch tinyint(1) FALSE NO 0 If set to 1 the ticket should be ignored by the API.
DispatchTimeStamp datetime FALSE YES NULL
DispatchMicroTime varchar(7) FALSE NO .0 String representation of the mircotime created by parsing microtime(). Ex: .123456
ReadyDate datetime FALSE YES NULL If a delivery is not ready for pick up when a request is made this value can be used to indicate this to a driver.
DispatchedTo int(11) FALSE NO 0 References drivers
DispatchedBy varchar(7) FALSE NO 1.1

Coded representation of who dispatched a ticket, ex: 1.2. Left of the dot refers to the level of the user 1 = dispatcher, 2 = driver. Right of the dot refers to the dispatcher / driver ID.

ReceivedDate datetime FALSE NO None
Transfers longtext FALSE YES NULL json encoded string representing ticket transfers. This should be a numeric array containing objects. Child objects should have only the following properties:

"holder": (number) ID of driver who had the ticket originally,

"receiver": (number) ID of driver receiving the ticket,

"transferedBy": (string) ID of driver / dispatcher transferring the ticket (see DispatchedBy),

"timestamp": (number) Unix timestamp.

Ex: [ { "holder":1, "receiver":2, "transferedBy":"2.1", "timestamp":1512574403296 }, { "holder":2, "receiver":1, "transferedBy":"2.2", "timestamp":1512574797926 } ]
TransferState tinyint FALSE NO 0 Describes the current state of a transfer. 0 = inactive, 1 = pending
PendingReceiver int(11) FALSE NO 0 Driver ID of the target receiver of the current transfer.
pTimeStamp datetime FALSE YES NULL
dTimeStamp datetime FALSE YES NULL
d2TimeStamp datetime FALSE YES NULL
pLat decimal(8,6) FALSE YES NULL Pick up location latitude
pLng decimal(9,6) FALSE YES NULL Pick up location longitude
dLat decimal(8,6) FALSE YES NULL Delivery location latitude
dLng decimal(9,6) FALSE YES NULL Delivery location longitude
d2Lat decimal(8,6) FALSE YES NULL Return location latitude
d2Lng decimal(9,6) FALSE YES NULL Return location longitude
pTime time FALSE YES NULL Used for scheduling
dTime time FALSE YES NULL Used for scheduling
d2Time time FALSE YES NULL Used for scheduling
invoice_id int(11) FALSE NO 1 Foreign Key Constraint. References invoice_index on the invoices endpoint. This value is returned when an invoice is succfully entered via the API, an array of values is returned if invoices are batch entered.
InvoiceNumber varchar(15) FALSE NO - Indicates what invoice, if any, the ticket is billed on.
Expects an invoice number in one of two formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/

invoicesInformation relating to invoices.

Resource Data Type Read Only Null Default Description
Return To Top
invoice_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API.
InvoiceNumber varchar(15) FALSE NO 00EX0000-0 This value should be confirmed unique before submission.
Expects an invoice number in one of two formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
The presence of a 't' after the dash indicates RepeatClient value of 0.
ClientID int(11) FALSE NO 0
RepeatClient tinyint(1) FALSE NO 1 Reference RepeatClient field of the clients table.
BalanceForwarded decimal(6,2) FALSE NO 0.00 This value is taken from the Balance field of the most recently closed invoice.
InvoiceSubTotal decimal(6,2) FALSE NO 0.00 This value is the sum of all tickets included on the invoice.
AmountDue decimal(6,2) FALSE NO 0.00 This value is calculated when an invoice is closed and is the difference between InvoiceSubTotal and BalanceForwarded.
InvoiceTotal decimal(6,2) FALSE NO 0.00 This value is the sum of InvoiceSubTotal, BalanceForwarded, and any past due invoices.
StartDate date FALSE NO None
EndDate date FALSE NO None
DateIssued date FALSE NO None
DatePaid date FALSE YES NULL
AmountPaid decimal(6,2) FALSE NO 0.00
Balance decimal(6,2) FALSE NO 0.00 This value is calculated when an invoice is closed and is the difference between AmountDue and AmountPaid.
Late30Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 30 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late30Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 30 days past due at the time of current invoice creation
Late60Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 60 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late60Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 60 days past due at the time of current invoice creation
Late90Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 90 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late90Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 90 days past due at the time of current invoice creation.
Over90Invoice varchar(50) FALSE YES NULL Identifies Invoice(s) that are more than 90 days past due at the time of current invoice creation.
Expects either a comma separated list of up to 4 Invoice Numbers as described on the InvoiceNumber end point or a comma separated list of three such Invoice Numbers followed by a plus symbol (+) an integer and the word 'more'. The last case should look like this (the letter 't' should follow the dash (-) in the case of non repeat clients):
##EX####-##, ##EX####-##, ##EX####-##, + ### more. Note the comma between the last invoice number and the plus symbol (+), if this is omitted data validation will fail.
Over90Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are more than 90 days past due at the time of current invoice creation
CheckNumber varchar(50) FALSE YES NULL Check number, transaction number, or any other identifier for payment method.
Closed tinyint(1) FALSE NO 0 bool
Deleted tinyint(1) FALSE NO 0 bool

webhooksInformation relating to webhook listeners.

Resource Data Type Read Only Null Default Description
Note: Currently the POST method is disabled for this endpoint.
Return To Top
webhook_index int(11)AI TRUE NO None Used internally for archiving.
Webhook_id varchar(30) TRUE NO None PRIMARY KEY index used for updateing via API
Protocol tinyint(1) FALSE NO 1 Boolean indicating if URL uses https.
Listener varchar(255) FALSE NO None URL listening for webhooks.
Events text FALSE NO None Comma-separated list of dot-notated events. Ex: ticket.oncall.receive,ticket.oncall.dispatch (Note: There are no white-spaces in this string). See Webhooks page for full list of values.
Deleted tinyint(1) FALSE NO 0 Boolean indicating if the webhook has been deactivated.
<?php
  function countryFromAbbr($abbr) {
    // Credits will have a value of '-' for pCountry and dCountry
    if ($abbr === '-') return $abbr;
    //Country names and abbreviations taken from FedEx international shipping guidelines
    //"XZ" is not on the abbreviation list so it will stand for "Not On File"
    if (strlen($abbr) === 2) {
      switch ($abbr) {
        case 'AL': return 'Albania';
        case 'DZ': return 'Algeria';
        case 'AD': return 'Andorra';
        case 'AO': return 'Angola';
        case 'AR': return 'Argentina';
        case 'AM': return 'Armenia';
        case 'AW': return 'Aruba';
        case 'AU': return 'Australia';
        case 'AT': return 'Austria';
        case 'AZ': return 'Azerbaijan';
        case 'PT': return 'Azores';
        case 'BS': return 'Bahamas';
        case 'BH': return 'Bahrain';
        case 'BD': return 'Bangladesh';
        case 'BB': return 'Barbados';
        case 'BY': return 'Belarus';
        case 'BE': return 'Belgium';
        case 'BZ': return 'Belize';
        case 'BJ': return 'Benin';
        case 'BM': return 'Bermuda';
        case 'BT': return 'Bhutan';
        case 'BO': return 'Bolivia';
        case 'BA': return 'Bosna-Herzegovina';
        case 'BW': return 'Botswana';
        case 'BR': return 'Brazil';
        case 'BN': return 'Brunei Darussalam';
        case 'BG': return 'Bulgaria';
        case 'BF': return 'Burkina FASO';
        case 'BI': return 'Burundi';
        case 'KH': return 'Cambodia';
        case 'CM': return 'Cameroon';
        case 'CA': return 'Canada';
        case 'CV': return 'Cape Verde';
        case 'KY': return 'Cayman Islands';
        case 'CF': return 'Cntl African Republic';
        case 'TS': return 'Chad';
        case 'CL': return 'Chile';
        case 'CN': return 'China';
        case 'CO': return 'Columbia';
        case 'ZR': return 'Democratic Republic of Congo';
        case 'CG': return 'Republic of the Congo (Brazaville)';
        case 'CR': return  'Costa Rica';
        case 'CI': return 'Cote d\'Ivoire (Ivory Coast)';
        case 'HR': return 'Croatia';
        case 'CY': return 'Cyprus';
        case 'CZ': return 'Czech Republic';
        case 'DK': return 'Denmark';
        case 'DJ': return 'Djibouti';
        case 'DO': return 'Dominican Republic';
        case 'EC': return 'Ecuador';
        case 'EG': return 'Egypt';
        case 'SV': return 'El Salvador';
        case 'GQ': return 'Equatorial Guinea';
        case 'ER': return 'Eritrea';
        case 'EE': return 'Estonia';
        case 'ET': return 'Ethiopia';
        case 'DK': return 'Faroe Islands';
        case 'FJ': return 'Fiji';
        case 'FI': return 'Finland';
        case 'FR': return 'France';
        case 'GF': return 'French Guiana';
        case 'PF': return 'French Polynesia (Tahitti)';
        case 'GA': return 'Gabon';
        case 'GE': return 'Georgia, Republic of';
        case 'DE': return 'Germany';
        case 'GH': return 'Ghana';
        case 'GB': return 'Great Britain & Northern Ireland';
        case 'GR': return 'Greece';
        case 'GD': return 'Grenada';
        case 'GP': return 'Guadeloupe';
        case 'GT': return 'Guatemala';
        case 'GN': return 'Guinea';
        case 'GW': return 'Guinea-Bissau';
        case 'GY': return 'Guyana';
        case 'HT': return 'Haiti';
        case 'HN': return 'Honduras';
        case 'HK': return 'Hong Kong';
        case 'HU': return 'Hungary';
        case 'IS': return 'Iceland';
        case 'IN': return 'India';
        case 'ID': return 'Indonesia';
        case 'IR': return 'Iran';
        case 'IQ': return 'Iraq';
        case 'IE': return 'Ireland (Eire)';
        case 'IL': return 'Israel';
        case 'IT': return 'Italy';
        case 'JM': return 'Jamaica';
        case 'JP': return 'Japan';
        case 'JO': return 'Jordan';
        case 'KG': return 'Kazakhstan';
        case 'KE': return 'Kenya';
        case 'KR': return 'South Korea, Republic of';
        case 'KW': return 'Kuwait';
        case 'KG': return 'Kyrgyzstan';
        case 'LA': return 'Laos';
        case 'LV': return 'Latvia';
        case 'LS': return 'Lesotho';
        case 'LR': return 'Liberia';
        case 'LI': return 'Liechtenstein';
        case 'LT': return 'Lithuania';
        case 'LU': return 'Luxembourg';
        case 'MO': return 'Macao';
        case 'MK': return 'Macedonia, Republic of';
        case 'MG': return 'Madagascar';
        case 'PT': return 'Madeira Islands';
        case 'MW': return 'Malawi';
        case 'MY': return 'Malaysia';
        case 'MV': return 'Maldives';
        case 'ML': return 'Mali';
        case 'MT': return 'Malta';
        case 'MQ': return 'Martinique';
        case 'MR': return 'Mauritania';
        case 'MU': return 'Mauritius';
        case 'MX': return 'Mexico';
        case 'MD': return 'Moldova';
        case 'MN': return 'Mongolia';
        case 'MA': return 'Morocco';
        case 'MZ': return 'Mozambique';
        case 'NA': return 'Namibia';
        case 'NR': return 'Nauru';
        case 'NP': return 'Nepal';
        case 'NL': return 'Netherlands (Holland)';
        case 'AN': return 'Netherlands Antilles';
        case 'NC': return 'New Caledonia';
        case 'NZ': return 'New Zealand';
        case 'NI': return 'Nicaragua';
        case 'NE': return 'Niger';
        case 'NG': return 'Nigeria';
        case 'NO': return 'Norway';
        case 'OM': return 'Oman';
        case 'PK': return 'Pakistan';
        case 'PA': return 'Panama';
        case 'PG': return 'Papua New Guinea';
        case 'PY': return 'Paraguay';
        case 'PE': return 'Peru';
        case 'PH': return 'Philippines';
        case 'PL': return 'Poland';
        case 'PT': return 'Portugal';
        case 'QA': return 'Qatar';
        case 'RO': return 'Romania';
        case 'RU': return 'Russia (Russia Federation)';
        case 'RW': return 'Rwanda';
        case 'KN': return 'St. Christopher (St. Kitts) & Nevis';
        case 'LC': return 'St. Lucia';
        case 'VC': return 'St. Vincent & the Grenadines';
        case 'SA': return 'Saudi Arabia';
        case 'SN': return 'Senegal';
        case 'YU': return 'Serbia Montenegro (Yugoslavia)';
        case 'SC': return 'Seychelles';
        case 'SL': return 'Sierra Leone';
        case 'SG': return 'Singapore';
        case 'SK': return 'Slovak Republic (Slovakia)';
        case 'SI': return 'Slovenia';
        case 'SB': return 'Solomon Islands';
        case 'SO': return 'Somalia';
        case 'ZA': return 'South Africa';
        case 'ES': return 'Spain';
        case 'LK': return 'Sri Lanka';
        case 'SD': return 'Sudan';
        case 'SZ': return 'Swaziland';
        case 'SE': return 'Sweden';
        case 'CH': return 'Switzerland';
        case 'SY': return 'Syrian Arab Republic';
        case 'TW': return 'Taiwan';
        case 'TJ': return 'Tajikistan';
        case 'TZ': return 'Tanzania';
        case 'TH': return 'Thailand';
        case 'TG': return 'Togo';
        case 'TT': return 'Trinidad & Tobago';
        case 'TN': return 'Tunisia';
        case 'TR': return 'Turkey';
        case 'TM': return 'Turkmenistan';
        case 'UG': return 'Uganda';
        case 'AE': return 'United Arab Emirates';
        case 'UA': return 'Ukraine';
        case 'US': return 'United States of America';
        case 'UY': return 'Uruguay';
        case 'VU': return 'Vanuatu';
        case 'VE': return 'Venezuela';
        case 'VN': return 'Vietnam';
        case 'WS': return 'Western Samoa';
        case 'YE': return 'Yemen';
        default: return 'Not On File';
      }
    } else {
      switch ($abbr) {
        case 'Albania': return 'AL';
        case 'Algeria': return 'DZ';
        case 'Andorra': return 'AD';
        case 'Angola': return 'AO';
        case 'Argentina': return 'AR';
        case 'Armenia': return 'AM';
        case 'Aruba': return 'AW';
        case 'Australia': return 'AU';
        case 'Austria': return 'AT';
        case 'Azerbaijan': return 'AZ';
        case 'Azores': return 'PT';
        case 'Bahamas': return 'BS';
        case 'Bahrain': return 'BH';
        case 'Bangladesh': return 'BD';
        case 'Barbados': return 'BB';
        case 'Belarus': return 'BY';
        case 'Belgium': return 'BE';
        case 'Belize': return 'BZ';
        case 'Benin': return 'BJ';
        case 'Bermuda': return 'BM';
        case 'Bhutan': return 'BT';
        case 'Bolivia': return 'BO';
        case 'Bosna-Herzegovina': return 'BA';
        case 'Botswana': return 'BW';
        case 'Brazil': return 'BR';
        case 'Brunei Darussalam': return 'BN';
        case 'Bulgaria': return 'BG';
        case 'Burkina FASO': return 'BF';
        case 'Burundi': return 'BI';
        case 'Cambodia': return 'KH';
        case 'Cameroon': return 'CM';
        case 'Canada': return 'CA';
        case 'Cape Verde': return 'CV';
        case 'Cayman Islands': return 'KY';
        case 'Cntl African Republic': return 'CF';
        case 'Chad': return 'TS';
        case 'Chile': return 'CL';
        case 'China': return 'CN';
        case 'Columbia': return 'CO';
        case 'Democratic Republic of Congo': return 'ZR';
        case 'Republic of the Congo (Brazaville)': return 'CG';
        case 'Costa Rica': return 'CR';
        case 'Cote d\'Ivoire (Ivory Coast)': return 'CI';
        case 'Croatia': return 'HR';
        case 'Cyprus': return 'CY';
        case 'Czech Republic': return 'CZ';
        case 'Denmark': return 'DK';
        case 'Djibouti': return 'DJ';
        case 'Dominican Republic': return 'DO';
        case 'Ecuador': return 'EC';
        case 'Egypt': return 'EG';
        case 'El Salvador': return 'SV';
        case 'Equatorial Guinea': return 'GQ';
        case 'Eritrea': return 'ER';
        case 'Estonia': return 'EE';
        case 'Ethiopia': return 'ET';
        case 'Faroe Islands': return 'DK';
        case 'Fiji': return 'FJ';
        case 'Finland': return 'FI';
        case 'France': return 'FR';
        case 'French Guiana': return 'GF';
        case 'French Polynesia (Tahitti)': return 'PF';
        case 'Gabon': return 'GA';
        case 'Georgia, Republic of': return 'GE';
        case 'Germany': return 'DE';
        case 'Ghana': return 'GH';
        case 'Great Britain & Northern Ireland': return 'GB';
        case 'Greece': return 'GR';
        case 'Grenada': return 'GD';
        case 'Guadeloupe': return 'GP';
        case 'Guatemala': return 'GT';
        case 'Guinea': return 'GN';
        case 'Guinea-Bissau': return 'GW';
        case 'Guyana': return 'GY';
        case 'Haiti': return 'HT';
        case 'Honduras': return 'HN';
        case 'Hong Kong': return 'HK';
        case 'Hungary': return 'HU';
        case 'Iceland': return 'IS';
        case 'India': return 'IN';
        case 'Indonesia': return 'ID';
        case 'Iran': return 'IR';
        case 'Iraq': return 'IQ';
        case 'Ireland (Eire)': return 'IE';
        case 'Israel': return 'IL';
        case 'Italy': return 'IT';
        case 'Jamaica': return 'JM';
        case 'Japan': return 'JP';
        case 'Jordan': return 'JO';
        case 'Kazakhstan': return 'KG';
        case 'Kenya': return 'KE';
        case 'South Korea, Republic of': return 'KR';
        case 'Kuwait': return 'KW';
        case 'Kyrgyzstan': return 'KG';
        case 'Laos': return 'LA';
        case 'Latvia': return 'LV';
        case 'Lesotho': return 'LS';
        case 'Liberia': return 'LR';
        case 'Liechtenstein': return 'LI';
        case 'Lithuania': return 'LT';
        case 'Luxembourg': return 'LU';
        case 'Macao': return 'MO';
        case 'Macedonia, Republic of': return 'MK';
        case 'Madagascar': return 'MG';
        case 'Madeira Islands': return 'PT';
        case 'Malawi': return 'MW';
        case 'Malaysia': return 'MY';
        case 'Maldives': return 'MV';
        case 'Mali': return 'ML';
        case 'Malta': return 'MT';
        case 'Martinique': return 'MQ';
        case 'Mauritania': return 'MR';
        case 'Mauritius': return 'MU';
        case 'Mexico': return 'MX';
        case 'Moldova': return 'MD';
        case 'Mongolia': return 'MN';
        case 'Morocco': return 'MA';
        case 'Mozambique': return 'MZ';
        case 'Namibia': return 'NA';
        case 'Nauru': return 'NR';
        case 'Nepal': return 'NP';
        case 'Netherlands (Holland)': return 'NL';
        case 'Netherlands Antilles': return 'AN';
        case 'New Caledonia': return 'NC';
        case 'New Zealand': return 'NZ';
        case 'Nicaragua': return 'NI';
        case 'Niger': return 'NE';
        case 'Nigeria': return 'NG';
        case 'Norway': return 'NO';
        case 'Oman': return 'OM';
        case 'Pakistan': return 'PK';
        case 'Panama': return 'PA';
        case 'Papua New Guinea': return 'PG';
        case 'Paraguay': return 'PY';
        case 'Peru': return 'PE';
        case 'Philippines': return 'PH';
        case 'Poland': return 'PL';
        case 'Portugal': return 'PT';
        case 'Qatar': return 'QA';
        case 'Romania': return 'RO';
        case 'Russia (Russia Federation)': return 'RU';
        case 'Rwanda': return 'RW';
        case 'St. Christopher (St. Kitts) & Nevis': return 'KN';
        case 'St. Lucia': return 'LC';
        case 'St. Vincent & the Grenadines': return 'VC';
        case 'Saudi Arabia': return 'SA';
        case 'Senegal': return 'SN';
        case 'Serbia Montenegro (Yugoslavia)': return 'YU';
        case 'Seychelles': return 'SC';
        case 'Sierra Leone': return 'SL';
        case 'Singapore': return 'SG';
        case 'Slovak Republic (Slovakia)': return 'SK';
        case 'Slovenia': return 'SI';
        case 'Solomon Islands': return 'SB';
        case 'Somalia': return 'SO';
        case 'South Africa': return 'ZA';
        case 'Spain': return 'ES';
        case 'Sri Lanka': return 'LK';
        case 'Sudan': return 'SD';
        case 'Swaziland': return 'SZ';
        case 'Sweden': return 'SE';
        case 'Switzerland': return 'CH';
        case 'Syrian Arab Republic': return 'SY';
        case 'Taiwan': return 'TW';
        case 'Tajikistan': return 'TJ';
        case 'Tanzania': return 'TZ';
        case 'Thailand': return 'TH';
        case 'Togo': return 'TG';
        case 'Trinidad & Tobago': return 'TT';
        case 'Tunisia': return 'TN';
        case 'Turkey': return 'TR';
        case 'Turkmenistan': return 'TM';
        case 'Uganda': return 'UG';
        case 'United Arab Emirates': return 'AE';
        case 'Ukraine': return 'UA';
        case 'United States of America': return 'US';
        case 'Uruguay': return 'UY';
        case 'Vanuatu': return 'VU';
        case 'Venezuela': return 'VE';
        case 'Vietnam': return 'VN';
        case 'Western Samoa': return 'WS';
        case 'Yemen': return 'YE';
        default: return 'XZ';
      }
    }
  }
      
Return To Top
<?php
  function scheduleFrequency($code) {
    // $code can be either a 2 character scheduling code or a 2 or 3 word schedule
    $x = $y = "";
    // Create an array to find out if we have a code or a schedule
    $test = explode(" ", $code);
    if (count($test) === 1) {
      switch(substr($code, 0, 1)) {
        case "a": $x = "Every"; break;
        case "b": $x = "Every Other"; break;
        case "c": $x = "Every First"; break;
        case "d": $x = "Every Second"; break;
        case "e": $x = "Every Third"; break;
        case "f": $x = "Every Fourth"; break;
        case "g": $x = "Every Last"; break;
      }
      switch (substr($code, 1, 1)) {
        case "1": $y = "Day"; break;
        case "2": $y = "Weekday"; break;
        case "3": $y = "Monday"; break;
        case "4": $y = "Tuesday"; break;
        case "5": $y = "Wednesday"; break;
        case "6": $y = "Thursday"; break;
        case "7": $y = "Friday"; break;
        case "8": $y = "Saturday"; break;
        case "9": $y = "Sunday"; break;
      }
      return "{$x} {$y}";
    } else {
      if(count($test) === 3) {
        // If $code is a 3 word schedule the first must be Every so drop it
        $test = array_shift($test);
      }
      switch($test[0]) {
        case "Every": $x = "a"; break;
        case "Other": $x = "b"; break;
        case "First": $x = "c"; break;
        case "Second": $x = "d"; break;
        case "Third": $x = "e"; break;
        case "Fourth": $x = "f"; break;
        case "Last": $x = "g"; break;
      }
      switch ($test[1]) {
        case "Day": $y = "1"; break;
        case "Weekday": $y = "2"; break;
        case "Monday": $y = "3"; break;
        case "Tuesday": $y = "4"; break;
        case "Wednesday": $y = "5"; break;
        case "Thursday": $y = "6"; break;
        case "Friday": $y = "7"; break;
        case "Saturday": $y = "8"; break;
        case "Sunday": $y = "9"; break;
      }
      return "{$x}{$y}";
    }
  }
      
Return To Top
<?php
  function call($method, $url, $payload=false) {
    $privateKey = 'Your_Private_API_Key';
    // Get the time
    $time = time();
    // Use api key to generate security token using the REQUEST_URI and the time
    $token = hash_hmac('sha256', substr($url, strpos($url, '.com') + 4) . $time, $privateKey);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_FAILONERROR,true);
    // CURLOPT_SSL_VERIFYPEER set to false for testing only
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    // CURLOPT_SSL_VERIFYHOST disabled for testing only. It should be set to 2 in production
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_CAINFO, '/path/to/gd_bundle-g2-g1.crt');
    //Set the authorization headers
    $headers = array();
    $hearers[] = "Authorization: Basic " . base64_encode("Your_Account_Number:Your_Public_API_Key";");
    $headers[] = "AUTH: $token";
    $headers[] = "TIME: $time";
    if ($payload) {
      // $payload should be an array of Resource Names and Values.
      $payloadJSON = json_encode($payload);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJSON);
      $headers[] = 'Content-Type: application/json';
      $headers[] = 'Content-Length: ' . strlen($payloadJSON);
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    if (!$result) {
      return curl_error($ch);
    }
    return $result;
  }
      
Return To Top
<?php
  function buildURI($endPoint, $data) {
    /***
    * $data['resources'] = array('res1', 'res2', 'res3', ...)
    * If $data['resources'] is omitted all resources for entries matching the filter will be returned
    *
    * $queryParams['filter'] = array(array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val'))
    * The above describes a simple query. The structure of an 'AND' statement simply addds elements to the primary arry. EX:
    * $queryParams['filter'] = array(array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val'), array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val'))
    * An 'OR' statement requires an array of simple or 'AND' statements. Ex:
    * $queryParams['filter'] = array(array(array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val'), array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val')), array('Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val'))
    * The available filters:
    *   cs: contain string (string contains value)
    *   sw: start with (string starts with value)
    *   ew: end with (string end with value)
    *   eq: equal (string or number matches exactly)
    *   lt: lower than (number is lower than value)
    *   le: lower or equal (number is lower than or equal to value)
    *   ge: greater or equal (number is higher than or equal to value)
    *   gt: greater than (number is higher than value)
    *   bt: between (number is between two comma separated values)
    *   in: in (number is in comma separated list of values)
    *   is: is null (field contains "NULL" value)
    *   Negate any filter by prepending a 'n' character, ex: 'eq' becomes 'neq'
    *
    *  If $data['filter'] is omitted requested resources will be returned for all entries
    *
    *  With the "order" parameter you can sort. By default the sort is in ascending order, but by specifying "desc" this can be reversed
    *  $queryParams['order'] = array(array('resource'=>'TicketNumber','dir'=>'desc'));
    *
    *  You may sort on multiple fields by using multiple "order" parameters
    *  $queryParams['order'] = array(array('resource'=>'TicketNumber', 'dir'=>'desc'),array('resource'=>'Contract'));
    *
    *
    * The "page" parameter holds the requested page. The default page size is 20, but can be adjusted (e.g. to 50)
    * $queryParams['page'] = '1'; or $queryParams['page'] = '1,50';
    *
    * Pages that are not ordered cannot be paginated
    *
    *  When using this function to update a resource (PUT):
    *    $data should be an empty array
    *    The function call should have a string appended
    *    This string should contain a forward slash followed by the PRIMARY KEY value of the resource to be modified
    *    An example using the call function defined above:
    *      call('PUT', buildURI($endPoint, array()) . '/' . $varHoldingPK, $payload);
    *    $payload should be an array of key => value pairs to be updated in the $endPoint
    *    See the table above for the PRIMARY KEY resources to be used for these calls
    ***/
    $baseURI = "https://rjdeliveryomaha.com/v2/records/";
    $queryURI = $baseURI . $endPoint;
    if (empty($data)) {
      return $queryURI;
    }
    $query = array();
    foreach ($data as $key => $value) {
      if ($key === 'resources') {
        $query['include'] = implode(",",$data['resources']);
      } elseif ($key === 'filter') {
        if (!isset($value[0][0])) {
              for ($i = 0; $i < count($value); $i++) {
                $this-?query['filter'][] = implode(',', array_values($value[$i]));
              }
            } else {
              for ($i = 0; $i < count($value); $i++) {
                $filter_index = $i + 1;
                for ($j = 0; $j < count($value[$i]); $j++) {
                  $this-?query["filter{$filter_index}"][] = implode(',', array_values($value[$i][$j]));
                }
              }
            }
      } else {
        $query[$key] = implode(",",$value);
      }
    }
    $query = http_build_query($query,NULL,'&',PHP_QUERY_RFC3986);
    // http://php.net/manual/en/function.http-build-query.php#111819
    if (isset($data['filter']) && count($data['filter']) > 1) {
      $query = preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $query);
    }
    return $queryURI . '?' . $query;
  }
      
Return To Top
<?php
  function testResponse($method, $response) {
    // A simple test to make sure an appropriate response was received
    switch ($method) {
      // POST: Expects the id of the last created resource
      // PUT, DELETE: Expects the number of rows affected
      // Expects an array of ids or rows affected when creating, updating, or deleting with array
      // Receives the string 'null' on failure
      case "DELETE":
      case "PUT":
      case "POST": return (is_numeric($response) || substr($response, 0, 1) === '[');
      // Expects a json encoded object or array of objects
      case "GET": return (substr($response,0,1) === '{' || substr($response,0,1) === '[');

      default: return FALSE;
    }
  }
      
Return To Top
<?php
  function responseError($response) {
    switch (filter_var($response, FILTER_SANITIZE_NUMBER_INT)) {
      case 400: $this->error = "Server Error: Invalid Request URI.\n"; break;
      case 401: echo 'Server Error: Invalid login credentials.'; break;
      case 403: $this->error = "Server Error: Login credentials not defined.\n"; break;
      case 404: echo 'Server Error: Failed to locate record.'; break;
      case 422: echo 'Server Error: Failed Data Validation. ' . substr($response, strpos($response,'422')+3); break;
      case 500: echo 'Server Error: Internal Error.'; break;
      case 503: $this->error = "Server Error: Service temporarily unavailable.\n"; default:
      default: echo 'Server Error: ' . $response; break;
    }
  }
      
Return To Top
<?php
  /**
  usage:
    if ($_SERVER['REQUEST_METHOD'] !== "POST") return FALSE;
    http_response_code(200); // PHP 5.4 or greater
    include_once("../ PATH / To / webhookHandler.class.php");
    $webhookHandler = new webhookHandler(@file_get_contents("php://input"));
    $webhookHandler->processWebhook();
  **/
  class webhookHandler {
    // user config
    private $timezone = "UTC";
    private $privateKey = "Your_Private_API_Key";
    private $userID = "Your_Account_Number";
    // properties in the webhook
    private $error;
    private $type;
    private $BillTo;
    private $DispatchedTo;
    private $TicketNumber;
    private $InvoiceNumber;
    private $ClientID;
    private $DriverID;
    private $DispatchID;
    private $RunNumber;
    private $ScheduleID;
    private $stored;
    private $received;
    // validation
    private $token;
    private $inputArray;
    // processing
    private $typeParts;
    private $transferKeys = array("error", "type", "BillTo", "DispatchedTo", "TicketNumber", "InvoiceNumber", "ClientID", "DriverID", "DispatchID", "stored", "received");
    // error handling
    private $content;
    private $fileWriteTry = 5;
    private $targetFile = "./logs/webhookHandler_error";
    private $Data;
    
    function __construct($jsonString) {
      if ($jsonString === NULL || $jsonString === false) $this->exitWithoutLog();
      
      $this->inputArray = json_decode($jsonString);
      
      if (json_last_error() !== JSON_ERROR_NONE || $this->inputArray === NULL) $this->exitWithoutLog();
      
      $this->token = hash_hmac('sha256', $jsonString . $this->userID . $this->inputArray->timestamp, $this->privateKey);
      // hash_equals for php < 5.6.0
      // https://php.net/manual/en/function.hash-equals.php#115664
      if (!hash_equals($this->token, $_SERVER['HTTP_AUTH'])) {
        $this->content = "Failed Auth \n" . print_r($jsonString, true);
        $this->exitWithLog();
      }
      
      if (!isset($this->inputArray["Data"]) || empty($this->inputArray["Data"])) {
        $this->content = "No webhook to process \n"  . print_r($jsonString, true);
      } else {
        $this->Data = $this->inputArray["Data"];
      }
      
      if (!date_default_timezone_set($this->timezone)) {
        $this->content = "Timezone error: Could not set timezone to \"{$this->timezone}\n----\n\n";
        $this->writeLoop();
      }
    }
    
    private function exitWithoutLog() {
      return false;
    }
    
    private function exitWithLog() {
      $this->content = ($this->content === NULL) ? date("dMY H:i:s") . "\nundefined error\n----\n\n" : date("dMY H:i:s") . "\n" . $this->content . "\n----\n\n";
      return $this->writeLoop();
    }

    private function writeLoop() {
      $i = 0;
      do {
        $test = $this->writeFile();
        $i++;
      } while ($test !== strlen($this->content) && $i < $this->fileWriteTry);
    }

    private function writeFile() {
      /*** http://php.net/manual/en/function.fwrite.php#81269 ***/
      $fp = fopen( $this->targetFile, "ab" );
      /*** write the new file content ***/
      $bytes_to_write = strlen($this->content);
      $bytes_written = 0;
      while ($bytes_written < $bytes_to_write) {
        if ($bytes_written == 0) {
          $rv = fwrite($fp, $this->content);
        } else {
          $rv = fwrite($fp, substr($this->content, $bytes_written));
        }
        if ($rv === false || $rv == 0) {
          return($bytes_written == 0 ? false : $bytes_written);
        }
        $bytes_written += $rv;
      }
      return $bytes_written;
    }
    
    public function processWebhook() {
      for ($i = 0; $i < count($this->Data); $i++) {
        // TODO
        // Implement deduplication for batch webhooks if desired
        foreach ($this->Data[$i] as $key => $value) {
          foreach ($this as $k => $v) {
            if (in_array($key, $this->transferKeys) && $key === $k) {
              $this->$k = $value;
            }
          }
        }
        $this->executeWebhook();
      }
    }
    
    private function executeWebhook() {
      if (empty($this->Data)) $this->exitWithoutLog();
      if ($this->error !== NULL) {
        $this->content = $this->error;
        $this->exitWithLog();
      } else {
        $this->typeParts = explode(".", $this->type);
        $method = $this->typeParts[0];
        if (method_exists($this, $method)) return $this->$method();
        $this->content = "Invalid primary hook component: {$this->type}";
        $this->exitWithLog();
      }
    }
    // ticket methods
    private function ticket() {
      $method = $this->typeParts[2] . "Ticket";
      switch($this->typeParts[1]) {
        case "contract":
          // TODO
          // differentiate between contract and on call tickets
        case "oncall":
          if (method_exists($this, $method)) return $this->$method();
          $this->content = "Invalid tertiary hook component: {$this->type}";
          $this->exitWithLog();
        break;
        default:
          $this->content = "Invalid secondary hook component: {$this->type}";
          $this->exitWithLog();
      }
    }
    // return and delete are key words and update is a common function between hooks; append "Ticket" to the end of each function
    private function receiveTicket() {
      $this->content = "Ticket {$this->TicketNumber} received";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function dispatchTicket() {
      $this->content = "Ticket {$this->TicketNumber} dispatched to driver {$this->DispatchedTo}";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function pickupTicket() {
      $this->content = "Ticket {$this->TicketNumber} picked up";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deliverTicket() {
      $this->content = "Ticket {$this->TicketNumber} delivered";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function returnTicket() {
      $this->content = "Ticket {$this->TicketNumber} returned";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateTicket() {
      // uncomment the following to prevent sending push notifications for each ticket when invoices are created via the API
      // if (count(get_object_vars($this->received)) === 1 && property_exists($this->received, "InoviceNumber")) return false;
      // uncomment the following to prevent sending push notifications when coordinates are updated for only one step
      // if (count(get_object_vars($this->received)) === 2 && ((property_exists($this->received, 'pLat') && property_exists($this->received, 'pLng')) || (property_exists($this->received, 'dLat') && property_exists($this->received, 'dLng')) || (property_exists($this->received, 'd2Lat') && property_exists($this->received, 'd2Lng')))) return false;
      $this->content = "Ticket {$this->TicketNumber} updated";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function transferTicket() {
      $this->content = "Ticket {$this->TicketNumber} transferred";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deleteTicket() {
      $this->content = "Ticket {$this->TicketNumber} deleted";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    // invoice methods
    private function invoice() {
      $method = $this->typeParts[1] . "Invoice";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    // delete is a key word and create and update are common functions between hooks; append "Invoice", "Client", "TClient", "OClient", "Driver", or "Dispatcher" to the end of each function as needed
    private function createInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} created";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} updated";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deleteInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} deleted";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    // client methods
    private function client() {
      $method = $this->typeParts[2] . "Client";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createClient() {
      $this->content = "{$this->typeParts[1]} Client {$this->ClientID} {$this->typeParts[2]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateClient() {
      return $this->createClient();
    }
    
    private function deleteClient() {
      return $this->createClient();
    }
    
    private function t_client() {
      return $this->client();
    }
    
    private function o_client() {
      return $this->client();
    }
    // driver / dispatcher methods
    private function driver() {
      $method = $this->typeParts[1] . "Driver";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createDriver() {
      $this->content = "{$this->type[0]} {$this->DriverID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateDriver() {
      return $this->createDriver();
    }
    
    private function deleteDriver() {
      return $this->createDriver();
    }
    
    private function dispatcher() {
      $method = $this->typeParts[1] . "Dispatcher";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createDispatcher() {
      $this->content = "{$this->type[0]} {$this->DispatchID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateDispatcher() {
      return $this->createDispatcher();
    }
    
    private function deleteDispatcher() {
      return $this->createDispatcher();
    }
    
    private function contract_run() {
      $method = $this->typeParts[1] . "Run";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createRun() {
      $this->content = "{$this->type[0]} {$this->RunNumber} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateRun() {
      return $this->createRun();
    }
    
    private function deleteRun() {
      return $this->createRun();
    }
    
    private function schedule_override() {
      $method = $this->typeParts[1] . "Schedule";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createSchedule() {
      $this->content = "{$this->type[0]} {$this->ScheduleID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateSchedule() {
      return $this->createSchedule();
    }
    
    private function deleteSchedule() {
      return $this->createSchedule();
    }
  }
      
Return To Top
<?php
  /**
    this should be run as a cron job the day following the end of a monthly billing cycle
    in terms of timing this is very simplistic
    this class does not account for leap years and assumes that invoices will not be generated after the 28th of any month
    
    usage:
    include_once '../ PATH / TO /invoiceCron.class.php';
    $invoiceCron = new invoiceCron();
    $invoiceCron->createInvoices();
  **/
  class invoiceCron {
    private $username = 'Your_Account_Number';
    private $publicKey = 'Your_Public_API_Key';
    private $privateKey = 'Your_Private_API_Key';
    private $timezoneString = 'UTC';
    private $logSuccess = false;
    // array of (int)ClientID that should not be processed on this schedule
    private $ignoreClients = array();
    private $ignoreNonRepeat = array();
    // invoice variables
    private $startDate;
    private $endDate;
    private $dateIssued;
    private $tickets;
    private $clientList;
    private $newInvoices;
    private $DateIssued;
    private $Over90InvoiceList;
    // update variables
    private $ticketUpdateKeys;
    private $ticketUpdateValues;
    // query variables
    private $method;
    private $primaryKey;
    private $baseURI;
    private $ch;
    private $timeVal;
    private $token;
    private $queryURI;
    private $headers;
    private $jsonData;
    private $message;
    private $query;
    private $result;
    private $payload = false;
    private $data;
    private $endPoint;
    // error catching
    private $timezone;
    private $today;
    private $content;
    private $fileWriteTry = 5;
    private $targetFile = './invoice_error';
    
    public function __construct() {
      // Extend timeout for large queries
      set_time_limit(3600);
      $this-?clients = $this-?t_clients = $this-?clientList = $this-?headers = $this-?query = $this-?data = $this-?ticketUpdateKeys = $this-?ticketUpdateValues = array();
      // set timezone
      try {
        $this-?timezone = new dateTimezone($this-?timezoneString);
      } catch (Exception $e) {
        $this-?content = date('Y-m-d H:i:s') . "\nTimezone Error: {$e-?getMessage()}\n\n";
        $this-?writeLoop();
        exit;
      }
      // set today's date
      try {
        $this-?today = new dateTime('NOW', $this-?timezone);
      } catch(Exception $e) {
        $this-?content = date('Y-m-d H:i:s') . "\nDate Error: {$e-?getMessage()}\n\n";
        $this-?writeLoop();
        exit;
      }
      // set the invoice start date
      $tempDate = clone $this-?today;
      $tempDate-?modify('- 1 month');
      $this-?startDate = $tempDate-?format('Y-m-d');
      // set the invoice end date
      $tempDate = clone $this-?today;
      $tempDate-?modify('- 1 day');
      $this-?endDate = $tempDate-?format('Y-m-d');
      // set DateIssued for all invoices
      $this-?DateIssued = $this-?today-?format('Y-m-d');
      // define the base uri
      $this-?baseURI = 'https://rjtesting.ddns.net/v2/records/';
    }
    
    public function createInvoices() {
      // fetch all tickets that have not been billed this cycle
      $this-?fetchTickets();
      // fetch most recent invoice numbers and forwarded balances
      $this-?fetchLastInvoice();
      // process new invoices
      $this-?processInvoices();
      // submit invoices to the API
      $this-?submitInvoices();
      // submit ticket updates to API
      $this-?submitTickets();
      // log success
      if ($this-?logSuccess === true) {
        $this-?content = $this-?today-?format("d M Y H:i:s.u") . "\n" . count($this-?newInvoices) . " Invoices Created\n\n";
        $this-?writeLoop();
      }
      exit;
    }
    // start utility functions
    private function clearParameters() {
      $this-?headers = $this-?query = $this-?data = array();
      $this-?jsonData = '';
      $this-?payload = false;
    }

    private function test_int($val) {
      return (int)round($this-?test_float($val), 0, PHP_ROUND_HALF_EVEN);
    }

    private function test_float($val) {
      return (float)filter_var($val, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
    }
    // Search function for returning part of a string
    private function after($needle, $haystack) {
      if (!is_bool(strpos($haystack, $needle)))
      return substr($haystack, strpos($haystack,$needle)+strlen($needle));
    }

    private function before($needle, $haystack) {
      return substr($haystack, 0, strpos($haystack, $needle));
    }

    private function between($needle, $needle2, $haystack) {
      return $this-?before($needle2, $this-?after($needle, $haystack));
    }
    
    private function writeLoop() {
      $i = 0;
      do {
        $test = $this-?writeFile();
        $i++;
      } while ($test !== strlen($this-?content) && $i < $this-?fileWriteTry);
    }

    private function writeFile() {
      /*** http://php.net/manual/en/function.fwrite.php#81269 ***/
      /*** open the file for writing and truncate it to zero length ***/
      $fp = fopen( $this-?targetFile, 'ab' );
      /*** write the new file content ***/
      $bytes_to_write = strlen($this-?content);
      $bytes_written = 0;
      while ($bytes_written < $bytes_to_write) {
        if ($bytes_written == 0) {
          $rv = fwrite($fp, $this-?content);
        } else {
          $rv = fwrite($fp, substr($this-?content, $bytes_written));
        }
        if ($rv === FALSE || $rv == 0) {
          return($bytes_written == 0 ? FALSE : $bytes_written);
        }
        $bytes_written += $rv;
      }
      return $bytes_written;
    }

    private function testResponse() {
      // A simple test to make sure an appropriate response was received
      switch (strtoupper($this-?method)) {
        // POST: Expects the id of the last created resource
        // PUT, DELETE: Expects the number of rows affected
        // Expects an array of ids or rows affected when creating, updating, or deleting with array
        // Receives the string 'null' on failure
        case 'DELETE':
        case 'PUT':
        case 'POST': return (is_numeric($this-?result) || substr($this-?result, 0, 1) === '[');
        // Expects a json encoded object or array of objects
        case 'GET': return (substr($this-?result,0,1) === '{' || substr($this-?result,0,1) === '[');
    
        default: return FALSE;
      }
    }

    private function responseError() {
      switch ($this-?test_int($this-?result)) {
        case 400:
          $this-?error = "Server Error: Invalid Request URI.\n";
        break;
        case 401:
          $this-?error = "Server Error: Invalid login credentials.\n";
        break;
        case 403:
          $this-?error = "Server Error: Login credentials not defined.\n";
        break;
        case 404:
          $this-?error = "Server Error: Failed to locate record.\n";
        break;
        case 422:
          $this-?error = "Server Error: Failed Data Validation. {$this-?after('422', $this-?result)}\n";
        break;
        case 500:
          $this-?error = "Server Error: Internal Error.\n";
        break;
        case 503:
          $this-?error = "Server Error: Service temporarily unavailable.\n";
        default:
          $this-?error = "Server Error: {$this-?result}.\n";
        break;
      }
      $this-?writeLoop();
      exit;
    }

    private function buildURI() {
      if (!is_array($this-?data) || empty($this-?data)) {
        $this-?queryURI = $this-?baseURI . $this-?endPoint;
      } else {
        foreach ($this-?data as $key =? $value) {
          if ($key === 'resources') {
            $this-?query['include'] = implode(",",$this-?data['resources']);
          } elseif ($key === 'filter') {
            if (!isset($value[0][0])) {
              for ($i = 0; $i < count($value); $i++) {
                $this-?query['filter'][] = implode(',', array_values($value[$i]));
              }
            } else {
              for ($i = 0; $i < count($value); $i++) {
                $filter_index = $i + 1;
                for ($j = 0; $j < count($value[$i]); $j++) {
                  $this-?query["filter{$filter_index}"][] = implode(',', array_values($value[$i][$j]));
                }
              }
            }
          } else {
            $this-?query[$key] = implode(',',$value);
          }
        }
        $temp2 = http_build_query($this-?query,NULL,'&',PHP_QUERY_RFC3986);
        // http://php.net/manual/en/function.http-build-query.php#111819
        $temp2 = preg_replace('/%5B[0-9]+%5D/simU', '', $temp2);
        $this-?queryURI = "{$this-?baseURI}{$this-?endPoint}?{$temp2}";
      }
    }

    private function call() {
      if ($this-?primaryKey != NULL) {
        $this-?queryURI .= "/{$this-?primaryKey}";
      }
      // Use api key to generate security token
      $this-?timeVal = time();
      // Generate the security key using the REQUEST_URI
      $this-?token = hash_hmac('sha256', substr($this-?queryURI, strpos($this-?queryURI, '.com') + 4) . $this-?timeVal, $this-?privateKey);
      $this-?ch = curl_init();
      curl_setopt($this-?ch, CURLOPT_CUSTOMREQUEST, strtoupper($this-?method));
      curl_setopt($this-?ch, CURLOPT_URL, $this-?queryURI);
      curl_setopt($this-?ch, CURLOPT_FAILONERROR, TRUE);
      //CURLOPT_SSL_VERIFYPEER set to FALSE for testing only
      curl_setopt($this-?ch, CURLOPT_SSL_VERIFYPEER, TRUE);
      // CURLOPT_SSL_VERIFYHOST disabled for testing only
      curl_setopt($this-?ch, CURLOPT_SSL_VERIFYHOST, 2);
      /**
      ** How to create and store cacert.pem:
      ** http://unitstep.net/blog/2009/05/05/using-curl-in-php-to-access-https-ssltls-private-sites/
      ** gd_bundle-g2-g1.crt can be downloaded from the GoDaddy Repository:
      ** https://certs.godaddy.com/repository/
      ** if it is not the sole bundle it should be appended to the bundle in use
      **/
      curl_setopt($this-?ch, CURLOPT_CAINFO, __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem');
      // Set the authorization headers
      $this-?headers[] = "Authorization: Basic " . base64_encode("{$this-?username}:{$this-?publicKey}");
      $this-?headers[] = "auth: {$this-?token}";
      $this-?headers[] = "time: {$this-?timeVal}";
      if ($this-?payload !== FALSE) {
        $this-?jsonData = json_encode($this-?payload);
        curl_setopt($this-?ch, CURLOPT_POSTFIELDS, $this-?jsonData);
        $this-?headers[] = 'Content-Type: application/json';
        $this-?headers[] = 'Content-Length: ' . strlen($this-?jsonData);
      }
      curl_setopt($this-?ch, CURLOPT_HTTPHEADER, $this-?headers);
      curl_setopt($this-?ch, CURLOPT_RETURNTRANSFER, TRUE);
      //var_dump(curl_getinfo($this-?ch)); //return FALSE;
      $this-?result =  curl_exec($this-?ch);
      //var_dump(curl_getinfo($this-?ch)); //return FALSE;
      if (!$this-?result) {
        $this-?result = curl_error($this-?ch);
      }
      curl_close($this-?ch);
      if (!$this-?testResponse()) {
        $this-?responseError();
      }
    }
    // end utility functions
    private function fetchTickets() {
      $this-?clearParameters();
      $this-?endPoint = 'tickets';
      $this-?method = 'GET';
      $this-?data['resources'] = array('ticket_index', 'TicketPrice', 'Charge', 'RepeatClient', 'BillTo');
      // Create filter for repeat clients
      $repeatFilter = [ [ 'Resource'=?'ReceivedDate', 'Filter'=?'bt', 'Value'=?"{$this-?startDate} 00:00:00, {$this-?endDate} 11:59:59" ], [ 'Resource'=?'InvoiceNumber', 'Filter'=?'eq', 'Value'=?'-' ], [ 'Resource'=?'RepeatClient', 'Filter'=?'eq', 'Value'=?1 ] ];
      if (!empty($this-?ignoreClients)) $repeatFilter[] = [ 'Resource'=?'ClientID', 'Filter'=?'nin', 'Value'=?implode(',', $this-?ignoreClients) ];
      // Create filter for non-repeat clients
      $nonrepeatFilter = [ [ 'Resource'=?'ReceivedDate', 'Filter'=?'bt', 'Value'=?"{$this-?startDate} 00:00:00, {$this-?endDate} 11:59:59" ], [ 'Resource'=?'InvoiceNumber', 'Filter'=?'eq', 'Value'=?'-' ], [ 'Resource'=?'RepeatClient', 'Filter'=?'eq', 'Value'=?0 ] ];
      if (!empty($this-?ignoreNonRepeat)) $repeatFilter[] = [ 'Resource'=?'ClientID', 'Filter'=?'nin', 'Value'=?implode(',', $this-?ignoreNonRepeat) ];
      $this-?data['filter'] = [ $repeatFilter, $nonrepeatFilter ];
      $this-?buildURI();
      $this-?call();
      $temp = json_decode($this-?result);
      if (empty($temp-?records)) {
        $this-?content = "{$this-?today-?format('d M Y H:i:s.u')}\nNo Tickets To Process\n\n" . print_r($this-?data, true) . "\n" . print_r($this-?result, true) . "\n----\n";
        $this-?writeLoop();
        exit;
      }
      for ($i = 0; $i < count($temp-?records); $i++) {
        $key = ($temp-?records[$i]['RepeatClient'] === 1) ? $temp-?records[$i]['BillTo'] : "t{$temp-?records[$i]['BillTo']}";
        if (!array_key_exists($key, $this-?clientList)) $this-?clientList[$key] = [ 'tickets'=?[], 'lastInvoice'=?[], 'openInvoices'=?[] ];
        $this-?clientList[$key]['tickets'][] = $temp[$i];
      }
    }
    
    private function fetchLastInvoice() {
      // Grabbing all invoices in a single call then sorting them seems more efficient than trying to compose multiple queries to filter by Closed state, ClientID, and most recent DateIssued
      $this-?clearParameters();
      $this-?endPoint = 'invoices';
      $this-?method = 'GET';
      $this-?data['resources'] = array('InvoiceNumber', 'RepeatClient', 'BalanceForwarded', 'InvoiceSubTotal', 'DateIssued', 'Closed', 'Deleted');
      // Split repeat and non-repeat clientIDs into separate arrays
      $repeats = $nonrepeats = $repeatFilter = $nonrepeatFilter = [];
      foreach($this-?clientList as $key =? $value) {
        if (strpos($key,'t') === false) {
          $repeats[] = $key;
        } else {
          $nonrepeats[] = substr($key, 1);
        }
      }
      if (!empty($repeats)) {
        $repeatFilter = array(array('Resource'=?'ClientID', 'Filter'=?'in', 'Value'=?implode(',', $repeats)), array('Resource'=?'RepeatClient', 'Filter'=?'eq', 'Value'=?1));
      }
      if (!empty($nonrepeats)) {
        $nonrepeatFilter = array(array('Resource'=?'ClientID', 'Filter'=?'in', 'Value'=?implode(',', $nonrepeats)), array('Resource'=?'RepeatClient', 'Filter'=?'eq', 'Value'=?0));
      }
      if (!empty($repeatFilter) && !empty($nonrepeatFilter)) {
        $this-?data['filter'] = [ $repeatFilter, $nonrepeatFilter ];
      } else {
        $this-?data['filter'] = (empty($nonrepeatFilter)) ? $repeatFilter : $nonrepeatFilter;
      }
      $this-?buildURI();
      $this-?call();
      $temp = json_decode($this-?result);
      for ($i = 0; $i < count($temp-?records); $i++) {
        $tempID = substr($temp-?records[$i]['InvoiceNumber'], strpos($temp-?records[$i]['InvoiceNumber'],'-') + 1);
        if ($temp-?records[$i]['Closed'] === 0 && $temp-?records[$i]['Deleted'] === 0) {
            $this-?clientList[$tempID]['openInvoices'][] = $temp-?records[$i];
        }
        if (empty($this-?clientList[$tempID]['lastInvoice'])) {
          $this-?clientList[$tempID]['lastInvoice'] = $temp-?records[$i];
        } else {
          $this-?clientList[$tempID]['lastInvoice'] = ($this-?clientList[$tempID]['lastInvoice']['DateIssued'] ? $temp-?records[$i]['DateIssued']) ? $this-?clientList[$tempID]['lastInvoice'] : $temp-?records[$i];
        }
      }
    }
    
    private function processInvoices() {
      foreach ($this-?clientList as $key =? $value) {
        if (empty($value['tickets'])) continue;
        // create invoice object for submission
        $tempInvoice = new stdClass();
        $tempInvoice-?ClientID = $this-?test_int($key);
        $tempInvoice-?RepeatClient = (substr($key,0,1) === 't') ? 0 : 1;
        $tempInvoice-?StartDate = $this-?startDate;
        $tempInvoice-?EndDate = $this-?endDate;
        $tempInvoice-?DateIssued = $this-?DateIssued;
        // create new InvoiceNumber
        $invoicePointer = mt_rand(1000, 1100);
        if (!empty($value['lastInvoice'])) {
          $invoicePointer = (int)$this-?between('X', '-', $value['lastInvoice']['InvoiceNumber']) + 1;
        }
        $tempInvoice-?InvoiceNumber = "{$this-?today-?format('y')}EX{$invoicePointer}-{$key}";
        // solve the invoice subtotal and prep tickets to be update with new invoice number
        $tempInvoice-?InvoiceTotal = $tempInvoice-?InvoiceSubTotal = $this-?getTotal($value['tickets'], $tempInvoice-?InvoiceNumber);
        // solve the amount due for the current invoice
        $tempInvoice-?AmountDue = (!empty($value['lastInvoice'])) ? $tempInvoice-?InvoiceSubTotal - $value['lastInvoice']['BalanceForwarded'] : $tempInvoice-?InvoiceSubTotal;
        // solve the total due for all open invoices
        if (array_key_exists('openInvoices', $value) && !empty($value['openInvoices'])) {
          // Past due invoices need to be sortted by age and added to InvoiceTotal
          for ($i = 0; $i < count($value['openInvoices']); $i++) {
            try {
              $tempDate = new dateTime($value['openInvoices'][$i]['DateIssued'], $this-?timezone);
            } catch(Exception $e) {
              $this-?content = "{$today-?format("d M Y H:i:s.u")}\nDate Error Line " . __line__ . ": {$e-?getMessage()}\n\n";
              $this-?writeLoop();
              exit;
            }
            $plusOneMonth = clone $tempDate;
            $plusOneMonth-?modify('+ 1 month');
            $plusTwoMonth = clone $tempDate;
            $plusTwoMonth-?modify('+ 2 month');
            $plusThreeMonth = clone $tempDate;
            $plusThreeMonth-?modify('+ 3 month');
            $diff = $tempDate-?diff($this-?today);
            if ($diff-?days ? $tempDate-?format('t') && $diff-?days < ($tempDate-?format('t') + $plusOneMonth-?format('t'))) {
              $tempInvoice-?InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late30Invoice')) {
                $tempInvoice-?Late30Invoice .= (strpos($tempInvoice-?Late30Invoice, '+') === FALSE) ? '+' : '';
              } else {
                $tempInvoice-?Late30Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late30Value')) {
                $tempInvoice-?Late30Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice-?Late30Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($diff-?days ?= ($tempDate-?format('t') + $plusOneMonth-?format('t')) && $diff-?days < ($tempDate-?format('t') + $plusOneMonth-?format('t') + $plusTwoMonth-?format('t'))) {
              $tempInvoice-?InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late60Invoice')) {
                $tempInvoice-?Late60Invoice .= (strpos($tempInvoice-?Late60Invoice, '+') === FALSE) ? '+' : '';
              } else {
                $tempInvoice-?Late60Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late60Value')) {
                $tempInvoice-?Late60Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice-?Late60Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($diff-?days ?= ($tempDate-?format('t') + $plusOneMonth-?format('t') + $plusTwoMonth-?format('t')) && $diff-?days < ($tempDate-?format('t') + $plusOneMonth-?format('t') + $plusTwoMonth-?format('t') + $plusThreeMonth-?format('t'))) {
              $tempInvoice-?InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late90Invoice')) {
                $tempInvoice-?Late90Invoice .= (strpos($tempInvoice-?Late90Invoice, '+') === FALSE) ? '+' : '';
              } else {
                $tempInvoice-?Late90Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late90Value')) {
                $tempInvoice-?Late90Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice-?Late90Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($diff-?days ?= ($tempDate-?format('t') + $plusOneMonth-?format('t') + $plusTwoMonth-?format('t') + $plusThreeMonth-?format('t'))) {
              $tempInvoice-?InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              $this-?Over90InvoiceList[] = $value['openInvoices'][$i]['InvoiceNumber'];
              if (property_exists($tempInvoice, 'Over90Value')) {
                $tempInvoice-?Over90Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice-?Over90Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            }
            // Fix the over90Invioce to display a maximum of 4 invoice numbers or 3 invoice numbers and how many are not displayed, but only if there is at least one
            if (!empty($this-?Over90InvoiceList)) {
              if (count($this-?Over90InvoiceList) ? 4) {
                $j = count($this-?Over90InvoiceList) - 3;
                $appendment = ", + $j more";
                $tempInvoice-?Over90Invoice = implode(', ', array_slice($this-?Over90InvoiceList, 0, 3));
                $tempInvoice-?Over90Invoice .= $appendment;
              } else {
                $tempInvoice-?Over90Invoice = implode(', ',$this-?Over90InvoiceList);
              }
            }
          }
          // Clear this list for the next iteration
          $this-?Over90InvoiceList = [];
        }
        // add invoice object to array for submission
        $this-?newInvoices[] = $tempInvoice;
      }
    }
    
    private function getTotal($ticketArray, $invoiceNumber) {
      if (!is_array($ticketArray)) return 0;
      $total = 0;
      for ($i = 0; $i < count($ticketArray); $i++) {
        $total += ($ticketArray[$i]['Charge'] !== 0) ? $ticketArray[$i]['TicketPrice'] : 0;
        $this-?ticketUpdateKeys[] = $ticketArray[$i]['ticket_index'];
        $temp = new stdClass();
        $temp-?InvoiceNumber = $invoiceNumber;
        $this-?ticketUpdateValues[] = $temp;
      }
      return $total;
    }
    
    private function submitInvoices() {
      $this-?clearParameters();
      $this-?payload = $this-?newInvoices;
      $this-?endPoint = 'invoices';
      $this-?method = 'POST';
      $this-?buildURI();
      $this-?call();
    }
    
    private function submitTickets() {
      $this-?clearParameters();
      $this-?primaryKey = implode(',', $this-?ticketUpdateKeys);
      $this-?payload = $this-?ticketUpdateValues;
      $this-?endPoint = 'tickets';
      $this-?method = 'PUT';
      $this-?buildURI();
      $this-?call();
    }
  }