0% found this document useful (0 votes)
45 views108 pages

AcumaticaERP PluginDevelopmentGuide

The Plug-In Development Guide provides comprehensive instructions for expanding Acumatica ERP functionality through custom plug-ins and widgets. It covers the creation of widgets for dashboards, including widget parameters, graphs, and classes, as well as implementing connectors for e-commerce systems and processing credit card payments. The guide also includes steps for real-time synchronization and push notifications for commerce connectors.

Uploaded by

Charls Faces
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
45 views108 pages

AcumaticaERP PluginDevelopmentGuide

The Plug-In Development Guide provides comprehensive instructions for expanding Acumatica ERP functionality through custom plug-ins and widgets. It covers the creation of widgets for dashboards, including widget parameters, graphs, and classes, as well as implementing connectors for e-commerce systems and processing credit card payments. The guide also includes steps for real-time synchronization and push notifications for commerce connectors.

Uploaded by

Charls Faces
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Developer Guide

Plug-In Development Guide


2022 R1
Contents | 2

Contents
Copyright...............................................................................................................................................4
Plug-In Development Guide.....................................................................................................................5
Creating Widgets for Dashboards.............................................................................................................6
Widget Creation................................................................................................................................................. 6
Use of the Widgets.............................................................................................................................................7
To Create a Simple Widget................................................................................................................................ 9
To Create an Inquiry-Based Widget................................................................................................................ 11
To Load a Widget Synchronously or Asynchronously....................................................................................13
To Add a Script to a Widget.............................................................................................................................14
To Add Custom Controls to the Widget Properties Dialog Box..................................................................... 14
Customizing Business Events in Code..................................................................................................... 17
To Define a Custom Subscriber Type for Business Events............................................................................ 17
Implementing Plug-Ins for Processing Credit Card Payments................................................................... 23
Interfaces for Processing Credit Card Payments............................................................................................23
To Implement a Plug-In for Processing Credit Card Payments..................................................................... 24
Implementing a Connector for an E-Commerce System............................................................................28
Architecture of a Commerce Connector.........................................................................................................28
Classes for Acumatica ERP Entities....................................................................................................... 29
Classes for External Entities...................................................................................................................30
Mapping Classes..................................................................................................................................... 32
Bucket Classes........................................................................................................................................ 33
Processor Classes................................................................................................................................... 34
Connector Class...................................................................................................................................... 41
Processor Factory Class......................................................................................................................... 44
Connector Descriptor Class....................................................................................................................45
Connector Factory Class........................................................................................................................ 46
Use of a Commerce Connector....................................................................................................................... 47
Detection of the Connector in an Application...................................................................................... 47
Establishment of the Connection with the External System................................................................48
Preparation of Data for the Synchronization........................................................................................ 49
Synchronization of the Prepared Data.................................................................................................. 51
Real-Time Synchronization Through Push Notifications..................................................................... 55
To Create a Connector for an External System.............................................................................................. 56
Step 1: Creating an Extension Library for Acumatica ERP....................................................................56
Contents | 3

Step 2: Creating Classes for Acumatica ERP Entities............................................................................57


Step 3: Creating Classes for External Entities....................................................................................... 58
Step 4: Defining the Mappings Between Internal and External Entities.............................................. 65
Step 5: Creating the Buckets for the Mapped Entities..........................................................................66
Step 6: Creating a DAC with the Configuration Settings.......................................................................67
Step 7: Implementing a REST Client of the External System................................................................69
Step 8: Implementing the Processor Classes........................................................................................86
Step 9: Implementing the Processor Factory Class.............................................................................. 91
Step 10: Implementing the Connector Class........................................................................................ 92
Step 11: Implementing the Connector Descriptor Class...................................................................... 95
Step 12: Implementing the Connector Factory Class........................................................................... 96
Step 13: Creating the Configuration Form............................................................................................ 97
Step 14: Testing the Connector............................................................................................................103
To Implement Push Notifications for a Commerce Connector................................................................... 104
Step 1: Defining the Data for Real-Time Synchronization.................................................................. 104
Step 2: Supporting Push Notifications in the Connector................................................................... 106
Step 3: Testing Push Notifications....................................................................................................... 107
Copyright | 4

Copyright

© 2022 Acumatica, Inc.

ALL RIGHTS RESERVED.

No part of this document may be reproduced, copied, or transmitted without the express prior consent of
Acumatica, Inc.
3933 Lake Washington Blvd NE, # 350, Kirkland, WA 98033

Restricted Rights
The product is provided with restricted rights. Use, duplication, or disclosure by the United States Government is
subject to restrictions as set forth in the applicable License and Services Agreement and in subparagraph (c)(1)(ii)
of the Rights in Technical Data and Computer Soware clause at DFARS 252.227-7013 or subparagraphs (c)(1) and
(c)(2) of the Commercial Computer Soware-Restricted Rights at 48 CFR 52.227-19, as applicable.

Disclaimer
Acumatica, Inc. makes no representations or warranties with respect to the contents or use of this document, and
specifically disclaims any express or implied warranties of merchantability or fitness for any particular purpose.
Further, Acumatica, Inc. reserves the right to revise this document and make changes in its content at any time,
without obligation to notify any person or entity of such revisions or changes.

Trademarks
Acumatica is a registered trademark of Acumatica, Inc. HubSpot is a registered trademark of HubSpot, Inc.
Microso Exchange and Microso Exchange Server are registered trademarks of Microso Corporation. All other
product names and services herein are trademarks or service marks of their respective companies.

Soware Version: 2022 R1


Last Updated: 09/19/2022
Plug-In Development Guide | 5

Plug-In Development Guide


In this guide, you can find information about how to expand Acumatica ERP functionality by developing custom
plug-ins.
Creating Widgets for Dashboards | 6

Creating Widgets for Dashboards


In Acumatica ERP or an Acumatica Framework-based application, a dashboard is a collection of widgets that
are displayed on a single page to give you information at a glance. A widget is a small component that delivers a
particular type of information. Acumatica ERP and any Acumatica Framework-based application support a number
of predefined types of widgets that you can add to dashboards. For more information on the supported widgets
and their types, see Configuring Widgets.
If none of the predefined widget types suits your task, you can create custom widgets, as the topics in this
chapter describe, and use these widgets in the dashboards of Acumatica ERP or an Acumatica Framework-based
application.

Widget Creation

When you create a custom widget to be used in Acumatica ERP or an Acumatica Framework-based application, you
must implement at least the three classes described in the sections below. (For details on the implementation of
the classes, see To Create a Simple Widget and To Create an Inquiry-Based Widget.)

Data Access Class with the Widget Parameters


The data access class (DAC) with the widget parameters is a general DAC that implements the IBqlTable
interface.
You can use any DAC attributes with this DAC. However, only the fields with PXDB attributes, such as PXDBString
and PXDBInt, are stored in the database. The values of these fields are stored in the database automatically; you
do not need to create a database table for them.
The fields of the DAC are displayed as controls in the Widget Properties dialog box when a user adds a new
widget instance to a dashboard or modifies the parameters of an existing widget instance. (The way the controls
are displayed depends on PXFieldState of the corresponding fields.) For information on adding comprehensive
controls to the dialog box, see To Add Custom Controls to the Widget Properties Dialog Box.

Graph for the Widget


The graph for the widget is used to manage the widget parameters and read data for the widget. The graph must
be inherited from the [Link] class. For the widget graph, you can manage widget
parameters by defining new events and redefining the events that are available in the base classes, such as the
RowSelected and FieldSelecting events. (You work with the events of the widget graph in the same way as
you work with the events of a general graph.)

Class of the Widget


The widget class is used by the system to work with the widget instances. The widget class must implement the
[Link] interface. The system treats as widgets the classes that implement this
interface and are available in a library in the Bin folder of an instance of Acumatica ERP or Acumatica Framework-
based application. The caption and description of the widget from these classes are displayed in the Add Widget
dialog box automatically when a user adds a new widget to a dashboard. The methods of this class are used by the
system to manage the parameters of a widget instance and display the widget on a dashboard page.
Related Links
• To Create a Simple Widget
• To Create an Inquiry-Based Widget
Creating Widgets for Dashboards | 7

Use of the Widgets

In this topic, you will learn how a custom widget is used in Acumatica ERP or an Acumatica Framework-based
application. For more information on how to work with widgets on a dashboard page, see Configuring Widgets.

How the Widget Is Detected in an Application


Once the library that contains the class of the widget is placed in the Bin folder of an instance of Acumatica
ERP or an Acumatica Framework-based application, the Add Widget dialog box, which you open to add a new
widget to a dashboard page, contains the caption and description of the new widget. The system takes the caption
and description of the widget from the implementation of the Caption and Description properties of the
IDashboardWidget interface.
The following diagram illustrates the relations of the class of the widget and the Add Widget dialog box. In the
diagram, the items that you add for the custom widget are shown in rectangles with a red border.

Figure: Widget detection

How a Widget Instance Is Configured


When you select the widget in the Add Widget dialog box and click Next, the controls for the parameters that
should be configured for a new widget instance—that is, the predefined Caption box and the controls for the
parameters, which are defined in the custom DAC—are displayed in the Widget Properties dialog box. These
controls are maintained by using the graph for the widget, to which the reference is provided by the class of the
widget. The graph uses the Widget DAC and the custom DAC with the widget parameters to save the parameters of
a widget instance in the Widget table of the application database.
To display custom controls, such as buttons and pop-up panels, in the Widget Properties dialog box, the system
uses the implementations of the RenderSettings() and RenderSettingsComplete() methods of the
IDashboardWidget interface from the class of the widget. For more information on adding custom controls to
the dialog box, see To Add Custom Controls to the Widget Properties Dialog Box.
The following diagram illustrates the interaction of the components of the widget with the Widget Properties
dialog box. In the diagram, the items that you add for the custom widget are shown in rectangles with a red border.
Creating Widgets for Dashboards | 8

Figure: Configuration of a widget instance

How a Widget Instance Is Displayed on a Dashboard Page


Aer a widget instance is configured and saved in the Widget Properties dialog box, the widget instance is
displayed on the dashboard page. To display the caption of the widget instance on the dashboad page, the widget
graph retrieves the caption from the Widget table of the database by using the Widget DAC. To display the
widget contents, the system uses the implementations of the Render()and RenderComplete() methods of
the IDashboardWidget interface from the class of the widget. These implementations use the parameters of the
widget, which the widget graph retrieves from the Widget table of the application database by using the DAC with
the widget parameters.
You can specify how the widget should be loaded to a dashboard page and implement custom scripts to manage
the way the widget is displayed on a dashboard page. For more information, see To Load a Widget Synchronously or
Asynchronously and To Add a Script to a Widget.

The following diagram illustrates the interaction of the components of the widget with a dashboard page. In the
diagram, the items that you add for the custom widget are shown in rectangles with a red border.

Figure: Widget instance on a dashboard page

Related Links
• Designing Dashboard Contents
• Administering Dashboard Forms
• Configuring Widgets
Creating Widgets for Dashboards | 9

To Create a Simple Widget

A simple widget displays information from a single data source and does not require calculations or comprehensive
data retrieval. Multiple simple widgets, such as wiki widgets or embedded page widgets, are available in Acumatica
ERP or an Acumatica Framework-based application by default.
To create a simple widget, you need to perform the basic steps that are described in this topic.

To Create a Simple Widget


1. In the project of your Acumatica ERP extension library or your Acumatica Framework-based application,
create a data access class (DAC), which stores the parameters of the widget. The DAC must implement the
IBqlTable interface; you can use any DAC attributes with this DAC.
The following code fragment shows an example of a DAC for a simple widget, which uses one parameter.

using [Link];

[PXHidden]
public class YFSettings : IBqlTable
{
#region PagePath
[PXDBString]
[PXDefault]
[PXUIField(DisplayName = "Help Article")]
public string PagePath { get; set; }
public abstract class pagePath : IBqlField { }
#endregion
}

2. In the project, create a graph for working with the parameters of the widget and reading the data for the
widget. The graph must be inherited from the [Link] class.
The following code fragment shows an example of a graph for a widget.

using [Link];

public class YFSettingsMaint : WizardMaint<YFSettingsMaint, YFSettings>


{
}

3. In the project, create a widget class that implements the [Link] interface.
Use the following instructions for implementation:
• Inherit the widget class from the [Link] abstract class. This
class implements part of the required functionality of the IDashboardWidget interface, such as
localization of the caption and the description of the widget (which are displayed in the Add Widget
dialog box when a user adds a new widget to a dashboard). This class also stores useful properties of the
widget, such as Settings, DataGraph, Page, DataSource, and WidgetID.
• Store the values of the caption and the description of the widget in a Messages class that has the
PXLocalizable attribute. This approach is required for localization functionality to work properly.
• Perform initialization of a widget class instance in the Initialize() method of the
IDashboardWidget interface.
• Create the tree of controls of the widget in the Render() method of the IDashboardWidget
interface.
Creating Widgets for Dashboards | 10

• If you need to check the access rights of a user to the data displayed in the widget, implement the
IsAccessible() method of the IDashboardWidget interface. If the user has access to the data
in the widget, the method must return true; if the user has insufficient rights to access the data in the
widget, the method must return false.
• If you want to specify the way the widget is loaded, override the AsyncLoading() method of the
PXWidgetBase abstract class, as described in To Load a Widget Synchronously or Asynchronously.
The following code fragment gives an example of a widget class. This class is inherited from the
PXWidgetBase class. The caption and description of the widget are specified in the Messages class,
which has PXLocalizable attribute. The widget class implements the Render() method to create the
control of the widget and performs the configuration of the control in the RenderComplete() method.

using [Link];
using [Link];
using [Link];
using [Link];

public class YFWidget: PXWidgetBase<YFSettingsMaint, YFSettings>


{
public YFWidget()
: base([Link], [Link])
{
}

protected override WebControl Render(PXDataSource ds, int height,


bool designMode)
{
if ([Link]([Link])) return null;

WebControl frame = _frame = new WebControl([Link])


{ CssClass = "iframe" };
[Link]["frameborder"] = "0";
[Link] = [Link] = [Link](100);
[Link]["src"] = "javascript:void 0";
return frame;
}

public override void RenderComplete()


{
if (_frame != null)
{
var renderer = [Link]([Link]);
[Link]([Link](),
[Link]([Link]),
[Link]("[Link]('{0}').src = '{1}';",
_frame.ClientID, [Link]), true);
}
[Link]();
}

private WebControl _frame;


}

