Delivery service connectors add logistics services with the ability to send packages, track delivery, print labels, and extend the order processing form in CRM.

Creating a connector

Working with connectors for delivery services is carried out in the "Delivery Services" section by clicking the "Files" button. The connector files themselves are located in the delivery folder of your hack. The file name can consist of small Latin letters and numbers. It must begin with a letter, not a number. You cannot use capital letters and special characters, including hyphens or underscores.

When a connector is created, the corresponding class is automatically created in the file. The class name consists of the word delivery, the name of the hack and the name of the connector, separated by an underscore, for example: delivery_dhl. The connector must inherit from the class deliverybase.

class delivery_dhl extends deliverybase { … }

A service class can implement the following fields and functions:

  • track() - parcel tracking, the basis of the work of delivery services.
  • calc() - calculation of the cost and delivery time.
  • send() - sending a parcel to the delivery service.
  • $cansend - sending parcels is supported.
  • $canauto - automatic sending of parcels is supported.
  • bulkprint() - bulk printing of labels.
  • $canbulk - bulk label printing is supported.
  • options() - available delivery service buttons.
  • actions() - implementation of actions for delivery service buttons.
  • block() - content block and additional form for delivery service.
  • ajax() - implementation of actions and data loading for the content block.
  • _courier( $id ) - returns the internal name of the courier by ID from the courier field for analytics.
  • _tariff( $id ) - returns the internal name of the tariff by ID from the tariff field for analytics.

There is no mandatory implementation, but at least a tracking implementation is recommended.

Connecting to the system

Connectors will not automatically appear in the list of delivery services, you need to specify them through the startup file. To do this, specify the delivery key in the initialization array, and the list of connectors available in your hack as the value. For example:

return [
  /* other initialization lines */
  'delivery' => [ 'cdek', 'dhl', 'ponyexpress' ],
];

Tracking

The main functionality of any delivery service is package tracking. The track function must take two parameters as input: a track code and an array with order data. As a result, it should return an associative array with the history of statuses, the current status of the package, and the time of the next check.

return [
  'next'   => time() + 43000, // Next check time, UNIX timestamp
  'status' => $status, // Current parcel status
  'time'   => $tm, // Status change time, UNIX timesramp
  'stage'  => $stage // Statuses array
];

The status field can take one of the following numeric values:

  1. wait — the package is waiting to be sent.
  2. transfer — the parcel is on the way, the status is "Delivery".
  3. problem — there was a problem with the delivery of the package, the advertiser's response is required.
  4. delivered — the package has arrived at its destination, the status is "Delivered".
  5. paid — the package has been successfully paid, the status is changed to "Paid".
  6. return — the parcel is returned or disposed of, it is transferred to the "Return" status.
  7. comment — arbitrary comment that does not change the state of the send.

The stage array should consist of parcel progress records. Records are recommended to be sorted from oldest to newest. Each entry can contain the following fields:

  • status - numeric status identifier.
  • time - the time the status occurred.
  • country - two-character ISO code of the country where the status originated.
  • zip - index or ZIP code of the post office where the status occurred.
  • city - the city where the status originated.
  • comment - arbitrary text comment to the status.
  • md5 - unique status hash used to check for duplicates.

Preparing an array of statuses and defining the final status and time might look like this:

$stage = [];
$tm = $status = 0;
foreach ( $info as $i ) {

  // Make the stage
  $s = [
    'status'  => $this->s2i[$i['status']],
    'time'    => $core->text->number( $i['time'] ),
    'country' => $core->text->link( $i['country'] ),
    'zip'     => $core->text->anum( $i['zip'] ),
    'city'    => $core->text->line( $i['city'] ),
    'comment' => $core->text->line( $i['comment'] ),
  ];

  // Add stage to the list
  $tm = $s['time'];
  $status = $s['status'];
  $s['md5'] = md5( $s['status'].$s['time'].$s['country'].$s['zip'].$s['city'].$s['comment'] );
  $stage[] = $s;

}

It is recommended that you familiarize yourself with the examples of the implementation of the mechanisms for checking the status of parcels that are available in the system. The code for integration modules with delivery services is open. The files are located in the core/delivery folder of your platform.

Cost and time calculator

The cost and time calculator allows you to substitute the price and cost of delivery in the package. The calc function receives an array with order data as input. As a result, it should call the calcs function with four parameters: delivery price, order data, minimum term, maximum term. Only the shipping price is a mandatory parameter.

An example of a simple function implementation:

public function calc( $o ) {
    $price = $this->somemagic( $o );
    return $this->calcs( $price, $o );
}