[PXLocalizable]
public static class Messages
{
public const string YFWidgetCaption = "YogiFon Help Page";
public const string YFWidgetDescription = "Displays a Help page.";
Creating Widgets for Dashboards | 11

4. Compile your Acumatica ERP extension library or Acumatica Framework-based application.


5. Run the application and make sure that the new widget appears in the Add Widget dialog box. The widget
class, which implements the IDashboardWidget interface, is detected by the system and automatically
added to the list of widgets available for selection in the dialog box.

Related Links
• To Load a Widget Synchronously or Asynchronously

To Create an Inquiry-Based Widget

An inquiry widget retrieves data for the widget from an inquiry form, such as a generic inquiry form or a custom
inquiry form. Inquiry-based widgets that are available in Acumatica ERP or an Acumatica Framework-based
application by default include chart widgets, table widgets, and KPI widgets.
To create an inquiry-based widget, you need to perform the basic steps that are described in the section below. In
these steps, you use the predefined classes, which provide the following functionality to simplify the development
of an inquiry-based widget:
• Selection of the inquiry form in the widget settings
• Selection of a shared inquiry filter
• Selection of the parameters of the inquiry
• The ability to drill down in the inquiry form when a user clicks on the widget caption
• Verification of the user's access rights to the widget, which is performed based on the user's access rights to
the inquiry form

To Create an Inquiry-Based Widget


1. In the project of your Acumatica ERP extension library or your Acumatica Framework-based
application, create a data access class (DAC) that implements the IBqlTable interface
and stores the parameters of the widget. We recommend that you inherit the DAC from the
[Link] class, which provides the following
parameters for the widget:
• InquiryScreenID: Specifies the inquiry form on which the widget is based
• FilterID: Specifies one of the shared filters available for the specified inquiry form
The following code shows a fragment of the DAC for the predefined inquiry-based data table widget. The
DAC is inherited from the InquiryBasedWidgetSettings class.

using [Link];
using [Link];

[PXHidden]
public class TableSettings : InquiryBasedWidgetSettings, IBqlTable
{
#region AutoHeight
[PXDBBool]
[PXDefault(true)]
[PXUIField(DisplayName = "Automatically Adjust Height")]
public bool AutoHeight { get; set; }
public abstract class autoHeight : IBqlField { }
#endregion
Creating Widgets for Dashboards | 12

...
}

2. In the project, create a graph for working with widget parameters and reading data for the widget. Use the
following instructions when you implement the graph:
• Inherit the graph from the [Link] abstract
class, which is inherited from the PXWidgetBase abstract class.
• Implement the SettingsRowSelected() event, which is the RowSelected event for the DAC with
widget parameters; it contains the current values of the parameters of the widget instance and the list of
available fields of the inquiry form. (For information on how to work with the fields of the inquiry form,
see To Use the Parameters and Fields of the Inquiry Form in the Widget.) The signature of the method is
shown below.
protected virtual void SettingsRowSelected(PXCache cache,
TPrimary settings, InqField[] inqFields)

3. In the project, create a widget class. We recommend that you inherit this class from the
[Link] class.
4. Compile your Acumatica ERP extension library or Acumatica Framework-based application.
5. Run the application, and make sure that the new widget appears in the Add Widget dialog box. The widget
class, which implements the IDashboardWidget interface, is detected by the system and automatically
added to the list of widgets available for selection in the dialog box.

To Use the Parameters and Fields of the Inquiry Form in the Widget
You can access the parameters and fields of the inquiry from that is used in the widget by using the
DataScreenBase class, which is available through the DataScreen property in the widget graph and in the
widget class. An instance of the DataScreenBase class, which is created based on the inquiry form selected by a
user in the widget settings, contains the following properties:
• ViewName: Specifies the name of the data view from which the data for the widget is taken.
• View: Returns the data view from which the data for the widget is taken.
• ParametersViewName: Specifies the name of the data view with the parameters of the inquiry.
• ParametersView: Returns the data view with the parameters of the inquiry. It can be null if the inquiry
has no parameters.
• ScreenID: Specifies the ID of the inquiry form.
• DefaultAction: Specifies the action that is performed when a user double-clicks on the row in the
details table of the inquiry form.
To access the fields of the inquiry form in the widget, use the GetFields() method of the DataScreenBase
class. This method returns the InqField class, which provides the following properties:
• Name: Specifies the internal name of the field
• DisplayName: Specifies the name of the field as it is displayed in the UI
• FieldType: Specifies the C# type of the field
• Visible: Specifies whether the field is visible in the UI
• Enabled: Specifies whether the field is enabled in the UI
• LinkCommand: Specifies the linked command of the field
To access the parameters of the inquiry form in the widget, use the GetParameters() method of the
DataScreenBase class, which returns the InqField class.
Creating Widgets for Dashboards | 13

To Load a Widget Synchronously or Asynchronously

By default, if a widget class is inherited from the [Link] abstract class,


the widget is loaded asynchronously aer the page has been loaded. You can change this behavior by using the
AsyncLoading() method of the widget class, as described in the following sections.

To Load a Widget Synchronously


To load a widget synchronously along with the page, override the AsyncLoading() method, as the following
code shows.

using [Link];
using [Link];

public override AsyncLoadMode AsyncLoading


{
get { return [Link]; }
}

To Load a Widget Asynchronously


To load a widget asynchronously aer the page has been loaded, you do not need to perform any actions, because
the following implementation of the AsyncLoading() method is used by default.

using [Link];
using [Link];

public override AsyncLoadMode AsyncLoading


{
get { return [Link]; }
}

To Load a Widget that Performs a Long-Running Operation


If a widget performs a long-running operation during loading, such as reading data that takes a long time, use the
following approach to load this widget:
1. Override the AsyncLoading() method, as the following code shows. In this case, for processing the data
of the widget, the system starts the long-running operation in a separate thread.

using [Link];
using [Link];

public override AsyncLoadMode AsyncLoading


{
get { return [Link]; }
}

2. Override the ProcessData() method of the widget class so that it implements all the logic for the widget
that consumes significant time while loading.
3. Override the SetProcessResult() method of the widget class so that it retrieves the result of the
processing of the widget data.
Creating Widgets for Dashboards | 14

If these methods are implemented, the system calls them automatically when it loads the widget on a dashboard
page.

To Add a Script to a Widget

You can specify how a widget should be displayed on a dashboard page by using a custom script. For example, you
can implement a script that handles the way the widget is resized.
To add a custom script to a widget, you override the RegisterScriptModules() method of the
[Link] abstract class. The following code shows an example of the
method implementation for a predefined data table widget.

using [Link];
using [Link];

internal const string JSResource =


"[Link].px_dashboardGrid.js";

public override void RegisterScriptModules(Page page)


{
var renderer = [Link](page);
[Link]([Link](), JSResource);
[Link](page);
}

To Add Custom Controls to the Widget Properties Dialog Box

The Widget Properties dialog box is displayed when a user creates or edits a widget. If you need to add
custom controls, such as buttons or grids, to this dialog box, you need to create these controls in the
RenderSettings() or RenderSettingsComplete() method of the widget class, as is described in the
sections below.

To Add Buttons to the Widget Properties Dialog Box Dynamically


If you need to add buttons to the Widget Properties dialog box that appear based on a particular user action in the
dialog box, override the RenderSettings() method of the widget class so that it dynamically adds the needed
controls to the dialog box. The method must return true if all controls are created in the method implementation
(that is, no automatic generation of controls is required). The default implementation of the RenderSettings()
method of the PXWidgetBase class returns false.
The following example provides the implementation of this method in the predefined Power BI tile widget. When a
user clicks the Sign In button in the Widget Properties dialog box and successfully logs in to Microso Azure Active
Directory, the Dashboard and Tile boxes appear in the dialog box.

public override bool RenderSettings(PXDataSource ds, WebControl owner)


{
var cc = [Link];
var btn = new PXButton() { ID = "btnAzureLogin", Text =
[Link]([Link], typeof(Messages).FullName),
Width = [Link](100) };
[Link] = "[Link]";

[Link](new PXLayoutRule() { StartColumn = true, ControlSize = "XM",


Creating Widgets for Dashboards | 15

LabelsWidth = "SM" });


[Link](new PXTextEdit() { DataField = "ClientID", CommitChanges = true });
[Link](new PXLayoutRule() { Merge = true });
[Link](new PXTextEdit() { DataField = "ClientSecret",
CommitChanges = true });
[Link](btn);
[Link](new PXLayoutRule() { });
[Link](new PXDropDown() { DataField = "DashboardID",
CommitChanges = true });
[Link](new PXDropDown() { DataField = "TileID", CommitChanges = true });
[Link](new PXTextEdit() { DataField = "AccessCode",
CommitChanges = true });
[Link](new PXTextEdit() { DataField = "RedirectUri" });
[Link](new PXTextEdit() { DataField = "AccessToken" });
[Link](new PXTextEdit() { DataField = "RefreshToken" });

foreach (Control wc in cc)


{
IFieldEditor fe = wc as IFieldEditor;
if (fe != null) [Link] = [Link];
[Link]([Link]);

PXTextEdit te = wc as PXTextEdit;
if (te != null) switch ([Link])
{
case "ClientID":
[Link] =
"[Link]";
break;
case "ClientSecret":
[Link] =
"[Link]";
break;
case "AccessCode":
[Link] =
"[Link]";
break;
case "RedirectUri":
[Link] =
"[Link]";
break;
}
}
return true;
}

To Open a Pop-Up Panel in the Widget Properties Dialog Box


If you need to open a pop-up panel in the Widget Properties dialog box, override the
RenderSettingsComplete() method of the widget class and create the panel within it.
The following code shows a sample implementation of the method in the predefined chart widget. The method
adds the buttons to the dialog box and creates the pop-up panel aer the standard controls of the dialog box have
been created.

public override void RenderSettingsComplete(PXDataSource ds, WebControl owner)


{
Creating Widgets for Dashboards | 16

var btn = _btnConfig = new PXButton() {


ID = "btnConfig", Width = [Link](150),
Text = [Link]([Link],
typeof(Messages).FullName),
PopupPanel = "pnlConfig", Enabled = false, CallbackUpdatable = true };
[Link](btn);
[Link]([Link]);

[Link](CreateSettingsPanel(ds, [Link]));
([Link] as ChartSettingsMaint).InquiryIDChanged += (s, e) =>
_btnConfig.Enabled = ![Link](e);
[Link](ds, owner);
}
Customizing Business Events in Code | 17

Customizing Business Events in Code


In Acumatica ERP, various business processes are executed, which may require the monitoring of particular
activities and conditions in the system. To eliminate the need for a user to monitor these business processes,
you can configure the system to monitor the company data and to perform an action (such as sending an email
notification or performing the instructions defined by an import scenario) or multiple actions in the system.
To configure the system to monitor a business process, on the Business Events (SM302050) form, you define a
business event that relates to this business process and that causes the system to perform an action or multiple
actions in the system. To define the actions that the system should perform in the system once the business event
has occurred, you specify the subscribers of this business event on the Subscribers tab of the Business Events form.
You can define custom subscribers of business events in addition to the subscribers available in the system, as
described in this chapter. For more information on business events, see Business Events: General Information.

To Define a Custom Subscriber Type for Business Events

To define the actions that the system should perform once a business event has occurred, you specify the
subscribers of this business event on the Subscribers tab of the Business Events (SM302050) form. A subscriber
of a business event is an entity that the system processes when the business event occurs. The subscriber types
available in the system include import scenarios, email notifications, mobile push notifications, and mobile SMS
notifications. You can define a custom subscriber type for business events, as described in this topic.

A custom subscriber type can be implemented in a project of your Acumatica ERP extension library.
You cannot include the custom subscriber type in a Code item of a customization project.

For more information on business events, see Business Events: General Information.

Creation of a Custom Subscriber Type for Business Events


1. Define a class that implements the
[Link] interface, which is a
subscriber that the system executes once the business event has occurred.
2. In the class that implements the IEventAction interface, implement the following methods and
properties of the interface:
• The Id property, which is the GUID that identifies the subscriber. For the predefined subscriber types,
the system assigns the value of this property to a new subscriber created by a user. The property uses the
following syntax.
Guid Id { get; set; }

• The Name property, which is the name of the subscriber of the custom type. For the predefined
subscriber types, a user specifies the value of this property on the form that corresponds to the
subscriber. For example, for email notifications, the user specifies the value of this property in the
Description box on the Notification Templates (SM204003) form. Use the following syntax for the property.
string Name { get; }

• The Process method, which implements the actions that the system should perform once the business
event has occurred. For example, for email notifications, the method inserts values in the notification
template and sends the notification. The method uses the following syntax.
void Process(MatchedRow[] eventRows, CancellationToken cancellation);
Customizing Business Events in Code | 18

3. Define a class that implements the


[Link]
interface, which creates and executes the subscriber.
4. In the class that implements the IBPSubscriberActionHandlerFactory interface, implement the
following methods and properties of the interface:
• The CreateActionHandler method, which creates a subscriber with the specified ID. Use the
following syntax for the method.
IEventAction CreateActionHandler(Guid handlerId, bool stopOnError,
IEventDefinitionsProvider eventDefinitionsProvider);

• The GetHandlers method, which retrieves the list of subscribers of the custom type. This list is
displayed in the lookup dialog box in the Subscriber ID column on the Subscribers tab of the Business
Events (SM302050) form. The method uses the following syntax.

IEnumerable<BPHandler> GetHandlers(PXGraph graph);

• The RedirectToHandler method, which performs redirection to the subscriber. For example, for
email notifications, the method opens the Notification Templates form, which displays the subscriber
(which is a notification template) with the specified ID. Use the following syntax for the method.
void RedirectToHandler(Guid? handlerId);

• The Type property, which is a string identifier of the subscriber type that is exactly four characters
long. The value of this property is stored in the database. The property uses the following syntax.
string Type { get; }

• The TypeDescription property, which is a string label of the subscriber type. A user views this
value in the Type column on the Subscribers tab of the Business Events form. Use the following syntax
for the property.
string TypeDescription { get; }

If you want an action to be displayed in the Create Subscriber menu on the toolbar of
the Subscribers tab of the Business Events (SM302050) form, instead of implementing
the IBPSubscriberActionHandlerFactory interface, implement the
IBPSubscriberActionHandlerFactoryWithCreateAction interface, which also
provides methods and properties that define the creation action.

5. Compile your Acumatica ERP extension library with the implementation of the classes.
6. Open Acumatica ERP and test the new subscriber type.

Example
The following code shows an example of the implementation of a custom subscriber type. This custom subscriber
writes the body of the notification to a text file.

using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
Customizing Business Events in Code | 19

using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];

namespace CustomSubscriber
{
//The custom subscriber that the system executes once the business event
//has occurred
public class CustomEventAction : IEventAction
{
//The GUID that identifies a subscriber
public Guid Id { get; set; }

//The name of the subscriber of the custom type


public string Name { get; protected set; }

//The notification template


private readonly Notification _notificationTemplate;

//The method that writes the body of the notification to a text file
//once the business event has occurred
public void Process(MatchedRow[] eventRows,
CancellationToken cancellation)
{
using (StreamWriter file =
new StreamWriter(@"C:\tmp\[Link]"))
{
var graph = [Link](
_notificationTemplate.ScreenID);
var parameters = @[Link](
r => [Link]<IDictionary<string, object>,
IDictionary<string, object>>(
[Link]?.ToDictionary(c => [Link], c => [Link]),
[Link]?.ToDictionary(c => [Link],
c => ([Link] as ValueWithInternal)?.ExternalValue ??
[Link]))).ToArray();
var body = [Link](
_notificationTemplate.Body, parameters, graph, null);
[Link](body);
}
}

//The CustomEventAction constructor


public CustomEventAction(Guid id, Notification notification)
{
Id = id;
Name = [Link];
_notificationTemplate = notification;
}
}

//The class that creates and executes the custom subscriber


class CustomSubscriberHandlerFactory :
Customizing Business Events in Code | 20

IBPSubscriberActionHandlerFactoryWithCreateAction
{
//The method that creates a subscriber with the specified ID
public IEventAction CreateActionHandler(Guid handlerId,
bool stopOnError, IEventDefinitionsProvider eventDefinitionsProvider)
{
var graph = [Link]<PXGraph>();
Notification notification = PXSelect<Notification,
Where<[Link],
Equal<Required<[Link]>>>>
.Select(graph, handlerId).AsEnumerable().SingleOrDefault();

return new CustomEventAction(handlerId, notification);


}

//The method that retrieves the list of subscribers of the custom type
public IEnumerable<BPHandler> GetHandlers(PXGraph graph)
{
return PXSelect<Notification, Where<[Link],
Equal<Current<[Link]>>,
Or<Current<[Link]>, IsNull>>>
.Select(graph).[Link](c => c != null)
.Select(c => new BPHandler { Id = [Link], Name = [Link],
Type = [Link] });
}

//The method that performs redirection to the subscriber


public void RedirectToHandler(Guid? handlerId)
{
var notificationMaint =
[Link]<SMNotificationMaint>();
[Link] =
[Link].
Search<[Link]>(handlerId);
[Link](notificationMaint,
[Link]);
}

//A string identifier of the subscriber type that is


//exactly four characters long
public string Type
{
get { return "CTTP"; }
}

//A string label of the subscriber type


public string TypeName
{
get { return [Link]; }
}

//A string identifier of the action that creates


//a subscriber of the custom type
public string CreateActionName
{
get { return "NewCustomNotification"; }
}
Customizing Business Events in Code | 21

//A string label of the button that creates


//a subscriber of the custom type
public string CreateActionLabel
{
get { return [Link]; }
}

//The delegate for the action that creates


//a subscriber of the custom type
public Tuple<PXButtonDelegate, PXEventSubscriberAttribute[]>
getCreateActionDelegate(BusinessProcessEventMaint maintGraph)
{
PXButtonDelegate handler = (PXAdapter adapter) =>
{
if ([Link]?.Current?.ScreenID == null)
return [Link]();

var graph = [Link]<SMNotificationMaint>();


var cache = [Link]<Notification>();
var notification = (Notification)[Link]();
var row = [Link](notification);
[Link] = [Link];
[Link](row);

var subscriber = new BPEventSubscriber();


var subscriberRow =
[Link](subscriber);
[Link] = Type;
[Link] = [Link];
[Link][typeof(BPEventSubscriber)].Insert(subscriberRow);

[Link](graph,
[Link]);
return [Link]();
};
return [Link](handler,
new PXEventSubscriberAttribute[]
{new PXButtonAttribute {
OnClosingPopup = [Link]}});
}
}

//Localizable messages
[PXLocalizable]
public static class LocalizableMessages
{
public const string CustomNotification = "Custom Notification";
public const string CreateCustomNotification = "Custom Notification";
}
}

To test this example, do the following:


1. Add this code to an Acumatica ERP extension library. For details on creating the library, see To Create an
Extension Library.
2. Make sure that the following references are added to the project of the extension library:
Customizing Business Events in Code | 22

• [Link]
• [Link]
• [Link]
• [Link]
3. Create a business event on the Business Events (SM302050) form. For details on the creation of a business
event, see To Monitor Changes of the Business Process Data and To Monitor the Business Process Data by a
Schedule.
4. On the Subscribers tab, make sure Custom Notification is available in the Type column.
5. Select Custom Notification in the Type column. Make sure the list in the Subscriber ID column is empty.
6. On the table toolbar, make sure Create Subscriber > Custom Notification is available.
7. Click Create Subscriber > Custom Notification. Make sure the Notification Templates (SM204003) form
opens.
8. On the Notification Templates form, create a notification template with a message that is not empty on the
Messages tab. For details about notification templates, see Notification Templates.
9. Make sure the created notification template is added to the Subscribers tab of the Business Events form as
the Custom Notification subscriber and save the business event.
[Link] changes in the system to trigger the business event that you have configured.
[Link] sure the text file (in this example, C:\tmp\[Link]) contains the body of the notification.

Related Links
• Business Events: General Information
Implementing Plug-Ins for Processing Credit Card Payments | 23

Implementing Plug-Ins for Processing Credit Card


Payments
Acumatica ERP processes credit card and debit card payments through the [Link] payment gateway. To
work with the [Link] payment gateway, Acumatica ERP supports the [Link] API payment plug-in. For
more information on this plug-in, see Means of Integration with [Link].
In a customized Acumatica ERP application, you can implement payment plug-ins that work with credit card
payment processing centers other than [Link]. In this chapter, you can find information about how to
develop custom plug-ins and use them in Acumatica ERP.

Interfaces for Processing Credit Card Payments

Acumatica ERP provides the interfaces for the implementation of plug-ins for credit card payment processing.
By using these interfaces, you can implement tokenized processing plug-ins. When a tokenized processing plug-
in is used, the credit card information is not saved to the application database; this information is stored only at
the processing center. The Acumatica ERP database stores only the identification tokens that link customers and
payment methods in the application with the credit card data at the processing center.
For details on how to implement custom plug-ins, see To Implement a Plug-In for Processing Credit Card Payments.

In Acumatica ERP, the [Link] API ([Link]) plug-in


implements the interfaces described in the sections below. This plug-in works with the [Link]
processing center. For more information about the built-in plug-in, see Integration with [Link]
Through the API Plug-in.

Mandatory Interfaces
The root interface for implementation of custom plug-ins for credit card processing is
[Link]. The system automatically discovers
the class that implements the ICCProcessingPlugin interface in the Bin folder of the Acumatica ERP instance
and includes it in the list in the Payment Plug-In (Type) box on the Processing Centers (CA205000) form. For
creation of a custom plug-in, you also need to implement the ICCTransactionProcessor interface to process
credit card transactions.

Additional Interfaces
You can implement the following additional functionality:
• Tokenized credit card processing: Implement the ICCProfileProcessor and
ICCHostedFormProcessor interfaces.
• Processing of payments from new credit cards: To use this functionality, implement
the ICCProfileCreator, ICCHostedPaymentFormProcessor,
ICCHostedPaymentFormResponseParser, and ICCTransactionGetter interfaces. If this
functionality is implemented in a custom plug-in, a user can select the Accept Payment from New Card
check box on the Processing Centers form for the processing centers that use this custom plug-in.
• Synchronization of credit cards with the processing center: Implement the ICCTransactionGetter
interface in a custom plug-in. If this functionality is implemented, on the Synchronize Cards (CA206000)
form, users can work with the processing centers that use this custom plug-in.
Implementing Plug-Ins for Processing Credit Card Payments | 24

• Retrieval of the information about suspicious credit card transactions (without the use of the hosted form
that accepts payments): To use this functionality, implement the ICCTranStatusGetter interface.
• Webhooks as a way to obtain a response from the processing center: Implement the
ICCWebhookProcessor and ICCWebhookResolver interfaces.

Related Links
• To Implement a Plug-In for Processing Credit Card Payments

To Implement a Plug-In for Processing Credit Card Payments

In Acumatica ERP, the [Link] API built-in plug-in processes transactions in [Link]. For more
information on this plug-in, see Integration with [Link] Through the API Plug-in. You can implement your own
plug-in for working with processing centers other than [Link], as described in this topic.

To Implement a Credit Card Processing Plug-In


1. In a class library project, define a class that implements the
[Link] interface, which
provides credit card processing functionality. In the class, implement the DoTransaction method, which
processes the transaction in the payment gateway. For details on the implementation of the method, see
ICCTransactionProcessor Interface in the API Reference.
The following code shows the declaration of a class that implements the ICCTransactionProcessor
interface.

using [Link].V2;

public class MyTransactionProcessor : ICCTransactionProcessor


{
public ProcessingResult DoTransaction(ProcessingInput inputData)
{
...
}
}

2. If you need to implement tokenized processing, define the following classes:


• A class that implements the [Link]
interface (which provides methods to manage customer and payment profiles)
• A class that implements the
[Link] interface (which
provides methods to work with hosted forms for the creation and management of payment profiles)
In the classes, implement the methods of the interfaces. For details on the implementation of the interfaces,
see ICCProfileProcessor Interface and ICCHostedFormProcessor Interface in the API Reference.
3. If you need to implement payment processing without the preliminary creation of payment profiles, define
the following classes:
• A class that implements the [Link]
interface (which provides the method to create the payment profile for a new credit card)
• A class that implements the
[Link] interface
(which provides methods to work with hosted forms for processing payments without preliminary
creation of payment profiles)
Implementing Plug-Ins for Processing Credit Card Payments | 25

• A class that implements the


[Link]
interface (which provides a method to parse the response aer a successful operation on a hosted form
that processes payments without preliminary creation of payment profiles)
• A class that implements the
[Link] interface (which provides
the methods to obtain information about transactions by transaction ID)
In the classes, implement the methods of the interfaces. For details on the implementation
of the interfaces, see ICCProfileCreator Interface, ICCHostedPaymentFormProcessor Interface,
ICCHostedPaymentFormResponseParser Interface, and ICCTransactionGetter Interface in the API Reference.
4. If you need to implement the retrieval of detailed information about transactions, which can be used
for the synchronization of transactions with the processing center, define a class that implements the
[Link] interface (which provides the
methods to obtain information about transactions by transaction ID).
In the class, implement the methods of the interface. For details on the implementation of the interface, see
ICCTransactionGetter Interface in the API Reference.
5. If you need to retrieve the information about suspicious credit card transactions with the Held for Review
status (without the use of the hosted form that accepts payments), define a class that implements
the supplementary [Link]
interface (which provides the method to obtain the transaction status aer the execution of the
[Link] method).
In the class, implement the methods of the interface. For details on the implementation of the interface, see
ICCTranStatusGetter Interface in the API Reference.
6. If you need to support webhooks as a way to obtain a response from the processing center, define the
following classes:
• A class that implements the [Link]
interface (which provides the methods to add, update, and delete webhooks and retrieve the list of
webhooks)
• A class that implements the [Link]
interface (which parses the information that comes from the processing center through webhooks)
In the classes, implement the methods of the interfaces. For details on the implementation of the interfaces,
see ICCWebhookProcessor Interface and ICCWebhookResolver Interface in the API Reference.
7. Define a class that implements the
[Link] interface, which is the root
interface for credit card payment processing and is used by Acumatica ERP to find the plug-in in the
application libraries. The class should have a public parameterless constructor (either explicit or default).
In the class, implement the methods of the ICCProcessingPlugin interface (described in detail in
ICCProcessingPlugin Interface in the API Reference) as follows:
• In the ExportSettings method, which exports the required settings from the plug-in to the
Processing Centers (CA205000) form, return a collection that contains the settings that can be configured
by the user on the form. The syntax of the method is shown in the following code.
IEnumerable<SettingsDetail> ExportSettings();

• In the ValidateSettings method, validate the settings modified on the Processing Centers form,
which are passed as the parameter of the method, and return null if validation was successful or an
error message if validation failed. The syntax of the method is shown in the following code.
string ValidateSettings(SettingsValue setting);

• In the TestCredentials method, check the connection to the payment gateway by using the
credentials that are specified by the user on the Processing Centers form. The syntax of the method is
shown in the following code.
Implementing Plug-Ins for Processing Credit Card Payments | 26

void TestCredentials(IEnumerable<SettingsValue> settingValues);

• In the CreateProcessor<T> method, return a new object of the T type, and initialize the object with
the settings passed to the method. The T type can be any of the following:
• ICCTransactionProcessor
• ICCProfileProcessor
• ICCHostedFormProcessor
• ICCProfileCreator
• ICCHostedPaymentFormProcessor
• ICCTransactionGetter
• ICCTranStatusGetter
• ICCWebhookResolver
• ICCWebhookProcessor
If a particular T type is not supported by your plug-in, return null for this type. The following code
shows a sample implementation of the method.
public T CreateProcessor<T>(IEnumerable<SettingsValue> settingValues)
where T : class
{
if (typeof(T) == typeof(ICCProfileProcessor))
{
return new MyProfileProcessor(settingValues) as T;
}
if (typeof(T) == typeof(ICCHostedFormProcessor))
{
return new MyHostedFormProcessor(settingValues) as T;
}
if (typeof(T) == typeof(ICCHostedPaymentFormProcessor))
{
return new MyHostedPaymentFormProcessor(settingValues) as T;
}
if (typeof(T) == typeof(ICCHostedPaymentFormResponseParser))
{
return new MyHostedPaymentFormResponseParser(settingValues) as T;
}
if (typeof(T) == typeof(ICCTransactionProcessor))
{
return new MyTransactionProcessor(settingValues) as T;
}
if (typeof(T) == typeof(ICCTransactionGetter))
{
return new MyTransactionGetter(settingValues) as T;
}
if (typeof(T) == typeof(ICCProfileCreator))
{
return new MyProfileCreator(settingValues) as T;
}
if (typeof(T) == typeof(ICCWebhookResolver))
{
return new MyWebhookResolver() as T;
}
if (typeof(T) == typeof(ICCWebhookProcessor))
{
return new MyWebhookProcessor(settingValues) as T;
Implementing Plug-Ins for Processing Credit Card Payments | 27

}
if (typeof(T) == typeof(ICCTranStatusGetter))
{
return new MyTranStatusGetter() as T;
}
return null;
}

8. Build your project.


9. To add your plug-in to Acumatica ERP, include the assembly in the customization project. (When the
customization project is published, the assembly is copied to the Bin folder of the Acumatica ERP
website automatically.) During startup, the system automatically discovers the class that implements the
ICCProcessingPlugin interface and includes it in the list in the Payment Plug-In (Type) box on the
Processing Centers form.

Related Links
• Interfaces for Processing Credit Card Payments
• [Link].V2 Namespace
Implementing a Connector for an E-Commerce System | 28

Implementing a Connector for an E-Commerce System


Acumatica ERP provides built-in integrations with the BigCommerce and Shopify e-commerce systems. For details
about these integrations, see Integration with BigCommerce and Integration with Shopify. The integrations use
Acumatica Commerce Framework to implement the connectors between Acumatica ERP and the e-commerce
systems.
By using Acumatica Commerce Framework, you can implement a custom connector for any e-commerce system. In
this chapter, you can find information about how to develop a custom connector and use it in Acumatica ERP.

Architecture of a Commerce Connector

A connector between Acumatica ERP and an external e-commerce system is a plug-in that synchronizes particular
entities in Acumatica ERP with the corresponding entities in the external system. This synchronization can be
performed based on a schedule or by a request received with a push notification.

Parts of the Connector


The connector consists of the following major parts, which are shown in the diagram below:
• Classes for Acumatica ERP entities, which are adapters for the entities of the contract-based REST API of
Acumatica ERP. For more information about these classes, see Classes for Acumatica ERP Entities.
• Classes for external entities, which are adapters for the entities of the REST API of the external e-commerce
system. For details about these classes, see Classes for External Entities.
• Mapping classes, which define the mappings between internal and external entities. Mapping classes are
described in Mapping Classes.
• Bucket classes, which define buckets of entities whose synchronization depends on one another. For more
information, see Bucket Classes.
• Processor classes, which implement the synchronization of the entities between the external system and
Acumatica ERP. Processor classes are described in greater detail in Processor Classes.
• A processor factory class, which creates processor classes for all entities used by the connector. The
processor factory class is described in more detail in Processor Factory Class.
• A connector class, which connects other classes of the plug-in to the commerce forms of Acumatica ERP
and defines the settings for these forms. For details about the implementation of the connector class, see
Connector Class.
• A connector factory class, which initializes the connector. For details about the connector factory class, see
Connector Factory Class.
• A connector descriptor class, which provides supplementary methods for the work of the connector. For
more information, see Connector Descriptor Class.
• A configuration form, where the basic settings of the connector are specified. For example, for the built-in
BigCommerce connector, the configuration form is BigCommerce Stores (BC201000).
Implementing a Connector for an E-Commerce System | 29

Figure: Commerce connector architecture

Classes for Acumatica ERP Entities

Classes for Acumatica ERP entities are adapters for the entities of the contract-based REST API of
Acumatica ERP. To implement these classes, you have two options: Define the classes on your own, or
use the classes defined for the default BigCommerce and Shopify integrations, which are available in the
[Link] namespace. For example, if you need to work with customers in Acumatica ERP, you
can use the predefined [Link] class. If the entity is not implemented in the
[Link] namespace, you implement a custom class for this entity.

Base Class and Interface


The classes for Acumatica ERP entities derive from the [Link] base
class, which implements the [Link] interface. The CBAPIEntity class
includes the set of standard properties of a contract-based API entity, such as Id, RowNumber, Delete, and
ReturnBehavior. This class also defines a set of properties that are necessary for the synchronization of the
entity with an entity from an external e-commerce system, such as SyncID or SyncTime.

Properties of Each Custom Class


In each custom class, you define only the properties of the contract-based API entity that you need to use,
including the key fields of the entity. The name of the class and its properties should be exactly the names of the
corresponding contract-based API entity and its properties in the eCommerce/20.200.001 endpoint.

If the API defined in the eCommerce/20.200.001 endpoint is not sufficient for the implementation of
your connector, you can create a custom endpoint and use it for your connector. For details about
the creation of a custom endpoint, see To Create a Custom Endpoint in the Integration Development
Guide. To use the custom endpoint for your connector, you need to replace the PXDefault attribute
of the [Link] and [Link] fields by
using the corresponding CacheAttached event handlers in the graph of the configuration form. For
details about the replacement, see Replacement of Attributes for DAC Fields in CacheAttached.
Implementing a Connector for an E-Commerce System | 30

You assign the [Link] attributes with the names of the entity and its
properties to the class and its properties, respectively. These names are used as the names of the Acumatica ERP
objects and fields on the mapping and filtering tabs of the Entities (BC202000) form.

Example
An example of the implementation of a class derived from the CBAPIEntity is shown in the following code.

using [Link];
using [Link];
using [Link];