An example of the implementation of a function with delivery times:

public function calc( $o ) {
    $price = $this->_price( $o );
    $term = $this->_term( $o );
    return $this->calcs( $price, $o, $term['min'], $term['max'] );
}

Sending a parcel

Automatic sending of a parcel is performed through the send function, which takes an array of order data as input and should return true or false depending on success.

In the process of work, the function itself must edit the order and enter the track code and other necessary data into it. This is implemented by the $core->lead->edit() function, specifying the track code in the track parameter and activating tracking in the trackon parameter .

$core->lead->edit( $o['order_id'], [ 'track' => $code, 'trackon' => 1 ] );

An example implementation of the send function:

public function send( $o ) {
    $code = $this->somemagic( $o );
    if ( $code ) {
        $this->core->lead->edit( $o['order_id'], [ 'track' => $code, 'trackon' => 1 ] );
        return true;
    } else return false;
}

To activate automatic sending of parcels, specify at the beginning of the class declaration:

public $canauto = true;
public $cansend = true;

Print labels

Mass printing of labels is implemented through the bulkprint function, which takes an array of parcel track codes as input. In this case, the array key is the order ID in the system. The function must return an array of links or paths to files that store the finished labels to print. Automation itself will download the necessary print forms, create an archive from them and send it to the user.

An example of the implementation of the bulk print function:

public function bulkprint( $ids ) {
    $files = [];
    foreach ( $ids as $i ) $files[] = $this->_print( $i );
    return $files;
}

To enable mass printing of labels, specify at the beginning of the class declaration:

public $canprint = true;

Also, it would be good practice to create the print function, which takes an array of order data as input and returns a link to the file with the label. An example implementation of such a function:

public function print( $o ) {
  if ( $o['order_status'] > 9 ) return false;
  if ( $o['order_status'] < 6 ) return false;
  if ( ! $o['track_code'] ) return false;
  return $this->_print( $o['track_code'] );
}

Action buttons

Action buttons are added to the right side of the order form under the block with technical data (tags, geo, sources). With these buttons, you can call internal actions of your delivery service, such as sending a package, printing documents of the desired type, or canceling a package.

The options function is responsible for the list of actions, which takes one parameter as input - an order array. It should return an array that contains the description of the menu items. Each menu item is an array containing fields:

  • name - the title of the menu item, for example "Send package".
  • short - a short title that is displayed in the list of orders, for example "Submit".
  • icon is the name of the Font Awesome icon that is attached to the menu item, e.g. fa-paper-plane.
  • confirm - the text of the action confirmation pop-up window, for example "Are you sure you want to send this package?"
  • url - link to the action itself.

A link to an action should be formed with the following function:

$core->u([ 'order', $o['order_id'] ], 'action=dlaction&oa=send' )

Where send will be your action name. An example function might look like this:

public function options( $o ) {
  if ( $o['order_status'] > 9 ) return false;
  if ( $o['order_status'] < 6 ) return false;
  $core = $this->core;
  if ( $o['track_code'] ) {
    return [[
      'icon'    => 'fa-address-card',
      'url'     => $core->u([ 'order', $o['order_id'] ], 'action=dlaction&oa=print' ),
      'name'    => $core->lang['delivery_print'],
      'short'   => $core->lang['delivery_print_s'],
    ]];
  } elseif ( $this->config['api'] ) {
    return [[
      'icon'    => 'fa-paper-plane',
      'url'     => $core->u([ 'order', $o['order_id'] ], 'action=dlaction&oa=send' ),
      'name'    => $core->lang['delivery_send'],
      'short'   => $core->lang['delivery_send_s'],
      'confirm' => $core->lang['delivery_send_c'],
    ]];
  }
}

The action function is responsible for processing actions. It takes two parameters - the name of the action and the order array. The function should process the action and redirect the user to the order page with the action performed or an error.

An example of a function that implements the previous action array:

public function action( $action, $o ) {
  switch ( $action ) {

    case 'send':
    if ( $this->send( $o ) ) {
      $this->core->msgo( 'ok', $this->core->u([ 'order', $o['order_id'] ]) );
    } else $this->core->msgo( 'error', $this->core->u([ 'order', $o['order_id'] ]) );

    case 'print':
    if ( $url = $this->print( $o ) ) {
      $this->core->go( $url );
    } else $this->core->msgo( 'error', $this->core->u([ 'order', $o['order_id'] ]) );

    default: $this->core->msgo( 'error', $this->core->u([ 'order', $o['order_id'] ]) );

  }
}

To handle an error or success, use a smart redirect like this:

$this->core->msgo( 'error', $this->core->u([ 'order', $o['order_id'] ]) );

Instead of 'error', 'ok' can be given if successful.

Forms

The form is added to the order editing page in CRM. It is located below the list of products, above the main data of the order. In this form, you can implement the choice of couriers, delivery methods, pickup points, date and time of delivery, and any other internal parameters specific to delivery.

The block function is responsible for displaying the form, which takes one parameter as input - an array of order data. The function should return the generated code to insert into the page.

It is recommended to generate the form code using the built-in templating engine. To load a template, use the $core->hack->dhl->tpl( 'innerform', 'shpt-dhl' ) function, where dhl is replaced by the name your hack, and shpt-dhl is the name of your template. To generate a template, use $core->tpl->make( 'innerform' ).

An example of a function implementation:

public function block( $o ) {
  $core = $this->core;
  $tm = xxdec( $o['track_meta'] );
  $courier = isset( $tm['courier'] ) ? $tm['courier'] : -1;
  $core->hack->dhl->tpl( 'innerform', 'shpt-dhl' );
  $core->tpl->vars( 'innerform', [
    'u_check'  => $core->u( [ 'order', $o['order_id'] ], 'action=dlajax&oa=check' ),
    'couriers' => json_encode( $this->courier, JSON_PRETTY_PRINT ),
  ]);
  foreach ( $this->courier as $i => $n ) $core->tpl->block( 'innerform', 'courier', [ 'i' => $i, 'n' => $n, 's' => $i == $courier ]);
  return $core->tpl->make( 'innerform' );
}

AJAX Actions

AJAX actions can be used to retrieve data and perform tasks on inline forms. The ajax function is responsible for handling the actions, which takes the action code and the order array as input. The function must return an array of data, which will be passed in the response as a JSON object.

An example of a function implementation:

public function ajax( $action, $o ) {
  switch ( $action ) {
    case 'cities':
    $area = $this->core->post['area'];
    return $this->cities( $area );
  }
}

The link to the function itself must be formed using the code:

$core->u( [ 'order', $o['order_id'] ], 'action=dlajax&oa=cities' )

Where cities will be the code of the action to be performed.

You can save additional data inside the order that you use in forms and pass through AJAX actions. This data can store identifiers of tariffs, couriers, points of pickup and other internal delivery entities.

It is customary to store delivery data in the track_meta field of the order data array. The data is retrieved like this:

$tm = xxdec( $o['track_meta'] );

To save shipping metadata, use the following code:

$core->lead->edit( $o['order_id'], [ 'tmeta' => $tm ] );

Note that $tm must contain all fields at once. If you submit only part of the fields, other pre-existing fields will be removed.

Analytics by tariffs and couriers

The track_meta array uses two "magic" parameters that allow you to build delivery analytics in terms of domestic rates and couriers. To implement such statistics, add two fields:

  • tariff - identifier of the delivery service tariff.
  • courier - identifier of the delivery service courier.

It is recommended to use numeric identifiers. For the beauty of displaying statistics, you can add the functions _tariff and _courier, which take the identifier of the tariff or courier as input and return its beautiful name.

Setting function

To add additional fields to the settings, you need to implement two functions:

  • form - returns an array with additional form fields.
  • save - accepts raw data and returns an array with configuration fields.

The form function has no parameters. Configuration fields must be accessed via the $this->config array. The result of the function should be an array of form fields. Each field is an array that can contain the following fields:

  • type - field type: text, email, textarea, number , select, mselect, radio, checkbox.
  • name - field name, it is recommended to add a prefix with the module name
  • head - field heading, use language file for storage.
  • descr - description of the field, use the language file for storage.
  • value - current field value, use $this->config
  • options - a set of options for fields like select, mselect, radio.
  • checked - flag for a field of type checkbox.

Form implementation example:

public function form() {
  return [
    [ 'type' => 'head', 'value' => $this->core->lang['settings'] ],
    [ 'type' => 'text', 'name' => 'generic_url', 'head' => $this->core->lang['delivery_url_track'], 'value' => $this->config['url'] ],
    [ 'type' => 'text', 'name' => 'generic_doc', 'head' => $this->core->lang['delivery_url_doc'], 'value' => $this->config['doc'] ],
  ];
}

The save function receives the $data parameter as input and must return the processed parameter array as a key-value. Process text values with $this->esc(). Implementation example:

public function save( $data ) {
  return [
    'url' => str_replace( '&', '&', $this->esc( $data['generic_url'] ) ),
    'doc' => str_replace( '&', '&', $this->esc( $data['generic_doc'] ) ),
  ];
}