[CommerceDescription("Case")]
public partial class Case : CBAPIEntity
{
public GuidValue NoteID { get; set; }

public DateTimeValue LastModifiedDateTime { get; set; }

[CommerceDescription("CaseCD")]
public StringValue CaseCD { get; set; }

[CommerceDescription("Subject")]
public StringValue Subject { get; set; }

[CommerceDescription("Description")]
public StringValue Description { get; set; }
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Classes for External Entities

Classes for external entities are adapters for the entities of the REST API of the external e-commerce system.

Base Class and Interface


You define a class that derives from the [Link] base class, which implements the
[Link] interface. In this class, you define the properties of the external REST
API entity that you need to synchronize with the properties of the entity stored in Acumatica ERP.

Attributes of the Class and its Properties


You assign the [Link] attribute with the name of the entity to the
class. Entity names are used as the names of the external objects on the Entities (BC202000) form.
You assign the CommerceDescription attributes to the properties of the class as well. In each attribute, you
specify the following parameters:
• The name of the property, which is used as the name of the external field on the mapping and filtering tabs
of the Entities form.
Implementing a Connector for an E-Commerce System | 31

• Optional: A FieldFilterStatus value, which specifies whether the field is used on the filtering tabs of
the Entities form.
• Optional: A FieldMappingStatus value, which specifies whether the field is used on the mapping tabs
of the Entities form.
• Optional: A String value, which specifies the feature on which the field depends, such as
"[Link]+userDefinedOrderTypes". The field is available on the Entities
form if the specified feature is enabled on the Enable/Disable Features (CS100000) form.
You also assign the [Link] attribute to the properties of the class to map them to
the properties of the external entity retrieved in JSON format through the external REST API.

The classes and the communication with the external system depend on the external system.

Example
The following code shows an example of the declaration of a customer entity for the WooCommerce connector.

using System;
using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
[CommerceDescription([Link])]
public class CustomerData : BCAPIEntity, IWooEntity
{
[JsonProperty("id")]
[CommerceDescription([Link], [Link],
[Link])]
public int? Id { get; set; }

[JsonProperty("date_created")]
public DateTime? DateCreatedUT { get; set; }

[CommerceDescription([Link])]
[ShouldNotSerialize]
public virtual DateTime? CreatedDateTime
{
get
{
return DateCreatedUT != null ? (DateTime)[Link]() : default;
}
}

[JsonProperty("date_modified_gmt")]
public DateTime? DateModified { get; set; }

[CommerceDescription([Link])]
[ShouldNotSerialize]
public virtual DateTime? ModifiedDateTime
{
get
{
return DateModified != null ? (DateTime)[Link]() : default;
}
Implementing a Connector for an E-Commerce System | 32

[JsonProperty("email")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired]
public string Email { get; set; }

[JsonProperty("first_name")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired]
public string FirstName { get; set; }

[JsonProperty("last_name")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired()]
public string LastName { get; set; }

[JsonProperty("username")]
[CommerceDescription([Link], [Link],
[Link])]
public string Username { get; set; }

[JsonProperty("billing")]
public CustomerAddressData Billing { get; set; }

[JsonProperty("shipping")]
public CustomerAddressData Shipping { get; set; }
}

public interface IWooEntity


{
DateTime? DateCreatedUT { get; set; }

DateTime? DateModified { get; set; }

}
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Mapping Classes

For each pair of an internal entity and an external entity that should be synchronized, you implement a mapping
class, which defines the correspondence between the properties of the internal and external entities.

Base Class and Interfaces


You derive a mapping class from the [Link]<ExternType, LocalType>
base class, which implements the following interfaces:
Implementing a Connector for an E-Commerce System | 33

• [Link]: Represents the mapping between the properties of the local


and external objects
• [Link]<ExternType>: Provides methods for the addition of
an external entity to the mapping
• [Link]<LocalType>: Provides a method for the addition of an
internal entity to the mapping
The MappedEntity<ExternType, LocalType> class provides the default implementation of these
interfaces. The class also provides the default constructors from which you can derive the constructors of the
mapping classes. These constructors use as input parameters a string identifier of the mapped entities, which is
exactly two characters long, and a string identifier of the connector, which is defined in the ConnectorType
property of the connector class.

Example
An example of the implementation of a mapping class is shown in the following code.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class MappedCustomer : MappedEntity<CustomerData, Customer>
{
public const string TYPE = [Link];

public MappedCustomer()
: base([Link], TYPE)
{ }
public MappedCustomer(Customer entity, Guid? id, DateTime? timestamp)
: base([Link], TYPE, entity, id, timestamp) { }
public MappedCustomer(CustomerData entity, string id, DateTime? timestamp)
: base([Link], TYPE, entity, id, timestamp) { }
}
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Bucket Classes

For the synchronization of data between an external system and Acumatica ERP, for each entity that you need to
synchronize, you need to define a bucket class that defines the entities to be synchronized before and aer the
synchronization of this entity.

Base Class and Interface


You define this bucket in the bucket class, which derives from the [Link]
interface and, optionally, the [Link] base class. The
EntityBucketBase class is a supplementary class that provides the default implementation of the
PreProcessors and PostProcessors properties of the IEntityBucket interface.
Implementing a Connector for an E-Commerce System | 34

In the bucket class, you need to define the entity that is being synchronized in the Primary property of the bucket
class. In the Entities property of the bucket class, you specify all entities that are being synchronized in this
bucket. The PreProcessors property contains the list of entities that should be synchronized before the entity
is, while the PostProcessors property specifies the list of entities that should be synchronized aer the entity
is.

Example
Suppose that aer the synchronization of address entities between an external system and Acumatica ERP, you
need to synchronize customer entities. The following code shows an example of the bucket class implementation
for the address and customer entities.

public class BCLocationEntityBucket : EntityBucketBase, IEntityBucket


{
public IMappedEntity Primary => Address;
public IMappedEntity[] Entities =>
new IMappedEntity[] { Address, Customer };

public override IMappedEntity[] PostProcessors =>


new IMappedEntity[] { Customer };

public MappedLocation Address;


public MappedCustomer Customer;
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Processor Classes

For each pair of entities that you want to synchronize between an e-commerce system and Acumatica ERP, you
need to create a processor class.
A processor classes perform the following functions:
• Retrieval of the Acumatica ERP records and external records
• Providing of the default mapping logic
• Providing of the export and import logic

Base Classes and Interface


A processor class implements the [Link] interface and derives from one of the
following base abstract classes:
• [Link]<TGraph, TEntityBucket,
TPrimaryMapped>, which is a standard base class for processors that synchronizes records one by one.
• [Link]<TGraph, TEntityBucket,
TPrimaryMapped>, which can mass-process records for the entities that require higher performance
during synchronization. For example, this base class is used for the processor classes for the availability and
price list entities in the BigCommerce connector.
Implementing a Connector for an E-Commerce System | 35

Both of these classes are graphs. Therefore, the processor class is a graph as well. In the processor base class, you
pass as the type parameters of the class the graph itself, the bucket class, and the mapping class for the pair of
entities that you want to synchronize.

Attributes of the Processor Class


You assign the following attributes to the processor class:
• BCProcessor, which specifies the basic functions of the processor, such as which directions of
synchronization are supported by the processor
• Optional: BCProcessorRealtime, which specifies whether the processor works with push notifications
and webhooks

Methods of the Processor Class


In the processor class, you implement the methods that perform the following functions:
• The fetching of the records that have been changed in Acumatica ERP or the external system
• The checking and merging of duplicated records
• The handling of real-time processing
• Other supplementary functions
For details about the methods, see the API Reference.

Example
The following code shows an example of the processor implementation.

using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
[BCProcessor(typeof(WooCommerceConnector), [Link],
[Link],
IsInternal = false,
Direction = [Link],
PrimaryDirection = [Link],
PrimarySystem = [Link],
PrimaryGraph = typeof([Link]),
ExternTypes = new Type[] { typeof(CustomerData) },
LocalTypes = new Type[] { typeof([Link]) },
AcumaticaPrimaryType = typeof([Link]),
AcumaticaPrimarySelect = typeof([Link]),
URL = "[Link]?user_id={0}"
)]
[BCProcessorRealtime(PushSupported = false, HookSupported = false)]
public class WooCustomerProcessor : BCProcessorSingleBase<WooCustomerProcessor,
WooCustomerEntityBucket, MappedCustomer>, IProcessor
{
public WooRestClient client;
Implementing a Connector for an E-Commerce System | 36

protected CustomerDataProvider customerDataProvider;

//protected List<Country> countries;


public CommerceHelper helper = [Link]<CommerceHelper>();

#region Constructor
public override void Initialise(IConnector iconnector,
ConnectorOperation operation)
{
[Link](iconnector, operation);
client = [Link](
GetBindingExt<BCBindingWooCommerce>());
customerDataProvider = new CustomerDataProvider(client);
}
#endregion

#region Import
public override void MapBucketImport(WooCustomerEntityBucket bucket,
IMappedEntity existing)
{
MappedCustomer customerObj = [Link];

[Link] customerImpl = [Link] =


new [Link]();
[Link] = GetCustomFieldsForImport();

[Link] = GetBillingCustomerName([Link]).
ValueField();
[Link] = [Link]([Link],
GetBinding().BindingName).ValueField();
[Link] = [Link] == null ||
existing?.Local == null ?
GetBindingExt<BCBindingExt>().CustomerClassID?.ValueField() : null;

//Primary Contact
[Link] = GetPrimaryContact([Link]);

[Link] = SetBillingContact([Link]);

[Link] = [Link]();
[Link] = [Link]();
[Link] = SetShippingContact([Link]);

BCBindingExt bindingExt = GetBindingExt<BCBindingExt>();


if ([Link] != null)
{
CustomerClass customerClass = PXSelect<CustomerClass,
Where<[Link],
Equal<Required<[Link]>>>>.
Select(this, [Link]);
if (customerClass != null)
{
[Link]();

}
}
Implementing a Connector for an E-Commerce System | 37

public virtual Contact GetPrimaryContact(CustomerData customer)


{
var contact = new Contact();
[Link] = ![Link]([Link]) &&
[Link]([Link]) ?
[Link]() : [Link]();
[Link] = ![Link]([Link]) ?
[Link]() :
![Link]([Link]) &&
[Link]([Link]) ?
[Link]() : [Link]();
[Link] = [Link]();

return contact;
}

public virtual Contact SetBillingContact(CustomerData customerObj)


{
Contact contactImpl = new Contact();
[Link] = GetBillingCustomerName(customerObj).
ValueField();
[Link] = $"{[Link]?.FirstName} {
[Link]?.LastName}".ValueField(); ;
[Link] = GetBillingCustomerName(customerObj).ValueField();
[Link] = [Link]?.[Link]();
[Link] = [Link]();
[Link] = [Link]();
[Link] = MapAddress([Link]);
[Link] = [Link]();
contactImpl.Phone1 = [Link]?.[Link]();
contactImpl.Phone1Type = [Link]();

return contactImpl;
}

public virtual Contact SetShippingContact(CustomerData customerObj)


{
Contact contactImpl = new Contact();
[Link] = GetShippingCustomerName(customerObj).
ValueField();
[Link] = $"{[Link]?.FirstName} {
[Link]?.LastName}".ValueField();
[Link] = GetShippingCustomerName(customerObj).ValueField();
[Link] = [Link]?.[Link]();
[Link] = [Link]();
[Link] = MapAddress([Link]);
[Link] = [Link]();

return contactImpl;
}

public virtual string GetBillingCustomerName(CustomerData data)


{
return ![Link]([Link])
Implementing a Connector for an E-Commerce System | 38

? [Link]
: [Link]($"{[Link]?.FirstName} {
[Link]?.LastName}") ? [Link] : $"{
[Link]?.FirstName} {[Link]?.LastName}";
}

public virtual string GetShippingCustomerName(CustomerData data)


{
return ![Link]([Link]?.Company)
? [Link]
: $"{[Link]?.FirstName} {[Link]?.LastName}";
}

public virtual Address MapAddress(CustomerAddressData addressObj)


{
var address = new Address();
address.AddressLine1 = [Link]();
address.AddressLine2 = [Link]();
[Link] = [Link]();
[Link] = [Link]();
if (![Link]([Link]))
{
var selectedCountry = [Link];
if (selectedCountry != null)
{
[Link] = [Link]();
if (![Link]([Link]))
{
[Link] = [Link]();
}
}
}

return address;
}

public override IEnumerable<MappedCustomer> PullSimilar(IExternEntity entity,


out string uniqueField)
{
uniqueField = [Link](((CustomerData)entity)?.Billing.
Email) ? ((CustomerData)entity)?.Email : ((CustomerData)entity)?.
[Link];
if (uniqueField == null) return null;

List<MappedCustomer> result = new List<MappedCustomer>();


foreach ([Link] item in [Link](
uniqueField))
{
[Link] data = new [Link].
Customer() { SyncID = [Link], SyncTime =
[Link] };
[Link](new MappedCustomer(data, [Link], [Link]));
}

if (result == null || result?.Count == 0) return null;

return result;
Implementing a Connector for an E-Commerce System | 39

public override IEnumerable<MappedCustomer> PullSimilar(ILocalEntity entity,


out string uniqueField)
{
uniqueField = (([Link])entity)?.MainContact?.
Email?.Value;
if (uniqueField == null) return null;
IEnumerable<CustomerData> datas = [Link](null);
if (datas == null) return null;

return [Link](data => new MappedCustomer(data, [Link](),


[Link]()));
}

public override void SaveBucketImport(WooCustomerEntityBucket bucket,


IMappedEntity existing, string operation)
{
MappedCustomer obj = [Link];

[Link] impl = [Link]([Link], [Link]);

[Link](impl, [Link], [Link]);


UpdateStatus(obj, operation);
}
#endregion

#region Export

public override void MapBucketExport(WooCustomerEntityBucket bucket,


IMappedEntity existing)
{
}

public override void SaveBucketExport(WooCustomerEntityBucket bucket,


IMappedEntity existing, string operation)
{
}
#endregion

#region Pull
public override MappedCustomer PullEntity(Guid? localID,
Dictionary<string, object> externalInfo)
{
throw new NotImplementedException();
}
public override MappedCustomer PullEntity(string externID,
string externalInfo)
{
throw new NotImplementedException();
}

public override void FetchBucketsForImport(DateTime? minDateTime,


DateTime? maxDateTime, PXFilterRow[] filters)
{
IEnumerable<CustomerData> customers = [Link](null);
Implementing a Connector for an E-Commerce System | 40

foreach (CustomerData data in customers)


{
WooCustomerEntityBucket bucket = CreateBucket();

MappedCustomer mapped = [Link] = [Link](


data, [Link](), [Link]());
EnsureStatus(mapped, [Link]);
}
}

public override void FetchBucketsForExport(DateTime? minDateTime,


DateTime? maxDateTime, PXFilterRow[] filters)
{
throw new NotImplementedException();
}

public override EntityStatus GetBucketForImport(WooCustomerEntityBucket bucket,


BCSyncStatus syncstatus)
{
CustomerData data = [Link]<CustomerData>(
[Link]("/customers/{0}", [Link]));

MappedCustomer obj = [Link] = [Link](data,


[Link](), [Link]());
EntityStatus status = EnsureStatus(obj, [Link]);

return status;
}

public override EntityStatus GetBucketForExport(WooCustomerEntityBucket bucket,


BCSyncStatus bcstatus)
{
[Link] impl =
[Link]<[Link]>([Link],
GetCustomFieldsForExport());
if (impl == null) return [Link];

[Link] = [Link](impl, [Link], [Link]);


EntityStatus status = EnsureStatus([Link], [Link]);

return status;
}
#endregion
}
public static class PhoneTypes
{
public static string Business1 = "B1";
}
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System
Implementing a Connector for an E-Commerce System | 41

Connector Class

The connector class is the main class of a connector for an e-commerce system.
The connector class performs the following functions:
• Provides the settings for connection with the external system
• Implements navigation to external records
• Performs the synchronization of records of the external system and Acumatica ERP
• Implements real-time subscription and processing

Base Class and Interface


The connector class implements the [Link] interface and derives
from the [Link]<TConnector> base abstract class. The
BCConnectorBase<TConnector> class is a graph. Therefore, the connector class is a graph as well.

Properties and Methods of the Connector Class


In the ConnectorType property of the connector class, you define a string identifier of the connector type;
this identifier can be no more than three characters long. In the ConnectorName property of this class, you
define the name of the connector, which is displayed on Acumatica ERP forms. For example, the value is displayed
in the Connector box on the Entities (BC202000) form.
For details about the methods of the connector class, see the API Reference.

Example
The following code shows an example of the connector class implementation.

using [Link];
using System;
using [Link];
using [Link];
using [Link];
using CommonServiceLocator;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceConnector: BCConnectorBase<WooCommerceConnector>,
IConnector
{
public const string TYPE = "WOO";
public const string NAME = "WooCommerce";

public class WCConnectorType : [Link]<WCConnectorType>


{
public WCConnectorType() : base(TYPE) { }
Implementing a Connector for an E-Commerce System | 42

public override string ConnectorType { get => TYPE; }


public override string ConnectorName { get => NAME; }

public virtual IEnumerable<TInfo> GetExternalInfo<TInfo>(string infoType,


int? bindingID)
where TInfo : class
{
if ([Link](infoType) || bindingID == null) return null;
BCBindingWooCommerce binding = [Link](this,
bindingID);
if (binding == null) return null;

try
{
List<TInfo> result = new List<TInfo>();
return result;
}
catch (Exception ex)
{
LogError(new BCLogTypeScope(typeof(WooCommerceConnector)), ex);
}

return null;
}

public void NavigateExtern(ISyncStatus status)


{
if (status?.ExternID == null) return;

EntityInfo info = GetEntities().FirstOrDefault(e => [Link] ==


[Link]);
BCBindingWooCommerce bCBindingBigCommerce =
[Link](this, [Link]);

if ([Link](bCBindingBigCommerce?.StoreAdminUrl) ||
[Link]([Link])) return;

string[] parts = [Link](new char[] { ';' });


string url = [Link]([Link], [Link] > 2 ?
[Link](2).ToArray() : parts);
string redirectUrl = [Link]('/') +
"/" + url;

throw new PXRedirectToUrlException(redirectUrl,


[Link], [Link]);
}

//Create an instance of the processor graph that corresponds to the entity


//and run its Process method.
public virtual SyncInfo[] Process(ConnectorOperation operation,
int?[] syncIDs = null)
{
LogInfo([Link](), [Link], NAME);

EntityInfo info = GetEntities().FirstOrDefault(e =>


Implementing a Connector for an E-Commerce System | 43

[Link] == [Link]);
using (IProcessor graph = (IProcessor)CreateInstance([Link]))
{
[Link](this, operation);
return [Link](syncIDs);
}
}

public DateTime GetSyncTime(ConnectorOperation operation)


{
BCBindingWooCommerce binding = [Link](this,
[Link]);
//Acumatica Time
[Link](out DateTime dtLocal, out DateTime dtUtc);
dtLocal = [Link](dtUtc,
[Link]());

return dtLocal;
}

public override void StartWebHook(string baseUrl, BCWebHook hook)


{
throw new NotImplementedException();
}

public virtual void ProcessHook(IEnumerable<BCExternQueueMessage> messages)


{
throw new NotImplementedException();
}

public override void StopWebHook(string baseUrl, BCWebHook hook)


{
throw new NotImplementedException();
}

public static WooRestClient GetRestClient(BCBindingWooCommerce binding)


{
return GetRestClient([Link], [Link],
[Link]);
}

public static WooRestClient GetRestClient(String url, String clientID,


String token)
{
RestOptions options = new RestOptions
{
BaseUri = url,
XAuthClient = clientID,
XAuthTocken = token
};
JsonSerializer serializer = new JsonSerializer
{
MissingMemberHandling = [Link],
NullValueHandling = [Link],
DefaultValueHandling = [Link],
DateFormatHandling = [Link],
Implementing a Connector for an E-Commerce System | 44

DateTimeZoneHandling = [Link],
ContractResolver = new GetOnlyContractResolver()
};
RestJsonSerializer restSerializer = new RestJsonSerializer(serializer);
WooRestClient client = new WooRestClient(restSerializer, restSerializer,
options,
[Link]<[Link]>());

return client;
}
}
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Processor Factory Class

A processor factory class creates processor classes for all entities used by the specified connector.

Base Interface
The processor factory class implements the [Link] base interface. You
implement one factory class for all processor classes.
In the processor factory class, you specify the type of the connector in the ConnectorType property. This is the
type that is defined in the ConnectorType property of the connector class.
You also implement the GetProcessorTypes() method, which returns the list of processors of the connector.
This method defines the order in which the entities are processed if Process All is clicked on the form toolbar of the
Process Data (BC501500) form.

Example
An example of the implementation of a processor factory class is shown in the following code.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceProcessorsFactory : IProcessorsFactory
{
public string ConnectorType => [Link];

public IEnumerable<KeyValuePair<Type, int>> GetProcessorTypes()


{
yield return new KeyValuePair<Type, int>(typeof(WooCustomerProcessor), 20);
}
}
}
Implementing a Connector for an E-Commerce System | 45

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Connector Descriptor Class

A connector descriptor class is a supplementary class that is used by the connector class. A connector descriptor
class provides performance-optimized access to particular functions of the connector. A connector descriptor
generates unique IDs for the real-time synchronization, returns the list of supported entities, and provides access to
the dynamic fields of the external system for the mapping.

Base Interface
A connector descriptor class implements the [Link] base interface.

Example
The following code shows an example of the implementation of the connector descriptor class.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommercesConnectorDescriptor : IConnectorDescriptor
{
protected IList<EntityInfo> _entities;

public WooCommercesConnectorDescriptor(IList<EntityInfo> entities)


{
_entities = entities;
}

public virtual Guid? GenerateExternID(BCExternNotification message)


{
throw new NotImplementedException();
}
public virtual Guid? GenerateLocalID(BCLocalNotification message)
{
throw new NotImplementedException();
}
public List<Tuple<string, string, string>> GetExternalFields(
string type, int? binding, string entity)
{
List<Tuple<string, string, string>> fieldsList =
new List<Tuple<string, string, string>>();
if (entity != [Link] &&
entity != [Link]) return fieldsList;

return fieldsList;
}
}
Implementing a Connector for an E-Commerce System | 46

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Connector Factory Class

A connector factory class initializes a connector that has the specified type and name.

Base Class and Interface


The connector factory class implements the [Link] interface and
optionally derives from the [Link]<GraphType> base abstract
class, which provides the default implementation of particular methods of the interface.

Members of the Class


In the connector factory class, you specify the type of the connector in the Type property and the name of the
connector in the Description property. These are the type and name that are defined in the ConnectorType
and ConnectorName properties of the connector class.
You also define the condition when the connector is available in Acumatica ERP in the Enabled property.
For example, the connector can be available if a particular feature is enabled on the Enable/Disable Features
(CS100000) form.
If your connector factory class derives from the BaseConnectorFactory<GraphType>, you need to
implement only the following items: a constructor of the factory class, and the GetDescriptor() method.

Example
An example of the implementation of a connector factory class is shown in the following code.

using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceConnectorFactory :
BaseConnectorFactory<WooCommerceConnector>, IConnectorFactory
{
public override string Description => [Link];
public override bool Enabled => true;

public override string Type => [Link];

public WooCommerceConnectorFactory(IEnumerable<IProcessorsFactory> processors)


: base(processors)
{
}

public IConnectorDescriptor GetDescriptor()


{
Implementing a Connector for an E-Commerce System | 47

return new WooCommercesConnectorDescriptor(_processors.[Link]());


}
}
}

Related Links
• Architecture of a Commerce Connector
• To Create a Connector for an External System

Use of a Commerce Connector

In this chapter, you will learn how a custom commerce connector—that is, a custom connector for any e-commerce
system—is used in Acumatica ERP.

Detection of the Connector in an Application

The Connector box of the configuration form of an e-commerce store contains the name of the connector if both of
the following conditions are met:
• The library that contains the connector class is placed in the Bin folder of an instance of Acumatica ERP or
an Acumatica Framework-based application.
• The ASPX file of the configuration form of the connector is available in the Pages folder of the application.

The description in this topic assumes that the connector uses a standard configuration form, which is
similar to the BigCommerce Stores (BC201000) and Shopify Stores (BC201010) forms. As an example,
this topic uses the WooCommerce Stores form, whose implementation is described in Step 13:
Creating the Configuration Form of this guide.

The data of the Summary area of the configuration form is obtained from the Bindings predefined data view of
the BCStoreMaint graph, which is a base class of the graph through which the configuration form is managed
(which is the WooCommerceStoreMaint graph in the WooCommerce example). The Bindings data view
retrieves data from the BCBinding predefined database table. This table includes the ConnectorType column,
which stores the type of the connector.
The system obtains the type and name of a connector from the implementation of the Type and Description
properties of the IConnectorFactory interface. The ConnectorType field of the BCBinding DAC, which
corresponds to the BCBinding table, has the BCConnectors attribute assigned to it. The BCConnectors
attribute, which derives from the PXStringList attribute, matches the type of the connector to its name.
The custom CacheAttached<[Link]> event handler sets the default value of the
ConnectorType field to the type of the connector. The Connector box displays the connector name, which
corresponds to this default value.
When a user specifies the settings of a commerce store in the Summary area of the configuration form, the system
saves these settings in the BCBinding database table.
The following diagram illustrates the relations of the classes of the connector and the ASPX page.
Implementing a Connector for an E-Commerce System | 48

Figure: Connector box

Establishment of the Connection with the External System

When a user specifies the connection settings of a store on the Configuration Settings tab of the configuration
form, the system saves the settings in the custom table (the BCBindingWooCommerce database table in the
WooCommerce example).
When the user clicks the Test Connection button on the form toolbar of the configuration form, the
system retrieves the data for the connection with the store from this custom table. The system calls the
GetRestClient() static method of the implementation of the IConnector interface, which creates the REST
client of the external system. The REST client establishes the connection with the external system through the REST
API.

The classes and the communication with the external system depend on the external system.
Implementing a Connector for an E-Commerce System | 49

Figure: Connection with the external system

Preparation of Data for the Synchronization

The preparation for data synchronization can be started manually on the Prepare Data (BC501000) form, by an
automation schedule, or as a result of a push notification. The following sections describe in order the general
steps involved in the preparation of data for synchronization; the process diagram in the end of this topic shows the
data preparation process in its entirety.

This topic describes the preparation for data synchronization when it is initiated on the Prepare
Data form. The preparation process is slightly different if it is initiated by an automation schedule
or as a result of a push notification. The preparation process initiated by an automation schedule
starts with Fetching of the Data from Acumatica ERP and the External System. The preparation
process initiated as a result of a push notification calls the [Link]()
method instead of the [Link]() and
[Link]() methods.

Beginning of the Data Preparation


When the data preparation is started (see Item 1 in the process diagram), the system executes
the ProcessSync() method of the [Link] graph. This
method creates an instance of the connector class that corresponds to the store with which the
synchronization is performed. The method also specifies the information about the current operation
in a [Link] object and runs the implementation of the
[Link]() method available in the connector class (Item 2).
For each entity that should be prepared for synchronization, the implementation of the
[Link]() method creates an instance of the processor class, initializes it with the information
about the current operation, and runs its [Link]() method (Item 3), whose default
implementation is available in the BCProcessorSingleBase<> class.
Implementing a Connector for an E-Commerce System | 50

Fetching of the Data from Acumatica ERP and the External System
The processor calls the FetchBucketsImport() (Item 4a) or FetchBucketsExport() (Item
4b) method, depending on the sync direction of the entity. These methods retrieve the information
about the synchronization of the entity from the BCEntityStats database table (Items 5a and
5b), determine the time frame for the entities that should be prepared (depending on the prepare
mode, which for manual preparation is specified in the Prepare Mode box on the Prepare Data
form), and call the BCProcessorSingleBase<>.FetchBucketsForImport() (Item 6a) or
BCProcessorSingleBase<>.FetchBucketsForExport() (Item 6b) method, respectively. The
FetchBucketsForImport() method retrieves entity records from the external system (Item 7a). The
FetchBucketsForExport() method obtains entity records from Acumatica ERP (Item 7b).

Saving of the Synchronization Data to the Database


For each entity record retrieved by the processor, the method creates a bucket of the entities by using
the BCProcessorBase<>.CreateBucket() method, initializes an object that implements the
[Link] interface for the entity type, and checks the preparation status of the
object by using the BCProcessorBase<>.EnsureStatus() method.
Finally, in the BCProcessorBase<>.UpdateEntity() method (Item 8), the processor saves data about the
synchronization status of the entity to the BCEntityStats and BCSyncStatus database tables (Item 9).

Process Diagram
The following diagram illustrates the preparation process.

Figure: Data preparation


Implementing a Connector for an E-Commerce System | 51

Synchronization of the Prepared Data

The synchronization of the prepared data can be started manually on the Process Data (BC501500) form, by an
automation schedule, or as a result of the push notification. The following sections describe in order the general
steps involved in the synchronization of the prepared data; the process diagram in the end of this topic shows the
synchronization process in its entirety.

Beginning of the Synchronization


When the synchronization is started (see Item 1 in the process diagram), the system executes
the ProcessSync() method of the [Link] graph. The
synchronization is performed in parallel threads. The parallel processing settings are specified in the
PXParallelProcessingOptions object of the Statuses data view of the graph.
The ProcessSync() method creates an instance of the connector class that corresponds to the store
with which the synchronization is performed. The method also specifies the information about the current
operation in a [Link] object and runs the implementation of the
[Link]() method (Item 2) available in the connector class.
For each entity that should be synchronized, the implementation of the [Link]() method
creates an instance of the processor class, initializes it with the information about the current operation,
and runs its [Link]() method (Item 3), whose default implementation is available in the
BCProcessorSingleBase<> class.

Retrieval of the Records for the Synchronization


The processor retrieves the records for synchronization as follows:
1. Retrieves the information about synchronized records from the BCSyncStatus database table (Item 4).
2. By calling BCProcessorSingleBase<>.GetBucketForExport() (Item 5a) or
BCProcessorSingleBase<>.GetBucketForImport() (Item 5b), pulls the records that correspond
to the entity through the APIs. These methods create a bucket object for the export or import of records,
respectively. The processor pulls the records from Acumatica ERP (Item 6a) by the value of the LocalID
column of the table, and pulls the records from the external system (Item 6b) by the value of the ExternID
column of the table.

Preprocessing of Data Before the Synchronization


If one entity has to be synchronized before the other—for example, before the synchronization of
customer entities between an external system and Acumatica ERP, you need to synchronize address
entities—the processor synchronizes the entities that should be synchronized before the entity in the
BCProcessorBase<>.ProcessPreProcessors() method (Item 7).

Exclusion of Possible Duplicate Records


The processor searches for the records that correspond to one another in Acumatica ERP and the external system
as follows:
1. If the value in ExternID of the BCEntityStats record is empty, searches for an external record that
corresponds to the internal record in BCProcessorSingleBase<>.CheckExistingForExport()
(Item 8a).
Implementing a Connector for an E-Commerce System | 52

2. If the value in LocalID of the BCEntityStats record is empty, searches for an internal record that
corresponds to the external record in BCProcessorSingleBase<>.CheckExistingForImport()
(Item 8b).

The connector performs the following actions during the search:


a. Pulls records from the destination system by using a unique key (such as the customer's
email address, the product name, or the order's external reference number) (Item 9a or 9b).
b. If no records are found, continues with the synchronization of the new record, as described
in the following section.
c. If multiple records are returned from the destination system, throws an error and aborts
the synchronization.
d. If only one record is returned from the destination system, checks whether this record has
already been mapped to any other record by using the BCSyncStatus table (Item 10a or
10b) as follows:
a. If there is a record that has already been mapped to another record, throws an error and
aborts the synchronization.
b. If there are no mapped records, maps the currently processed record to the found
record. The record will be synchronized as an existing record, as described below.

Control of the Synchronization Direction


The processor compares the time stamps of the synchronized records and decides in which direction these records
should be synchronized as follows:
1. If both the external records and the internal records are changed (which is defined by their time stamps), the
synchronization from the primary system to the secondary system is performed.
2. If neither an external record nor an internal record is changed (which is defined by time stamps),
the synchronization from the primary system to the secondary system is performed only if forced
synchronization is being performed.

Forced synchronization is performed if a user clicks Sync on the toolbar of the Sync History
(BC301000) form.

3. If only one time stamp is changed, the processor chooses the direction to synchronize changes from the
changed record.

Then an implementation of the BCProcessorSingleBase<>.ControlDirection() method (Item 11) is


called, in which you can add additional logic to the control of the synchronization direction.
When a direction is chosen, in the BCProcessorBase<>.EnsureSync() method (Item 12), the processor
locks the record so that it cannot be synchronized from the other threads by updating the SyncInProcess flag
of the BCSyncStatus table (Item 13). If the record has been locked already by the other process, the connector
aborts the operation and proceeds to the processing of the next record.

Import of Records
The processor imports the data to Acumatica ERP in the BCProcessorSingleBase<>.ProcessImport()
method (Item 14a), which internally performs the following steps:
1. For the records that have been deleted, removes the related records in Acumatica ERP in the
BCProcessorBase<>.DeleteRelatedEntities() method.
Implementing a Connector for an E-Commerce System | 53

2. Maps the properties of the external object to the properties of the Acumatica ERP object in
BCProcessorBase<>.MapBucketImport() method.
3. Applies the mappings that have been defined by the user on the Entities (BC202000) form in the
BCProcessorBase<>.RemapBucketImport() method.
4. In the BCProcessorBase<>.ValidateImport() method, validates the entity that is going to be
saved to the Acumatica ERP database to ensure that no required fields are le empty.
5. Saves the data to the database in BCProcessorSingleBase<>.SaveBucketImport() (Item 15a).

In the end of the implementation of the SaveBucketImport() method, you need to call
the BCProcessorBase<>.UpdateStatus() method to update the time stamps and IDs
of the synchronized records. For details, see Update of the Synchronization Status.

Export of Records
The processor exports the data from Acumatica ERP to the external system in the
BCProcessorSingleBase<>.ProcessExport() method (Item 14b), which internally performs the
following steps:
1. For the records that have been deleted, removes the related records in the external system in the
BCProcessorBase<>.DeleteRelatedEntities() method.
2. Maps the properties of the Acumatica ERP object to the properties of the external object in
BCProcessorBase<>.MapBucketExport() method.
3. Applies the mappings that have been defined by the user on the Entities (BC202000) form in the
BCProcessorBase<>.RemapBucketExport() method.
4. In the BCProcessorBase<>.ValidateExport() method, validates the record that is going to be
saved to the external system to ensure that no required fields are le empty.
5. Saves the data to the external system in BCProcessorSingleBase<>.SaveBucketExport() (Item
15b).

In the end of the implementation of the SaveBucketExport() method, you need to call
the BCProcessorBase<>.UpdateStatus() method to update the time stamps and IDs
of the synchronized records. For details, see Update of the Synchronization Status.

Update of the Synchronization Status


In the BCProcessorBase<>.UpdateStatus() method (Item 16), the processor updates the
BCSyncStatus record (Item 17) with the result of the operation as follows:
• If the synchronization was successful, the processor updates the record to reflect the new time stamps and
IDs. PendingSync is set to false.
• If the synchronization has failed, the processor updates the status to Failed, updates the error message and
increments the number of failed attempts.
• If the synchronization of this record has failed more than the configured maximum number of attempts, the
processor automatically assigns the Skipped status to the record.
The processor unlocks the record by updating the SyncInProcess flag of the BCSyncStatus table. The
processor then saves all the changes to the database.
Implementing a Connector for an E-Commerce System | 54

Synchronization of the Dependent Records


The processor synchronizes the entities that should be synchronized aer the entity in the
BCProcessorBase<>.ProcessPostProcessors() method (Item 18). If post-processing has failed, the
synchronization of the current entity is not affected.

Process Diagram
The following diagram illustrates the process of data synchronization.
Implementing a Connector for an E-Commerce System | 55

Real-Time Synchronization Through Push Notifications

With real-time synchronization, Acumatica ERP attempts to prepare data or prepare and process data as soon
as a change occurs in Acumatica ERP or in the e-commerce system. Depending on the entity involved and your
company's processes, real-time synchronization can involve import or export or can be bidirectional. Real-time
import relies on webhooks that Acumatica ERP receives from an e-commerce system, and real-time export makes
use of the push notification mechanism available in Acumatica ERP.
For details about push notifications in Acumatica ERP, see Configuring Push Notifications. For information about
how to implement push notifications for a custom connector, see To Implement Push Notifications for a Commerce
Connector.

Predefined System Configuration


Acumatica ERP includes a number of predefined generic inquiries that define the data changes for which
Acumatica ERP should send notifications to Acumatica Commerce Framework. The names of these generic
inquiries begin with the BC-PUSH prefix. The generic inquiries are listed on the Generic Inquiries tab of the Push
Notifications (SM302000) form for the Commerce notification destination, which is predefined in the system.

You can create custom generic inquiries for the monitoring of entities that are not included in the predefined
inquiries. You can then include these custom generic inquiries in the list of inquiries for the Commerce notification
destination. Also, you can create a custom notification destination of the Commerce Push Destination type on the
Push Notifications form and include both the custom generic inquires and the needed predefined generic inquiries
in the list of inquiries for this notification destination.

Processing of Commerce Push Notifications


When a change in the results of a data query (defined by a predefined or custom generic inquiry) has been
identified by the system, the system generates a notification and adds it to a private queue in the Microso Message
Queuing (MSMQ) service. This queue has the primaryqueue prefix and is used for all push notifications generated by
Acumatica ERP.
An Acumatica ERP background thread processes the notifications from this queue and sends the
commerce notifications to the notification destinations of the Commerce Push Destination type. By
default, the system includes only one notification destination of the Commerce Push Destination type,
which is the Commerce notification destination. The Commerce Push Destination type is defined in the
[Link] class, which
implements the [Link]
interface.
The notification destination of the Commerce Push Destination type processes the commerce notifications and
places them in another private queue in the MSMQ service, which has the commercequeue prefix. You can monitor
the status of this queue on the System Queues Monitor (SM302010) form.
The commerce background thread processes the commerce notifications. This thread works with a 20-second
delay. That is, when the thread receives the message, it waits for 20 seconds before it starts to process this
message. This is done to improve the performance of the commerce connector. For example, if a user has saved
a customer record 3 times in the last 15 seconds, the system generates 3 push notifications about the changes,
but only the modification of the record from the latest push notification will be synchronized by the commerce
connector.
The commerce background thread updates the BCSyncStatus database table with the information about the
modified record. You can view the data from this table on the Sync History (BC301000) form. Depending on the
value selected in the Real-Time Mode box on the Entities (BC202000) form, the commerce background thread does
one of the following:
Implementing a Connector for an E-Commerce System | 56

• If Prepare is selected, places the record in the BCSyncStatus table with the Pending status. For details
about preparation of data for synchronization, see Preparation of Data for the Synchronization.
• If Prepare & Process is selected, aer placing the record in the BCSyncStatus table, immediately starts the
synchronization of the record. For more information about data synchronization, see Synchronization of the
Prepared Data.
The processing of notifications is implemented in the [Link]() method, whose
default implementation is available in the [Link]<TConnector>.
This default implementation calls the [Link]() method, which is implemented in the
[Link]<>.ProcessPush() method.
If any error appears during the preparation or processing of the data for synchronization, the error is displayed on
the System Events tab of the System Monitor (SM201530) form.

To Create a Connector for an External System

To create a commerce connector, you need to perform the basic steps that are described in this chapter.
The chapter presents an example of the integration of Acumatica ERP with WooCommerce in which you need to
synchronize customers in Acumatica ERP with customers in a WooCommerce store.
You can find the code and customization project of the example described in this chapter in the Help-and-Training-
Examples repository on GitHub. The full code and customization project of the WooCommerce connector is
available in the Acumatica-WooCommerce repository on GitHub.

Step 1: Creating an Extension Library for Acumatica ERP

You need to develop a commerce connector in an extension library, which you include in an Acumatica ERP
customization package.

1. Creating an Extension Library for Acumatica ERP


1. On the main menu of the Customization Project Editor, click Extension Library > Create New.
The Create Extension Library dialog box opens.
2. Specify the location and name of the Visual Studio project that will contain the extension library.
3. Click OK to close the dialog box and start the process of creating the library.

For details about the creation of an extension library, see To Create an Extension Library.
Acumatica Customization Platform adds the default references to the project of the extension library, such as
[Link] and [Link]. For the creation of a commerce connector, you also need to add the references to the
Acumatica ERP commerce libraries, as described below.

2. Adding References to the Extension Library


1. In the project of the extension library, add references to the following assemblies located in the Bin folder
of the Acumatica ERP instance:
• [Link]: Required for the implementation of any commerce connector
• [Link]: Required for the implementation of any commerce connector
• [Link]: Necessary only if you want to use particular features
implemented for the BigCommerce connector
Implementing a Connector for an E-Commerce System | 57

• [Link]: Necessary only if you implement custom classes for Acumatica ERP
entities, as described in Step 2: Creating Classes for Acumatica ERP Entities
• [Link]: Necessary only if you use fluent BQL queries instead of traditional BQL
queries
2. Add references to the following external libraries:

You need to use the same version of the library as the one located in the Bin folder.

• RestSharp
• CommonServiceLocator
• [Link]
• [Link]
• Serilog
3. Build the project.

Now you will include the dll file of the extension library, which is located in the Bin folder of the website, in the
customization project.

3. Including the Library in the Customization Project


1. Open the customization project in the Customization Project Editor. (See To Open a Project for details.)
2. Click Files in the navigation pane to open the Custom Files page.
3. On the page toolbar, click Add New Record.
4. In the Add Files dialog box, which opens, find the file in the table and select the check box in the Selected
column for it.

• You can select multiple custom files to add them to the project at the same time.
• For any files other than the ones placed in the Bin folder, you can click Refresh on the
toolbar of the Add Files dialog box to make the system update the list of files in the table. If
you have changed files in the Bin folder of the website, you should refresh the page in the
browser.

5. In the dialog box, click Save to save each selected file to the customization project as a File item.

Step 2: Creating Classes for Acumatica ERP Entities

You need to create classes for the Acumatica ERP entities to be synchronized with the external system through the
connector. For details about the classes, see Classes for Acumatica ERP Entities.

1. Identifying the Acumatica ERP Entities to Be Used


1. On the Web Service Endpoints (SM207060) form, open the eCommerce/20.200.001 endpoint and identify
the entities and their fields that you need to use, including the key fields. For example, for the Case entity,
you may need to use the CaseID, Subject, Description, NoteID, and LastModifiedDateTime
fields.
2. Identify the default classes for Acumatica ERP entities (which are available in the
[Link] namespace) that you can use in your connector.
Implementing a Connector for an E-Commerce System | 58

If the default classes are enough for your implementation, skip the instructions in the
following section.

For the WooCommerce connector in this example, you will use the default Customer entity.

2. Creating Classes for Acumatica ERP Entities


1. If you need to implement your own classes for Acumatica ERP entities, in the Visual Studio project of the
extension library, create a class with the name of an Acumatica ERP entity that you want to process in the
connector. In this example, you are creating the Case class. Make sure that you have done the following:
• Defined only the properties that correspond to the fields of the contract-based API entity that you
identified on the Web Service Endpoints form, including the key fields. The names of the properties must
be the same as the names of the fields.
• Assigned the Description attributes to the class and its properties, as shown in the following code.
The names that are specified in these attributes will be used as the names of the Acumatica ERP objects
and fields on the mapping and filtering tabs of the Entities (BC202000) form.
using [Link];
using [Link];
using [Link];

[CommerceDescription("Case")]
public partial class Case : CBAPIEntity
{
public GuidValue NoteID { get; set; }

public DateTimeValue LastModifiedDateTime { get; set; }

[CommerceDescription("CaseCD")]
public StringValue CaseCD { get; set; }

[CommerceDescription("Subject")]
public StringValue Subject { get; set; }

[CommerceDescription("Description")]
public StringValue Description { get; set; }
}

2. Repeat the previous instruction for each Acumatica ERP entity that you need to use in your connector.
3. Build the project.

Step 3: Creating Classes for External Entities

Now you will create classes for the external entities that you need to synchronize with the Acumatica ERP system
through the connector. For details about the classes, see Classes for External Entities.

The classes and the communication with the external system depend on the external system.
Implementing a Connector for an E-Commerce System | 59

Creating Classes for External Entities


1. In the Visual Studio project of the extension library, create a class with the name of the external entity that
you want to process in the connector. In this example, you are creating the CustomerData class, shown in
the code below. In this class, make sure that you have done the following:
• Defined only the properties that correspond to the fields of the Acumatica ERP entity.
• Assigned the JsonProperty attributes to the properties. The names that are specified in these
attributes must be the names of the properties of the external entity retrieved in JSON format through
the external REST API.
• Assigned the [Link] attributes to the class and its
properties. The names that are specified in these attributes will be used as the names of the external
objects and fields on the mapping and filtering tabs of the Entities (BC202000) form.
using System;
using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
[CommerceDescription([Link])]
public class CustomerData : BCAPIEntity, IWooEntity
{
[JsonProperty("id")]
[CommerceDescription([Link], [Link],
[Link])]
public int? Id { get; set; }

[JsonProperty("date_created")]
public DateTime? DateCreatedUT { get; set; }

[CommerceDescription([Link])]
[ShouldNotSerialize]
public virtual DateTime? CreatedDateTime
{
get
{
return DateCreatedUT != null ? (DateTime)[Link]() :
default;
}
}

[JsonProperty("date_modified_gmt")]
public DateTime? DateModified { get; set; }

[CommerceDescription([Link])]
[ShouldNotSerialize]
public virtual DateTime? ModifiedDateTime
{
get
{
return DateModified != null ? (DateTime)[Link]() :
default;
}
}
Implementing a Connector for an E-Commerce System | 60

[JsonProperty("email")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired]
public string Email { get; set; }

[JsonProperty("first_name")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired]
public string FirstName { get; set; }

[JsonProperty("last_name")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired()]
public string LastName { get; set; }

[JsonProperty("username")]
[CommerceDescription([Link], [Link],
[Link])]
public string Username { get; set; }

[JsonProperty("billing")]
public CustomerAddressData Billing { get; set; }

[JsonProperty("shipping")]
public CustomerAddressData Shipping { get; set; }
}

public interface IWooEntity


{
DateTime? DateCreatedUT { get; set; }

DateTime? DateModified { get; set; }

}
}

2. Repeat the previous instruction for each external entity that you need to use in your connector. For the
WooCommerce connector, you also need the following classes:
• CustomerAddressData, shown in the code below
using System;
using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
public class CustomerAddressData : BCAPIEntity,
IEquatable<CustomerAddressData>
{
[JsonProperty("customer_id")]
[CommerceDescription([Link],
[Link], [Link])]
Implementing a Connector for an E-Commerce System | 61

public virtual int? CustomerId { get; set; }

[JsonProperty("first_name")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired()]
public virtual string FirstName { get; set; }

[JsonProperty("last_name")]
[CommerceDescription([Link], [Link],
[Link])]
public virtual string LastName { get; set; }

[JsonProperty("company")]
[CommerceDescription([Link], [Link],
[Link])]
public virtual string Company { get; set; }

[JsonProperty("address_1")]
[CommerceDescription(WCCaptions.AddressLine1, [Link],
[Link])]
[ValidateRequired(AutoDefault = true)]
public string Address1 { get; set; }

[JsonProperty("address_2")]
[CommerceDescription(WCCaptions.AddressLine2, [Link],
[Link])]
public string Address2 { get; set; }

[JsonProperty("city")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired(AutoDefault = true)]
public virtual string City { get; set; }

[JsonProperty("postcode")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired(AutoDefault = true)]
public virtual string PostalCode { get; set; }

[JsonProperty("country")]
[CommerceDescription([Link], [Link],
[Link])]
[ValidateRequired()]
public virtual string Country { get; set; }

[JsonProperty("state")]
[Description([Link])]
[ValidateRequired(AutoDefault = true)]
public virtual string State { get; set; }

[JsonProperty("phone")]
[Description([Link])]
[ValidateRequired(AutoDefault = true)]
public virtual string Phone { get; set; }
Implementing a Connector for an E-Commerce System | 62

[JsonProperty("email")]
[Description([Link])]
[ValidateRequired(AutoDefault = true)]
public virtual string Email { get; set; }

public bool Equals(CustomerAddressData newObject)


{
var oldObject = this;
if (ReferenceEquals(newObject, oldObject)) return true;
if (newObject == null || oldObject == null) return false;

if ([Link]() != [Link]()) return false;

var result = true;

foreach (var property in [Link]().GetProperties())


{
var objValue = [Link](newObject);
var anotherValue = [Link](oldObject);
if (objValue == null || anotherValue == null)
result = objValue == anotherValue;
else if (![Link](anotherValue)) result = false;

if (!result && !([Link] == nameof([Link]) ||


[Link] == nameof([Link])))
return result;
}

return result;
}
}
}

• SystemStatusData and related entities, which are shown in the following code
using [Link];

namespace WooCommerceTest
{
public class SystemStatusData
{
[JsonProperty("environment")]
public Environment Environment { get; set; }

[JsonProperty("settings")]
public Settings Settings { get; set; }
}

public class Settings


{
[JsonProperty("api_enabled")]
public bool? ApiEnabled { get; set; }

[JsonProperty("force_ssl")]
public bool? ForceSsl { get; set; }

[JsonProperty("currency")]
public string Currency { get; set; }
Implementing a Connector for an E-Commerce System | 63

[JsonProperty("currency_symbol")]
public string CurrencySymbol { get; set; }

[JsonProperty("currency_position")]
public string CurrencyPosition { get; set; }

[JsonProperty("thousand_separator")]
public string ThousandSeparator { get; set; }

[JsonProperty("decimal_separator")]
public string DecimalSeparator { get; set; }

[JsonProperty("number_of_decimals")]
public int? NumberOfDecimals { get; set; }

[JsonProperty("geolocation_enabled")]
public bool? GeolocationEnabled { get; set; }

[JsonProperty("woocommerce_com_connected")]
public string WoocommerceComConnected { get; set; }
}

public class Environment


{
[JsonProperty("home_url")]
public string HomeUrl { get; set; }

[JsonProperty("site_url")]
public string SiteUrl { get; set; }

[JsonProperty("version")]
public string Version { get; set; }

[JsonProperty("log_directory")]
public string LogDirectory { get; set; }

[JsonProperty("log_directory_writable")]
public bool? LogDirectoryWritable { get; set; }

[JsonProperty("wp_version")]
public string WpVersion { get; set; }

[JsonProperty("wp_multisite")]
public bool? WpMultisite { get; set; }

[JsonProperty("wp_memory_limit")]
public int? WpMemoryLimit { get; set; }

[JsonProperty("wp_debug_mode")]
public bool? WpDebugMode { get; set; }

[JsonProperty("wp_cron")]
public bool? WpCron { get; set; }

[JsonProperty("language")]
public string Language { get; set; }
Implementing a Connector for an E-Commerce System | 64

[JsonProperty("external_object_cache")]
public object ExternalObjectCache { get; set; }

[JsonProperty("server_info")]
public string ServerInfo { get; set; }

[JsonProperty("php_version")]
public string PhpVersion { get; set; }

[JsonProperty("php_post_max_size")]
public int? PhpPostMaxSize { get; set; }

[JsonProperty("php_max_execution_time")]
public int? PhpMaxExecutionTime { get; set; }

[JsonProperty("php_max_input_vars")]
public int? PhpMaxInputVars { get; set; }

[JsonProperty("curl_version")]
public string CurlVersion { get; set; }

[JsonProperty("suhosin_installed")]
public bool? SuhosinInstalled { get; set; }

[JsonProperty("max_upload_size")]
public int? MaxUploadSize { get; set; }

[JsonProperty("mysql_version")]
public string MysqlVersion { get; set; }

[JsonProperty("mysql_version_string")]
public string MysqlVersionString { get; set; }

[JsonProperty("default_timezone")]
public string DefaultTimezone { get; set; }

[JsonProperty("fsockopen_or_curl_enabled")]
public bool? FsockopenOrCurlEnabled { get; set; }

[JsonProperty("soapclient_enabled")]
public bool? SoapclientEnabled { get; set; }

[JsonProperty("domdocument_enabled")]
public bool? DomdocumentEnabled { get; set; }

[JsonProperty("gzip_enabled")]
public bool? GzipEnabled { get; set; }

[JsonProperty("mbstring_enabled")]
public bool? MbstringEnabled { get; set; }

[JsonProperty("remote_post_successful")]
public bool? RemotePostSuccessful { get; set; }

[JsonProperty("remote_post_response")]
public int RemotePostResponse { get; set; }
Implementing a Connector for an E-Commerce System | 65

[JsonProperty("remote_get_successful")]
public bool? RemoteGetSuccessful { get; set; }

[JsonProperty("remote_get_response")]
public int? RemoteGetResponse { get; set; }
}
}

3. Add the constants that you use for the descriptions of the entities and their fields to a class with the
PXLocalizable attribute, as shown in the following code.

using [Link];

namespace WooCommerceTest
{
[PXLocalizable]
public static class WCCaptions
{
public const string AddressLine1 = "Address Line 1";
public const string AddressLine2 = "Address Line 2";
public const string City = "City";
public const string CustomerId = "Customer ID";
public const string FirstName = "First Name";
public const string LastName = "Last Name";
public const string CompanyName = "Company Name";
public const string Country = "Country";
public const string PostalCode = "Postal Code";
public const string State = "State";
public const string Phone = "Phone";
public const string Email = "Email";
public const string Customer = "Customer";
public const string DateCreatedUT = "Date Created UT";
public const string DateModifiedUT = "Date Modified UT";
public const string ID = "ID";
public const string UserName = "UserName";
}
}

4. Build the project.

Step 4: Defining the Mappings Between Internal and External Entities

For each pair of an internal entity and an external entity that should be synchronized, you must create a mapping
class. For details about the mapping class, see Mapping Classes.

Defining the Mappings Between Internal and External Entities


1. In the Visual Studio project of the extension library, in the connector class, define the TYPE and NAME
constants in it, as shown in the following code. You will need these constants in the mapping classes.

namespace WooCommerceTest
{
public class WooCommerceConnector
{
Implementing a Connector for an E-Commerce System | 66

public const string TYPE = "WCC";


public const string NAME = "WooCommerce";
...
}
}

2. For each pair of an internal entity and an external entity that should be synchronized, in
the Visual Studio project of the extension library, create a mapping class that derives from
[Link]<ExternType, LocalType>. The following example shows
the implementation of the MappedCustomer class.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class MappedCustomer : MappedEntity<CustomerData, Customer>
{
public const string TYPE = [Link];

public MappedCustomer()
: base([Link], TYPE)
{ }
public MappedCustomer(Customer entity, Guid? id, DateTime? timestamp)
: base([Link], TYPE, entity, id, timestamp) { }
public MappedCustomer(CustomerData entity, string id, DateTime? timestamp)
: base([Link], TYPE, entity, id, timestamp) { }
}
}

3. Build the project.

Step 5: Creating the Buckets for the Mapped Entities

For each mapping class, you need to create a bucket class. For details about bucket classes, see Bucket Classes.

Defining the Buckets for the Mapped Entities


1. For each mapping class, in the Visual Studio project of the extension library, create a bucket
class that derives from the [Link] interface and, optionally,
the [Link] class. In this example, you are creating the
WooCustomerEntityBucket class.
2. In this class, define the entity that is being synchronized in the Primary property of the bucket class and
all entities that are being synchronized in this bucket in the Entities property. The following code shows
an example of the implementation.

using [Link];

namespace WooCommerceTest
{
public class WooCustomerEntityBucket : EntityBucketBase, IEntityBucket
{
public IMappedEntity Primary => Customer;
Implementing a Connector for an E-Commerce System | 67

public IMappedEntity[] Entities => new IMappedEntity[] { Customer };

public MappedCustomer Customer;


}
}

3. Build the project.

Step 6: Creating a DAC with the Configuration Settings

You will now create a DAC with the configuration settings that will be used by the connector. For this DAC, you will
add the database table and include it in the customization project.

1. Creating the Database Table and Adding It to the Customization Project


1. In the database of the instance that you are using for the development of the connector, create a database
table with the settings used by the connector. An example of the SQL script for such table is shown below.

GO
IF NOT EXISTS (SELECT * FROM SYSOBJECTS WHERE NAME='BCBINDINGWOOCOMMERCE' AND
XTYPE='U')
BEGIN
CREATE TABLE [dbo].[BCBindingWooCommerce](
[CompanyID] [int] NOT NULL,
[BindingID] [int] NOT NULL,
[StoreBaseUrl] [nvarchar](50) NULL,
[StoreXAuthToken] [nvarchar](max) NULL,
[StoreXAuthClient] [nvarchar](max) NULL,
[StoreAdminUrl] [nvarchar](200) NULL,
[WooCommerceStoreTimeZone] [nvarchar](100) NULL,
[WooCommerceDefaultCurrency] [nvarchar](12) NULL,
[tstamp] [timestamp] NOT NULL,
CONSTRAINT [PK_BCBindingWooCommerce] PRIMARY KEY CLUSTERED
(
[CompanyID] ASC,
[BindingID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

ALTER TABLE [dbo].[BCBindingWooCommerce] ADD DEFAULT ((0)) FOR [CompanyID]


END

2. Add the table to the customization project as follows:


a. Open the customization project in the Customization Project Editor. (See To Open a Project for details.)
b. In the navigation pane, click Database Scripts to open the Database Scripts page.
c. On the More menu (under Actions), click Add Custom Table Schema.
d. In the Add Custom Table Schema dialog box, which opens, select the custom table in the Table box, and
click OK.
Acumatica Customization Platform generates the table schema and adds the schema to the
customization project as an Sql item.
Implementing a Connector for an E-Commerce System | 68

2. Creating the DAC


1. In the Visual Studio project of the extension library, create the DAC that stores the WooCommerce connector
settings, as shown in the following code.

You can generate the DAC from the table in the database by using the Customization Project
Editor (as described in To Create a New DAC) and move it to the extension library (as
described in To Move a Code Item to the Extension Library). Alternatively, you can create the
DAC directly in the Visual Studio project.

using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
[PXCacheName("WooCommerce Settings")]
public class BCBindingWooCommerce : IBqlTable
{
public class PK : PrimaryKeyOf<BCBindingWooCommerce>.
By<[Link]>
{
public static BCBindingWooCommerce Find(PXGraph graph, int? binding) =>
FindBy(graph, binding);
}

#region BindingID
[PXDBInt(IsKey = true)]
[PXDBDefault(typeof([Link]))]
[PXUIField(DisplayName = "Store", Visible = false)]
[PXParent(typeof(Select<BCBinding, Where<[Link],
Equal<Current<[Link]>>>>))]
public int? BindingID { get; set; }
public abstract class bindingID : [Link]<bindingID> { }
#endregion

//Connection
#region StoreBaseUrl
[PXDBString(50, IsUnicode = true, InputMask = "")]
[PXUIField(DisplayName = "API Path")]
[PXDefault()]
public virtual string StoreBaseUrl { get; set; }
public abstract class storeBaseUrl :
[Link]<storeBaseUrl> { }
#endregion
#region StoreXAuthClient
[PXRSACryptString(IsUnicode = true, InputMask = "")]
[PXUIField(DisplayName = "Consumer Key")]
[PXDefault()]
public virtual string StoreXAuthClient { get; set; }
public abstract class storeXAuthClient :
[Link]<storeXAuthClient> { }
#endregion
#region StoreXAuthToken
[PXRSACryptString(IsUnicode = true, InputMask = "")]
Implementing a Connector for an E-Commerce System | 69

[PXUIField(DisplayName = "Consumer Secret")]


[PXDefault()]
public virtual string StoreXAuthToken { get; set; }
public abstract class storeXAuthToken :
[Link]<storeXAuthToken> { }
#endregion

#region WooCommerceDefaultCurrency
[PXDBString(12, IsUnicode = true)]
[PXUIField(DisplayName = "Default Currency", IsReadOnly = true)]
public virtual string WooCommerceDefaultCurrency { get; set; }
public abstract class wooCommerceDefaultCurrency :
[Link]<wooCommerceDefaultCurrency> { }
#endregion

#region WooCommerceStoreTimeZone
[PXDBString(100, IsUnicode = true)]
[PXUIField(DisplayName = "Store Time Zone", IsReadOnly = true)]
public virtual string WooCommerceStoreTimeZone { get; set; }
public abstract class wooCommerceStoreTimeZone :
[Link]<wooCommerceStoreTimeZone> { }
#endregion

#region StoreAdminURL
[PXDBString(100, IsUnicode = true, InputMask = "")]
[PXUIField(DisplayName = "Store Admin Path")]
[PXDefault()]
public virtual string StoreAdminUrl { get; set; }
public abstract class storeAdminUrl :
[Link]<storeAdminUrl> { }
#endregion

}
}

2. Build the project.

Step 7: Implementing a REST Client of the External System

You will now implement a REST API client of the external system.

The classes and the communication with the external system depend on the external system.

Implementing a REST Client of the External System


1. In the Visual Studio project of the extension library, for each external entity, define the data provider class,
as shown in the following example. This is a helper class that is used by the processor class to retrieve
the data of a particular entity from the external system. In this example, the implementation of the data
provider classes for the customer entity and the system status entity involves the following classes and
interfaces:
• RestDataProviderBase, which is shown in the following code
using [Link];
Implementing a Connector for an E-Commerce System | 70

using [Link];
using [Link];
using [Link];
using [Link];
using BigCom = [Link];

namespace WooCommerceTest
{
public abstract class RestDataProviderBase
{
internal const string COMMERCE_RETRY_COUNT = "CommerceRetryCount";
protected const int BATCH_SIZE = 10;
protected const string ID_STRING = "id";
protected const string PARENT_ID_STRING = "parent_id";
protected const string OTHER_PARAM = "other_param";
protected readonly int commerceRetryCount =
[Link](COMMERCE_RETRY_COUNT, 3);
protected IWooRestClient _restClient;

protected abstract string GetListUrl { get; }


protected abstract string GetSingleUrl { get; }

public RestDataProviderBase()
{
}

public virtual T Create<T>(T entity,


[Link] urlSegments = null)
where T : class, IWooEntity, new()
{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.ForContext("Object", entity)
.Verbose("{CommerceCaption}: WooCommerce REST API - Creating new
{EntityType} entity with parameters {UrlSegments}",
[Link], typeof(T).ToString(),
urlSegments?.ToString() ?? "none");

int retryCount = 0;
while (true)
{
try
{
var request = _restClient.MakeRequest(GetListUrl,
urlSegments?.GetUrlSegments());
[Link] = [Link];

T result = _restClient.Post<T>(request, entity);

return result;
}
catch ([Link] ex)
{
if (ex?.ResponceStatusCode ==
default(HttpStatusCode).ToString() &&
retryCount < commerceRetryCount)
Implementing a Connector for an E-Commerce System | 71

{
_restClient.Logger?.ForContext("Scope", new BCLogTypeScope(
GetType()))
.Error("{CommerceCaption}: Operation failed, RetryCount
{
RetryCount}, Exception {ExceptionMessage}",
[Link], retryCount,
ex?.ToString());

retryCount++;
[Link](1000 * retryCount);
}
else throw;
}
}
}

public virtual TE Create<T, TE>(List<T> entities,


[Link] urlSegments = null)
where T : class, IWooEntity, new()
where TE : IList<T>, new()
{
_restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType()))
.ForContext("Object", entities)
.Verbose("{CommerceCaption}: WooCommerce REST API - Creating new
{EntityType} entity with parameters {UrlSegments}",
[Link], typeof(T).ToString(),
urlSegments?.ToString() ?? "none");

int retryCount = 0;
while (true)
{
try
{
var request = _restClient.MakeRequest(GetListUrl,
urlSegments?.GetUrlSegments());

TE result = _restClient.Post<T, TE>(request, entities);

return result;
}
catch ([Link] ex)
{
if (ex?.ResponceStatusCode ==
default(HttpStatusCode).ToString() &&
retryCount < commerceRetryCount)
{
_restClient.Logger?.ForContext("Scope", new BCLogTypeScope(
GetType()))
.Error("{CommerceCaption}: Operation failed, RetryCount
{RetryCount}, Exception {ExceptionMessage}",
[Link], retryCount,
ex?.ToString());

retryCount++;
[Link](1000 * retryCount);
}
Implementing a Connector for an E-Commerce System | 72

else throw;
}
}
}

public virtual T Update<T>(T entity, [Link] urlSegments)


where T : class, new()
{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.ForContext("Object", entity)
.Verbose("{CommerceCaption}: WooCommerce REST API - Updating
{EntityType} entity with parameters {UrlSegments}",
[Link], typeof(T).ToString(),
urlSegments?.ToString() ?? "none");

int retryCount = 0;
while (true)
{
try
{
var request = _restClient.MakeRequest(GetSingleUrl,
urlSegments?.GetUrlSegments());

T result = _restClient.Put<T>(request, entity);

return result;
}
catch ([Link] ex)
{
if (ex?.ResponceStatusCode ==
default(HttpStatusCode).ToString() &&
retryCount < commerceRetryCount)
{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.Error("{CommerceCaption}: Operation failed, RetryCount
{RetryCount}, Exception {ExceptionMessage}",
[Link], retryCount,
ex?.ToString());

retryCount++;
[Link](1000 * retryCount);
}
else throw;
}
}
}

public virtual bool Delete([Link] urlSegments)


{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.Verbose("{CommerceCaption}: WooCommerce REST API - Deleting
{EntityType} entry with parameters {UrlSegments}",
[Link], GetType().ToString(),
urlSegments?.ToString() ?? "none");
Implementing a Connector for an E-Commerce System | 73

int retryCount = 0;
while (true)
{
try
{
var request = _restClient.MakeRequest(GetSingleUrl,
[Link]());

var result = _restClient.Delete(request);

return result;
}
catch ([Link] ex)
{
if (ex?.ResponceStatusCode ==
default(HttpStatusCode).ToString() &&
retryCount < commerceRetryCount)
{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.Error("{CommerceCaption}: Operation failed, RetryCount
{RetryCount}, Exception {ExceptionMessage}",
[Link], retryCount,
ex?.ToString());

retryCount++;
[Link](1000 * retryCount);
}
else throw;
}
}
}

protected static [Link] MakeUrlSegments(string id)


{
var segments = new [Link]();
[Link](ID_STRING, id);
return segments;
}

protected static [Link] MakeParentUrlSegments(


string parentId)
{
var segments = new [Link]();
[Link](PARENT_ID_STRING, parentId);

return segments;
}

protected static [Link] MakeUrlSegments(string id,


string parentId)
{
var segments = new [Link]();
[Link](PARENT_ID_STRING, parentId);
[Link](ID_STRING, id);
Implementing a Connector for an E-Commerce System | 74

return segments;
}
protected static [Link] MakeUrlSegments(string id,
string parentId, string param)
{
var segments = new [Link]();
[Link](PARENT_ID_STRING, parentId);
[Link](ID_STRING, id);
[Link](OTHER_PARAM, param);
return segments;
}
}
}

• IFilter, which is shown in the code below


using RestSharp;
using System;

namespace WooCommerceTest
{
public interface IFilter
{
void AddFilter(IRestRequest request);
int? Limit { get; set; }
int? Page { get; set; }

int? Offset { get; set; }

string Order { get; set; }

string OrderBy { get; set; }

DateTime? CreatedAfter { get; set; }


}
}

• Filter, which is shown in the following code


using RestSharp;
using [Link];
using System;
using [Link];

namespace WooCommerceTest
{
public class Filter : IFilter
{
protected const string RFC2822_DATE_FORMAT =
"{0:ddd, dd MMM yyyy HH:mm:ss} GMT";
protected const string ISO_DATE_FORMAT = "{0:yyyy-MM-ddTHH:mm:ss}";

[Description("per_page")]
public int? Limit { get; set; }

[Description("page")]
public int? Page { get; set; }
Implementing a Connector for an E-Commerce System | 75

[Description("offset")]
public int? Offset { get; set; }

[Description("order")]
public string Order { get; set; }

[Description("orderby")]
public string OrderBy { get; set; }

public DateTime? CreatedAfter { get; set; }

public virtual void AddFilter(IRestRequest request)


{
foreach (var propertyInfo in GetType().GetProperties())
{
DescriptionAttribute attr = propertyInfo.
GetAttribute<DescriptionAttribute>();
if (attr == null) continue;
string key = [Link];
object value = [Link](this);
if (value != null)
{
if ([Link] == typeof(DateTime) ||
[Link] == typeof(DateTime?))
{
value = [Link](ISO_DATE_FORMAT, value);
}

[Link](key, value);
}
}
}
}
}

• RestDataProvider, which is shown in the code below


using [Link];
using [Link];
using System;
using [Link];
using [Link];
using [Link];
using BigCom = [Link];

namespace WooCommerceTest
{
public abstract class RestDataProvider : RestDataProviderBase
{
public RestDataProvider() : base()
{

public virtual TE Get<T, TE>(IFilter filter = null,


[Link] urlSegments = null)
where T : class, IWooEntity, new()
Implementing a Connector for an E-Commerce System | 76

where TE : IEnumerable<T>, new()


{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.Verbose("{CommerceCaption}: WooCommerce REST API - Getting
{EntityType} entry with parameters {UrlSegments}",
[Link], GetType().ToString(),
urlSegments?.ToString() ?? "none");

var request = _restClient.MakeRequest(GetListUrl,


urlSegments?.GetUrlSegments());
filter?.AddFilter(request);

var response = _restClient.GetList<T, TE>(request);


return response;
}

public virtual IEnumerable<T> GetAll<T, TE>(IFilter filter = null,


[Link] urlSegments = null)
where T : class, IWooEntity, new()
where TE : IEnumerable<T>, new()
{
var localFilter = filter ?? new Filter();
var needGet = true;

[Link] = [Link] ? [Link] : 1;


[Link] = [Link] ? [Link] :
50;
[Link] = "desc";

TE entity = default;
while (needGet)
{
int retryCount = 0;
while (retryCount <= commerceRetryCount)
{
try
{
entity = Get<T, TE>(localFilter, urlSegments);
break;
}
catch (Exception ex)
{
_restClient.Logger?.ForContext("Scope",
new BCLogTypeScope(GetType()))
.Verbose("{CommerceCaption}: Failed at page {Page},
RetryCount {RetryCount}, Exception {ExceptionMessage}",
[Link],
[Link], retryCount, ex?.Message);

if (retryCount == commerceRetryCount)
throw;

retryCount++;
[Link](1000 * retryCount);
}
}
Implementing a Connector for an E-Commerce System | 77

if (entity == null) yield break;


foreach (T data in entity)
{
yield return data;
}

if ([Link]() < [Link])


needGet = false;
else if ([Link]() == [Link] &&
[Link] != null
&& [Link] >
[Link]()[[Link] - 1].DateCreatedUT)
needGet = false;

[Link]++;
}
}

public virtual T GetByID<T>([Link] urlSegments,


IFilter filter = null)
where T : class, new()
{
_restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType()))
.Verbose("{CommerceCaption}: WooCommerce REST API - Getting by ID
{EntityType} entry with parameters {UrlSegments}",
[Link], typeof(T).ToString(),
urlSegments?.ToString() ?? "none");

var request = _restClient.MakeRequest(GetSingleUrl,


[Link]());
if (filter != null)
[Link](request);
return _restClient.Get<T>(request);
}

public virtual bool Delete(IFilter filter = null)


{
var request = _restClient.MakeRequest(GetSingleUrl);
filter?.AddFilter(request);

var response = _restClient.Delete(request);


return response;
}
}
}

• CustomerDataProvider, which is shown in the following code


using [Link];

namespace WooCommerceTest
{
public class CustomerDataProvider : RestDataProvider
{
protected override string GetListUrl { get; } = "/customers";
Implementing a Connector for an E-Commerce System | 78

protected override string GetSingleUrl { get; } = "/customers/{id}";

public CustomerDataProvider(IWooRestClient restClient) : base()


{
_restClient = restClient;
}

public IEnumerable<CustomerData> GetAll(IFilter filter = null)


{
var localFilter = filter ?? new Filter();
[Link] = "registered_date";

return [Link]<CustomerData, List<CustomerData>>(filter);


}

public CustomerData GetCustomerById(int id)


{
var segments = [Link]([Link]());
return [Link]<CustomerData>(segments);
}
}
}

• SystemStatusProvider, which is shown below


namespace WooCommerceTest
{
public class SystemStatusProvider
{
private readonly IWooRestClient _restClient;

public SystemStatusProvider(IWooRestClient restClient)


{
_restClient = restClient;
}

public SystemStatusData Get()


{
const string resourceUrl = "/system_status";

var request = _restClient.MakeRequest(resourceUrl);


var country = _restClient.Get<SystemStatusData>(request);
return country;
}
}
}

2. Create a class for the REST client of the external system, which defines the methods that you need to
process REST requests to the external system. In this example, you are creating the WooRestClient class,
shown in the following code.

using [Link];
using RestSharp;
using [Link];
using [Link];
using System;
Implementing a Connector for an E-Commerce System | 79

using [Link];
using [Link];
using BigCom = [Link];

namespace WooCommerceTest
{
public class WooRestClient : WooRestClientBase, IWooRestClient
{
public WooRestClient(IDeserializer deserializer, ISerializer serializer,
[Link] options, [Link] logger)
: base(deserializer, serializer, options, logger)
{
}

public T Get<T>(string url)


where T : class, new()
{
RestRequest request = MakeRequest(url);

[Link] = [Link];
var response = Execute<T>(request);

if ([Link] == [Link] ||
[Link] == [Link] ||
[Link] == [Link])
{
T result = [Link];

if (result != null && result is BCAPIEntity) (


result as BCAPIEntity).JSON = [Link];

return result;
}

throw new Exception([Link]);


}

public T Post<T>(IRestRequest request, T entity)


where T : class, IWooEntity, new()
{
[Link] = [Link];
[Link](entity);
IRestResponse<T> response = Execute<T>(request);
if ([Link] == [Link] ||
[Link] == [Link])
{
T result = [Link];

if (result != null && result is BCAPIEntity) (


result as BCAPIEntity).JSON = [Link];

return result;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}
Implementing a Connector for an E-Commerce System | 80

public TE Post<T, TE>(IRestRequest request, List<T> entities)


where T : class, IWooEntity, new()
where TE : IEnumerable<T>, new()
{
[Link] = [Link];
[Link](entities);
IRestResponse<TE> response = Execute<TE>(request);
if ([Link] == [Link] ||
[Link] == [Link])
{
TE result = [Link];

if (result != null && result is IEnumerable<BCAPIEntity>)


(result as List<BCAPIEntity>).ForEach(
e => [Link] = [Link]);

return result;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}

public T Put<T>(IRestRequest request, T entity)


where T : class, new()
{
[Link] = [Link];
[Link](entity);

var response = Execute<T>(request);


if ([Link] == [Link] ||
[Link] == [Link] ||
[Link] == [Link])
{
T result = [Link];

if (result != null && result is BCAPIEntity) (


result as BCAPIEntity).JSON = [Link];

return result;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}

public T Get<T>(IRestRequest request)


where T : class, new()
{
[Link] = [Link];
var response = Execute<T>(request);

if ([Link] == [Link] ||
[Link] == [Link] ||
[Link] == [Link])
Implementing a Connector for an E-Commerce System | 81

{
T result = [Link];

if (result != null && result is BCAPIEntity) (


result as BCAPIEntity).JSON = [Link];

return result;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}
public TE GetList<T, TE>(IRestRequest request)
where T : class, IWooEntity, new()
where TE : IEnumerable<T>, new()
{
[Link] = [Link];
var response = Execute<TE>(request);

if ([Link] == [Link] ||
[Link] == [Link])
{
TE result = [Link];

return result;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}

public bool Delete(IRestRequest request)


{
[Link] = [Link];
var response = Execute(request);
if ([Link] == [Link] ||
[Link] == [Link] ||
[Link] == [Link])
{
return true;
}

LogError(BaseUrl, request, response);


throw new [Link](response);
}
}
}

In this example, the following supplementary classes and interfaces are used, which are shown in the
following code fragments:
• WooRestClientBase
using System;
using [Link];
using [Link];
using [Link];
using [Link];
Implementing a Connector for an E-Commerce System | 82

using RestSharp;
using [Link];
using [Link];

namespace WooCommerceTest
{
public abstract class WooRestClientBase : RestClient
{
protected ISerializer _serializer;
protected IDeserializer _deserializer;
public [Link] Logger { get; set; } = null;
protected WooRestClientBase(IDeserializer deserializer,
ISerializer serializer, IRestOptions options,
[Link] logger)
{
_serializer = serializer;
_deserializer = deserializer;
AddHandler("application/json", () => deserializer);
AddHandler("text/json", () => deserializer);
AddHandler("text/x-json", () => deserializer);

Authenticator = new Autentificator([Link],


[Link]);

try
{
BaseUrl = new Uri([Link]);
}
catch (UriFormatException e)
{
throw new UriFormatException(
"Invalid URL: The format of the URL could not be determined.",
e);
}
Logger = logger;
}

public RestRequest MakeRequest(string url,


Dictionary<string, string> urlSegments = null)
{
var request = new RestRequest(url) { JsonSerializer = _serializer,
RequestFormat = [Link] };

if (urlSegments != null)
{
foreach (var urlSegment in urlSegments)
{
[Link]([Link], [Link]);
}
}

return request;
}

protected void LogError(Uri baseUrl, IRestRequest request,


IRestResponse response)
Implementing a Connector for an E-Commerce System | 83

{
//Get the values of the parameters passed to the API
var parameters = [Link](", ", [Link](
x => [Link]() + "=" + ([Link] ?? "NULL")).ToArray());

//Set up the information message with the URL,


//the status code, and the parameters.
var info = "Request to " + [Link] + [Link] +
" failed with status code " + [Link] +
", parameters: " + parameters;
var description = "Response content: " + [Link];

//Acquire the actual exception


var ex = ([Link]?.Message) ?? info;

//Log the exception and info message


[Link]("Scope", new BCLogTypeScope(GetType()))
.ForContext("Exception", [Link]?.Message)
.Error("{CommerceCaption}: {ResponseError}, Status Code:
{StatusCode}",
[Link], description, [Link]);
}
}
}

• IWooRestClient
using RestSharp;
using Serilog;
using [Link];

namespace WooCommerceTest
{
public interface IWooRestClient
{
RestRequest MakeRequest(string url,
Dictionary<string, string> urlSegments = null);

T Post<T>(IRestRequest request, T entity)


where T : class, IWooEntity, new();
TE Post<T, TE>(IRestRequest request, List<T> entities)
where T : class, IWooEntity, new() where TE : IEnumerable<T>, new();
T Put<T>(IRestRequest request, T entity) where T : class, new();
T Get<T>(IRestRequest request) where T : class, new();
TE GetList<T, TE>(IRestRequest request)
where T : class, IWooEntity, new() where TE : IEnumerable<T>, new();
ILogger Logger { set; get; }
bool Delete(IRestRequest request);
}
}

• IWooEntity
using System;

namespace WooCommerceTest
{
public interface IWooEntity
Implementing a Connector for an E-Commerce System | 84

{
DateTime? DateCreatedUT { get; set; }

DateTime? DateModified { get; set; }


}
}

• Authenticator
using RestSharp;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class Autentificator : IAuthenticator
{
private readonly string _consumerKey;
private readonly string _consumerSecret;

public Autentificator(string consumerKey, string consumerSecret)


{
_consumerKey = consumerKey;
_consumerSecret = consumerSecret;
}

public void Authenticate(IRestClient client, IRestRequest request)


{
request.BuildOAuth1QueryString((RestClient)client,
_consumerKey, _consumerSecret);
}
}

public static class RestRequestExtensions


{
public static IRestRequest BuildOAuth1QueryString(
this IRestRequest request, RestClient client, string consumerKey,
string consumerSecret)
{
var auth = [Link](consumerKey,
consumerSecret);
[Link] = [Link];
[Link](client, request);

//Convert all these oauth params from cookie to querystring


[Link](x =>
{
if ([Link]("oauth_"))
[Link] = [Link];
});

return request;
}
}
}
Implementing a Connector for an E-Commerce System | 85

3. Create the connector class and define the GetRestClient static methods in it, as shown in the following
code.

using [Link];
using System;
using [Link];
using [Link];
using [Link];
using CommonServiceLocator;

namespace WooCommerceTest
{
public class WooCommerceConnector
{
public static WooRestClient GetRestClient(BCBindingWooCommerce binding)
{
return GetRestClient([Link], [Link],
[Link]);
}

public static WooRestClient GetRestClient(String url, String clientID,


String token)
{
RestOptions options = new RestOptions
{
BaseUri = url,
XAuthClient = clientID,
XAuthTocken = token
};
JsonSerializer serializer = new JsonSerializer
{
MissingMemberHandling = [Link],
NullValueHandling = [Link],
DefaultValueHandling = [Link],
DateFormatHandling = [Link],
DateTimeZoneHandling = [Link],
ContractResolver = new GetOnlyContractResolver()
};
RestJsonSerializer restSerializer =
new RestJsonSerializer(serializer);
WooRestClient client = new WooRestClient(restSerializer,
restSerializer, options,
[Link]<[Link]>());

return client;
}
}
}

4. Build the project.


Implementing a Connector for an E-Commerce System | 86

Step 8: Implementing the Processor Classes

For each pair of entities that you need to synchronize, you need to create a processor class. For details about
processor classes, see Processor Classes.

Implementing the Processor Classes


1. For each pair of entities that you need to synchronize, in the Visual Studio project of the extension library,
create a processor class that derives from the [Link] interface and,
optionally, the [Link]<TGraph, TEntityBucket,
TPrimaryMapped> abstract class. In this example, you are creating the WooCustomerProcessor
class.
2. Assign the BCProcessor and BCProcessorRealtime attributes to the class and define the methods
that perform the synchronization of the entities. The following code shows the implementation for the
example in this chapter.

using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
[BCProcessor(typeof(WooCommerceConnector), [Link],
[Link],
IsInternal = false,
Direction = [Link],
PrimaryDirection = [Link],
PrimarySystem = [Link],
PrimaryGraph = typeof([Link]),
ExternTypes = new Type[] { typeof(CustomerData) },
LocalTypes = new Type[] { typeof([Link]) },
AcumaticaPrimaryType = typeof([Link]),
AcumaticaPrimarySelect = typeof([Link]),
URL = "[Link]?user_id={0}"
)]
[BCProcessorRealtime(PushSupported = false, HookSupported = false)]
public class WooCustomerProcessor : BCProcessorSingleBase<WooCustomerProcessor,
WooCustomerEntityBucket, MappedCustomer>, IProcessor
{
public WooRestClient client;
protected CustomerDataProvider customerDataProvider;

//protected List<Country> countries;


public CommerceHelper helper = [Link]<CommerceHelper>();

#region Constructor
public override void Initialise(IConnector iconnector,
ConnectorOperation operation)
Implementing a Connector for an E-Commerce System | 87

{
[Link](iconnector, operation);
client = [Link](
GetBindingExt<BCBindingWooCommerce>());
customerDataProvider = new CustomerDataProvider(client);
}
#endregion

#region Import
public override void MapBucketImport(WooCustomerEntityBucket bucket,
IMappedEntity existing)
{
MappedCustomer customerObj = [Link];

[Link] customerImpl = [Link] =


new [Link]();
[Link] = GetCustomFieldsForImport();

[Link] = GetBillingCustomerName([Link]).
ValueField();
[Link] = [Link]([Link],
GetBinding().BindingName).ValueField();
[Link] = [Link] == null ||
existing?.Local == null ?
GetBindingExt<BCBindingExt>().CustomerClassID?.ValueField() : null;

//Primary Contact
[Link] = GetPrimaryContact([Link]);

[Link] = SetBillingContact([Link]);

[Link] = [Link]();
[Link] = [Link]();
[Link] = SetShippingContact([Link]);

BCBindingExt bindingExt = GetBindingExt<BCBindingExt>();


if ([Link] != null)
{
CustomerClass customerClass = PXSelect<CustomerClass,
Where<[Link],
Equal<Required<[Link]>>>>.
Select(this, [Link]);
if (customerClass != null)
{
[Link]();

}
}

public virtual Contact GetPrimaryContact(CustomerData customer)


{
var contact = new Contact();
[Link] = ![Link]([Link]) &&
[Link]([Link]) ?
[Link]() : [Link]();
Implementing a Connector for an E-Commerce System | 88

[Link] = ![Link]([Link]) ?
[Link]() :
![Link]([Link]) &&
[Link]([Link]) ?
[Link]() : [Link]();
[Link] = [Link]();

return contact;
}

public virtual Contact SetBillingContact(CustomerData customerObj)


{
Contact contactImpl = new Contact();
[Link] = GetBillingCustomerName(customerObj).
ValueField();
[Link] = $"{[Link]?.FirstName} {
[Link]?.LastName}".ValueField(); ;
[Link] = GetBillingCustomerName(customerObj).ValueField();
[Link] = [Link]?.[Link]();
[Link] = [Link]();
[Link] = [Link]();
[Link] = MapAddress([Link]);
[Link] = [Link]();
contactImpl.Phone1 = [Link]?.[Link]();
contactImpl.Phone1Type = [Link]();

return contactImpl;
}

public virtual Contact SetShippingContact(CustomerData customerObj)


{
Contact contactImpl = new Contact();
[Link] = GetShippingCustomerName(customerObj).
ValueField();
[Link] = $"{[Link]?.FirstName} {
[Link]?.LastName}".ValueField();
[Link] = GetShippingCustomerName(customerObj).ValueField();
[Link] = [Link]?.[Link]();
[Link] = [Link]();
[Link] = MapAddress([Link]);
[Link] = [Link]();

return contactImpl;
}

public virtual string GetBillingCustomerName(CustomerData data)


{
return ![Link]([Link])
? [Link]
: [Link]($"{[Link]?.FirstName} {
[Link]?.LastName}") ? [Link] : $"{
[Link]?.FirstName} {[Link]?.LastName}";
}

public virtual string GetShippingCustomerName(CustomerData data)


{
return ![Link]([Link]?.Company)
Implementing a Connector for an E-Commerce System | 89

? [Link]
: $"{[Link]?.FirstName} {[Link]?.LastName}";
}

public virtual Address MapAddress(CustomerAddressData addressObj)


{
var address = new Address();
address.AddressLine1 = [Link]();
address.AddressLine2 = [Link]();
[Link] = [Link]();
[Link] = [Link]();
if (![Link]([Link]))
{
var selectedCountry = [Link];
if (selectedCountry != null)
{
[Link] = [Link]();
if (![Link]([Link]))
{
[Link] = [Link]();
}
}
}

return address;
}

public override IEnumerable<MappedCustomer> PullSimilar(IExternEntity entity,


out string uniqueField)
{
uniqueField = [Link](((CustomerData)entity)?.Billing.
Email) ? ((CustomerData)entity)?.Email : ((CustomerData)entity)?.
[Link];
if (uniqueField == null) return null;

List<MappedCustomer> result = new List<MappedCustomer>();


foreach ([Link] item in [Link](
uniqueField))
{
[Link] data = new [Link].
Customer() { SyncID = [Link], SyncTime =
[Link] };
[Link](new MappedCustomer(data, [Link], [Link]));
}

if (result == null || result?.Count == 0) return null;

return result;
}

public override IEnumerable<MappedCustomer> PullSimilar(ILocalEntity entity,


out string uniqueField)
{
uniqueField = (([Link])entity)?.MainContact?.
Email?.Value;
if (uniqueField == null) return null;
IEnumerable<CustomerData> datas = [Link](null);
Implementing a Connector for an E-Commerce System | 90

if (datas == null) return null;

return [Link](data => new MappedCustomer(data, [Link](),


[Link]()));
}

public override void SaveBucketImport(WooCustomerEntityBucket bucket,


IMappedEntity existing, string operation)
{
MappedCustomer obj = [Link];

[Link] impl = [Link]([Link], [Link]);

[Link](impl, [Link], [Link]);


UpdateStatus(obj, operation);
}
#endregion

#region Export

public override void MapBucketExport(WooCustomerEntityBucket bucket,


IMappedEntity existing)
{
}

public override void SaveBucketExport(WooCustomerEntityBucket bucket,


IMappedEntity existing, string operation)
{
}
#endregion

#region Pull
public override MappedCustomer PullEntity(Guid? localID,
Dictionary<string, object> externalInfo)
{
throw new NotImplementedException();
}
public override MappedCustomer PullEntity(string externID,
string externalInfo)
{
throw new NotImplementedException();
}

public override void FetchBucketsForImport(DateTime? minDateTime,


DateTime? maxDateTime, PXFilterRow[] filters)
{
IEnumerable<CustomerData> customers = [Link](null);

foreach (CustomerData data in customers)


{
WooCustomerEntityBucket bucket = CreateBucket();

MappedCustomer mapped = [Link] = [Link](


data, [Link](), [Link]());
EnsureStatus(mapped, [Link]);
}
}
Implementing a Connector for an E-Commerce System | 91

public override void FetchBucketsForExport(DateTime? minDateTime,


DateTime? maxDateTime, PXFilterRow[] filters)
{
throw new NotImplementedException();
}

public override EntityStatus GetBucketForImport(WooCustomerEntityBucket


bucket,
BCSyncStatus syncstatus)
{
CustomerData data = [Link]<CustomerData>(
[Link]("/customers/{0}", [Link]));

MappedCustomer obj = [Link] = [Link](data,


[Link](), [Link]());
EntityStatus status = EnsureStatus(obj, [Link]);

return status;
}

public override EntityStatus GetBucketForExport(WooCustomerEntityBucket


bucket,
BCSyncStatus bcstatus)
{
[Link] impl =
[Link]<[Link]>([Link],
GetCustomFieldsForExport());
if (impl == null) return [Link];

[Link] = [Link](impl, [Link], [Link]);


EntityStatus status = EnsureStatus([Link], [Link]);

return status;
}
#endregion
}
public static class PhoneTypes
{
public static string Business1 = "B1";
}
}

3. Build the project.

Step 9: Implementing the Processor Factory Class

For the system to create processor classes for all entities used by the specified connector, you need to define the
processor factory class. For details about the processor factory class, see Processor Factory Class.

Implementing the Processor Factory Class


1. In the Visual Studio project of the extension library, create a processor factory class that derives from the
[Link] interface.
Implementing a Connector for an E-Commerce System | 92

2. In the class, specify the type of the connector in the ConnectorType property and implement the
GetProcessorTypes() method, which returns the list of processors of the connector, as shown in the
following code.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceProcessorsFactory : IProcessorsFactory
{
public string ConnectorType => [Link];

public IEnumerable<KeyValuePair<Type, int>> GetProcessorTypes()


{
yield return new KeyValuePair<Type, int>(typeof(WooCustomerProcessor),
20);
}
}
}

3. Build the project.

Step 10: Implementing the Connector Class

In the connector class, which you have already created, you will now implement the synchronization of the
Acumatica ERP entities and the external entities. For a detailed description of the connector class, see Connector
Class.

Implementing the Connector Class


1. In the connector class, implement the [Link] interface and derive the class
from the [Link]<TConnector> abstract class, as shown in the
following code.

using [Link];
using System;
using [Link];
using [Link];
using [Link];
using CommonServiceLocator;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceConnector: BCConnectorBase<WooCommerceConnector>,
IConnector
{
public const string TYPE = "WOO";
public const string NAME = "WooCommerce";
Implementing a Connector for an E-Commerce System | 93

public class WCConnectorType : [Link]<WCConnectorType>


{
public WCConnectorType() : base(TYPE) { }
}

public override string ConnectorType { get => TYPE; }


public override string ConnectorName { get => NAME; }

public virtual IEnumerable<TInfo> GetExternalInfo<TInfo>(string infoType,


int? bindingID)
where TInfo : class
{
if ([Link](infoType) || bindingID == null) return null;
BCBindingWooCommerce binding = [Link](this,
bindingID);
if (binding == null) return null;

try
{
List<TInfo> result = new List<TInfo>();
return result;
}
catch (Exception ex)
{
LogError(new BCLogTypeScope(typeof(WooCommerceConnector)), ex);
}

return null;
}

public void NavigateExtern(ISyncStatus status)


{
if (status?.ExternID == null) return;

EntityInfo info = GetEntities().FirstOrDefault(e => [Link] ==


[Link]);
BCBindingWooCommerce bCBindingBigCommerce =
[Link](this, [Link]);

if ([Link](bCBindingBigCommerce?.StoreAdminUrl) ||
[Link]([Link])) return;

string[] parts = [Link](new char[] { ';' });


string url = [Link]([Link], [Link] > 2 ?
[Link](2).ToArray() : parts);
string redirectUrl = [Link]('/') +
"/" + url;

throw new PXRedirectToUrlException(redirectUrl,


[Link], [Link]);
}

//Create an instance of the processor graph that corresponds to the entity


//and run its Process method.
public virtual SyncInfo[] Process(ConnectorOperation operation,
int?[] syncIDs = null)
Implementing a Connector for an E-Commerce System | 94

{
LogInfo([Link](), [Link], NAME);

EntityInfo info = GetEntities().FirstOrDefault(e =>


[Link] == [Link]);
using (IProcessor graph = (IProcessor)CreateInstance([Link]))
{
[Link](this, operation);
return [Link](syncIDs);
}
}

public DateTime GetSyncTime(ConnectorOperation operation)


{
BCBindingWooCommerce binding = [Link](this,
[Link]);
//Acumatica Time
[Link](out DateTime dtLocal, out DateTime dtUtc);
dtLocal = [Link](dtUtc,
[Link]());

return dtLocal;
}

public override void StartWebHook(string baseUrl, BCWebHook hook)


{
throw new NotImplementedException();
}

public virtual void ProcessHook(IEnumerable<BCExternQueueMessage> messages)


{
throw new NotImplementedException();
}

public override void StopWebHook(string baseUrl, BCWebHook hook)


{
throw new NotImplementedException();
}

public static WooRestClient GetRestClient(BCBindingWooCommerce binding)


{
return GetRestClient([Link], [Link],
[Link]);
}

public static WooRestClient GetRestClient(String url, String clientID,


String token)
{
RestOptions options = new RestOptions
{
BaseUri = url,
XAuthClient = clientID,
XAuthTocken = token
};
JsonSerializer serializer = new JsonSerializer
{
Implementing a Connector for an E-Commerce System | 95

MissingMemberHandling = [Link],
NullValueHandling = [Link],
DefaultValueHandling = [Link],
DateFormatHandling = [Link],
DateTimeZoneHandling = [Link],
ContractResolver = new GetOnlyContractResolver()
};
RestJsonSerializer restSerializer = new RestJsonSerializer(serializer);
WooRestClient client = new WooRestClient(restSerializer, restSerializer,
options,
[Link]<[Link]>());

return client;
}
}
}

2. Build the project.

Step 11: Implementing the Connector Descriptor Class

You need to create a connector descriptor class, which is required for the retrieval of external fields and for the
implementation of work with push notifications. For details about the connector descriptor class, see Connector
Descriptor Class.

Implementing a Connector Descriptor Class


1. In the Visual Studio project of the extension library, create a connector descriptor class that implements the
[Link] interface, as shown in the following code.

using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommercesConnectorDescriptor : IConnectorDescriptor
{
protected IList<EntityInfo> _entities;

public WooCommercesConnectorDescriptor(IList<EntityInfo> entities)


{
_entities = entities;
}

public virtual Guid? GenerateExternID(BCExternNotification message)


{
throw new NotImplementedException();
}
public virtual Guid? GenerateLocalID(BCLocalNotification message)
{
throw new NotImplementedException();
}
public List<Tuple<string, string, string>> GetExternalFields(
string type, int? binding, string entity)
Implementing a Connector for an E-Commerce System | 96

{
List<Tuple<string, string, string>> fieldsList =
new List<Tuple<string, string, string>>();
if (entity != [Link] &&
entity != [Link]) return fieldsList;

return fieldsList;
}
}
}

2. Build the project.

Step 12: Implementing the Connector Factory Class

For the system to create the connector class, you need to implement the connector factory class. For details about
the connector factory class, see Connector Factory Class.

Implementing the Connector Factory Class


1. In the Visual Studio project of the extension library, create a processor factory class that
implements the [Link] interface and derives from the
[Link]<WooCommerceConnector> abstract class, as
shown in the following code.

using [Link];
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceConnectorFactory :
BaseConnectorFactory<WooCommerceConnector>, IConnectorFactory
{
public override string Description => [Link];
public override bool Enabled => true;

public override string Type => [Link];

public WooCommerceConnectorFactory(IEnumerable<IProcessorsFactory> processors)


: base(processors)
{
}

public IConnectorDescriptor GetDescriptor()


{
return new WooCommercesConnectorDescriptor(_processors.[Link]());
}
}
}

2. Build the project.


Implementing a Connector for an E-Commerce System | 97

Step 13: Creating the Configuration Form

For a user to specify the basic settings of the connection with the external system, you need to create a custom
form in Acumatica ERP. You have the following options for the creation of the form:
• You can create a custom form from scratch. For details about the creation of custom forms, see To Develop a
Custom Form in the Customization Guide, or review the T200 Maintenance Forms training course.
• You can use the configuration forms that are available for existing BigCommerce and Shopify connectors as
a template and adjust these forms. In this step, you will use this approach.

1. Creating the Configuration Form


1. Open the solution of your extension library, which consists of two projects: the project with the code of the
extension library, and the website project.
2. In the Pages folder of the website project, create a folder (such as WO) that will contain the custom form for
configuration of the connector.
3. Copy the [Link] file from the BC folder to the folder you have created in the previous
step, and rename it so that the file name has a prefix that is specific for your connector (for example,
[Link] has the WO prefix).
4. Review the BigCommerce Stores (BC201000) form, and identify which elements of the form you need to use
for the configuration of your connector. In this example, the configuration form has only the following tabs:
Connection Settings, Entity Settings, and Customer Settings.
5. In the project of the extension library, add the graph for the configuration form, such as the graph shown in
the following code.

using [Link];
using [Link];
using [Link];
using System;
using [Link];
using [Link];

namespace WooCommerceTest
{
public class WooCommerceStoreMaint : BCStoreMaint
{

public SelectFrom<BCBindingWooCommerce>.
Where<[Link].
IsEqual<[Link]>>.View
CurrentBindingWooCommerce;

public WooCommerceStoreMaint()
{
[Link]<Where<[Link].
IsEqual<[Link]>>>();
}

#region Actions
public PXAction<BCBinding> TestConnection;
[PXButton]
[PXUIField(DisplayName = "Test Connection", Enabled = false)]
Implementing a Connector for an E-Commerce System | 98

protected virtual IEnumerable testConnection(PXAdapter adapter)


{
[Link]();

BCBinding binding = [Link];


BCBindingWooCommerce bindingWooCommerce =
[Link] ??
[Link]();
if (binding == null || bindingWooCommerce == null ||
[Link] == null)
{
throw new PXException([Link]);
}

[Link](this, delegate
{
SystemStatusProvider restClient = new SystemStatusProvider(
[Link](bindingWooCommerce));
WooCommerceStoreMaint graph =
[Link]<WooCommerceStoreMaint>();
[Link] = binding;
[Link] = bindingWooCommerce;
try
{
var systemStatus = [Link]();

[Link] =
[Link];
[Link] =
[Link];
[Link]();

if (systemStatus == null)
throw new PXException([Link]);

[Link](
binding, nameof(BCBindingWooCommerce.
wooCommerceDefaultCurrency),
[Link]);
[Link](binding,
nameof([Link]),
[Link]);
[Link] = true;
[Link](
bindingWooCommerce);

[Link]();
}
catch (Exception ex)
{
throw new PXException(ex,
[Link], [Link]);
}
});

return [Link]();
}
Implementing a Connector for an E-Commerce System | 99

#endregion

protected virtual void _([Link]<BCBindingWooCommerce> e)


{
BCBindingWooCommerce row = [Link] as BCBindingWooCommerce;
if (row == null || [Link]([Link]) ||
[Link]([Link]) ||
[Link]([Link]))
return;

SystemStatusProvider restClient = new SystemStatusProvider(


[Link](row));
try
{
var store = [Link]();

[Link](row,
nameof([Link]),
[Link]?.Currency);
[Link](row,
nameof([Link]),
[Link]?.DefaultTimezone);
[Link] = true;
[Link](row);
}
catch (Exception) { }
}

//Set the default connector type. This type will be displayed


//in the Connector box on the configuration form.
[PXMergeAttributes(Method = [Link])]
[PXCustomizeBaseAttribute(typeof(BCConnectorsAttribute),
"DefaultConnector", [Link])]
public virtual void _([Link]<[Link]> e) { }

public override void _([Link]<BCBinding> e)


{
base._(e);

BCBinding row = [Link] as BCBinding;


if (row == null) return;

//Actions
[Link]([Link] > 0 &&
[Link] == [Link]);
}

public override void _([Link]<BCBinding> e)


{
base._(e);

bool dirty = [Link];


[Link]();
[Link] = dirty;
}

public override void _([Link]<BCBindingExt> e)


Implementing a Connector for an E-Commerce System | 100

{
base._(e);

BCBindingExt row = [Link] as BCBindingExt;


if (row == null) return;
[Link]<[Link]>(
[Link], row, [Link]);
}

}
}

6. Add the messages that you are using in the actions and events of the graph to a class with the
PXLocalizable attribute, as shown in the following code.

using [Link];

namespace WooCommerceTest
{
[PXLocalizable]
class Messages
{
public const string TestConnectionStoreNotFound =
"The store data cannot be retrieved through the WooCommerce REST API.
Check that the store URL is correct.";
}
}

7. In the [Link] file, change the properties of the PXDataSource control as follows:
• TypeName: Assign the name of the graph that you have just created, such as
[Link].
• PrimaryView: Assign the name of the primary view of the graph, such as Bindings.
See the following code example.

<asp:Content ID="cont1" ContentPlaceHolderID="phDS" runat="Server">


<px:PXDataSource PageLoadBehavior="GoFirstRecord" ID="ds" runat="server"
Visible="True" Width="100%"
TypeName="[Link]"
PrimaryView="Bindings">
<CallbackCommands>
</CallbackCommands>
</px:PXDataSource>
</asp:Content>

8. Change the DataMember property of the PXFormView control to the name of the primary view of the
graph, such as Bindings. See the following code example.

<asp:Content ID="cont2" ContentPlaceHolderID="phF" runat="Server">


<px:PXFormView ID="form" runat="server" DataSourceID="ds" DataMember="Bindings"
Width="100%" Height="100px" AllowAutoHide="false">
<Template>
<px:PXLayoutRule ID="PXLayoutRule1" runat="server" StartRow="True"></
px:PXLayoutRule>
<px:PXDropDown runat="server" ID="CstPXDropDown10"
DataField="ConnectorType" />
<px:PXSelector runat="server" ID="CstPXSelector9"
DataField="BindingName" />
Implementing a Connector for an E-Commerce System | 101

<px:PXLayoutRule runat="server" ID="CstPXLayoutRule13"


StartColumn="True" />
<px:PXCheckBox runat="server" ID="CstPXCheckBox11" DataField="IsActive" />
<px:PXCheckBox runat="server" ID="CstPXCheckBox12" DataField="IsDefault" /
>
</Template>
</px:PXFormView>
</asp:Content>

9. Adjust the DataMember property and the list of fields of the PXTabItem control that corresponds to the
Connection Settings tab, as shown in the following code.

<px:PXTabItem Text="Connection Settings">


<Template>
<px:PXLayoutRule runat="server" ID="CstLayoutRule26" StartColumn="True"></
px:PXLayoutRule>
<px:PXFormView runat="server" ID="CstFormView14"
DataMember="CurrentBindingWooCommerce">
<Template>
<px:PXLayoutRule runat="server" ID="CstLayoutRule23" ColumnWidth="XL"
LabelsWidth="SM" StartRow="True" StartColumn="True"></px:PXLayoutRule>
<px:PXTextEdit runat="server" ID="CstPXTextEdit15"
DataField="StoreAdminUrl"></px:PXTextEdit>
<px:PXLayoutRule GroupCaption="REST Settings" ColumnWidth="XL"
LabelsWidth="SM" StartGroup="True" runat="server" ID="CstLayoutRule24"></
px:PXLayoutRule>
<px:PXTextEdit runat="server" ID="CstPXTextEdit16"
DataField="StoreBaseUrl"></px:PXTextEdit>
<px:PXTextEdit runat="server" ID="CstPXTextEdit17"
DataField="StoreXAuthClient"></px:PXTextEdit>
<px:PXTextEdit runat="server" ID="CstPXTextEdit18"
DataField="StoreXAuthToken"></px:PXTextEdit>
<px:PXLayoutRule runat="server" ID="CstLayoutRule25"></
px:PXLayoutRule>
</Template>
</px:PXFormView>
<px:PXLayoutRule GroupCaption="Store Properties" runat="server"
ID="CstPXLayoutRule29" StartRow="True"></px:PXLayoutRule>
<px:PXFormView runat="server" ID="CstFormView30"
DataMember="CurrentBindingWooCommerce" RenderStyle="Simple">
<Template>
<px:PXTextEdit runat="server" ID="CstPXTextEdit31"
DataField="WooCommerceDefaultCurrency"></px:PXTextEdit>
<px:PXTextEdit runat="server" ID="CstPXTextEdit32"
DataField="WooCommerceStoreTimeZone"></px:PXTextEdit>
</Template>
</px:PXFormView>
</Template>
</px:PXTabItem>

[Link] unnecessary elements from the PXTabItem control that corresponds to the Customer Settings
tab, as shown in the following code.

<px:PXTabItem Text="Customer Settings">


<Template>
<px:PXLayoutRule runat="server" StartGroup="False" ControlSize="M" LabelsWidth="M"
StartColumn="True">
</px:PXLayoutRule>
Implementing a Connector for an E-Commerce System | 102

<px:PXLayoutRule GroupCaption="Customer" runat="server" ID="CstPXLayoutRule79"


StartGroup="True"></px:PXLayoutRule>
<px:PXSelector AllowEdit="True" ID="edCustomerClassID" runat="server"
DataField="CustomerClassID" CommitChanges="True">
</px:PXSelector>
<px:PXSelector runat="server" ID="CstPXSelector27" DataField="CustomerNumberingID"
AllowEdit="True"></px:PXSelector>
</Template>
<ContentLayout ControlSize="XM" LabelsWidth="M"></ContentLayout>
</px:PXTabItem>

[Link] the PXTabItem controls for the tabs that you do not need to use.

2. Adding the Form to the Customization Project


For the [Link] and [Link] files, do the following:
1. Open the customization project in the Customization Project Editor. (See To Open a Project for details.)
2. Click Files in the navigation pane to open the Custom Files page.
3. On the page toolbar, click Add New Record.
4. In the Add Files dialog box, which opens, find the file in the table and select the check box in the Selected
column for it.

• You can select multiple custom files to add them to the project at the same time.
• For any files other than the ones placed in the Bin folder, you can click Refresh on the
toolbar of the Add Files dialog box to make the system update the list of files in the table. If
you have changed files in the Bin folder of the website, you should refresh the page in the
browser.

5. In the dialog box, click Save to save each selected file to the customization project as a File item.

3. Adding the Form to the Site Map


1. Publish the customization project.
2. On the Enable/Disable Features (CS100000) form, enable the Commerce Integration feature which is listed
under Third Party Integrations.
3. In the navigation pane of the Customization Project Editor, click Site Map. The Site Map page opens.
4. On the page toolbar, click Manage Site Map.
The Site Map (SM200520) form opens.
5. On the form toolbar, click Add Row, and specify the site map position of the configuration form. For the
WooCommerce connector, you can specify the following settings:
• Screen ID: WO201000
• Title: WooCommerce Stores
• URL: ~/Pages/WO/[Link]
• Workspaces: Commerce
• Category: Configuration
6. Save your changes and close the form.
The new site map item has been created.
Implementing a Connector for an E-Commerce System | 103

7. Open the Commerce workspace. Notice that the WooCommerce Stores link is now available in the
workspace under Configuration.

4. Adding the Site Map Item to the Customization Project


1. Open the customization project in the Customization Project Editor. (See To Open a Project for details.)
2. Click Site Map in the navigation pane to open the Site Map page.
3. On the page toolbar, click Add New Record.
4. In the list of site map nodes in the Add Site Map dialog box, which opens, select the check box for each
screen that you want to include in the project.

The Add Site Map dialog box displays all the custom site map nodes that have been created in
the site map of Acumatica ERP and the nodes that have been modified in the site map. You can
select multiple custom site map nodes to add them to the project simultaneously.

5. In the dialog box, click Save to add each selected site map node to the customization project.

5. Adding the Form to the Screen Editor


You can edit a form created in Visual Studio both in Visual Studio and in the Screen Editor. To be able to edit the
form in the Screen Editor, you should add it to the Screen Editor by doing the following:
1. Open the customization project in the Customization Project Editor.
2. In the navigation pane, click Screens. The Customized Screens page opens.
3. On the page toolbar, click Customize Existing Screen.
4. In the Customize Existing Screen dialog box, which opens, select the configuration form.
5. Click OK.

Step 14: Testing the Connector

Now that you have completed the development of the connector, you will test the connector. For the testing, you
need a WordPress site with the WooCommerce plug-in installed. The instructions below describe the testing of the
WooCommerce Stores (WO201000) form.

Testing the Connector


1. On the Enable/Disable Features (CS100000) form, enable the following features:
• Customer Discounts (the corresponding check box can be found under Finance > Advanced Financials)
• Business Account Locations (the corresponding check box can be found under Finance > Standard
Financials)
2. On the WooCommerce Stores (WO201000) form, make sure the name in the Connector box is
WooCommerce.
3. In the Store Name box, type the name that will identify your store in Acumatica ERP, such as
WooCommerceTest.
4. On the Connection Settings tab, specify values in the following boxes:
• Store Admin Path: The URL of the WooCommerce admin portal, such as [Link]
wp-admin
Implementing a Connector for an E-Commerce System | 104

• API Path (in the REST Settings section): The request URL, including the version number, such as https://
[Link]/wp-json/wc/v3
• Consumer Key (in the REST Settings section): The API key that you have created on the WooCommerce
admin portal
• Consumer Secret (in the REST Settings section): The secret for the API key that you have created on the
WooCommerce admin portal
5. On the form toolbar, click Test Connection. If the connection is successful, the Default Currency and Store
Time Zone boxes in the Store Properties section are filled with the values from the store.
6. On the Entity Settings tab, select the Active check box for the Customer entity.
7. On the Customer Settings tab, specify values in the following boxes:
• Customer Class: The customer class that is assigned to new customers imported to Acumatica ERP from
the WooCommerce store
• Customer Auto-Numbering: The numbering sequence that the system uses for generating identifiers for
imported customers
8. Save your settings.
9. On the Prepare Data (BC501000) form, prepare the customer data for import as follows:
a. In the Entity box, select Customer.
b. In the table, select the Selected check box for the row with the Customer entity.
c. On the form toolbar, click Prepare.
d. In the Prepared Records column, click the link. The Process Data (BC501500) form opens.
[Link] the Process Data form, in the table, select the unlabeled check box for the customer records that should
be saved in Acumatica ERP.
[Link] the form toolbar, click Process.
[Link] the Customers (AR303000) form, review the imported customers.

To Implement Push Notifications for a Commerce Connector

To implement push notifications for a commerce connector, you need to perform the basic steps that are described
in this chapter.
The chapter presents an example of the integration of Acumatica ERP with WooCommerce in which you need
to export customers from Acumatica ERP to a WooCommerce store in real-time mode. This example is based on
the code and customization project prepared in To Create a Connector for an External System. In the Help-and-
Training-Examples repository on GitHub, you can find the files that are modified in this code to prepare the example
described in this chapter.

Step 1: Defining the Data for Real-Time Synchronization

You need to create generic inquiries or select existing ones that will define the data whose changes trigger sending
notifications from Acumatica ERP to a commerce connector.

1. Selecting the Push Notification Destination


1. On the Push Notifications (SM302000) form, select Commerce in the Destination Name box of the Summary
area, and review the list of generic inquiries that are predefined for the commerce push notifications on the
Generic Inquiries tab.
Implementing a Connector for an E-Commerce System | 105

2. If the list of generic inquiries is sufficient for your connector or you need to add multiple generic inquiries
for the entities that you want the system to send push notifications for, use the predefined Commerce
notification destination, skip the next instruction, and proceed with 2. Selecting the Generic Inquiries for
Real-Time Synchronization.
3. If you need a completely different list of generic inquiries for the entities that you want the system to send
push notifications for, create a custom notification destination as follows:
a. In the Destination Name box, type the name of the target notification destination, which can be the
name of your external application.
b. In the Destination Type box, select Commerce Push Destination. The Address box is filled in
automatically with Commerce.
c. Save your changes.

2. Selecting the Generic Inquiries for Real-Time Synchronization


1. Optional: On the Generic Inquiry (SM208000) form, create a generic inquiry or multiple generic inquiries with
the data whose changes trigger sending notifications from Acumatica ERP to a commerce connector. You
can create a generic inquiry either from the ground up or based on a copy of an existing generic inquiry. For
details on the creation of generic inquiries, see Managing Generic Inquiries.
2. On the Push Notifications (SM302000) form, select the notification destination that you decided to use in 1.
Selecting the Push Notification Destination, which has either the Commerce destination name or the custom
destination name.
3. For each generic inquiry for which you want Acumatica ERP to send notifications on changes in the inquiry
results, do the following on the Generic Inquiries tab:
a. On the table toolbar of the Inquiries table, click Add Row. The new row has the Active check box
selected.
b. In the Inquiry Title column of the added row, select the generic inquiry for which you want Acumatica
ERP to send notifications.
c. Do one of the following:
• If you want the system to send push notifications for changes in any field in the results of the generic
inquiry, select the Track All Fields check box in the added row.

If all fields in the results of the generic inquiry are tracked, the system produces push
notifications for changes of any field in the results of the generic inquiry, which can
cause overflow of the push notification queue. If you need to track only particular fields
in the results of the generic inquiry, push notifications for changes in other fields are
useless for you but consume system resources. Therefore, we recommend that you
specify particular fields to be tracked in the Fields table.

• If you need to track only particular fields in the results of the generic inquiry, while the row of the
Inquiries table is still selected, do the following in the Fields table for each field that you need to
track:
a. On the table toolbar, click Add Row.
b. In the Table Name column of the added row, select the name of the table that contains the field
that the system should track.
c. In the Field Name column, select the name of the field that the system should track.
4. On the form toolbar, click Save.
5. Add the generic inquiries that you included in the notification definition to the customization project
of the extension library for your commerce connector. For details about adding a generic inquiry to the
customization project, see To Add a Generic Inquiry to a Project.
Implementing a Connector for an E-Commerce System | 106

6. Include the notification definition in the customization project of your extension library. For details, see To
Add Push Notification Definitions to a Project.

Step 2: Supporting Push Notifications in the Connector

For the entities for which you need to support real-time synchronization through push notifications, you need
to adjust the connector descriptor class and the processor classes that correspond to these entities so that
the processor classes support real-time synchronization. For details about processor classes, see Processor
Classes. For information about the implementation of connector descriptor and processor classes, see Step
11: Implementing the Connector Descriptor Class and Step 8: Implementing the Processor Classes of To Create a
Connector for an External System.

Supporting Real-Time Synchronization in the Connector


1. Make sure the connector descriptor class implements the
[Link]() method. An example of the implementation is
shown in the following code.

public virtual Guid? GenerateLocalID(BCLocalNotification message)


{
Guid? noteId = [Link](v => [Link](
"NoteID", [Link])
&& [Link] != null).[Link]();
Byte[] bytes = new Byte[16];
[Link]([Link]()).CopyTo(
bytes, 0); //Connector
[Link]([Link]()).CopyTo(
bytes, 4); //EntityType
[Link]([Link]()).CopyTo(
bytes, 8); //Store
[Link]([Link]()).CopyTo(
bytes, 12); //ID
return new Guid(bytes);
}

2. For each processor class that needs to support real-time synchronization through push notifications, make
sure that the processor class supports the export of records from Acumatica ERP by checking the following
requirements:
• The BCProcessor attribute of the processor has the Direction property set to
[Link] or [Link].
• The FetchBucketsForExport(), GetBucketForExport(), MapBucketExport(),
SaveBucketExport(), and PullEntity() methods of the processor have been implemented. An
example of the implementation of the PullEntity() method is shown in the following code.
public override MappedCustomer PullEntity(Guid? localID,
Dictionary<string, object> fields)
{
[Link] impl =
[Link]<[Link]>(localID);
if (impl == null) return null;

MappedCustomer obj = new MappedCustomer(impl, [Link], [Link]);

return obj;
Implementing a Connector for an E-Commerce System | 107

3. For each processor class that needs to support real-time synchronization through push notifications, modify
the BCProcessorRealtime attribute of the processor so that it has the PushSupported property
set to true. In the PushSources property specify the names of the generic inquiries to be used for push
notifications. In the PushDestination property, specify the name of the push notification destination,
which is Commerce if you use the predefined notification destination.
The following code shows an example of the attribute for the customer processor class.

...
[BCProcessorRealtime(PushSupported = true, HookSupported = false,
PushSources = new String[] { "BC-PUSH-Customers" },
PushDestination = [Link]
)]
public class WooCustomerProcessor : BCProcessorSingleBase<WooCustomerProcessor,
WooCustomerEntityBucket, MappedCustomer>, IProcessor
{
...
}

4. Build the project of the extension library.

Related Links
• Real-Time Synchronization Through Push Notifications

Step 3: Testing Push Notifications

Now that you have completed the implementation of push notifications for the connector, you will test them. For
the testing, you need a WordPress site with the WooCommerce plug-in installed. The instructions below are based
on the assumption that you have already configured a WooCommerce store, as described in Step 14: Testing the
Connector of To Create a Connector for an External System.

Testing Push Notifications


1. In the Summary area of the Entities (BC202000) form, select the following values:
a. Store: The WooCommerce store that you have configured in Step 14: Testing the Connector of To Create a
Connector for an External System, such as WooCommerceTest
b. Entity: Customer
c. Sync Direction: Export
d. Real-Time Mode: Prepare
2. On the toolbar of the Entities form, click Start Real-Time Sync to start real-time synchronization. Make sure
the value in the Real-Time Export box becomes Running.

Once you have started the real-time synchronization, the system automatically activates the
notification destination (which is specified in the BCProcessorRealtime attribute of the
processor that corresponds to the entity) and the generic inquiry that corresponds to the
entity on the Push Notifications (SM302000) form.

3. On the Customers (AR303000) form, modify a customer record (for example, change the account name) and
save your changes.
4. Wait for 20 seconds for the push notification to be processed.
Implementing a Connector for an E-Commerce System | 108

5. On the Sync History (BC301000), make sure the modified customer record has appeared in the list on the
Ready to Process tab.

Related Links
• Real-Time Synchronization Through Push Notifications

You might also like