diff --git a/commerce_qb_webconnect.install b/commerce_qb_webconnect.install
index 30b88e6..17d60b2 100644
--- a/commerce_qb_webconnect.install
+++ b/commerce_qb_webconnect.install
@@ -48,6 +48,8 @@ function commerce_qb_webconnect_install() {
     drupal_set_message(t('A Quickbooks User account was unable to be created.  You will have to manually create a new user with the "Quickbooks User" role.'), 'error');
   }
 
+  // Add `qwc_owner_id` to import and export configuration.
+  commerce_qb_webconnect_ensure_qwc_owner_id();
 }
 
 /**
diff --git a/commerce_qb_webconnect.links.menu.yml b/commerce_qb_webconnect.links.menu.yml
index 171d2dc..2d37ffe 100644
--- a/commerce_qb_webconnect.links.menu.yml
+++ b/commerce_qb_webconnect.links.menu.yml
@@ -1,14 +1,33 @@
+commerce_qb_webconnect.configuration:
+  title: 'Quickbooks Webconnect Configuration'
+  route_name: 'commerce_qb_webconnect.configuration'
+  parent: 'commerce.configuration'
+  weight: 99
+
 commerce_qb_webconnect.quickbooks_admin_form:
-  title: 'Quickbooks Webconnect Settings'
+  title: 'Export Settings'
   route_name: commerce_qb_webconnect.quickbooks_admin_form
-  description: 'Commerce Quickbooks webconnect settings form'
-  parent: commerce.configuration
-  weight: 99
+  description: 'Commerce Quickbooks webconnect export settings form'
+  parent: commerce_qb_webconnect.configuration
+  weight: 1
 
 commerce_qb_webconnect.quickbooks_qwc_form:
-  title: 'Quickbooks QWC Generator'
+  title: 'Export QWC Generator'
   route_name: commerce_qb_webconnect.quickbooks_qwc_form
   description: 'Commerce Quickbooks QWC generation form'
-  parent: commerce_qb_webconnect.quickbooks_admin_form
-  weight: 99
+  parent: commerce_qb_webconnect.configuration
+  weight: 2
+
+commerce_qb_webconnect.import_settings_form:
+  title: 'Import Settings'
+  route_name: commerce_qb_webconnect.import_settings_form
+  description: 'Commerce Quickbooks webconnect import settings form'
+  parent: commerce_qb_webconnect.configuration
+  weight: 3
 
+commerce_qb_webconnect.import_qwc_form:
+  title: 'Import QWC Generator'
+  route_name: commerce_qb_webconnect.import_qwc_form
+  description: 'Commerce Quickbooks import QWC generation form'
+  parent: commerce_qb_webconnect.configuration
+  weight: 4
diff --git a/commerce_qb_webconnect.links.task.yml b/commerce_qb_webconnect.links.task.yml
index e0bcd16..ce9a8c5 100644
--- a/commerce_qb_webconnect.links.task.yml
+++ b/commerce_qb_webconnect.links.task.yml
@@ -1,9 +1,19 @@
 commerce_qb_webconnect.quickbooks_admin_form:
   route_name: commerce_qb_webconnect.quickbooks_admin_form
   base_route: commerce_qb_webconnect.quickbooks_admin_form
-  title: 'Configuration'
+  title: 'Export Settings'
+
 commerce_qb_webconnect.quickbooks_qwc_form:
   route_name: commerce_qb_webconnect.quickbooks_qwc_form
   base_route: commerce_qb_webconnect.quickbooks_admin_form
-  title: 'QWC Generator'
-  weight: 20
+  title: 'Export QWC Generator'
+
+commerce_qb_webconnect.import_settings_form:
+  route_name: commerce_qb_webconnect.import_settings_form
+  base_route: commerce_qb_webconnect.quickbooks_admin_form
+  title: 'Import Settings'
+
+commerce_qb_webconnect.import_qwc_form:
+  route_name: commerce_qb_webconnect.import_qwc_form
+  base_route: commerce_qb_webconnect.quickbooks_admin_form
+  title: 'Import QWC Generator'
diff --git a/commerce_qb_webconnect.module b/commerce_qb_webconnect.module
index 553118b..e9fa6db 100644
--- a/commerce_qb_webconnect.module
+++ b/commerce_qb_webconnect.module
@@ -2,11 +2,16 @@
 
 /**
  * @file
- * Contains commerce_qb_webconnect.module..
+ * Contains hook implementations for the commerce_qb_webconnect.module.
  */
 
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 
+use CommerceGuys\Addressing\Country\CountryRepository;
+
 /**
  * Implements hook_help().
  */
@@ -27,7 +32,8 @@ function commerce_qb_webconnect_help($route_name, RouteMatchInterface $route_mat
  * Implements hook_migration_plugins_alter().
  */
 function commerce_qb_webconnect_migration_plugins_alter(array &$migrations) {
-  $exportables = \Drupal::service('config.factory')->get('commerce_qb_webconnect.quickbooks_admin')->get('exportables');
+  $config_factory = \Drupal::service('config.factory');
+  $exportables = $config_factory->get('commerce_qb_webconnect.quickbooks_admin')->get('exportables');
 
   // Disable exporting payments if we aren't exporting as invoices.
   if (isset($exportables['order_type'])) {
@@ -43,4 +49,138 @@ function commerce_qb_webconnect_migration_plugins_alter(array &$migrations) {
     unset($migrations['qb_webconnect_product'], $migrations['qb_webconnect_product_variation']);
   }
 
+  /** Alterations for importing from Quickbooks */
+
+  // Only enable the specific migrations that have been enabled by the user.
+  $import_settings = $config_factory
+    ->get('commerce_qb_webconnect.import_settings');
+  $import_enabled = $import_settings->get('enabled');
+  $import_entity_types = $import_settings->get('entity_types');
+  if (!$import_enabled || !$import_entity_types['customer']) {
+    unset($migrations['qb_webconnect_customer_import']);
+  }
+  if (!$import_enabled || !$import_entity_types['product']) {
+    unset($migrations['qb_webconnect_product_import']);
+    unset($migrations['qb_webconnect_product_variation_import']);
+  }
+  if (!$import_enabled || !$import_entity_types['order']) {
+    unset($migrations['qb_webconnect_order_import']);
+  }
+
+  // Remove the payment import as Invoice imports have not been implemented yet.
+  unset($migrations['qb_webconnect_payment_import']);
+}
+
+/**
+ * Implements hook_entity_update().
+ *
+ * Update the migrate table to trigger the resync with Quickbooks if an order,
+ * payment, product, product variation, or customer profile was updated.
+ *
+ * @TODO: Figure out a better way to do this. How does migrate detect updates?
+ */
+function commerce_qb_webconnect_entity_update($entity) {
+  if (!$entity instanceof ContentEntityInterface) {
+    return;
+  }
+
+  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+  $entities_to_update = [
+    'commerce_order',
+    'commerce_payment',
+    'commerce_product',
+    'commerce_product_variation',
+    'profile',
+  ];
+  $entity_type_id = $entity->getEntityTypeId();
+  if (!in_array($entity_type_id, $entities_to_update)) {
+    return;
+  }
+
+  $migrations = \Drupal::service('plugin.manager.qb_webconnect_migration')
+    ->createInstancesByTag('QB Webconnect');
+  foreach ($migrations as $id => $migration) {
+    /** @var \Drupal\migrate\Plugin\Migration $migration */
+    $source_config = $migration->getSourceConfiguration();
+    if ($entity_type_id !== $source_config['entity_type']) {
+      continue;
+    }
+
+    /** @var \Drupal\migrate\Plugin\migrate\id_map\Sql $id_map */
+    // As we support both imports and exports the entity ID could be stored
+    // as either the source or destination.
+    // If the last action was an import from Quickbooks, the profile ID will be
+    // stored in the destid2 field. Whereas, if the last action was an export to
+    // Quickbooks, the profile ID will be stored in the sourceid1 field.
+    $id_map = $migration->getIdMap();
+    $database = \Drupal::database();
+    if ($database->schema()->tableExists($id_map->mapTableName())) {
+      $database
+        ->update($id_map->mapTableName())
+        ->condition('sourceid1', $entity->id())
+        ->fields(['source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE])
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_qb_webconnect_theme($existing, $type, $theme, $path) {
+  return [
+    'mod_invoice_qbxml' => [
+      'variables' => ['invoice' => NULL],
+    ],
+    'mod_sales_receipt_qbxml' => [
+      'variables' => ['invoice' => NULL],
+    ],
+  ];
+}
+
+/**
+ * Returns an array of countries keyed on the three letter ISO code.
+ *
+ * @return array
+ *   An array of countries.
+ */
+function commerce_qb_webconnect_get_country_codes() {
+  $countries = &drupal_static(__FUNCTION__);
+
+  if (!isset($countries)) {
+    $country_repository = new CountryRepository();
+    $countries_list = $country_repository->getAll();
+    // Now re-key them based on the three letter country code.
+    $countries = [];
+    foreach ($countries_list as $country) {
+      $countries[$country->getThreeLetterCode()] = $country->getCountryCode();
+    }
+  }
+
+  return $countries;
+}
+
+/**
+ * Ensures that `qwc_owner_id` are set for the import/export configuration.
+ *
+ * Should be called when the module is installed.
+ */
+function commerce_qb_webconnect_ensure_qwc_owner_id() {
+  $config_factory = \Drupal::service('config.factory');
+  $uuid = \Drupal::service('uuid');
+  $config_ids = [
+    'commerce_qb_webconnect.quickbooks_admin',
+    'commerce_qb_webconnect.import_settings',
+  ];
+
+  foreach ($config_ids as $config_id) {
+    $config = $config_factory->getEditable($config_id);
+    if (!$config->get('qwc_owner_id')) {
+      $config->set(
+        'qwc_owner_id',
+        $uuid->generate()
+      );
+      $config->save();
+    }
+  }
 }
diff --git a/commerce_qb_webconnect.post_update.php b/commerce_qb_webconnect.post_update.php
new file mode 100644
index 0000000..bff6b62
--- /dev/null
+++ b/commerce_qb_webconnect.post_update.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for Commerce Quickbooks Enterprise.
+ */
+
+/**
+ * Add `qwc_owner_id` to import and export configuration.
+ */
+function commerce_qb_webconnect_post_update_1() {
+  commerce_qb_webconnect_ensure_qwc_owner_id();
+}
diff --git a/commerce_qb_webconnect.routing.yml b/commerce_qb_webconnect.routing.yml
index 277c3d8..c0cf4ac 100644
--- a/commerce_qb_webconnect.routing.yml
+++ b/commerce_qb_webconnect.routing.yml
@@ -1,9 +1,26 @@
+commerce_qb_webconnect.configuration:
+  path: '/admin/commerce/config/commerce_qb_webconnect'
+  defaults:
+    _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
+    _title: 'Quickbooks Configuration'
+  requirements:
+    _permission: 'access administration pages'
 
 commerce_qb_webconnect.quickbooks_admin_form:
   path: '/admin/commerce/config/commerce_qb_webconnect/quickbook_sadmin'
   defaults:
     _form: '\Drupal\commerce_qb_webconnect\Form\QuickbooksAdminForm'
-    _title: 'Quickbooks Configuration'
+    _title: 'Quickbooks Export Configuration'
+  requirements:
+    _permission: 'access administration pages'
+  options:
+    _admin_route: TRUE
+
+commerce_qb_webconnect.import_settings_form:
+  path: '/admin/commerce/config/commerce_qb_webconnect/import_settings'
+  defaults:
+    _form: '\Drupal\commerce_qb_webconnect\Form\ImportSettingsForm'
+    _title: 'Quickbooks Import Configuration'
   requirements:
     _permission: 'access administration pages'
   options:
@@ -19,6 +36,16 @@ commerce_qb_webconnect.quickbooks_qwc_form:
   options:
     _admin_route: TRUE
 
+commerce_qb_webconnect.import_qwc_form:
+  path: '/admin/commerce/config/commerce_qb_webconnect/import_qwc'
+  defaults:
+    _form: '\Drupal\commerce_qb_webconnect\Form\ImportQwcForm'
+    _title: 'Quickbooks QWC Import'
+  requirements:
+    _permission: 'access administration pages'
+  options:
+    _admin_route: TRUE
+
 commerce_qb_webconnect.quickbooks_soap_controller:
   path: '/qb_soap'
   defaults:
@@ -26,3 +53,9 @@ commerce_qb_webconnect.quickbooks_soap_controller:
   requirements:
     _permission: 'access content'
 
+commerce_qb_webconnect.quickbooks_import_soap_controller:
+  path: '/qb_soap_import'
+  defaults:
+    _controller: '\Drupal\commerce_qb_webconnect\SoapBundle\ImportSoapServiceController::handleRequest'
+  requirements:
+    _permission: 'access content'
diff --git a/commerce_qb_webconnect.services.yml b/commerce_qb_webconnect.services.yml
index bed2ea6..37312ff 100644
--- a/commerce_qb_webconnect.services.yml
+++ b/commerce_qb_webconnect.services.yml
@@ -7,17 +7,36 @@ services:
     arguments: ['@state']
   commerce_qb_webconnect.validator:
     class: Drupal\commerce_qb_webconnect\SoapBundle\Services\Validator
+  commerce_qb_webconnect.import_soap_service:
+    class: Drupal\commerce_qb_webconnect\SoapBundle\Services\ImportSoapService
+    arguments:
+      - '@plugin.manager.qb_webconnect_migration'
+      - '@plugin.manager.migrate.id_map'
+      - '@user.auth'
+      - '@commerce_qb_webconnect.soap_session_manager'
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@state'
+      - '@datetime.time'
   commerce_qb_webconnect.soap_service:
     class: Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapService
     arguments:
-      - '@plugin.manager.migration'
+      - '@plugin.manager.qb_webconnect_migration'
       - '@plugin.manager.migrate.id_map'
       - '@user.auth'
       - '@commerce_qb_webconnect.soap_session_manager'
       - '@entity_type.manager'
       - '@config.factory'
       - '@state'
+      - '@renderer'
   commerce_qb_webconnect.migrate_subscriber:
     class: Drupal\commerce_qb_webconnect\EventSubscriber\MigrateSubscriber
     tags:
       - { name: event_subscriber }
+
+  plugin.manager.qb_webconnect_migration:
+    class: Drupal\commerce_qb_webconnect\Plugin\QbWebconnectMigrationPluginManager
+    arguments:
+      - '@module_handler'
+      - '@cache.discovery_migration'
+      - '@language_manager'
diff --git a/config/install/commerce_qb_webconnect.import_settings.yml b/config/install/commerce_qb_webconnect.import_settings.yml
new file mode 100644
index 0000000..243245e
--- /dev/null
+++ b/config/install/commerce_qb_webconnect.import_settings.yml
@@ -0,0 +1,9 @@
+enabled: false
+entity_types:
+  customer: false
+  product: false
+  order: false
+  payment_gateway: ''
+# We cannot provide a UUID here as it wouldn't be unique across projects.
+# Instead, it will be created by an install function.
+qwc_owner_id: ''
diff --git a/config/install/commerce_qb_webconnect.quickbooks_admin.yml b/config/install/commerce_qb_webconnect.quickbooks_admin.yml
index c6ed206..aaf41f0 100644
--- a/config/install/commerce_qb_webconnect.quickbooks_admin.yml
+++ b/config/install/commerce_qb_webconnect.quickbooks_admin.yml
@@ -11,4 +11,6 @@ adjustments:
 id_prefixes:
   po_number_prefix: ''
   payment_prefix: ''
-qwc_owner_id: b303d266-2429-4314-bd77-1259a3d163be
+# We cannot provide a UUID here as it wouldn't be unique across projects.
+# Instead, it will be created by an install function.
+qwc_owner_id: ''
diff --git a/config/schema/commerce_qb_webconnect.schema.yml b/config/schema/commerce_qb_webconnect.schema.yml
index 9c95c92..bb5b9c6 100644
--- a/config/schema/commerce_qb_webconnect.schema.yml
+++ b/config/schema/commerce_qb_webconnect.schema.yml
@@ -49,3 +49,34 @@ commerce_qb_webconnect.quickbooks_admin:
       type: string
       label: 'Quickbooks web connect owner id'
 
+# Import settings.
+# We store them separate as it is unlikely to be loaded together with the export
+# settings on the same request so there isn't any performance benefit in having
+# them on the same configuration object.
+commerce_qb_webconnect.import_settings:
+  type: config_object
+  label: 'Quickbooks webconnect import settings'
+  mapping:
+    enabled:
+      type: boolean
+      label: 'Whether data import is enabled or not'
+    # It seems that this needs to be different than the export QWC owner ID.
+    qwc_owner_id:
+      type: string
+      label: 'Quickbooks web connect owner id'
+    entity_types:
+      type: mapping
+      label: 'Entity types'
+      mapping:
+        product:
+          type: boolean
+          label: 'Import products'
+        customer:
+          type: boolean
+          label: 'Import customers'
+        order:
+          type: boolean
+          label: 'Import orders'
+        payment_gateway:
+          type: string
+          label: 'Payment gateway'
diff --git a/migrations/qb_webconnect_customer.yml b/migrations/qb_webconnect_customer.yml
index bd5f1a4..a57443a 100644
--- a/migrations/qb_webconnect_customer.yml
+++ b/migrations/qb_webconnect_customer.yml
@@ -21,3 +21,7 @@ process:
     source: address
 destination:
   plugin: commerce_qb_webconnect
+  keys:
+    - 'uuid'
+    - 'list_id'
+    - 'edit_sequence'
diff --git a/migrations/qb_webconnect_customer_import.yml b/migrations/qb_webconnect_customer_import.yml
new file mode 100644
index 0000000..eda6b5e
--- /dev/null
+++ b/migrations/qb_webconnect_customer_import.yml
@@ -0,0 +1,120 @@
+id: qb_webconnect_customer_import
+label: QB Webconnect Customer Import
+migration_tags:
+  - QB Webconnect Import
+source:
+  # We use the SOAP parser source plugin.
+  plugin: url
+  data_fetcher_plugin: http # Ignored - SoapClient does the fetching.
+  data_parser_plugin: commerce_qb_webconnect_qbxml_soap
+  # URL of a WSDL endpoint.
+  urls:
+    - ''
+  request_content: '<QBXML><QBXMLMsgsRs><CustomerQueryRs></CustomerQueryRs></QBXMLMsgsRs></QBXML>'
+  item_selector: /QBXML/QBXMLMsgsRs/CustomerQueryRs/CustomerRet
+  fields:
+    -
+      name: list_id
+      label: ListID
+      selector: ListID
+    -
+      name: email
+      label: Email
+      selector: Email
+    -
+      name: edit_sequence
+      label: EditSequence
+      selector: EditSequence
+    -
+      name: billing_first_name
+      label: First Name
+      selector: FirstName
+    -
+      name: billing_last_name
+      label: Last Name
+      selector: LastName
+    -
+      name: billing_street1
+      label: Addr1
+      selector: BillAddress/Addr1
+    -
+      name: billing_street2
+      label: Addr2
+      selector: BillAddress/Addr2
+    -
+      name: billing_city
+      label: City
+      selector: BillAddress/City
+    -
+      name: zone_code
+      label: State
+      selector: BillAddress/State
+    -
+      name: billing_postal_code
+      label: PostalCode
+      selector: BillAddress/PostalCode
+    -
+      name: country_iso_code_2
+      label: Country
+      selector: BillAddress/Country
+    -
+      name: billing_phone
+      label: Phone
+      selector: Phone
+    -
+      name: created
+      label: TimeCreated
+      selector: TimeCreated
+    -
+      name: modified
+      label: TimeModified
+      selector: TimeModified
+  ids:
+    list_id:
+      type: string
+  static:
+    send_callback: prepareCustomerImport
+process:
+  # This will be modified 'commerce_qb_webconnect_profile' destination plugin.
+  uid:
+    plugin: default_value
+    default_value: 0
+  type:
+    plugin: default_value
+    default_value: customer
+  status:
+    plugin: default_value
+    default_value: 1
+  is_default:
+    plugin: default_value
+    default_value: 0
+  'address/given_name': billing_first_name
+  'address/family_name': billing_last_name
+  'address/address_line1': billing_street1
+  'address/address_line2': billing_street2
+  'address/locality': billing_city
+  country:
+    plugin: skip_on_empty
+    method: process
+    source: country_iso_code_2
+  administrative_area:
+    plugin: skip_on_empty
+    method: process
+    source: zone_code
+  'address/administrative_area':
+    plugin: skip_on_empty
+    method: process
+    source: '@administrative_area'
+  'address/postal_code': billing_postal_code
+  'address/country_code': '@country'
+  phone: billing_phone
+  created:
+    plugin: callback
+    source: created
+    callable: strtotime
+  changed:
+    plugin: callback
+    source: modified
+    callable: strtotime
+destination:
+  plugin: commerce_qb_webconnect_profile_entity
diff --git a/migrations/qb_webconnect_order.yml b/migrations/qb_webconnect_order.yml
index bb0851b..f12b81c 100644
--- a/migrations/qb_webconnect_order.yml
+++ b/migrations/qb_webconnect_order.yml
@@ -18,6 +18,10 @@ process:
     source: billing_profile__target_id
 destination:
   plugin: commerce_qb_webconnect
+  keys:
+    - 'uuid'
+    - 'list_id'
+    - 'edit_sequence'
 migration_dependencies:
   required:
     - qb_webconnect_customer
diff --git a/migrations/qb_webconnect_order_import.yml b/migrations/qb_webconnect_order_import.yml
new file mode 100644
index 0000000..92cc610
--- /dev/null
+++ b/migrations/qb_webconnect_order_import.yml
@@ -0,0 +1,138 @@
+id: qb_webconnect_order_import
+label: QB Webconnect Order Import
+migration_tags:
+  - QB Webconnect Import
+source:
+  # We use the SOAP parser source plugin.
+  plugin: url
+  data_fetcher_plugin: http # Ignored - SoapClient does the fetching.
+  data_parser_plugin: commerce_qb_webconnect_qbxml_soap
+  # URL of a WSDL endpoint.
+  urls:
+    - ''
+  request_content: '<QBXML><QBXMLMsgsRs><SalesReceiptQueryRs></SalesReceiptQueryRs></QBXMLMsgsRs></QBXML>'
+  item_selector: /QBXML/QBXMLMsgsRs/SalesReceiptQueryRs/SalesReceiptRet
+  fields:
+    -
+      name: list_id
+      label: Txn ID
+      selector: TxnID
+    -
+      name: txn_number
+      label: Txn Number
+      selector: TxnNumber
+    -
+      name: txn_date
+      label: Txn Date
+      selector: TxnDate
+    -
+      name: ref_number
+      label: Reference Number
+      selector: RefNumber
+    -
+      name: edit_sequence
+      label: EditSequence
+      selector: EditSequence
+    -
+      name: customer_list_id
+      label: Customer
+      selector: CustomerRef/ListID
+    -
+      name: is_pending
+      label: Is Pending
+      selector: IsPending
+    -
+      name: payment_method_list_id
+      label: Payment Method
+      selector: PaymentMethodRef/ListID
+    -
+      name: sub_total
+      label: Sub Total
+      selector: Subtotal
+    -
+      name: total_amount
+      label: Total Amount
+      selector: TotalAmount
+    -
+      name: line_items
+      label: Line Items
+      selector: SalesReceiptLineRet
+    -
+      name: discount_line
+      label: Discount Line
+      selector: DiscountLineRet
+    -
+      name: sales_tax_line
+      label: Sales Tax Line
+      selector: SalesTaxLineRet
+    -
+      name: shipping_line
+      label: Shipping Line
+      selector: ShippingLineRet
+    -
+      name: created
+      label: TimeCreated
+      selector: TimeCreated
+    -
+      name: modified
+      label: TimeModified
+      selector: TimeModified
+  ids:
+    list_id:
+      type: string
+  static:
+    send_callback: prepareOrderImport
+process:
+  # This will be modified in the 'commerce_qb_webconnect_order' destination plugin.
+  type:
+    plugin: default_value
+    default_value: default
+  order_number: list_id
+  store_id:
+    plugin: default_value
+    default_value: 1
+  billing_profile_data:
+    -
+      plugin: migration_lookup
+      migration:
+        - qb_webconnect_customer_import
+        - qb_webconnect_customer
+      source: customer_list_id
+  billing_profile/target_id:
+    -
+      plugin: skip_on_empty
+      method: process
+      source: '@billing_profile_data'
+    -
+      plugin: extract
+      index: [0]
+  state:
+    plugin: default_value
+    default_value: completed
+  payment_gateway:
+    plugin: default_value
+    default_value: quickbooks_payment
+  ip_address: host
+  created:
+    plugin: callback
+    source: created
+    callable: strtotime
+  changed:
+    plugin: callback
+    source: modified
+    callable: strtotime
+  placed:
+    plugin: callback
+    source: txn_date
+    callable: strtotime
+  completed:
+    plugin: callback
+    source: txn_date
+    callable: strtotime
+destination:
+  plugin: commerce_qb_webconnect_commerce_order_entity
+migration_dependencies:
+  required:
+    - qb_webconnect_customer_import
+    - qb_webconnect_product_import
+    - qb_webconnect_product_variation_import
diff --git a/migrations/qb_webconnect_payment.yml b/migrations/qb_webconnect_payment.yml
index d5f63bc..6787202 100644
--- a/migrations/qb_webconnect_payment.yml
+++ b/migrations/qb_webconnect_payment.yml
@@ -18,6 +18,10 @@ process:
     source: order_id
 destination:
   plugin: commerce_qb_webconnect
+  keys:
+    - 'uuid'
+    - 'list_id'
+    - 'edit_sequence'
 migration_dependencies:
   required:
     - qb_webconnect_order
diff --git a/migrations/qb_webconnect_payment_import.yml b/migrations/qb_webconnect_payment_import.yml
new file mode 100644
index 0000000..3cc2a4c
--- /dev/null
+++ b/migrations/qb_webconnect_payment_import.yml
@@ -0,0 +1,98 @@
+id: qb_webconnect_payment_import
+label: QB Webconnect Payment Import
+migration_tags:
+  - QB Webconnect Import
+source:
+  # We use the SOAP parser source plugin.
+  plugin: url
+  data_fetcher_plugin: http # Ignored - SoapClient does the fetching.
+  data_parser_plugin: commerce_qb_webconnect_qbxml_soap
+  # URL of a WSDL endpoint.
+  urls:
+    - ''
+  request_content: '<QBXML><QBXMLMsgsRs><ReceivePaymentQueryRs></ReceivePaymentQueryRs></QBXMLMsgsRs></QBXML>'
+  item_selector: /QBXML/QBXMLMsgsRs/ReceivePaymentQueryRs/ReceivePaymentRet
+  fields:
+    -
+      name: list_id
+      label: Txn ID
+      selector: TxnID
+    -
+      name: txn_number
+      label: Txn Number
+      selector: TxnNumber
+    -
+      name: txn_date
+      label: Txn Date
+      selector: TxnDate
+    -
+      name: txn_number
+      label: Txn Number
+      selector: TxnNumber
+    -
+      name: ref_number
+      label: Reference Number
+      selector: RefNumber
+    -
+      name: edit_sequence
+      label: EditSequence
+      selector: EditSequence
+    -
+      name: customer_list_id
+      label: Customer
+      selector: CustomerRef/ListID
+    -
+      name: applied_to_txn
+      label: Applied to Transaction
+      selector: AppliedToTxnRet
+    -
+      name: payment_method_list_id
+      label: Payment Method
+      selector: PaymentMethodRef/ListID
+    -
+      name: total_amount
+      label: Total Amount
+      selector: TotalAmount
+    -
+      name: created
+      label: TimeCreated
+      selector: TimeCreated
+    -
+      name: modified
+      label: TimeModified
+      selector: TimeModified
+  ids:
+    list_id:
+      type: string
+  static:
+    send_callback: preparePaymentImport
+process:
+  # This will be modified in the 'commerce_qb_webconnect_payment' destination plugin.
+  payment_gateway:
+    plugin: default_value
+    default_value: quickbooks_payment
+  type:
+    plugin: default_value
+    default_value: payment_manual
+  order_id:
+    -
+      plugin: migration_lookup
+      migration:
+        - qb_webconnect_order_import
+        - qb_webconnect_order
+      source: ref_number
+    -
+      plugin: skip_on_empty
+      method: row
+  'amount/number': total_amount
+  'amount/currency_code':
+    plugin: default_value
+    default_value: USD
+  state:
+    plugin: default_value
+    default_value: completed
+destination:
+  plugin: commerce_qb_webconnect_commerce_payment_entity
+migration_dependencies:
+  required:
+    - qb_webconnect_order_import
diff --git a/migrations/qb_webconnect_product._variation.yml b/migrations/qb_webconnect_product._variation.yml
index c2fe70a..3309060 100644
--- a/migrations/qb_webconnect_product._variation.yml
+++ b/migrations/qb_webconnect_product._variation.yml
@@ -18,6 +18,10 @@ process:
     source: product_id
 destination:
   plugin: commerce_qb_webconnect
+  keys:
+    - 'uuid'
+    - 'list_id'
+    - 'edit_sequence'
 migration_dependencies:
   required:
     - qb_webconnect_product
diff --git a/migrations/qb_webconnect_product.yml b/migrations/qb_webconnect_product.yml
index 8e7b723..55b86da 100644
--- a/migrations/qb_webconnect_product.yml
+++ b/migrations/qb_webconnect_product.yml
@@ -14,6 +14,10 @@ source:
 process: {}
 destination:
   plugin: commerce_qb_webconnect
+  keys:
+    - 'uuid'
+    - 'list_id'
+    - 'edit_sequence'
 migration_dependencies:
   optional:
     - qb_webconnect_customer
diff --git a/migrations/qb_webconnect_product_import.yml b/migrations/qb_webconnect_product_import.yml
new file mode 100644
index 0000000..80df6a6
--- /dev/null
+++ b/migrations/qb_webconnect_product_import.yml
@@ -0,0 +1,106 @@
+id: qb_webconnect_product_import
+label: QB Webconnect Product Import
+migration_tags:
+  - QB Webconnect Import
+source:
+  # We use the SOAP parser source plugin.
+  plugin: url
+  data_fetcher_plugin: http # Ignored - SoapClient does the fetching.
+  data_parser_plugin: commerce_qb_webconnect_qbxml_soap
+  # URL of a WSDL endpoint.
+  urls:
+    - ''
+  request_content: '<QBXML><QBXMLMsgsRs><ItemInventoryQueryRs></ItemInventoryQueryRs></QBXMLMsgsRs></QBXML>'
+  item_selector: /QBXML/QBXMLMsgsRs/ItemInventoryQueryRs/ItemInventoryRet
+  fields:
+    -
+      name: list_id
+      label: ListID
+      selector: ListID
+    -
+      name: edit_sequence
+      label: EditSequence
+      selector: EditSequence
+    -
+      name: name
+      label: Name
+      selector: Name
+    -
+      name: sub_level
+      label: Sub Level
+      selector: Sublevel
+    -
+      name: manufacturer_part_number
+      label: SKU
+      selector: ManufacturerPartNumber
+    -
+      name: sales_description
+      label: Description
+      selector: SalesDesc
+    -
+      name: sales_price
+      label: Price
+      selector: SalesPrice
+    -
+      name: purchase_cost
+      label: Purchase Cost
+      selector: PurchaseCost
+    -
+      name: created
+      label: TimeCreated
+      selector: TimeCreated
+    -
+      name: modified
+      label: TimeModified
+      selector: TimeModified
+    -
+      name: income_account_full_name
+      label: Income Account Full Name
+      selector: IncomeAccountRef/FullName
+    -
+      name: is_active
+      label: Is Active
+      selector: IsActive
+  ids:
+    list_id:
+      type: string
+  static:
+    send_callback: prepareProductImport
+process:
+  # This will be modified in the 'commerce_qb_webconnect_commerce_product' destination plugin.
+  type:
+    plugin: default_value
+    default_value: default
+  # Only process parent inventory items.
+  sub_level:
+    plugin: skip_on_value
+    method: row
+    source: sub_level
+    not_equals: true
+    value:
+      - 0
+  uid:
+    plugin: default_value
+    default_value: 1
+  title: name
+  'body/value': sales_description
+  'body/format':
+    plugin: default_value
+    default_value: html
+  stores:
+    plugin: default_value
+    default_value: 1
+  # Import as a disabled product as the incoming information might be insufficient.
+  status:
+    plugin: default_value
+    default_value: 0
+  created:
+    plugin: callback
+    source: created
+    callable: strtotime
+  changed:
+    plugin: callback
+    source: modified
+    callable: strtotime
+destination:
+  plugin: commerce_qb_webconnect_commerce_product_entity
diff --git a/migrations/qb_webconnect_product_variation_import.yml b/migrations/qb_webconnect_product_variation_import.yml
new file mode 100644
index 0000000..752778a
--- /dev/null
+++ b/migrations/qb_webconnect_product_variation_import.yml
@@ -0,0 +1,125 @@
+id: qb_webconnect_product_variation_import
+label: QB Webconnect Product Variation Import
+migration_tags:
+  - QB Webconnect Import
+source:
+  # We use the SOAP parser source plugin.
+  plugin: url
+  data_fetcher_plugin: http # Ignored - SoapClient does the fetching.
+  data_parser_plugin: commerce_qb_webconnect_qbxml_soap
+  # URL of a WSDL endpoint.
+  urls:
+    - ''
+  request_content: '<QBXML><QBXMLMsgsRs><ItemInventoryQueryRs></ItemInventoryQueryRs></QBXMLMsgsRs></QBXML>'
+  item_selector: /QBXML/QBXMLMsgsRs/ItemInventoryQueryRs/ItemInventoryRet
+  fields:
+    -
+      name: list_id
+      label: ListID
+      selector: ListID
+    -
+      name: edit_sequence
+      label: EditSequence
+      selector: EditSequence
+    -
+      name: name
+      label: Name
+      selector: Name
+    -
+      name: sub_level
+      label: Sub Level
+      selector: Sublevel
+    -
+      name: parent_list_id
+      label: Parent Product
+      selector: ParentRef/ListID
+    -
+      name: manufacturer_part_number
+      label: SKU
+      selector: ManufacturerPartNumber
+    -
+      name: sales_description
+      label: Description
+      selector: SalesDesc
+    -
+      name: sales_price
+      label: Price
+      selector: SalesPrice
+    -
+      name: purchase_cost
+      label: Purchase Cost
+      selector: PurchaseCost
+    -
+      name: created
+      label: TimeCreated
+      selector: TimeCreated
+    -
+      name: modified
+      label: TimeModified
+      selector: TimeModified
+    -
+      name: income_account_full_name
+      label: Income Account Full Name
+      selector: IncomeAccountRef/FullName
+    -
+      name: quantity_on_hand
+      label: Quantity On Hand
+      selector: QuantityOnHand
+    -
+      name: is_active
+      label: Is Active
+      selector: IsActive
+  ids:
+    list_id:
+      type: string
+  static:
+    send_callback: prepareProductVariationImport
+process:
+  # This will be modified in the 'commerce_qb_webconnect_commerce_product_variation' destination plugin.
+  type:
+    plugin: default_value
+    default_value: default
+  # Only process child inventory items.
+  sub_level:
+    plugin: skip_on_value
+    method: row
+    source: sub_level
+    equals: false
+    value:
+      - 0
+  uid:
+    plugin: default_value
+    default_value: 1
+  sku: manufacturer_part_number
+  title: name
+  'price/number': sales_price
+  'price/currency_code':
+    plugin: default_value
+    default_value: USD
+  'list_price/number': purchase_cost
+  'list_price/currency_code':
+    plugin: default_value
+    default_value: USD
+  product_id:
+    -
+      plugin: migration_lookup
+      migration:
+        - qb_webconnect_product_import
+        - qb_webconnect_product
+      source: parent_list_id
+  status:
+    plugin: default_value
+    default_value: 1
+  created:
+    plugin: callback
+    source: created
+    callable: strtotime
+  changed:
+    plugin: callback
+    source: modified
+    callable: strtotime
+destination:
+  plugin: commerce_qb_webconnect_commerce_product_variation_entity
+migration_dependencies:
+  required:
+    - qb_webconnect_product_import
diff --git a/src/Form/ImportQwcForm.php b/src/Form/ImportQwcForm.php
new file mode 100644
index 0000000..30c0550
--- /dev/null
+++ b/src/Form/ImportQwcForm.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Form;
+
+/**
+ * Form for generating the import QWC file.
+ *
+ * @package Drupal\commerce_qb_webconnect\Form
+ */
+class ImportQwcForm extends QwcFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'quickbooks_qwc_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultAppUrlRoute() {
+    return 'commerce_qb_webconnect.quickbooks_import_soap_controller';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getConfigId() {
+    return 'commerce_qb_webconnect.import_settings';
+  }
+
+}
diff --git a/src/Form/ImportSettingsForm.php b/src/Form/ImportSettingsForm.php
new file mode 100644
index 0000000..7daa399
--- /dev/null
+++ b/src/Form/ImportSettingsForm.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Form;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form for the Quickbooks import settings.
+ *
+ * @package Drupal\commerce_qb_webconnect\Form
+ */
+class ImportSettingsForm extends ConfigFormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new ImportSettingsForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The user storage.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'commerce_qb_webconnect.import_settings',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'qb_webconnect_import_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('commerce_qb_webconnect.import_settings');
+
+    $form['#tree'] = TRUE;
+
+    $form['enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable data imports'),
+      '#default_value' => $config->get('enabled'),
+    ];
+
+    $wrapper_id = Html::getUniqueId('commerce-qb-webconnect-import-entity-types-settings');
+    $form['entity_types'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Import data'),
+      '#states' => [
+        'visible' => [
+          ':input[data-drupal-selector="edit-enabled"]' => ['checked' => TRUE],
+        ],
+      ],
+      '#prefix' => '<div id="' . $wrapper_id . '">',
+      '#suffix' => '</div>',
+    ];
+    $entity_types = [
+      'customer' => [
+        'title' => 'Customers',
+      ],
+      'product' => [
+        'title' => 'Products',
+      ],
+      'order' => [
+        'title' => 'Orders',
+        'description' => 'At the moment, only Sales Receipts will be imported',
+      ],
+    ];
+
+    $triggering_element = $form_state->getTriggeringElement();
+    foreach ($entity_types as $id => $info) {
+      $form['entity_types'][$id] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t($info['title']),
+        '#description' => isset($info['description']) ? $this->t($info['description']) : '',
+        '#default_value' => $config->get('entity_types')[$id],
+        '#disabled' => $id !== 'order'
+          && !$triggering_element
+          && $config->get('entity_types')['order'] == TRUE,
+      ];
+
+      if ($id === 'order') {
+        $form['entity_types']['order']['#ajax'] = [
+          'callback' => '::ajaxRefresh',
+          'wrapper' => $wrapper_id,
+        ];
+      }
+
+      // If order migration is checked, then, customers and products must also
+      // be checked, as we need to import those first for orders to come in.
+      if ($triggering_element) {
+        $order_value = $form_state->getUserInput()['entity_types']['order'];
+        if ($order_value) {
+          if ($id !== 'order') {
+            $form['entity_types'][$id]['#disabled'] = TRUE;
+          }
+          $form['entity_types'][$id]['#default_value'] = TRUE;
+        }
+      }
+    }
+
+    // Add a field to allow the user to select which manual payment gateway to
+    // use when importing orders.
+    $options = [];
+    $payment_gateways = $this
+      ->entityTypeManager
+      ->getStorage('commerce_payment_gateway')
+      ->loadByProperties(['plugin' => 'manual']);
+    foreach ($payment_gateways as $payment_gateway) {
+      $options[$payment_gateway->id()] = $payment_gateway->label();
+    }
+    $form['entity_types']['payment_gateway'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Payment gateway'),
+      '#description' => $this->t('Select the payment gateway to use when recording the payment for an order'),
+      '#options' => $options,
+      '#states' => [
+        'visible' => [
+          ':input[name="entity_types[order]"]' => ['checked' => TRUE]
+        ],
+      ],
+      '#default_value' => $config->get('entity_types')['payment_gateway'],
+    ];
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    $values = $form_state->getValues();
+
+    $config = $this->config('commerce_qb_webconnect.import_settings');
+    $config->set('enabled', (bool) $values['enabled']);
+
+    foreach ($values['entity_types'] as $entity_type => $value) {
+      if ($entity_type != 'payment_gateway') {
+        $value = (bool) $value;
+      }
+      $config->set(
+        "entity_types.$entity_type",
+        $value
+      );
+    }
+    $config->save();
+  }
+
+  /**
+   * Refresh the entity_types settings form fields.
+   */
+  public function ajaxRefresh(&$form, FormStateInterface $form_state) {
+    return $form['entity_types'];
+  }
+
+}
diff --git a/src/Form/QuickbooksAdminForm.php b/src/Form/QuickbooksAdminForm.php
index f10b982..1ed1b44 100644
--- a/src/Form/QuickbooksAdminForm.php
+++ b/src/Form/QuickbooksAdminForm.php
@@ -34,14 +34,6 @@ class QuickbooksAdminForm extends ConfigFormBase {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $config = $this->config('commerce_qb_webconnect.quickbooks_admin');
 
-    // Check if we have a GUID before creating one.
-    if (empty($config->get('qwc_owner_id'))) {
-      $uuid = \Drupal::service('uuid');
-      $qwc_owner_id = $uuid->generate();
-    }
-    else {
-      $qwc_owner_id = $config->get('qwc_owner_id');
-    }
     $form['#tree'] = TRUE;
     $form['exportables'] = [
       '#type' => 'fieldset',
@@ -140,11 +132,6 @@ class QuickbooksAdminForm extends ConfigFormBase {
       '#default_value' => $config->get('id_prefixes')['payment_prefix'],
     ];
 
-    $form['qwc_owner_id'] = [
-      '#type' => 'hidden',
-      '#value' => $qwc_owner_id,
-    ];
-
     return parent::buildForm($form, $form_state);
   }
 
diff --git a/src/Form/QuickbooksQWCForm.php b/src/Form/QuickbooksQWCForm.php
index d97b4e3..9e65f55 100644
--- a/src/Form/QuickbooksQWCForm.php
+++ b/src/Form/QuickbooksQWCForm.php
@@ -2,106 +2,12 @@
 
 namespace Drupal\commerce_qb_webconnect\Form;
 
-use Drupal\Component\Uuid\UuidInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Url;
-use Drupal\user\Entity\User;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
-use Symfony\Component\HttpFoundation\RequestStack;
-
 /**
  * Class QuickbooksQWCForm.
  *
  * @package Drupal\commerce_qb_webconnect\Form
  */
-class QuickbooksQWCForm extends FormBase {
-
-  /**
-   * The current request.
-   *
-   * @var null|\Symfony\Component\HttpFoundation\Request
-   */
-  protected $request;
-
-  /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The UUID service.
-   *
-   * @var \Drupal\Component\Uuid\UuidInterface
-   */
-  protected $uuid;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The renderer service.
-   *
-   * @var \Drupal\Core\Render\RendererInterface
-   */
-  protected $renderer;
-
-  /**
-   * Constructs a new QuickbooksQWCForm.
-   *
-   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
-   *   The request.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
-   *   The module handler.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
-   *   The entity type manager.
-   * @param \Drupal\Component\Uuid\UuidInterface $uuid
-   *   The UUID service.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
-   *   The config factory.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer service.
-   */
-  public function __construct(RequestStack $requestStack, ModuleHandlerInterface $moduleHandler, EntityTypeManagerInterface $entityTypeManager, UuidInterface $uuid, ConfigFactoryInterface $configFactory, RendererInterface $renderer) {
-    $this->request = $requestStack->getCurrentRequest();
-    $this->moduleHandler = $moduleHandler;
-    $this->entityTypeManager = $entityTypeManager;
-    $this->uuid = $uuid;
-    $this->configFactory = $configFactory;
-    $this->renderer = $renderer;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('request_stack'),
-      $container->get('module_handler'),
-      $container->get('entity_type.manager'),
-      $container->get('uuid'),
-      $container->get('config.factory'),
-      $container->get('renderer')
-    );
-  }
+class QuickbooksQWCForm extends QwcFormBase {
 
   /**
    * {@inheritdoc}
@@ -113,160 +19,15 @@ class QuickbooksQWCForm extends FormBase {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    // Determine if we're using a secure connection, and get the domain.
-    $secure = $this->request->isSecure();
-
-    // Useful path storage.
-    $qbwc_path = Url::fromRoute('commerce_qb_webconnect.quickbooks_soap_controller', [], ['absolute' => TRUE])->toString();
-    if ($this->moduleHandler->moduleExists('help')) {
-      $help_path = Url::fromRoute('help.page', ['name' => 'commerce_qb_webconnect'], ['absolute' => TRUE])->toString();
-    }
-
-    // Get all users with the 'access quickbooks soap service' permission.
-    $ids = $this->entityTypeManager->getStorage('user')->getQuery()
-      ->condition('status', 1)
-      ->condition('roles', 'quickbooks_user')
-      ->execute();
-    $users = User::loadMultiple($ids);
-
-    $user_options = [];
-
-    if (!empty($users)) {
-      foreach ($users as $user) {
-        $name = $user->getAccountName();
-        $user_options[$name] = $name;
-      }
-    }
-
-    // Generate a FileID (GUID for Quickbooks), and load our
-    // OwnerID (GUID for server).
-    $file_id = $this->uuid->generate();
-    $owner_id = $this->configFactory->get('commerce_qb_webconnect.quickbooks_admin')->get('qwc_owner_id');
-    $config_set = 0;
-
-    if (empty($owner_id)) {
-      // If the Owner ID is empty, then the user hasn't configured the module
-      // yet. That's OK, but it means we need to generate the Owner ID here and
-      // let the configuration know we've done so.
-      $owner_id = $this->uuid->generate();
-      $config_set = 1;
-    }
-
-    // Finished pre-setup, create form now.
-    $form['container'] = [
-      '#type' => 'fieldset',
-      '#title' => $this->t('QWC generation form'),
-    ];
-
-    $form['container']['app_name'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('App Name'),
-      '#description' => $this->t('The name of the application visible to the user. This name is displayed in the QB web connector. It is also the name supplied in the SDK OpenConnection call to QuickBooks or QuickBooks POS'),
-      '#maxlength' => 32,
-      '#size' => 64,
-      '#default_value' => '',
-      '#required' => TRUE,
-    ];
-
-    $description = $this->t('The URL of your web service.  For internal development and testing only, you can specify localhost or a machine name in place of the domain name.');
-    $form['container']['app_url'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('App URL'),
-      '#description' => $secure
-      ? $description
-      : $this->t('WARNING: Only local testing can be made over an insecure connection. :::') . ' ' . $description,
-      '#maxlength' => 64,
-      '#size' => 64,
-      '#default_value' => $qbwc_path,
-      '#required' => TRUE,
-    ];
-
-    $form['container']['app_support'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Support URL'),
-      '#description' => $this->t('The support URL.  This can most likely stay unchanged, but if change is desired then the domain or machine name must match the App URL domain or machine name.'),
-      '#size' => 64,
-      '#default_value' => empty($help_path) ?: $help_path,
-      '#required' => TRUE,
-    ];
-
-    $form['container']['user_name'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Quickbooks User'),
-      '#description' => $this->t("A user with specific permission to access the SOAP service calls on your site.  This list is populated by users with the 'access quickbooks soap service' permission."),
-      '#options' => $user_options,
-      '#required' => TRUE,
-    ];
-
-    $form['container']['file_id'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('File ID'),
-      '#description' => $this->t('An ID assigned to your Quickbooks application.  This should be left alone, but if necessary can be replaced if you have a working GUID already.'),
-      '#default_value' => $file_id,
-      '#maxlength' => 36,
-      '#size' => 64,
-      '#required' => TRUE,
-    ];
-
-    $form['owner_id'] = [
-      '#type' => 'hidden',
-      '#value' => $owner_id,
-    ];
-
-    $form['config_set'] = [
-      '#type' => 'hidden',
-      '#value' => $config_set,
-    ];
-
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Download QWC file'),
-    ];
-
-    return $form;
+  protected function getDefaultAppUrlRoute() {
+    return 'commerce_qb_webconnect.quickbooks_soap_controller';
   }
 
   /**
    * {@inheritdoc}
    */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->cleanValues()->getValues();
-    $app_name = $description = $app_url = $app_support = $user_name = $file_id = $owner_id = NULL;
-    extract($form_values, EXTR_IF_EXISTS);
-
-    // Update the config if we set a new Owner ID in this form.
-    $update_config = $form_values['config_set'];
-    unset($form_values['config_set']);
-    if ($update_config) {
-      $this->configFactory
-        ->getEditable('commerce_qb_webconnect.quickbooks_admin')
-        ->set('qwc_owner_id', $form_values['owner_id'])
-        ->save();
-    }
-
-    // Generate the XML file.
-    $qwc = new \QuickBooks_WebConnector_QWC($app_name, $description, $app_url, $app_support, $user_name, $file_id, $owner_id);
-    $xml = $qwc->generate();
-
-    // Save the generated QWC file as SERVER_HOST.qwc.
-    $file = file_save_data($xml, 'private://' . $this->request->getHost() . '.qwc');
-
-    if ($file) {
-      $uri = $file->getFileUri();
-
-      // Automatically sets content headers and opens the file stream.
-      // In order to get the full file name and attachment, we set the content
-      // type to 'text/xml' and the disposition to 'attachment'.  We also delete
-      // the file after sending it to ensure prying eyes don't find it later.
-      $response = new BinaryFileResponse($uri, 200, [], TRUE, 'attachment');
-      $response->deleteFileAfterSend(TRUE);
-
-      $form_state->setResponse($response);
-    }
-    else {
-      drupal_set_message(t('Unable to generate QWC file, check the log files for full details.'));
-    }
+  protected function getConfigId() {
+    return 'commerce_qb_webconnect.quickbooks_admin';
   }
 
 }
diff --git a/src/Form/QwcFormBase.php b/src/Form/QwcFormBase.php
new file mode 100644
index 0000000..7f202fb
--- /dev/null
+++ b/src/Form/QwcFormBase.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Form;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Url;
+use Drupal\user\Entity\User;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Class QuickbooksQWCForm.
+ *
+ * @package Drupal\commerce_qb_webconnect\Form
+ */
+abstract class QwcFormBase extends FormBase {
+
+  /**
+   * The current request.
+   *
+   * @var null|\Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The UUID service.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuid;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Returns the route for the default application URL.
+   *
+   * @return string
+   *   A valid route name.
+   */
+  abstract protected function getDefaultAppUrlRoute();
+
+  /**
+   * Returns the config object ID that contains the QWC owner ID.
+   *
+   * @return string
+   *   A valid config object ID.
+   */
+  abstract protected function getConfigId();
+
+  /**
+   * Constructs a new QuickbooksQWCForm.
+   *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
+   *   The request.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid
+   *   The UUID service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(RequestStack $requestStack, ModuleHandlerInterface $moduleHandler, EntityTypeManagerInterface $entityTypeManager, UuidInterface $uuid, ConfigFactoryInterface $configFactory, RendererInterface $renderer) {
+    $this->request = $requestStack->getCurrentRequest();
+    $this->moduleHandler = $moduleHandler;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->uuid = $uuid;
+    $this->configFactory = $configFactory;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('request_stack'),
+      $container->get('module_handler'),
+      $container->get('entity_type.manager'),
+      $container->get('uuid'),
+      $container->get('config.factory'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'quickbooks_qwc_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Determine if we're using a secure connection, and get the domain.
+    $secure = $this->request->isSecure();
+
+    // Useful path storage.
+    $qbwc_path = Url::fromRoute($this->getDefaultAppUrlRoute(), [], ['absolute' => TRUE])->toString();
+    if ($this->moduleHandler->moduleExists('help')) {
+      $help_path = Url::fromRoute('help.page', ['name' => 'commerce_qb_webconnect'], ['absolute' => TRUE])->toString();
+    }
+
+    // Get all users with the 'access quickbooks soap service' permission.
+    $ids = $this->entityTypeManager->getStorage('user')->getQuery()
+      ->condition('status', 1)
+      ->condition('roles', 'quickbooks_user')
+      ->execute();
+    $users = User::loadMultiple($ids);
+
+    $user_options = [];
+
+    if (!empty($users)) {
+      foreach ($users as $user) {
+        $name = $user->getAccountName();
+        $user_options[$name] = $name;
+      }
+    }
+
+    // Generate a FileID (GUID for Quickbooks), and load our
+    // OwnerID (GUID for server).
+    $file_id = $this->uuid->generate();
+    // We should always have a unique owner id as it is created during
+    // installation.
+    $owner_id = $this->configFactory
+      ->get($this->getConfigId())
+      ->get('qwc_owner_id');
+
+    // Finished pre-setup, create form now.
+    $form['container'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('QWC generation form'),
+    ];
+
+    $form['container']['app_name'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('App Name'),
+      '#description' => $this->t('The name of the application visible to the user. This name is displayed in the QB web connector. It is also the name supplied in the SDK OpenConnection call to QuickBooks or QuickBooks POS'),
+      '#maxlength' => 32,
+      '#size' => 64,
+      '#default_value' => '',
+      '#required' => TRUE,
+    ];
+
+    $description = $this->t('The URL of your web service.  For internal development and testing only, you can specify localhost or a machine name in place of the domain name.');
+    $form['container']['app_url'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('App URL'),
+      '#description' => $secure
+      ? $description
+      : $this->t('WARNING: Only local testing can be made over an insecure connection. :::') . ' ' . $description,
+      '#maxlength' => 64,
+      '#size' => 64,
+      '#default_value' => $qbwc_path,
+      '#required' => TRUE,
+    ];
+
+    $form['container']['app_support'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Support URL'),
+      '#description' => $this->t('The support URL.  This can most likely stay unchanged, but if change is desired then the domain or machine name must match the App URL domain or machine name.'),
+      '#size' => 64,
+      '#default_value' => empty($help_path) ?: $help_path,
+      '#required' => TRUE,
+    ];
+
+    $form['container']['user_name'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Quickbooks User'),
+      '#description' => $this->t("A user with specific permission to access the SOAP service calls on your site.  This list is populated by users with the 'access quickbooks soap service' permission."),
+      '#options' => $user_options,
+      '#required' => TRUE,
+    ];
+
+    $form['container']['file_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('File ID'),
+      '#description' => $this->t('An ID assigned to your Quickbooks application.  This should be left alone, but if necessary can be replaced if you have a working GUID already.'),
+      '#default_value' => $file_id,
+      '#maxlength' => 36,
+      '#size' => 64,
+      '#required' => TRUE,
+    ];
+
+    $form['owner_id'] = [
+      '#type' => 'hidden',
+      '#value' => $owner_id,
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Download QWC file'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $form_values = $form_state->cleanValues()->getValues();
+    $app_name = $description = $app_url = $app_support = $user_name = $file_id = $owner_id = NULL;
+    extract($form_values, EXTR_IF_EXISTS);
+
+    // Generate the XML file.
+    $qwc = new \QuickBooks_WebConnector_QWC($app_name, $description, $app_url, $app_support, $user_name, $file_id, $owner_id);
+    $xml = $qwc->generate();
+
+    // Save the generated QWC file as SERVER_HOST.qwc.
+    $file = file_save_data($xml, 'private://' . $this->request->getHost() . '.qwc');
+
+    if ($file) {
+      $uri = $file->getFileUri();
+
+      // Automatically sets content headers and opens the file stream.
+      // In order to get the full file name and attachment, we set the content
+      // type to 'text/xml' and the disposition to 'attachment'.  We also delete
+      // the file after sending it to ensure prying eyes don't find it later.
+      $response = new BinaryFileResponse($uri, 200, [], TRUE, 'attachment');
+      $response->deleteFileAfterSend(TRUE);
+
+      $form_state->setResponse($response);
+    }
+    else {
+      drupal_set_message(t('Unable to generate QWC file, check the log files for full details.'));
+    }
+  }
+
+}
diff --git a/src/Import/Event/Events.php b/src/Import/Event/Events.php
new file mode 100644
index 0000000..2457dd7
--- /dev/null
+++ b/src/Import/Event/Events.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Import\Event;
+
+/**
+ * Defines the events related to imports.
+ */
+final class Events {
+
+  /**
+   * Event that gets dispatched when an order is about to be imported from QB.
+   *
+   * @Event
+   *
+   * @see \Drupal\commerce_qb_webconnect\Import\Event\OrderTypeMappingEvent
+   */
+  const ORDER_TYPE_MAPPING = 'commerce_qb_webconnect.import.order_type_mapping';
+
+  /**
+   * Event that gets dispatched when a product is about to be imported from QB.
+   *
+   * @Event
+   *
+   * @see \Drupal\commerce_qb_webconnect\Import\Event\ProductTypeMappingEvent
+   */
+  const PRODUCT_TYPE_MAPPING = 'commerce_qb_webconnect.import.product_type_mapping';
+
+  /**
+   * Event that gets dispatched when a variation is imported from QB.
+   *
+   * @Event
+   *
+   * @see \Drupal\commerce_qb_webconnect\Import\Event\ProductVariationTypeMappingEvent
+   */
+  const PRODUCT_VARIATION_TYPE_MAPPING = 'commerce_qb_webconnect.import.product_variation_type_mapping';
+
+  /**
+   * Event that gets dispatched when a variation is imported from QB.
+   *
+   * @Event
+   *
+   * @see \Drupal\commerce_qb_webconnect\Import\Event\PurchasedEntityMappingEvent
+   */
+  const PURCHASED_ENTITY_MAPPING = 'commerce_qb_webconnect.import.purchased_entity_mapping';
+
+}
diff --git a/src/Import/Event/OrderTypeMappingEvent.php b/src/Import/Event/OrderTypeMappingEvent.php
new file mode 100644
index 0000000..f5daa6a
--- /dev/null
+++ b/src/Import/Event/OrderTypeMappingEvent.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Import\Event;
+
+use Drupal\migrate\Row;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event that gets dispatched when an order is about to be imported from QB.
+ *
+ * Allows modules to define which order type to map to when an order from
+ * Quickbooks is about to be imported.
+ */
+class OrderTypeMappingEvent extends Event {
+
+  /**
+   * The row that is about to be imported.
+   *
+   * @var \Drupal\migrate\Row
+   */
+  protected $row;
+
+  /**
+   * The order type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * Constructs a new OrderTypeMappingEvent object.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row that is about to be imported.
+   */
+  public function __construct(Row $row) {
+    $this->row = $row;
+  }
+
+  /**
+   * Gets the row.
+   *
+   * @return \Drupal\migrate\Row
+   *   The row.
+   */
+  public function getRow() {
+    return $this->row;
+  }
+
+  /**
+   * Gets the order type.
+   *
+   * @return string
+   *   The type.
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * Sets the order type.
+   *
+   * @param string $type
+   *   The order type.
+   */
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+}
diff --git a/src/Import/Event/ProductTypeMappingEvent.php b/src/Import/Event/ProductTypeMappingEvent.php
new file mode 100644
index 0000000..7b40c68
--- /dev/null
+++ b/src/Import/Event/ProductTypeMappingEvent.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Import\Event;
+
+use Drupal\migrate\Row;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event that gets dispatched when a product is about to be imported from QB.
+ *
+ * Allows modules to define which product type to map to when a product from
+ * Quickbooks is about to be imported.
+ */
+class ProductTypeMappingEvent extends Event {
+
+  /**
+   * The row that is about to be imported.
+   *
+   * @var \Drupal\migrate\Row
+   */
+  protected $row;
+
+  /**
+   * The product type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * Constructs a new ProductTypeMappingEvent object.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row that is about to be imported.
+   */
+  public function __construct(Row $row) {
+    $this->row = $row;
+  }
+
+  /**
+   * Gets the row.
+   *
+   * @return \Drupal\migrate\Row
+   *   The row.
+   */
+  public function getRow() {
+    return $this->row;
+  }
+
+  /**
+   * Gets the product type.
+   *
+   * @return string
+   *   The type.
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * Sets the product type.
+   *
+   * @param string $type
+   *   The product type.
+   */
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+}
diff --git a/src/Import/Event/ProductVariationTypeMappingEvent.php b/src/Import/Event/ProductVariationTypeMappingEvent.php
new file mode 100644
index 0000000..2893a8a
--- /dev/null
+++ b/src/Import/Event/ProductVariationTypeMappingEvent.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Import\Event;
+
+use Drupal\migrate\Row;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event that gets dispatched when a variation is about to be imported from QB.
+ *
+ * Allows modules to define which variation type to map to when a variation from
+ * Quickbooks is about to be imported.
+ */
+class ProductVariationTypeMappingEvent extends Event {
+
+  /**
+   * The row that is about to be imported.
+   *
+   * @var \Drupal\migrate\Row
+   */
+  protected $row;
+
+  /**
+   * The variation type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * Constructs a new ProductVariationTypeMappingEvent object.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row that is about to be imported.
+   */
+  public function __construct(Row $row) {
+    $this->row = $row;
+  }
+
+  /**
+   * Gets the row.
+   *
+   * @return \Drupal\migrate\Row
+   *   The row.
+   */
+  public function getRow() {
+    return $this->row;
+  }
+
+  /**
+   * Gets the variation type.
+   *
+   * @return string
+   *   The type.
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * Sets the variation type.
+   *
+   * @param string $type
+   *   The product type.
+   */
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+}
diff --git a/src/Import/Event/PurchasedEntityMappingEvent.php b/src/Import/Event/PurchasedEntityMappingEvent.php
new file mode 100644
index 0000000..90765ab
--- /dev/null
+++ b/src/Import/Event/PurchasedEntityMappingEvent.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Import\Event;
+
+use Drupal\commerce\PurchasableEntityInterface;
+use Drupal\commerce_order\Entity\OrderInterface;
+use Drupal\migrate\Row;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event that gets dispatched when an order is about to be imported from QB.
+ *
+ * Allows modules to define which purchase entity a particular order item should
+ * reference when an order from Quickbooks is about to be imported.
+ */
+class PurchasedEntityMappingEvent extends Event {
+
+  /**
+   * The row that is about to be imported.
+   *
+   * @var \Drupal\migrate\Row
+   */
+  protected $row;
+
+  /**
+   * The order entity that is about to be imported.
+   *
+   * @var \Drupal\commerce_order\Entity\OrderItemInterface
+   */
+  protected $order;
+
+  /**
+   * The purchased entity.
+   *
+   * @var \Drupal\commerce\PurchasableEntityInterface
+   */
+  protected $purchasedEntity;
+  /**
+   * Constructs a new PurchaseEntityMappingEvent object.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row that is about to be imported.
+   * @param \Drupal\commerce_order\Entity\OrderItemInterface $order
+   *   The order entity.
+   */
+  public function __construct(Row $row, OrderInterface $order) {
+    $this->row = $row;
+    $this->order = $order;
+  }
+
+  /**
+   * Gets the row.
+   *
+   * @return \Drupal\migrate\Row
+   *   The row.
+   */
+  public function getRow() {
+    return $this->row;
+  }
+
+  /**
+   * Gets the parent order entity.
+   *
+   * @return \Drupal\commerce_order\Entity\OrderItemInterface
+   *   The order.
+   */
+  public function getOrder() {
+    return $this->order;
+  }
+
+  /**
+   * Gets the purchase entity.
+   *
+   * @return \\Drupal\commerce\PurchasableEntityInterface
+   *   The purchased entity.
+   */
+  public function getPurchasedEntity() {
+    return $this->purchasedEntity;
+  }
+
+  /**
+   * Sets the order type.
+   *
+   * @param string $type
+   *   The order type.
+   */
+  public function setPurchasedEntity(
+    PurchasableEntityInterface $purchased_entity
+  ) {
+    $this->purchasedEntity = $purchased_entity;
+  }
+
+}
diff --git a/src/Plugin/QbWebconnectMigrationPluginManager.php b/src/Plugin/QbWebconnectMigrationPluginManager.php
new file mode 100644
index 0000000..4499c2e
--- /dev/null
+++ b/src/Plugin/QbWebconnectMigrationPluginManager.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin;
+
+use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
+use Drupal\migrate\Plugin\MigrationPluginManager;
+use Drupal\migrate\Plugin\NoSourcePluginDecorator;
+
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
+
+/**
+ * Plugin manager for QB Webconnect migration plugins.
+ *
+ * The migration plugin manager provided by the Migrate module loads and
+ * instantiates all plugins before filtering them by the `QB Webconnect` tag.
+ * Most of them being irrelevant to our case though and this is both inefficient
+ * but also causes errors in some cases, such as when loading a Drupal-to-Drupal
+ * plugin that requires a database connection that is not available.
+ *
+ * This plugin manager can be used to load only QB Webconnect defined directly
+ * by this module.
+ */
+class QbWebconnectMigrationPluginManager extends MigrationPluginManager {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery() {
+    if (isset($this->discovery)) {
+      return $this->discovery;
+    }
+
+    $directories = [$this->moduleHandler->getModuleDirectories()['commerce_qb_webconnect'] . '/migrations'];
+
+    // This gets rid of migrations which try to use a non-existent source
+    // plugin. The common case for this is if the source plugin has, or
+    // specifies, a non-existent provider.
+    $only_with_source_discovery = new NoSourcePluginDecorator(
+      new YamlDirectoryDiscovery($directories, 'migrate')
+    );
+    // This gets rid of migrations with explicit providers set if one of the
+    // providers do not exist before we try to use a potentially non-existing
+    // deriver. This is a rare case.
+    $this->discovery = new ContainerDerivativeDiscoveryDecorator(
+      new ProviderFilterDecorator(
+        $only_with_source_discovery,
+        [$this->moduleHandler, 'moduleExists']
+      )
+    );
+
+    return $this->discovery;
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/Order.php b/src/Plugin/migrate/destination/Order.php
new file mode 100644
index 0000000..67ad5c8
--- /dev/null
+++ b/src/Plugin/migrate/destination/Order.php
@@ -0,0 +1,406 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\commerce_order\Adjustment;
+use Drupal\commerce_order\Entity\OrderInterface;
+use Drupal\commerce_payment\Entity\Payment;
+use Drupal\commerce_price\Price;
+use Drupal\commerce_qb_webconnect\Import\Event\Events;
+use Drupal\commerce_qb_webconnect\Import\Event\OrderTypeMappingEvent;
+use Drupal\commerce_qb_webconnect\Import\Event\PurchasedEntityMappingEvent;
+use Drupal\commerce_qb_webconnect\QbWebConnectUtilities;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Saves a commerce_order entity.
+ *
+ * @MigrateDestination(
+ *   id = "commerce_qb_webconnect_commerce_order_entity"
+ * )
+ */
+class Order extends QbEntityContentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import(Row $row, array $old_destination_id_values = []) {
+    $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
+
+    // Allow other modules to map the order type.
+    $event = new OrderTypeMappingEvent($row);
+    $this->eventDispatcher->dispatch(Events::ORDER_TYPE_MAPPING, $event);
+    $type = $event->getType();
+    // Now, set the order type.
+    if ($type) {
+      $row->setDestinationProperty('type', $type);
+    }
+
+    // Get the entity.
+    $entity = $this->getEntity($row, $old_destination_id_values);
+    if (!$entity) {
+      throw new MigrateException('Unable to get entity');
+    }
+
+    // Don't do any further processing if the incoming entity from Quickbooks
+    // has an edit time earlier than the current entity because we don't want to
+    // import stale data.
+    /** @var \Drupal\commerce_order\Entity\OrderInterface $entity */
+    $filters = ['order_id' => $entity->id()];
+    if (!$entity->isNew() && !$this->entityNeedsUpdate($row, $entity, $filters)) {
+      return $filters;
+    }
+
+    // Set the order number with the order ID + list ID.
+    if (!$entity->isNew()) {
+      $entity->setOrderNumber($entity->id() . ' (' . $row->getSourceProperty('list_id') . ')');
+    }
+
+    // Set the order UID, and email if we have a profile.
+    $this->addCustomer($entity, $row);
+
+    // Go through the SalesReceiptLineRet lines and create the order items.
+    $this->addOrderItems($entity, $row);
+
+    // Add discount adjustments.
+    $this->addDiscountAdjustment($entity, $row);
+
+    // Add shipping adjustments.
+    $this->addShippingAdjustment($entity, $row);
+
+    // Add any tax adjusments.
+    $this->addTaxAdjustment($entity, $row);
+
+    assert($entity instanceof ContentEntityInterface);
+    if ($this->isEntityValidationRequired($entity)) {
+      $this->validateEntity($entity);
+    }
+    $ids = $this->save($entity, $old_destination_id_values);
+    if ($this->isTranslationDestination()) {
+      $ids[] = $entity->language()->getId();
+    }
+
+    // Finally, add the total amount as a manual payment to this order.
+    // @todo Figure out if this is the proper way to do this
+    $this->addPayments($entity, $row);
+
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getEntityTypeId($plugin_id) {
+    return 'commerce_order';
+  }
+
+  /**
+   * Add order items to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addOrderItems(OrderInterface $order, $row) {
+    $line_items = $row->getSourceProperty('line_items');
+
+    $order_items = [];
+    $custom_adjustments = [];
+    $current_line_item_list_ids = [];
+    if (!is_array($line_items)) {
+      $line_items = [$line_items];
+    }
+    foreach ($line_items as $line_item) {
+      $line_item_details = $line_item->ItemRef;
+
+      // Adjustments in QB will not have a quantity, if so, create that instead.
+      $export_settings = $this->configFactory->get('commerce_qb_webconnect.quickbooks_admin');
+      $shipping_service = $export_settings->get('adjustments')['shipping_service'];
+      $discount_service = $export_settings->get('adjustments')['discount_service'];
+      if (!isset($line_item->Quantity[0])
+       || stripos($line_item_details->FullName[0], $shipping_service) !== FALSE
+       || stripos($line_item_details->FullName[0], $discount_service) !== FALSE
+      ) {
+        $custom_adjustments[] = $this->addCustomAdjustment($order, $line_item);
+        continue;
+      }
+
+      // Keep track of the item list IDs so we can remove deleted ones later.
+      $order_item_txn_line_id = (string) $line_item->TxnLineID[0];
+      $current_line_item_list_ids[$order_item_txn_line_id] = $order_item_txn_line_id;
+
+      // Check if we can map the line item to a product that has been
+      // imported/exported from Quickbooks.
+      $product_list_id = $line_item_details->ListID[0];
+      $filters = [
+        'uuid' => $product_list_id,
+        'list_id' => $product_list_id,
+        'edit_sequence' => NULL,
+      ];
+      $purchased_entity_id = QbWebConnectUtilities::getEntityId(
+        'product_variation',
+        $filters
+      );
+      $product_variation = NULL;
+      if ($purchased_entity_id) {
+        $product_variation = $this
+        ->entityTypeManager
+        ->getStorage('commerce_product_variation')
+        ->load($purchased_entity_id);
+      }
+
+      // Allow other modules to map the purchased entity.
+      $event = new PurchasedEntityMappingEvent($row, $order);
+      if ($product_variation) {
+        $event->setPurchasedEntity($product_variation);
+      }
+      $this->eventDispatcher->dispatch(Events::PURCHASED_ENTITY_MAPPING, $event);
+      $purchased_entity = $event->getPurchasedEntity();
+
+      // Now, load the order item or create a new one.
+      $order_item = NULL;
+      foreach ($order->getItems() as $item) {
+        // Check if this is an already existing line item on the order, if so,
+        // load that.
+        /** @var \Drupal\commerce_order\Entity\OrderItemInterface $item */
+        $data = $item->getData('quickbooks_list_id');
+        if ($data && $data === $order_item_txn_line_id) {
+          $order_item = $item;
+          break;
+        }
+
+        // Else, map to an existing order item with the same purchased entity.
+        if ($item->getPurchasedEntityId() === $purchased_entity->id()) {
+          $order_item = $item;
+          break;
+        }
+      }
+
+      // Create a new order item if it doesn't exist.
+      if (!isset($order_item)) {
+        $order_item = $this
+          ->entityTypeManager
+          ->getStorage('commerce_order_item')
+          ->create([
+            'type' => $row->getDestinationProperty('type'),
+        ]);
+      }
+
+      /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
+      // @todo Import all product types from Quickbooks, instead of having an
+      // unreference purchased entity.
+      if ($purchased_entity) {
+        $order_item->set('purchased_entity', $purchased_entity);
+      }
+      $order_item->setTitle($line_item_details->FullName[0]);
+      $order_item->setQuantity($line_item->Quantity[0]);
+      $order_item->setUnitPrice(new Price($line_item->Rate[0], 'USD'));
+
+      // Save the order item, if it's new.
+      $order_item->setData('quickbooks_list_id', (string) $order_item_txn_line_id);
+      $order_item->save();
+
+      $order_items[] = $order_item;
+    }
+
+    // Delete all items that are not part of the incoming order anymore.
+    if (!$order->isNew() && $order_items) {
+      $order = $this->removeDeletedOrderItems($order, $current_line_item_list_ids);
+    }
+
+    // Set any custom adjustments.
+    if ($custom_adjustments) {
+      $order->setAdjustments($custom_adjustments);
+    }
+
+    // Finally, set the order items.
+    if ($order_items) {
+      $order->setItems($order_items);
+    }
+  }
+
+  /**
+   * Deletes order items that have been removed from Quickbooks.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param array $current_line_item_list_ids
+   *   The currently existing line item list IDs in Quickbooks.
+   */
+  protected function removeDeletedOrderItems(
+    OrderInterface $order,
+    array $current_line_item_list_ids
+  ) {
+    foreach ($order->getItems() as $order_item) {
+      $data = $order_item->getData('quickbooks_list_id');
+
+      if (!$data || ($data && !isset($current_line_item_list_ids[$data]))) {
+        $order->removeItem($order_item);
+        $order_item->delete();
+      }
+    }
+    return $order;
+  }
+
+  /**
+   * Add the customer details to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addCustomer(OrderInterface $order, $row) {
+    $profile_id = $row->getDestinationProperty('billing_profile/target_id');
+
+    if (!$profile_id) {
+      return;
+    }
+
+    $profile = $this
+      ->entityTypeManager
+      ->getStorage('profile')
+      ->load($profile_id);
+    if (!$profile) {
+      return;
+    }
+
+    /** @var \Drupal\profile\Entity\ProfileInterface $profile */
+    $order->setBillingProfile($profile);
+    $customer = $profile->getOwner();
+    if ($customer->id()) {
+      $order->setCustomer($customer);
+      $order->setCustomerId($customer->id());
+      $order->setEmail($customer->getEmail());
+    }
+  }
+
+  /**
+   * Add the payment details to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addPayments(OrderInterface $order, $row) {
+    $import_settings = $this->configFactory
+      ->get('commerce_qb_webconnect.import_settings');
+    $import_entity_types = $import_settings->get('entity_types');
+
+    // Load the quickbooks payment if it already exists.
+    if (!$order->isNew()) {
+      $payments = $this
+        ->entityTypeManager
+        ->getStorage('commerce_payment')
+        ->loadMultipleByOrder($order);
+      foreach ($payments as $payment_item) {
+        if ($payment_item->getPaymentGatewayId() === $import_entity_types['payment_gateway']) {
+          $payment = $payment_item;
+          break;
+        }
+      }
+    }
+
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
+    if (!isset($payment)) {
+      $payment = Payment::create([
+        'type' => 'payment_manual',
+        'payment_gateway' => $import_entity_types['payment_gateway'],
+        'state' => 'completed',
+      ]);
+    }
+    $payment->set('order_id', $order->id());
+    $payment->setAmount(new Price(
+      $row->getSourceProperty('total_amount'), 'USD')
+    );
+    $payment->save();
+  }
+
+  /**
+   * Add any custom adjustments to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param mixed $line_item
+   *   The line item source.
+   */
+  protected function addCustomAdjustment($order, $line_item) {
+    $price_amount = $line_item->Amount[0] ? $line_item->Amount[0] : '0.00';
+    $adjustment = new Adjustment([
+      'type' => 'custom',
+      'label' => !empty($line_item->ItemRef->FullName[0]) ? $line_item->ItemRef->FullName[0] : $this->t('Custom Adjustment'),
+      'amount' => new Price($price_amount, 'USD'),
+    ]);
+    return $adjustment;
+  }
+
+  /**
+   * Add any discount adjustments to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addDiscountAdjustment(OrderInterface $order, $row) {
+    $discount_line = $row->getSourceProperty('discount_line');
+    if ($discount_line) {
+      $price_amount = $discount_line->Amount[0] ? $discount_line->Amount[0] : '0.00';
+      $adjustment = new Adjustment([
+        'type' => 'promotion',
+        'label' => !empty($discount_line->AccountRef->FullName[0]) ? $discount_line->AccountRef->FullName[0] : $this->t('Discount Adjustment'),
+        'amount' => new Price($price_amount, 'USD'),
+      ]);
+      $order->setAdjustments([$adjustment]);
+    }
+  }
+
+  /**
+   * Add any shipping adjustments to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addShippingAdjustment(OrderInterface $order, $row) {
+    $shipping_line = $row->getSourceProperty('shipping_line');
+    if ($shipping_line) {
+      $price_amount = $shipping_line->Amount[0] ? $shipping_line->Amount[0] : '0.00';
+      $adjustment = new Adjustment([
+        'type' => 'shipping',
+        'label' => !empty($shipping_line->AccountRef->FullName[0]) ? $shipping_line->AccountRef->FullName[0] : $this->t('Shipping Adjustment'),
+        'amount' => new Price($price_amount, 'USD'),
+      ]);
+      $order->setAdjustments([$adjustment]);
+    }
+  }
+
+  /**
+   * Add any tax adjustments to the order.
+   *
+   * @param OrderInterface $order
+   *   The order entity.
+   * @param \Drupal\migrate\Row $row
+   *   The row.
+   */
+  protected function addTaxAdjustment(OrderInterface $order, $row) {
+    $tax_line = $row->getSourceProperty('sales_tax_line');
+    if ($tax_line) {
+      $price_amount = $tax_line->Amount[0] ? $tax_line->Amount[0] : '0.00';
+      $adjustment = new Adjustment([
+        'type' => 'tax',
+        'label' => !empty($tax_line->AccountRef->FullName[0]) ? $tax_line->AccountRef->FullName[0] : $this->t('Tax Adjustment'),
+        'amount' => new Price($price_amount, 'USD'),
+      ]);
+      $order->setAdjustments([$adjustment]);
+    }
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/Payment.php b/src/Plugin/migrate/destination/Payment.php
new file mode 100644
index 0000000..a0852e3
--- /dev/null
+++ b/src/Plugin/migrate/destination/Payment.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Saves a commerce_payment entity.
+ *
+ * @MigrateDestination(
+ *   id = "commerce_qb_webconnect_commerce_payment_entity"
+ * )
+ */
+class Payment extends QbEntityContentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import(Row $row, array $old_destination_id_values = []) {
+    $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
+
+    // Get the entity.
+    $entity = $this->getEntity($row, $old_destination_id_values);
+    if (!$entity) {
+      throw new MigrateException('Unable to get entity');
+    }
+
+    // Don't do any further processing if the incoming entity from Quickbooks
+    // has an edit time earlier than the current entity because we don't want to
+    // import stale data.
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $entity */
+    $filters = ['payment_id' => $entity->id()];
+    if (!$entity->isNew() && !$this->entityNeedsUpdate($row, $entity, $filters)) {
+      return $filters;
+    }
+
+    assert($entity instanceof ContentEntityInterface);
+    if ($this->isEntityValidationRequired($entity)) {
+      $this->validateEntity($entity);
+    }
+    $ids = $this->save($entity, $old_destination_id_values);
+    if ($this->isTranslationDestination()) {
+      $ids[] = $entity->language()->getId();
+    }
+
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getEntityTypeId($plugin_id) {
+    return 'commerce_payment';
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/Product.php b/src/Plugin/migrate/destination/Product.php
new file mode 100644
index 0000000..f826a9f
--- /dev/null
+++ b/src/Plugin/migrate/destination/Product.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\commerce_qb_webconnect\Import\Event\Events;
+use Drupal\commerce_qb_webconnect\Import\Event\ProductTypeMappingEvent;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Saves a commerce_product entity.
+ *
+ * @MigrateDestination(
+ *   id = "commerce_qb_webconnect_commerce_product_entity"
+ * )
+ */
+class Product extends QbEntityContentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import(Row $row, array $old_destination_id_values = []) {
+    $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
+
+    // Allow other modules to map the product type.
+    $event = new ProductTypeMappingEvent($row);
+    $this->eventDispatcher->dispatch(Events::PRODUCT_TYPE_MAPPING, $event);
+    $type = $event->getType();
+    // Now, set the product type.
+    if ($type) {
+      $row->setDestinationProperty('type', $type);
+    }
+
+    // Get the entity.
+    $entity = $this->getEntity($row, $old_destination_id_values);
+    if (!$entity) {
+      throw new MigrateException('Unable to get entity');
+    }
+
+    // Don't do any further processing if the incoming entity from Quickbooks
+    // has an edit time earlier than the current entity because we don't want to
+    // import stale data.
+    /** @var \Drupal\commerce_product\Entity\ProductInterface $entity */
+    $filters = [
+      'product_id' => $entity->id(),
+      'langcode' => $entity->language()->getId(),
+    ];
+    if (!$entity->isNew() && !$this->entityNeedsUpdate($row, $entity, $filters)) {
+      if ($this->isTranslationDestination()) {
+        return $filters;
+      }
+      return [$filters['product_id']];
+    }
+
+    // Remove the product ID from the beginning of the product title.
+    if (!$entity->isNew()) {
+      $name = $row->getSourceProperty('name');
+      $name = preg_replace('/^[0-9]+-/', '', $name);
+      $entity->setTitle($name);
+    }
+
+    assert($entity instanceof ContentEntityInterface);
+    if ($this->isEntityValidationRequired($entity)) {
+      $this->validateEntity($entity);
+    }
+    $ids = $this->save($entity, $old_destination_id_values);
+    if ($this->isTranslationDestination()) {
+      $ids[] = $entity->language()->getId();
+    }
+
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getEntityTypeId($plugin_id) {
+    return 'commerce_product';
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/ProductVariation.php b/src/Plugin/migrate/destination/ProductVariation.php
new file mode 100644
index 0000000..417a797
--- /dev/null
+++ b/src/Plugin/migrate/destination/ProductVariation.php
@@ -0,0 +1,221 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\commerce\Context;
+use Drupal\commerce_qb_webconnect\Import\Event\Events;
+use Drupal\commerce_qb_webconnect\Import\Event\ProductVariationTypeMappingEvent;
+use Drupal\commerce_stock\StockServiceManagerInterface;
+use Drupal\commerce_stock\StockTransactionsInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\migrate\Plugin\MigrationInterface;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Saves a commerce_product_variation entity.
+ *
+ * @MigrateDestination(
+ *   id = "commerce_qb_webconnect_commerce_product_variation_entity"
+ * )
+ */
+class ProductVariation extends QbEntityContentBase {
+
+  /**
+   * The Stock Service Manager.
+   *
+   * @var \Drupal\commerce_stock\StockServiceManager
+   */
+  protected $stockServiceManager;
+
+  /**
+   * Constructs a product variation object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration entity.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The storage for this entity type.
+   * @param array $bundles
+   *   The list of bundles this entity type has.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type plugin manager service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config object factory.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   An event dispatcher instance to use for configuration events.
+   * @param \Drupal\commerce_stock\StockServiceManager $stock_service_manager
+  *    The stock service manager interface.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    MigrationInterface $migration,
+    EntityStorageInterface $storage,
+    array $bundles,
+    EntityFieldManagerInterface $entity_field_manager,
+    FieldTypePluginManagerInterface $field_type_manager,
+    ConfigFactoryInterface $config_factory,
+    Connection $database,
+    EntityTypeManagerInterface $entity_type_manager,
+    EventDispatcherInterface $event_dispatcher,
+    StockServiceManagerInterface $stock_service_manager
+  ) {
+    parent::__construct(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $storage,
+      $bundles,
+      $entity_field_manager,
+      $field_type_manager,
+      $config_factory,
+      $database,
+      $entity_type_manager,
+      $event_dispatcher
+    );
+
+    $this->stockServiceManager = $stock_service_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(
+    ContainerInterface $container,
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    MigrationInterface $migration = NULL
+  ) {
+    $entity_type = static::getEntityTypeId($plugin_id);
+
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $container->get('entity_type.manager')->getStorage($entity_type),
+      array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
+      $container->get('entity_field.manager'),
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('config.factory'),
+      $container->get('database'),
+      $container->get('entity_type.manager'),
+      $container->get('event_dispatcher'),
+      $container->get('commerce_stock.service_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import(Row $row, array $old_destination_id_values = []) {
+    $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
+
+    // Allow other modules to map the product variation type.
+    $event = new ProductVariationTypeMappingEvent($row);
+    $this->eventDispatcher->dispatch(Events::PRODUCT_VARIATION_TYPE_MAPPING, $event);
+    $type = $event->getType();
+    // Now, set the product variation type.
+    if ($type) {
+      $row->setDestinationProperty('type', $type);
+    }
+
+    // Get the entity.
+    $entity = $this->getEntity($row, $old_destination_id_values);
+    if (!$entity) {
+      throw new MigrateException('Unable to get entity');
+    }
+
+    // Don't do any further processing if the incoming entity from Quickbooks
+    // has an edit time earlier than the current entity because we don't want to
+    // import stale data.
+    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $entity */
+    $entity_is_new = $entity->isNew();
+    $filters = [
+      'variation_id' => $entity->id(),
+      'langcode' => $entity->language()->getId(),
+    ];
+    if (!$entity_is_new && !$this->entityNeedsUpdate($row, $entity, $filters)) {
+      if ($this->isTranslationDestination()) {
+        return $filters;
+      }
+      return [$filters['variation_id']];
+    }
+
+    // Remove the variation ID from the beginning of the variation title.
+    if (!$entity->isNew()) {
+      $name = $row->getSourceProperty('name');
+      $name = preg_replace('/^[0-9]+-/', '', $name);
+      $entity->setTitle($name);
+    }
+
+    assert($entity instanceof ContentEntityInterface);
+    if ($this->isEntityValidationRequired($entity)) {
+      $this->validateEntity($entity);
+    }
+    $ids = $this->save($entity, $old_destination_id_values);
+    if ($this->isTranslationDestination()) {
+      $ids[] = $entity->language()->getId();
+    }
+
+    // If the entity is new, bring in the stock information if stock is enabled.
+    if ($entity_is_new) {
+      if ($entity->hasField('field_stock')) {
+        $quantity_on_hand = $row->getSourceProperty('quantity_on_hand');
+        $stores = $entity->getStores();
+        $context = new Context($entity->getOwner(), reset($stores));
+        $location = $this->stockServiceManager->getTransactionLocation(
+          $context,
+          $entity,
+          $quantity_on_hand
+        );
+        $price = $entity->getPrice();
+        $this->stockServiceManager->createTransaction(
+          $entity,
+          $location->id(),
+          '',
+          $quantity_on_hand,
+          $price->getNumber(),
+          $price->getCurrencyCode(),
+          StockTransactionsInterface::STOCK_IN
+        );
+      }
+    }
+
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getEntityTypeId($plugin_id) {
+    return 'commerce_product_variation';
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/Profile.php b/src/Plugin/migrate/destination/Profile.php
new file mode 100644
index 0000000..0a4b36f
--- /dev/null
+++ b/src/Plugin/migrate/destination/Profile.php
@@ -0,0 +1,304 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\user\Entity\User;
+use Drupal\user\UserInterface;
+
+/**
+ * Saves a profile entity.
+ *
+ * @MigrateDestination(
+ *   id = "commerce_qb_webconnect_profile_entity"
+ * )
+ */
+class Profile extends QbEntityContentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import(Row $row, array $old_destination_id_values = []) {
+    $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
+
+    // Convert values from QB into values Drupal can understand.
+    // Convert the three letter country code from QB to its equivalent two
+    // letter code.
+    $country_codes = commerce_qb_webconnect_get_country_codes();
+    $row->setDestinationProperty(
+      'address/country_code',
+      $country_codes[$row->getSourceProperty('country_iso_code_2')]
+    );
+
+    // Fetch the user by email if it exists.
+    $email = $row->getSourceProperty('email');
+    if ($email) {
+      $user = $this->getUser($email);
+      $row->setDestinationProperty('uid', $user->id());
+    }
+
+    // Get the entity.
+    $entity = $this->getEntity($row, $old_destination_id_values);
+    if (!$entity) {
+      throw new MigrateException('Unable to get entity');
+    }
+
+    // Don't do any further processing if the incoming entity from Quickbooks
+    // has an edit time earlier than the current entity because we don't want to
+    // import stale data.
+    /** @var \Drupal\profile\Entity\ProfileInterface $entity */
+    $filters = [
+      'profile_id' => $entity->id(),
+      'revision_id' => $entity->getRevisionId(),
+    ];
+    if (!$entity->isNew() && !$this->entityNeedsUpdate($row, $entity, $filters)) {
+      return $filters;
+    }
+
+    // Set the owner.
+    if ($user) {
+      $entity->set('uid', $user->id());
+      $entity->setOwner($user);
+      $entity->setOwnerId($user->id());
+    }
+    assert($entity instanceof ContentEntityInterface);
+    if ($this->isEntityValidationRequired($entity)) {
+      $this->validateEntity($entity);
+    }
+    $ids = $this->save($entity, $old_destination_id_values);
+    if ($this->isTranslationDestination()) {
+      $ids[] = $entity->language()->getId();
+    }
+
+    return $ids;
+  }
+
+  /**
+   * Creates or loads an entity.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row object.
+   * @param array $old_destination_id_values
+   *   The old destination IDs.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity we are importing into.
+   */
+  protected function getEntity(Row $row, array $old_destination_id_values) {
+    $entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
+
+    // If there is no entity ID, check for any entities by matching the address.
+    if (!$entity_id && ($matched_entity = $this->getProfileAddressMatch($row))) {
+      $entity_id = $matched_entity->id();
+    }
+
+    if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
+      // Allow updateEntity() to change the entity.
+      // But before that, save the last changed date of this entity so that
+      // we can use it to compare if the QB data is outdated for this entity.
+      $this->entityLastChangedTime = $entity->getChangedTime();
+      $entity = $this->updateEntity($entity, $row) ?: $entity;
+    }
+    else {
+      // Attempt to ensure we always have a bundle.
+      if ($bundle = $this->getBundle($row)) {
+        $row->setDestinationProperty($this->getKey('bundle'), $bundle);
+      }
+
+      // Stubs might need some required fields filled in.
+      if ($row->isStub()) {
+        $this->processStubRow($row);
+      }
+      $entity = $this->storage->create($row->getDestination());
+      $entity->enforceIsNew();
+    }
+    return $entity;
+  }
+
+  /**
+   * Check if there are any profiles matching the migration row.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row object.
+   *
+   * @return mixed
+   *   The entity we are importing into.
+   */
+  protected function getProfileAddressMatch(Row $row) {
+    // Ensure there is a billing address.
+    if (!$row->getSourceProperty('billing_street1')) {
+      return NULL;
+    }
+
+    $address_line_1 = $row->getSourceProperty('billing_street1');
+    $address_line_2 = $row->getSourceProperty('billing_street2');
+    $locality = $row->getSourceProperty('billing_city');
+    $postal_code = $row->getSourceProperty('billing_postal_code');
+    $first_name = $row->getSourceProperty('billing_first_name');
+    $last_name = $row->getSourceProperty('billing_last_name');
+
+    // Load all profiles with matching address parameters.
+    $query = $this->database->select('profile', 'profile');
+    $query->fields('profile', ['profile_id']);
+    $query->leftJoin('profile__address', 'address', 'profile.profile_id = address.entity_id');
+    $query->condition('profile.type', 'customer');
+    $query->condition('address.address_given_name', $first_name, 'LIKE');
+    $query->condition('address.address_family_name', $last_name, 'LIKE');
+
+    // The first address line could have the customer's name as the first line
+    // in QB. So address line 1 would be in address line 2 field.
+    $or_condition = $query->orConditionGroup()
+      ->condition('address.address_address_line1', $address_line_1, 'LIKE')
+      ->condition('address.address_address_line1', $address_line_2, 'LIKE');
+    $query->condition($or_condition);
+
+    $query->condition('address.address_locality', $locality, 'LIKE');
+    $query->condition('address.address_postal_code', $postal_code);
+
+    $profile_id = $query->execute()->fetchField();
+
+    if ($profile_id) {
+      return $this->entityTypeManager->getStorage('profile')
+        ->load($profile_id);
+    }
+
+    return  $query->execute()->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids = parent::getIds();
+    $ids[$this->getKey('revision')]['type'] = 'integer';
+
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getEntityTypeId($plugin_id) {
+    return 'profile';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function save(
+    ContentEntityInterface $entity,
+    array $old_destination_id_values = []
+  ) {
+    $entity->save();
+
+    return [
+      $this->getKey('id') => $entity->id(),
+      $this->getKey('revision') => $entity->getRevisionId(),
+    ];
+  }
+
+  /**
+   * Fetch a user entity by email, if one exists, otherwise create a new one.
+   *
+   * @param string $email
+   *   The user email.
+   *
+   * @return \Drupal\user\UserInterface
+   *   The user entity
+   */
+  protected function getUser($email) {
+    // Check if we have an existing user for this email.
+    $users = $this
+      ->entityTypeManager
+      ->getStorage('user')
+      ->loadByProperties(['mail' => $email]);
+    if ($users) {
+      return reset($users);
+    }
+
+    // Otherwise, create a new user entity for this email.
+    // Strip off everything after the @ sign.
+    $new_name = preg_replace('/@.*$/', '', $email);
+    // Clean up the username.
+    $new_name = $this->cleanupUsername($new_name);
+    // Ensure whatever name we have is unique.
+    $new_name = $this->uniqueUsername($new_name);
+
+    // Now create the new user.
+    $user = User::create([
+      'name' => $new_name,
+      'mail' => $email,
+      'status' => 1,
+    ]);
+    $user->save();
+
+    return $user;
+  }
+
+  /**
+   * Cleans up username.
+   *
+   * Run username sanitation, e.g.:
+   *     Replace two or more spaces with a single underscore
+   *     Strip illegal characters.
+   *
+   * @param string $name
+   *   The username to be cleaned up.
+   *
+   * @return string
+   *   Cleaned up username.
+   */
+  protected function cleanupUsername($name) {
+    // Strip illegal characters.
+    $name = preg_replace('/[^\x{80}-\x{F7} a-zA-Z0-9@_.\'-]/', '', $name);
+
+    // Strip leading and trailing spaces.
+    $name = trim($name);
+
+    // Convert any other series of spaces to a single underscore.
+    $name = preg_replace('/\s+/', '_', $name);
+
+    // If there's nothing left use a default.
+    $name = ('' === $name) ? t('user') : $name;
+
+    // Truncate to a reasonable size.
+    $name = (mb_strlen($name) > (UserInterface::USERNAME_MAX_LENGTH - 10)) ? mb_substr($name, 0, UserInterface::USERNAME_MAX_LENGTH - 11) : $name;
+    return $name;
+  }
+
+  /**
+   * Makes the username unique.
+   *
+   * Given a starting point for a Drupal username (e.g. the name portion of an
+   * email address) return a legal, unique Drupal username.
+   *
+   * @param string $name
+   *   A name from which to base the final user name.
+   *   May contain illegal characters; these will be stripped.
+   *
+   * @return string
+   *   A unique user name based on $name.
+   *
+   * @see user_validate_name()
+   */
+  protected function uniqueUsername($name) {
+    // Iterate until we find a unique name.
+    $i = 0;
+    do {
+      $new_name = empty($i) ? $name : $name . '_' . $i;
+      $found = $this
+        ->database
+        ->queryRange("SELECT uid from {users_field_data} WHERE name = :name", 0, 1, [
+          ':name' => $new_name
+      ])->fetchAssoc();
+      $i++;
+    } while (!empty($found));
+
+    return $new_name;
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/QbEntityContentBase.php b/src/Plugin/migrate/destination/QbEntityContentBase.php
new file mode 100644
index 0000000..2beb29e
--- /dev/null
+++ b/src/Plugin/migrate/destination/QbEntityContentBase.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate\destination;
+
+use Drupal\commerce_qb_webconnect\QbWebConnectUtilities;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
+use Drupal\migrate\Row;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Serves as a base class for saving an entity from Quickbooks into Drupal.
+ */
+class QbEntityContentBase extends EntityContentBase {
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The database connection to be used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * A timestamp of when the last entity was updated in Drupal.
+   *
+   * @var int
+   */
+  protected $entityLastChangedTime = NULL;
+
+  /**
+   * An event dispatcher instance to use for configuration events.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Constructs a content entity.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration entity.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The storage for this entity type.
+   * @param array $bundles
+   *   The list of bundles this entity type has.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type plugin manager service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config object factory.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   An event dispatcher instance to use for configuration events.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    MigrationInterface $migration,
+    EntityStorageInterface $storage,
+    array $bundles,
+    EntityFieldManagerInterface $entity_field_manager,
+    FieldTypePluginManagerInterface $field_type_manager,
+    ConfigFactoryInterface $config_factory,
+    Connection $database,
+    EntityTypeManagerInterface $entity_type_manager,
+    EventDispatcherInterface $event_dispatcher
+  ) {
+    parent::__construct(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $storage,
+      $bundles,
+      $entity_field_manager,
+      $field_type_manager
+    );
+
+    $this->configFactory = $config_factory;
+    $this->database = $database;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->eventDispatcher = $event_dispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(
+    ContainerInterface $container,
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    MigrationInterface $migration = NULL
+  ) {
+    $entity_type = static::getEntityTypeId($plugin_id);
+
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $container->get('entity_type.manager')->getStorage($entity_type),
+      array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
+      $container->get('entity_field.manager'),
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('config.factory'),
+      $container->get('database'),
+      $container->get('entity_type.manager'),
+      $container->get('event_dispatcher')
+    );
+  }
+
+  /**
+   * Creates or loads an entity.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row object.
+   * @param array $old_destination_id_values
+   *   The old destination IDs.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity we are importing into.
+   */
+  protected function getEntity(Row $row, array $old_destination_id_values) {
+    $entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
+    if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
+      // Allow updateEntity() to change the entity.
+      // But before that, save the last changed date of this entity so that
+      // we can use it to compare if the QB data is outdated for this entity.
+      $this->entityLastChangedTime = $entity->getChangedTime();
+      $entity = $this->updateEntity($entity, $row) ?: $entity;
+    }
+    else {
+      // Attempt to ensure we always have a bundle.
+      if ($bundle = $this->getBundle($row)) {
+        $row->setDestinationProperty($this->getKey('bundle'), $bundle);
+      }
+
+      // Stubs might need some required fields filled in.
+      if ($row->isStub()) {
+        $this->processStubRow($row);
+      }
+      $entity = $this->storage->create($row->getDestination());
+      $entity->enforceIsNew();
+    }
+    return $entity;
+  }
+
+  /**
+   * Gets the entity ID of the row.
+   *
+   * As Quickbooks does not store the entity ID, we'll have to use the migrate
+   * map table to determine if this customer has already been imported into
+   * Drupal using the 'list_id' as the lookup.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row of data.
+   *
+   * @return string
+   *   The entity ID for the row that we are importing.
+   */
+  protected function getEntityId(Row $row) {
+    // Return the ID if it exists in the same QB import table.
+    $entity_id = $row->getDestinationProperty($this->getKey('id'));
+    if ($entity_id) {
+      return $entity_id;
+    }
+
+    // If it doesn't exist in the same table, check to see if it exists in the
+    // QB Export table.
+    $filters = [
+      'uuid' => $row->getSourceProperty('list_id'),
+      'list_id' => $row->getSourceProperty('list_id'),
+      'edit_sequence' => $row->getSourceProperty('edit_sequence'),
+    ];
+    return QbWebConnectUtilities::getEntityId(
+      $this->getQbEntityType(),
+      $filters
+    );
+  }
+
+  /**
+   * Returns the corresponding entity type name in QB for a Drupal entity type.
+   *
+   * @return string
+   *   The entity type name in QB.
+   */
+  protected function getQbEntityType() {
+    $qb_entity_types = [
+      'profile' => 'customer',
+      'commerce_order' => 'order',
+      'commerce_payment' => 'payment',
+      'commerce_product' => 'product',
+      'commerce_product_variation' => 'product_variation',
+    ];
+
+    return $qb_entity_types[$this->storage->getEntityTypeId()];
+  }
+
+  /**
+   * Returns TRUE if the incoming entity is more recent than the existing.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row of data.
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity being updated.
+   * @param array $filters
+   *   The filters for the entity to look up the edit sequence.
+   *
+   * @return string
+   *   The entity ID for the row that we are importing.
+   */
+  protected function entityNeedsUpdate(
+    Row $row,
+    ContentEntityInterface $entity,
+    array $filters
+  ) {
+    $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+      $this->getQbEntityType(),
+      $filters
+    );
+    $incoming_edit_sequence = $row->getSourceProperty('edit_sequence');
+    if (
+      ($this->entityLastChangedTime && $this->entityLastChangedTime < $incoming_edit_sequence)
+      && (!isset($qb_row_details['edit_sequence']) || $incoming_edit_sequence > $qb_row_details['edit_sequence'])
+    ) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/src/Plugin/migrate/destination/QbWebConnectDestination.php b/src/Plugin/migrate/destination/QbWebConnectDestination.php
index 4ac721d..fe850ba 100644
--- a/src/Plugin/migrate/destination/QbWebConnectDestination.php
+++ b/src/Plugin/migrate/destination/QbWebConnectDestination.php
@@ -75,9 +75,19 @@ class QbWebConnectDestination extends DestinationBase implements ContainerFactor
    */
   public function import(Row $row, array $old_destination_id_values = []) {
     $row->setDestinationProperty('uuid', $this->uuid->generate());
+    // We set the list_id and edit_sequence with the old values as it contains
+    // the mapping details.
+    $row->setDestinationProperty('list_id', isset($old_destination_id_values[1]) ? $old_destination_id_values[1] : NULL);
+    $row->setDestinationProperty('edit_sequence', isset($old_destination_id_values[2]) ? $old_destination_id_values[2] : NULL);
+
     $this->state->set('qb_webconnect.current_row', $row);
     $this->state->set('qb_webconnect.current_migration', $this->migration->getPluginId());
-    return [$row->getDestinationProperty('uuid')];
+
+    return [
+      $row->getDestinationProperty('uuid'),
+      $row->getDestinationProperty('list_id'),
+      $row->getDestinationProperty('edit_sequence')
+    ];
   }
 
   /**
@@ -86,6 +96,8 @@ class QbWebConnectDestination extends DestinationBase implements ContainerFactor
   public function getIds() {
     return [
       'uuid' => ['type' => 'string'],
+      'list_id' => ['type' => 'string'],
+      'edit_sequence' => ['type' => 'string'],
     ];
   }
 
diff --git a/src/Plugin/migrate_plus/data_parser/QbXmlSoap.php b/src/Plugin/migrate_plus/data_parser/QbXmlSoap.php
new file mode 100644
index 0000000..83912c1
--- /dev/null
+++ b/src/Plugin/migrate_plus/data_parser/QbXmlSoap.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\Plugin\migrate_plus\data_parser;
+
+use Drupal\migrate\MigrateException;
+use Drupal\migrate_plus\Plugin\migrate_plus\data_parser\SimpleXml;
+
+/**
+ * Obtain XML data for migration using the SimpleXML API.
+ *
+ * The xml content is passed in from the response received from Quickbooks
+ * instead of via the URL.
+ *
+ * @DataParser(
+ *   id = "commerce_qb_webconnect_qbxml_soap",
+ *   title = @Translation("QBXML SOAP")
+ * )
+ */
+class QbXmlSoap extends SimpleXml {
+
+  /**
+   * The request content returned from the QBXML SOAP service.
+   *
+   * @var mixed
+   */
+  protected $requestContent;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->requestContent = $configuration['request_content'];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Drupal\migrate\MigrateException
+   *   If we can't resolve the XML function or its response property.
+   */
+  protected function openSourceUrl($url) {
+    if (!$this->requestContent) {
+      throw new MigrateException('There is no request content from the QBXML Soap Service.');
+    }
+
+    // Clear XML error buffer. Other Drupal code that executed during the
+    // migration may have polluted the error buffer and could create false
+    // positives in our error check below. We are only concerned with errors
+    // that occur from attempting to load the XML string into an object here.
+    libxml_clear_errors();
+
+    $xml_data = $this->requestContent;
+    $xml = simplexml_load_string(trim($xml_data));
+    foreach (libxml_get_errors() as $error) {
+      $error_string = self::parseLibXmlError($error);
+      throw new MigrateException($error_string);
+    }
+    $this->registerNamespaces($xml);
+    $xpath = $this->configuration['item_selector'];
+    $this->matches = $xml->xpath($xpath);
+    return TRUE;
+  }
+
+}
diff --git a/src/QbWebConnectUtilities.php b/src/QbWebConnectUtilities.php
index cae097f..6f96a37 100644
--- a/src/QbWebConnectUtilities.php
+++ b/src/QbWebConnectUtilities.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\commerce_qb_webconnect;
 
+use Drupal\Core\Database\Query\Condition;
+use Drupal\profile\Entity\ProfileInterface;
+
 /**
  * Class QbWebConnectUtilities.
  *
@@ -10,7 +13,7 @@ namespace Drupal\commerce_qb_webconnect;
 final class QbWebConnectUtilities {
 
   /**
-   * Extract a unique record identifier from an XML response.
+   * Extracts and returns unique record identifiers from an XML response.
    *
    * Some (most?) records within QuickBooks have unique identifiers which are
    * returned with the qbXML responses. This method will try to extract all
@@ -54,11 +57,11 @@ final class QbWebConnectUtilities {
    *   The entity type.
    *
    * @return string
-   *   The identifier for the content type.
+   *   All the key identifiers fetched from the response.
    *
    * @see \QuickBooks_WebConnector_Handlers::_extractIdentifiers()
    */
-  public static function extractIdentifiers($xml, $entity_type) {
+  public static function extractIdentifiers($xml) {
     $fetch_tagdata = [
       'ListID',
       'TxnID',
@@ -82,6 +85,7 @@ final class QbWebConnectUtilities {
       'newMessageSetID',
       'messageSetStatusCode',
     ];
+
     $list = [];
     foreach ($fetch_tagdata as $tag) {
       if (FALSE !== ($start = strpos($xml, '<' . $tag . '>')) &&
@@ -95,19 +99,8 @@ final class QbWebConnectUtilities {
         $list[$attribute] = substr($xml, $start + strlen($attribute) + 3, $end - $start - strlen($attribute) - 3);
       }
     }
-    switch ($entity_type) {
-      case 'profile':
-      case 'commerce_product':
-      case 'commerce_product_variation':
-      case 'commerce_payment':
-        $attribute = 'ListID';
-        break;
-
-      default:
-        $attribute = 'TxnID';
-    }
 
-    return $list[$attribute] ?? NULL;
+    return $list ?? NULL;
   }
 
   /**
@@ -165,8 +158,234 @@ final class QbWebConnectUtilities {
   public static function isQuickbooksIdentifier($identifier) {
     // UUIDs from drupal look like: e37a610c-30b9-49f0-b701-d7cf4602c130.
     // Identifiers from Quickbooks look like: 110000-1232697602.
+    if (strpos($identifier, '-') == FALSE) {
+      return FALSE;
+    }
+
     str_replace('-', '', $identifier, $count);
     return $count < 2;
   }
 
+  /**
+   * Returns a Drupal entity ID from the QB migrate map tables for a QB List ID.
+   *
+   * @param string $entity_type
+   *   The migration entity type. ie. customer, order.
+   * @param array $filters
+   *   An array of filters to pass to the getRowBy_x function. ie. list_id, nid.
+   *
+   * @return string
+   *   The entity ID. FALSE if not found.
+   */
+  public static function getEntityId($entity_type, array $filters) {
+    // First query the export table.
+    $plugin_manager = \Drupal::service('plugin.manager.migration');
+    $migrations = self::getQbMigrationIds();
+    $migration_id = $migrations[$entity_type]['export'];
+    /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
+    $migration = $plugin_manager->createInstance($migration_id);
+    $id_map = $migration->getIdMap();
+    $row = $id_map->getRowByDestination($filters);
+    if ($row) {
+      return $row['sourceid1'];
+    }
+
+    // Now, query the import table.
+    $migration_id = $migrations[$entity_type]['import'];
+    /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
+    $migration = $plugin_manager->createInstance($migration_id);
+    $id_map = $migration->getIdMap();
+    $row = $id_map->getRowBySource($filters);
+    if ($row) {
+      return $row['destid1'];
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Returns the QB details from the migrate map table given a Drupal entity ID.
+   *
+   * @param string $entity_type
+   *   The migration entity type. ie. customer, order.
+   * @param array $filters
+   *   An array of filters to pass to the getRowBy_x function. ie. list_id, nid.
+   *
+   * @return array
+   *   An array with the List ID and edit sequence. FALSE if not found.
+   */
+  public static function getQbDetailsForEntity($entity_type, array $filters) {
+    // First query the export table.
+    $plugin_manager = \Drupal::service('plugin.manager.migration');
+    $migrations = self::getQbMigrationIds();
+    $migration_id = $migrations[$entity_type]['export'];
+    /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
+    $migration = $plugin_manager->createInstance($migration_id);
+    $id_map = $migration->getIdMap();
+    $row = $id_map->getRowBySource($filters);
+    if ($row && self::isQuickbooksIdentifier($row['destid2'])) {
+      return [
+        'list_id' => $row['destid2'],
+        'edit_sequence' => $row['destid3'],
+      ];
+    }
+
+    // Now, query the import table.
+    $migration_id = $migrations[$entity_type]['import'];
+    /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
+    $migration = $plugin_manager->createInstance($migration_id);
+    $id_map = $migration->getIdMap();
+    $row = $id_map->getRowByDestination($filters);
+    if ($row && self::isQuickbooksIdentifier($row['sourceid1'])) {
+      return [
+        'list_id' => $row['sourceid1'],
+      ];
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Returns the migration ids for both import and export to/from QB.
+   *
+   * @return array
+   *   An array of table names keyed on the entity type and then the migrate
+   *   action type.
+   */
+  public static function getQbMigrationIds() {
+    return [
+      'customer' => [
+        'export' => 'qb_webconnect_customer',
+        'import' => 'qb_webconnect_customer_import',
+      ],
+      'product' => [
+        'export' => 'qb_webconnect_product',
+        'import' => 'qb_webconnect_product_import',
+      ],
+      'product_variation' => [
+        'export' => 'qb_webconnect_product_variation',
+        'import' => 'qb_webconnect_product_variation_import',
+      ],
+      'order' => [
+        'export' => 'qb_webconnect_order',
+        'import' => 'qb_webconnect_order_import',
+      ],
+      'payment' => [
+        'export' => 'qb_webconnect_payment',
+        'import' => 'qb_webconnect_payment_import',
+      ],
+    ];
+  }
+
+  /**
+   * Provides the qb list ID associated with the given profile.
+   *
+   * Drupal creates duplicate profiles. So, we fetch all the matching profiles
+   * for the given profile and check if any exist in the qb export/import map
+   * tables. If a match is found, return that particular list ID. So as to avoid
+   * creating duplicate profiles in quickbooks.
+   *
+   * @param \Drupal\profile\Entity\ProfileInterface $profile
+   *   The customer profile entity.
+   *
+   * @return mixed
+   *   The listId if a match was found, null otherwise.
+   */
+  public static function getQbListId(ProfileInterface $profile) {
+    $database = \Drupal::database();
+    $db_or = new Condition('OR');
+    $profiles = self::getAllMatchingProfiles($profile);
+
+    if (empty($profiles)) {
+      return NULL;
+    }
+
+    // The migration map table names.
+    $export_table_name = self::getMigrateMapTableName('customer', 'export');
+    $import_table_name = self::getMigrateMapTableName('customer', 'import');
+
+    $query = $database->select($export_table_name, 'migrate_map_export');
+    $query->leftJoin($import_table_name, 'migrate_map_import', 'migrate_map_import.destid1 = migrate_map_export.sourceid1');
+    $query->fields('migrate_map_import', ['sourceid1']);
+    $query->fields('migrate_map_export', ['destid2']);
+    $db_or->condition('migrate_map_export.sourceid1', $profiles, 'IN');
+    $db_or->condition('migrate_map_import.destid1', $profiles, 'IN');
+    $query->condition($db_or);
+    $results = $query->execute()->fetchAll();
+    foreach ($results as $result) {
+      if (!empty($result->sourceid1)) {
+        return $result->sourceid1;
+      }
+
+      if (!empty($result->destid2)) {
+        return $result->destid2;
+      }
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Provides all matching profiles for the given profile.
+   *
+   * This method returns all duplicate profiles for the given profile.
+   * It checks for an exact address match by checking all address fields.
+   *
+   * @param \Drupal\profile\Entity\ProfileInterface $profile
+   *   The customer profile entity.
+   *
+   * @return mixed
+   *   The duplicate profile array if any found.
+   */
+  public static function getAllMatchingProfiles(ProfileInterface $profile) {
+    $address = $profile->get('address')->first()->getValue();
+
+    $database = \Drupal::database();
+
+    $first_name = $address['given_name'];
+    $last_name = $address['family_name'];
+
+    $query = $database->select('profile', 'profile');
+    $query->fields('profile', ['profile_id']);
+    $query->leftJoin('profile__address', 'address', 'profile.profile_id = address.entity_id');
+    $query->condition('profile.type', 'customer');
+    $query->condition('address.address_given_name', $address['given_name'], 'LIKE');
+    $query->condition('address.address_family_name', $address['family_name'], 'LIKE');
+    $query->condition('address.address_country_code', $address['country_code']);
+    // The first address line could have the customer's name as the first line
+    // in QB.
+    $name_and_address_line_1 = "$first_name $last_name";
+    $or_condition = $query->orConditionGroup()
+      ->condition('address.address_address_line1', $address['address_line1'], 'LIKE')
+      ->condition('address.address_address_line1', $name_and_address_line_1, 'LIKE');
+    $query->condition($or_condition);
+    $query->condition('address.address_locality', $address['locality'], 'LIKE');
+    $query->condition('address.address_postal_code', $address['postal_code']);
+    $query->condition('profile.uid', $profile->getOwnerId());
+    $profiles = $query->execute()->fetchAllKeyed(0, 0);
+
+    return $profiles;
+  }
+
+  /**
+   * Provides the migration map table name for an operation.
+   *
+   * @param string $entity_type
+   *   The type of entity.
+   * @param string $operation
+   *   Import or Export operation.
+   *
+   * @return string
+   *   The table name.
+   */
+  public function getMigrateMapTableName(string $entity_type, string $operation) {
+    $plugin_manager = \Drupal::service('plugin.manager.migration');
+    $migrations = self::getQbMigrationIds();
+
+    $migration_map = $plugin_manager->createInstance(
+      $migrations[$entity_type][$operation]
+    );
+    return $migration_map->getIdMap()->mapTableName();
+  }
+
 }
diff --git a/src/SoapBundle/ImportSoapServiceController.php b/src/SoapBundle/ImportSoapServiceController.php
new file mode 100644
index 0000000..988c1af
--- /dev/null
+++ b/src/SoapBundle/ImportSoapServiceController.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\SoapBundle;
+
+use Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapServiceInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Class SoapServiceController.
+ *
+ * @package Drupal\commerce_qb_webconnect\SoapBundle
+ */
+class ImportSoapServiceController extends ControllerBase {
+
+  /**
+   * The SOAP service.
+   *
+   * @var \Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapService
+   */
+  protected $soapService;
+
+  /**
+   * The logger service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $logger;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The SOAP server.
+   *
+   * @var \SoapServer
+   */
+  protected $server;
+
+  /**
+   * SoapServiceController constructor.
+   *
+   * @param \Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapServiceInterface $soapService
+   *   The SOAP service.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
+   *   The logger service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler.
+   */
+  public function __construct(SoapServiceInterface $soapService, LoggerChannelFactoryInterface $logger, ModuleHandlerInterface $moduleHandler) {
+    $this->soapService = $soapService;
+    $this->logger = $logger;
+    $this->moduleHandler = $moduleHandler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('commerce_qb_webconnect.import_soap_service'),
+      $container->get('logger.factory'),
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * Construct the SOAP service and handle the request.
+   *
+   * @TODO: Pass in WSDL file location as a parameter.
+   */
+  public function handleRequest() {
+    // Allow other modules to make changes to the SOAP service, such as swapping
+    // out the validation or qbxml parser plugins.
+    $this->moduleHandler->alter('commerce_qb_webconnect_import_soapservice', $this->soapService);
+
+    // Clear the wsdl caches.
+    ini_set('soap.wsdl_cache_enabled', 0);
+    ini_set('soap.wsdl_cache_ttl', 0);
+
+    // Create the Soap server.
+    $this->server = new \SoapServer(__DIR__ . '/QBWebConnectorSvc.wsdl');
+    $this->server->setObject($this->soapService);
+
+    $response = new Response();
+    $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1');
+
+    ob_start();
+    $this->server->handle();
+    $response->setContent(ob_get_clean());
+
+    return $response;
+  }
+
+}
diff --git a/src/SoapBundle/Services/ImportSoapService.php b/src/SoapBundle/Services/ImportSoapService.php
new file mode 100644
index 0000000..bb43b0c
--- /dev/null
+++ b/src/SoapBundle/Services/ImportSoapService.php
@@ -0,0 +1,707 @@
+<?php
+
+namespace Drupal\commerce_qb_webconnect\SoapBundle\Services;
+
+use Drupal\commerce_qb_webconnect\QbWebConnectUtilities;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\MigrateMessage;
+use Drupal\migrate\Plugin\MigratePluginManagerInterface;
+use Drupal\migrate\Plugin\MigrationPluginManager;
+use Drupal\user\Entity\User;
+use Drupal\user\UserAuthInterface;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\State\StateInterface;
+
+/**
+ * Handle SOAP requests and return a response.
+ *
+ * Class SoapService.
+ *
+ * @package Drupal\commerce_qb_webconnect\SoapBundle\Services
+ */
+class ImportSoapService implements SoapServiceInterface {
+
+  /**
+   * The current time.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManager
+   */
+  protected $migrationPluginManager;
+
+  /**
+   * The id map plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigratePluginManagerInterface
+   */
+  protected $idMapPluginManager;
+
+  /**
+   * The user auth service.
+   *
+   * @var \Drupal\user\UserAuthInterface
+   */
+  private $userAuthInterface;
+
+  /**
+   * The session manager.
+   *
+   * Responsible for managing, validating and invalidating SOAP sessions.
+   *
+   * @var \Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapSessionManager
+   */
+  protected $sessionManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The module's configuration.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $config;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The current server version.
+   *
+   * @var string
+   */
+  protected $serverVersion = '1.0';
+
+  /**
+   * The version returned by the client.
+   *
+   * @var string
+   */
+  protected $clientVersion;
+
+  /**
+   * Constructs a new SoapService.
+   *
+   * @param \Drupal\migrate\Plugin\MigrationPluginManager $migrationPluginManager
+   *   The migration plugin manager.
+   * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $idMapPluginManager
+   *   The id mapping plugin migrate manager.
+   * @param \Drupal\user\UserAuthInterface $userAuthInterface
+   *   The user auth service.
+   * @param \Drupal\commerce_qb_webconnect\SoapBundle\Services\SoapSessionManager $sessionManager
+   *   The session manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The current time.
+   */
+  public function __construct(
+    MigrationPluginManager $migrationPluginManager,
+    MigratePluginManagerInterface $idMapPluginManager,
+    UserAuthInterface $userAuthInterface,
+    SoapSessionManager $sessionManager,
+    EntityTypeManagerInterface $entityTypeManager,
+    ConfigFactoryInterface $configFactory,
+    StateInterface $state,
+    TimeInterface $time
+  ) {
+    $this->migrationPluginManager = $migrationPluginManager;
+    $this->idMapPluginManager = $idMapPluginManager;
+    $this->userAuthInterface = $userAuthInterface;
+    $this->sessionManager = $sessionManager;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->config = $configFactory->get('commerce_qb_webconnect.quickbooks_admin');
+    $this->state = $state;
+    $this->time = $time;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __call($method, array $data) {
+    $public_services = ['clientVersion', 'serverVersion', 'authenticate'];
+
+    $request = $this->prepareResponse($method, $data);
+
+    $uc = ucfirst($method);
+    $callable = "call$uc";
+
+    $response = NULL;
+
+    // If the method being requested requires a validated user, do that now.
+    if (!in_array($method, $public_services)) {
+      // The request must have a ticket to proceed.
+      if (empty($request->ticket)) {
+        return $request;
+      }
+
+      $valid = $this->sessionManager
+        ->setUuid($request->ticket)
+        ->validateSession($method);
+
+      // If the client has a valid ticket and request, log in now.
+      if ($valid) {
+        /** @var \Drupal\user\UserInterface $user */
+        $user = User::load($this->sessionManager->getUid());
+        user_login_finalize($user);
+
+        if (!$user->hasPermission('access quickbooks soap service')) {
+          // @todo Inject the logger service.
+          \Drupal::logger('commerce_qb_webconnect')->warning('User logged in successfully but didn\'t have Quickbooks SOAP Service access permissions.');
+          return $request;
+        }
+      }
+      else {
+        \Drupal::logger('commerce_qb_webconnect')->error('The user had an invalid session token or made an invalid request.  Aborting communication...');
+        return $request;
+      }
+    }
+
+    // If a valid method method is being called, parse the incoming request
+    // and call the method with the parsed data passed in.
+    if (is_callable([$this, $callable])) {
+      // Prepare the response to the client.
+      $response = $this->$callable($request);
+    }
+
+    return $response;
+  }
+
+
+  /****************************************************
+   * Private helper functions                         *
+   ****************************************************/
+
+  /**
+   * Builds the stdClass object required by a service response handler.
+   *
+   * @param string $method_name
+   *   The Quickbooks method being called.
+   * @param string $data
+   *   The raw incoming soap request.
+   *
+   * @return \stdClass
+   *   An object with the following properties:
+   *   stdClass {
+   *     methodNameResult => '',
+   *     requestParam1 => 'foo',
+   *     ...
+   *     requestParamN => 'bar',
+   *   }
+   */
+  private function prepareResponse($method_name, $data) {
+    $response = isset($data[0]) ? $data[0] : new \stdClass();
+    $response->$method_name = '';
+
+    return $response;
+  }
+
+  /**
+   * Calculate the completion progress of the current SOAP session.
+   *
+   * @return int
+   *  The percentage completed.
+   */
+  private function getCompletionProgress() {
+    $migrations = $this
+      ->migrationPluginManager
+      ->createInstancesByTag('QB Webconnect Import');
+    if (!$migrations) {
+      return 100;
+    }
+
+    $migration_ids = array_keys($migrations);
+    $last_migration_id = end($migration_ids);
+    $current_migration_id = $this
+      ->state
+      ->get('qb_webconnect.import.current_migration');
+
+    // Still more to process for current migration.
+    // If we are in the midst of an iteration and there are still more left to
+    // import, return that we have not completed the current migration.
+    $iterator_id = $this->state->get($current_migration_id . '_iterator_id');
+    if ($iterator_id) {
+      // Calculate how many migrations we've done out of the total migrations.
+      $done = array_search($current_migration_id, $migration_ids);
+      $todo = count($migration_ids) - $done;
+
+      return $done + $todo ? (int) (100 * ($done / ($done + $todo))) : 1;
+    }
+
+    // Session is complete if we've processed all migrations.
+    if ($current_migration_id === $last_migration_id) {
+      // Set the current migration to NULL so we start over with the first
+      // migration, the next time the session starts.
+      $this
+        ->state
+        ->set('qb_webconnect.import.current_migration', NULL);
+      return 100;
+    }
+
+    // Current migration is complete.
+    // Calculate how many migrations we've done out of the total migrations.
+    $done = array_search($current_migration_id, $migration_ids) + 1;
+    $todo = count($migration_ids) - $done;
+
+    return $done + $todo ? (int) (100 * ($done / ($done + $todo))) : 1;
+  }
+
+  /****************************************************
+   * The WSDL defined SOAP service calls              *
+   ****************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callServerVersion(\stdClass $request) {
+    $request->serverVersionResult = $this->serverVersion;
+    return $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callClientVersion(\stdClass $request) {
+    $this->clientVersion = $request->strVersion;
+
+    $request->clientVersionResult = '';
+    return $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @TODO: Reset failed import ids requested.
+   */
+  public function callAuthenticate(\stdClass $request) {
+    $strUserName = $request->strUserName;
+    $strPassword = $request->strPassword;
+
+    // Initial "fail" response.
+    $result = ['', 'nvu'];
+
+    // If the service isn't set for whatever reason we can't continue.
+    if (!isset($this->userAuthInterface)) {
+      \Drupal::logger('commerce_qb_webconnect')->error("User Auth service couldn't be initialized.");
+    }
+    else {
+      $uid = $this->userAuthInterface->authenticate($strUserName, $strPassword);
+
+      if (!$uid) {
+        \Drupal::logger('commerce_qb_webconnect')->error("Invalid login credentials, aborting quickbooks SOAP service.");
+      }
+      else {
+        $uuid = \Drupal::service('uuid')->generate();
+        $this->sessionManager->startSession($uuid, $uid);
+
+        $result = [$uuid, ''];
+      }
+    }
+
+    $request->authenticateResult = $result;
+    return $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callSendRequestXML(\stdClass $request) {
+    // Fetch the next migration that needs to be run.
+    $migration = $this->nextMigration();
+    if (!$migration) {
+      return $request;
+    }
+
+    // Set in the state which migration we're currently doing.
+    $this
+      ->state
+      ->set('qb_webconnect.import.current_migration', $migration->id());
+
+    // Make a call to fetch all the entities that need to be migrated.
+    /** @var \Drupal\migrate\Plugin\Migration $migration */
+    $source = $migration->getSourceConfiguration();
+    $callback = $this->prepareCallback($source['static']['send_callback']);
+    if (is_callable($callback)) {
+      $qbxml = call_user_func($callback);
+      $request->sendRequestXMLResult = $this->addXMLEnvelope($qbxml);
+
+      return $request;
+    }
+
+    return $request;
+  }
+
+  /**
+   * Fetches the next migration that needs to be run.
+   *
+   * @return \Drupal\migrate\Plugin\Migration|NULL
+   *   The next migration that needs to be run or NULL if imports are not
+  *    enabled.
+   */
+  protected function nextMigration() {
+    // Get all the import migrations.
+    $migrations = $this
+      ->migrationPluginManager
+      ->createInstancesByTag('QB Webconnect Import');
+    if (!$migrations) {
+      return NULL;
+    }
+
+    // Get the current migration, if we're in the process of running migrations.
+    $current_migration_id = $this
+      ->state
+      ->get('qb_webconnect.import.current_migration');
+
+    // First run for the session.
+    if (!$current_migration_id) {
+      return reset($migrations);
+    }
+
+    // Check the current migration that was run. If we are in the midst of the
+    // iterator and there are remaining items to be imported for this migration,
+    // return that.
+    $iterator_id = $this->state->get(
+      $current_migration_id . '_iterator_id'
+    );
+    if ($iterator_id) {
+      return $migrations[$current_migration_id];
+    }
+
+    // If we only have one migration in the list, return that.
+    if (count($migrations) == 1) {
+      return reset($migrations);
+    }
+
+    // If all else fails, determine which migration is next in the sequence.
+    $migration_ids = array_keys($migrations);
+    $last_migration_id = end($migration_ids);
+
+    // If we're at the end of all migrations, run from the top.
+    if ($current_migration_id === $last_migration_id) {
+      return reset($migrations);
+    }
+
+    // Otherwise, return the next migration in the list.
+    $current_migration_key = array_search($current_migration_id, $migration_ids);
+    $next_migration_id = $migration_ids[$current_migration_key + 1];
+
+    return $migrations[$next_migration_id];
+  }
+
+  /**
+   * Converts callback notations to a valid callable.
+   *
+   *
+   * @param string|array $callback
+   *   The callback.
+   *
+   * @return array
+   *   A valid callable.
+   */
+  protected function prepareCallback($callback) {
+    if (is_string($callback)) {
+      $callback = [get_called_class(), $callback];
+    }
+    return $callback;
+  }
+
+  /**
+   * Add an XML envelope.
+   *
+   * @param string $qbxml
+   *   The qbxml.
+   *
+   * @return string
+   *   The xml wrapped in an envelope.
+   */
+  protected function addXMLEnvelope($qbxml) {
+    return '<?xml version="1.0" encoding="utf-8"?><?qbxml version="13.0"?><QBXML><QBXMLMsgsRq onError="stopOnError">' . $qbxml . '</QBXMLMsgsRq></QBXML>';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callReceiveResponseXML(\stdClass $request) {
+    // Initiate the migration import if we have received a successful response.
+    if (!empty($request->response)) {
+      $status_code = QbWebConnectUtilities::extractStatusCode($request->response);
+
+      if (!$status_code) {
+        // Perform the migration of the fetched items here.
+        $current_migration_id = $this->state->get('qb_webconnect.import.current_migration');
+        $migration = $this->migrationPluginManager->createInstance(
+          $current_migration_id
+        );
+        $source = $migration->getSourceConfiguration();
+        $source['request_content'] = $request->response;
+        $migration->set('source', $source);
+        // @todo Figure out a better way than updating every row.
+        // Because even rows that were skipped or have failed will have a status
+        // of 1, making it difficult to debug.
+        $migration->getIdMap()->prepareUpdate();
+        $executable = new MigrateExecutable($migration, new MigrateMessage());
+        $executable->import();
+      }
+    }
+
+    // Now, update the last run time for this migration.
+    $this->updateLastRunTime($request);
+    $request->receiveResponseXMLResult = $this->getCompletionProgress();
+
+    return $request;
+  }
+
+  /**
+   * Update the last run time in the state for the current migration.
+   *
+   * @param \stdClass $request
+   *   The request.
+   */
+  protected function updateLastRunTime(\stdClass $request) {
+    // If we are in the midst of an iterator, set the iterator ID for the
+    // migration.
+    $identifiers = QbWebConnectUtilities::extractIdentifiers($request->response);
+    $current_migration_id = $this
+      ->state
+      ->get('qb_webconnect.import.current_migration');
+    if (isset($identifiers['iteratorID']) && $identifiers['iteratorRemainingCount'] != 0) {
+      $this->state->set(
+        $current_migration_id . '_iterator_id',
+        $identifiers['iteratorID']
+      );
+    }
+
+    // If we are done with iteration for this migration, set the iterator ID
+    // to NULL and set the last run time, so we only import data updated from
+    // this time.
+    if (!isset($identifiers['iteratorRemainingCount']) || $identifiers['iteratorRemainingCount'] == 0) {
+      $this->state->set(
+        $current_migration_id . '_iterator_id',
+        NULL
+      );
+      $this->state->set(
+        $current_migration_id . '_last_run_time',
+        $this->time->getCurrentTime()
+      );
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callGetLastError(\stdClass $request) {
+    $progress = $this->getCompletionProgress();
+
+    if ($progress == 100) {
+      $request->getLastErrorResult = 'No new imports remaining.';
+    }
+    else {
+      $request->getLastErrorResult = "$progress% remaining.";
+    }
+
+    return $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function callCloseConnection(\stdClass $request) {
+    $this->sessionManager->closeSession();
+    $request->closeConnectionResult = 'OK';
+
+    return $request;
+  }
+
+  /**
+   * Prepares a Customer Query to QB to fetch new or updated customers.
+   */
+  protected function prepareCustomerImport() {
+    $customer = new \QuickBooks_QBXML_Object_Customer();
+    $customer->set('ActiveStatus', 'ActiveOnly');
+
+    // Only fetch customers updated since the last run timestamp, if we've
+    // already run this import process before.
+    $last_run_time = $this->state->get(
+      'qb_webconnect_customer_import_last_run_time'
+    );
+    if ($last_run_time) {
+      $customer->set('FromModifiedDate', date('Y-m-d', $last_run_time));
+      return $customer->asQBXML(QUICKBOOKS_QUERY_CUSTOMER);
+    }
+
+    // Else, fetch 1000 records at a time and import.
+    $customer->set('MaxReturned', 1000);
+    $iterator = 'iterator="Start"';
+    $iterator_id = $this->state->get(
+      'qb_webconnect_customer_import_iterator_id'
+    );
+    if ($iterator_id) {
+      $iterator = 'iterator="Continue"';
+      $iterator .= ' iteratorID="' . $iterator_id . '"';
+    }
+    $xml_string = $customer->asQBXML(QUICKBOOKS_QUERY_CUSTOMER);
+    $xml_string = substr_replace($xml_string, ' ' . $iterator, 16, 0);
+
+    return $xml_string;
+  }
+
+  /**
+   * Prepares a Inventory Query to QB to fetch new or updated products.
+   */
+  protected function prepareProductImport() {
+    $product = new \QuickBooks_QBXML_Object_InventoryItem();
+    $product->set('ActiveStatus', 'ActiveOnly');
+
+    // Only fetch customers updated since the last run timestamp, if we've
+    // already run this import process before.
+    $last_run_time = $this->state->get(
+      'qb_webconnect_product_import_last_run_time'
+    );
+    if ($last_run_time) {
+      $product->set('FromModifiedDate', date('Y-m-d', $last_run_time));
+      return $product->asQBXML(QUICKBOOKS_QUERY_INVENTORYITEM);
+    }
+
+    // Else, fetch 1000 records at a time and import.
+    $product->set('MaxReturned', 1000);
+    $iterator = 'iterator="Start"';
+    $iterator_id = $this->state->get(
+      'qb_webconnect_product_import_iterator_id'
+    );
+    if ($iterator_id) {
+      $iterator = 'iterator="Continue"';
+      $iterator .= ' iteratorID="' . $iterator_id . '"';
+    }
+    $xml_string = $product->asQBXML(QUICKBOOKS_QUERY_INVENTORYITEM);
+    $xml_string = substr_replace($xml_string, ' ' . $iterator, 21, 0);
+
+    return $xml_string;
+  }
+
+  /**
+   * Prepares a Inventory Query to QB to fetch new or updated product variatons.
+   */
+  protected function prepareProductVariationImport() {
+    $product = new \QuickBooks_QBXML_Object_InventoryItem();
+    $product->set('ActiveStatus', 'ActiveOnly');
+
+    // Only fetch customers updated since the last run timestamp, if we've
+    // already run this import process before.
+    $last_run_time = $this->state->get(
+      'qb_webconnect_product_variation_import_last_run_time'
+    );
+    if ($last_run_time) {
+      $product->set('FromModifiedDate', date('Y-m-d', $last_run_time));
+      return $product->asQBXML(QUICKBOOKS_QUERY_INVENTORYITEM);
+    }
+
+    // Else, fetch 1000 records at a time and import.
+    $product->set('MaxReturned', 1000);
+    $iterator = 'iterator="Start"';
+    $iterator_id = $this->state->get(
+      'qb_webconnect_product_variation_import_iterator_id'
+    );
+    if ($iterator_id) {
+      $iterator = 'iterator="Continue"';
+      $iterator .= ' iteratorID="' . $iterator_id . '"';
+    }
+    $xml_string = $product->asQBXML(QUICKBOOKS_QUERY_INVENTORYITEM);
+    $xml_string = substr_replace($xml_string, ' ' . $iterator, 21, 0);
+
+    return $xml_string;
+  }
+
+  /**
+   * Prepares a SalesReceipt Query to QB to fetch new or updated orders.
+   */
+  protected function prepareOrderImport() {
+    $order = new \QuickBooks_QBXML_Object_SalesReceipt();
+    $order->set('IncludeLineItems', 'true');
+
+    // Only fetch customers updated since the last run timestamp, if we've
+    // already run this import process before.
+    $last_run_time = $this->state->get(
+      'qb_webconnect_order_import_last_run_time'
+    );
+    if ($last_run_time) {
+      $order->set(
+        'ModifiedDateRangeFilter FromModifiedDate',
+        date('Y-m-d', $last_run_time)
+      );
+      return $order->asQBXML(QUICKBOOKS_QUERY_SALESRECEIPT);
+    }
+
+    // Else, fetch 1000 records at a time and import.
+    $order->set('MaxReturned', 500);
+    $iterator = 'iterator="Start"';
+    $iterator_id = $this->state->get(
+      'qb_webconnect_order_import_iterator_id'
+    );
+    if ($iterator_id) {
+      $iterator = 'iterator="Continue"';
+      $iterator .= ' iteratorID="' . $iterator_id . '"';
+    }
+    $xml_string = $order->asQBXML(QUICKBOOKS_QUERY_SALESRECEIPT);
+    $xml_string = substr_replace($xml_string, ' ' . $iterator, 20, 0);
+
+    return $xml_string;
+  }
+
+  /**
+   * Prepares a ReceivePayment Query to QB to fetch new or updated payments.
+   */
+  protected function preparePaymentImport() {
+    $payment = new \QuickBooks_QBXML_Object_ReceivePayment();
+
+    // Only fetch customers updated since the last run timestamp, if we've
+    // already run this import process before.
+    $last_run_time = $this->state->get(
+      'qb_webconnect_payment_import_last_run_time'
+    );
+    if ($last_run_time) {
+      $payment->set(
+        'ModifiedDateRangeFilter FromModifiedDate',
+        date('Y-m-d', $last_run_time)
+      );
+
+      return $payment->asQBXML(QUICKBOOKS_QUERY_RECEIVEPAYMENT);
+    }
+
+    // Else, fetch 1000 records at a time and import.
+    $payment->set('MaxReturned', 1000);
+    $iterator = 'iterator="Start"';
+    $iterator_id = $this->state->get(
+      'qb_webconnect_payment_import_iterator_id'
+    );
+    if ($iterator_id) {
+      $iterator = 'iterator="Continue"';
+      $iterator .= ' iteratorID="' . $iterator_id . '"';
+    }
+    $xml_string = $payment->asQBXML(QUICKBOOKS_QUERY_RECEIVEPAYMENT);
+    $xml_string = substr_replace($xml_string, ' ' . $iterator, 22, 0);
+
+    return $xml_string;
+  }
+
+}
diff --git a/src/SoapBundle/Services/SoapService.php b/src/SoapBundle/Services/SoapService.php
index 6f30a37..58eb653 100644
--- a/src/SoapBundle/Services/SoapService.php
+++ b/src/SoapBundle/Services/SoapService.php
@@ -5,6 +5,7 @@ namespace Drupal\commerce_qb_webconnect\SoapBundle\Services;
 use Drupal\commerce_qb_webconnect\QbWebConnectUtilities;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\migrate\MigrateExecutable;
 use Drupal\migrate\MigrateMessage;
@@ -24,7 +25,6 @@ use Drupal\user\UserAuthInterface;
  */
 class SoapService implements SoapServiceInterface {
 
-
   /**
    * The row currently being migrated.
    *
@@ -97,6 +97,13 @@ class SoapService implements SoapServiceInterface {
    */
   protected $clientVersion;
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a new SoapService.
    *
@@ -114,6 +121,8 @@ class SoapService implements SoapServiceInterface {
    *   The config factory.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
    */
   public function __construct(
     MigrationPluginManager $migrationPluginManager,
@@ -122,7 +131,8 @@ class SoapService implements SoapServiceInterface {
     SoapSessionManager $sessionManager,
     EntityTypeManagerInterface $entityTypeManager,
     ConfigFactoryInterface $configFactory,
-    StateInterface $state
+    StateInterface $state,
+    RendererInterface $renderer
   ) {
     $this->migrationPluginManager = $migrationPluginManager;
     $this->idMapPluginManager = $idMapPluginManager;
@@ -131,6 +141,7 @@ class SoapService implements SoapServiceInterface {
     $this->entityTypeManager = $entityTypeManager;
     $this->config = $configFactory->get('commerce_qb_webconnect.quickbooks_admin');
     $this->state = $state;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -184,9 +195,8 @@ class SoapService implements SoapServiceInterface {
     return $response;
   }
 
-
   /****************************************************
-   * Private helper functions                         *
+   * Private helper functions                         *.
    ****************************************************/
 
   /**
@@ -197,7 +207,7 @@ class SoapService implements SoapServiceInterface {
    * @param string $data
    *   The raw incoming soap request.
    *
-   * @return \stdClass
+   * @return object
    *   An object with the following properties:
    *   stdClass {
    *     methodNameResult => '',
@@ -225,15 +235,14 @@ class SoapService implements SoapServiceInterface {
     foreach ($this->migrationPluginManager->createInstancesByTag('QB Webconnect') as $id => $migration) {
       $map = $migration->getIdMap();
       $done += $map->processedCount();
-      $todo += $migration->getSourcePlugin()->count() - $map->processedCount();
+      $todo += ($migration->getSourcePlugin()->count() - $map->processedCount()) + $migration->getIdMap()->updateCount();
     }
 
     return $done + $todo ? (int) (100 * ($done / ($done + $todo))) : 1;
   }
 
-
   /****************************************************
-   * The WSDL defined SOAP service calls              *
+   * The WSDL defined SOAP service calls              *.
    ****************************************************/
 
   /**
@@ -297,9 +306,10 @@ class SoapService implements SoapServiceInterface {
     foreach ($this->migrationPluginManager->buildDependencyMigration($migrations, []) as $migration) {
       // Proceed to next migration if there are no remaining items to import.
       $remaining = $migration->getSourcePlugin()->count() - $migration->getIdMapPlugin()->processedCount();
-      if (!$remaining && !$migration->getIdMap()->updateCount()) {
+      if ($remaining <= 0 && !$migration->getIdMap()->updateCount()) {
         continue;
       }
+
       // Our MigrateSubscriber stops this migration after a single row.
       (new MigrateExecutable($migration, new MigrateMessage()))->import();
       // Let's end the import for now and we'll continue next time.
@@ -321,7 +331,6 @@ class SoapService implements SoapServiceInterface {
   /**
    * Converts callback notations to a valid callable.
    *
-   *
    * @param string|array $callback
    *   The callback.
    *
@@ -386,15 +395,43 @@ class SoapService implements SoapServiceInterface {
   /**
    * Update identifiers.
    *
-   * @param \stdClass $request
+   * @param object $request
    *   The request.
    */
   protected function updateIdentifier(\stdClass $request) {
-    $identifier = $this->row->getDestinationProperty('uuid');
+    // Fetch the identifiers retrieved from the response so that we can save
+    // it in the id map.
+    $id_map = $this->row->getIdMap();
+    $identifiers = [
+      'uuid' => $this->row->getDestinationProperty('uuid'),
+    ];
+    // Retain the old mappings even if there's an error.
+    if (isset($id_map['destid2'])) {
+      $identifiers['list_id'] = $id_map['destid2'];
+    }
+    if (isset($id_map['destid3'])) {
+      $identifiers['edit_sequence'] = $id_map['destid3'];
+    }
 
-    if ($extracted = QbWebConnectUtilities::extractIdentifiers($request->response, $this->row->getSource()['entity_type'])) {
-      $identifier = $extracted;
+
+    if ($extracted = QbWebConnectUtilities::extractIdentifiers($request->response)) {
+      switch ($this->row->getSource()['entity_type']) {
+        case 'commerce_order':
+          $attribute = 'TxnID';
+          break;
+
+        default:
+          $attribute = 'ListID';
+      }
+      if (isset($extracted[$attribute]) && !empty($extracted[$attribute])) {
+        $identifiers['uuid'] = $extracted[$attribute];
+        $identifiers['list_id'] = $extracted[$attribute];
+      }
+      if (isset($extracted['EditSequence']) && !empty($extracted['EditSequence'])) {
+        $identifiers['edit_sequence'] = $extracted['EditSequence'];
+      }
     }
+
     $import_status = MigrateIdMapInterface::STATUS_IMPORTED;
     // Mark a row as needing update so it can be re-imported if an error occurs.
     if ($code = QbWebConnectUtilities::extractStatusCode($request->response)) {
@@ -420,7 +457,7 @@ class SoapService implements SoapServiceInterface {
     }
     /** @var \Drupal\migrate\Plugin\Migration $migration */
     $migration = $this->migrationPluginManager->createInstance($this->state->get('qb_webconnect.current_migration'));
-    $migration->getIdMap()->saveIdMapping($this->row, ['uuid' => $identifier], $import_status);
+    $migration->getIdMap()->saveIdMapping($this->row, $identifiers, $import_status);
   }
 
   /**
@@ -454,15 +491,14 @@ class SoapService implements SoapServiceInterface {
    */
   protected function prepareCustomerExport() {
     if ($this->row->getSourceProperty('bundle') == 'customer') {
-      $uuid = $this->row->getDestinationProperty('uuid');
       $profile_id = $this->row->getSourceProperty('profile_id');
+      $profile = $this
+        ->entityTypeManager
+        ->getStorage('profile')
+        ->load($profile_id);
       $addresses = $this->row->getSourceProperty('address');
       $address = reset($addresses);
       $customer = new \QuickBooks_QBXML_Object_Customer();
-      if (QbWebConnectUtilities::isQuickbooksIdentifier($uuid)) {
-        $customer->setListID($uuid);
-        return $customer->asQBXML(QUICKBOOKS_QUERY_CUSTOMER);
-      }
 
       $address1 = $address['address_line1'];
       $address2 = $address['address_line2'];
@@ -474,19 +510,74 @@ class SoapService implements SoapServiceInterface {
       $province = '';
       $postal_code = $address['postal_code'];
       $country = $address['country_code'];
-      $customer->setBillAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
+      $customer->setBillAddress(
+        $address1,
+        $address2,
+        $address3,
+        $address4,
+        $address5,
+        $city,
+        $state,
+        $province,
+        $postal_code,
+        $country
+      );
+
+      // Check if this customer has already been synced with Quickbooks.
+      /** @var \Drupal\profile\Entity\ProfileInterface $profile */
+      $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+        'customer',
+        [
+         'profile_id' => $profile_id,
+         'revision_id' => $profile->getRevisionId(),
+        ]
+      );
+
+      $list_id = $qb_row_details['list_id'];
+
+      // If no list ID found for this profile, check for any duplicate profiles
+      // already synced.
+      if (!$list_id) {
+        // If a duplicate of this profile has already been synced, use that ID.
+        $list_id_exists = QbWebConnectUtilities::getQbListId($profile);
+        if ($list_id_exists) {
+          $list_id = $list_id_exists;
+        }
+      }
+
+      $is_update = QbWebConnectUtilities::isQuickbooksIdentifier($list_id);
+
+      if ($is_update) {
+        $customer->setListID($list_id);
+        // If this entity is being exported for the first time and was a result
+        // of an import from QB, the edit sequence will not exist, so let's use
+        // the changed time instead.
+        // @todo Figure out a better way to store/retrieve the edit sequence
+        $edit_sequence = isset($qb_row_details['edit_sequence'])
+          ? $qb_row_details['edit_sequence']
+          : $profile->getChangedTime();
+        $customer->setEditSequence($edit_sequence);
+      }
+
       $customer->setFirstName($address['given_name']);
       $customer->setLastName($address['family_name']);
       $customer->setName("{$address['given_name']} {$address['family_name']} ($profile_id)");
       if ($company = $customer->getCompanyName()) {
-        $customer->setName($company ($profile_id));
+        $customer->setName($company($profile_id));
         $customer->setCompanyName($company);
       }
-      $user = $this->entityTypeManager->getStorage('user')->load($this->row->getSourceProperty('uid'));
+      $customer->setPhone($profile->phone->value);
+      $user = $this
+        ->entityTypeManager
+        ->getStorage('user')
+        ->load($this->row->getSourceProperty('uid'));
       $customer->setEmail($user->mail->value);
+
+      if ($is_update) {
+        return $customer->asQBXML(QUICKBOOKS_MOD_CUSTOMER);
+      }
       return $customer->asQBXML(QUICKBOOKS_ADD_CUSTOMER);
     }
-
   }
 
   /**
@@ -500,13 +591,41 @@ class SoapService implements SoapServiceInterface {
     /** @var \QuickBooks_QBXML_Object_Invoice|\QuickBooks_QBXML_Object_SalesReceipt $invoice */
     $invoice = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice() : new \QuickBooks_QBXML_Object_SalesReceipt();
     $orderId = $this->row->getSourceProperty('order_id');
+
+    // Check if this order has already been synced with Quickbooks, if so, we
+    // return the appropriate modification xml to update the existing Invoice or
+    // Sales Receipt in Quickbooks.
+    // @TODO: Figure out how to do this with the existing Quickbooks PHP-SDK.
+    $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+      'order',
+      ['order_id' => $orderId]
+    );
+    $is_update = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']);
+    if ($is_update) {
+      return $this->prepareOrderModExport($qb_row_details);
+    }
+
     /** @var \Drupal\commerce_order\Entity\Order $order */
     $order = $this->entityTypeManager->getStorage('commerce_order')->load($orderId);
-    if ($customerListID = $this->row->getDestinationProperty('billing_profile_list_id')) {
+    $customerListID = $this->row->getDestinationProperty('billing_profile_list_id');
+    if (!$customerListID || !is_string($customerListID)) {
+      $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+        'customer',
+        [
+          'profile_id' => $this->row->getSourceProperty('billing_profile__target_id'),
+          'revision_id' => NULL,
+        ]
+      );
+      $customerListID = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']) ? $qb_row_details['list_id'] : NULL;
+    }
+    if ($customerListID) {
       $invoice->setCustomerListID($customerListID);
     }
     /** @var \Drupal\commerce_payment\Entity\PaymentInterface[] $payments */
-    $payments = $this->entityTypeManager->getStorage('commerce_payment')->loadMultipleByOrder($order);
+    $payments = $this
+      ->entityTypeManager
+      ->getStorage('commerce_payment')
+      ->loadMultipleByOrder($order);
     $orderPrefix = $this->config->get('id_prefixes')['po_number_prefix'];
     $invoice->setRefNumber($orderPrefix . $orderId);
     $invoice->setTransactionDate($order->getCompletedTime());
@@ -526,7 +645,18 @@ class SoapService implements SoapServiceInterface {
       $province = '';
       $postal_code = $address->getPostalCode();
       $country = $address->getCountryCode();
-      $invoice->setBillAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
+      $invoice->setBillAddress(
+        $address1,
+        $address2,
+        $address3,
+        $address4,
+        $address5,
+        $city,
+        $state,
+        $province,
+        $postal_code,
+        $country
+      );
     }
 
     if ($order->hasField('shipments')) {
@@ -551,7 +681,18 @@ class SoapService implements SoapServiceInterface {
       $province = '';
       $postal_code = $address->getPostalCode();
       $country = $address->getCountryCode();
-      $invoice->setShipAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
+      $invoice->setShipAddress(
+        $address1,
+        $address2,
+        $address3,
+        $address4,
+        $address5,
+        $city,
+        $state,
+        $province,
+        $postal_code,
+        $country
+      );
     }
     foreach ($payments as $payment) {
       if ($gateway = $payment->getPaymentGateway()) {
@@ -567,30 +708,61 @@ class SoapService implements SoapServiceInterface {
       $line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
       /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $purchasedItem */
       if ($purchasedItem = $item->getPurchasedEntity()) {
+        $line->setItemName($purchasedItem->getSku());
         $line->setDescription($purchasedItem->label());
         $langcode = \Drupal::service('language_manager')->getDefaultLanguage()->getId();
         /** @var \Drupal\migrate\Plugin\Migration $variationMigration */
         $variationMigration = $this->migrationPluginManager->createInstance('qb_webconnect_product_variation');
-        if ($variationMigration && ($db_row = $variationMigration->getIdMap()->getRowBySource(['variation_id' => $purchasedItem->id(), 'langcode' => $langcode]))) {
-          $line->setItemListID($db_row['destid1']);
+        $filters = [
+          'variation_id' => $purchasedItem->id(),
+          'langcode' => $langcode,
+        ];
+        $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity('product_variation', $filters);
+        if (
+          $variationMigration
+          && ($qb_row_details)
+          && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id'])
+        ) {
+          $line->setItemListID($qb_row_details['list_id']);
         }
       }
       else {
+        $line->setItemName($item->getTitle());
         $line->setDescription($item->getTitle());
       }
       $line->setQuantity($item->getQuantity());
-      $line->setRate($item->getAdjustedUnitPrice()->getNumber());
+      $line->setRate($item->getUnitPrice()->getNumber());
       if ($isInvoice) {
         $invoice->addInvoiceLine($line);
       }
       else {
-        $invoice->addSalesReceiptLine($line);
+        $invoice->addListItem('SalesReceiptLineAdd', $line);
       }
     }
 
     /** @var \Drupal\commerce_order\Adjustment $adjustment */
     foreach ($order->getAdjustments() as $adjustment) {
       switch ($adjustment->getType()) {
+        // Take into account custom cases as well. Migrated shipment items from
+        // Ubercart are some times classified as a custom adjustment.
+        case 'custom':
+          $line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
+          $line->setItemName($adjustment->getLabel());
+          if (stripos($adjustment->getLabel(), 'ship') !== FALSE) {
+            $line->setItemName(
+              $this->config->get('adjustments')['shipping_service']
+            );
+          }
+          $line->setQuantity(1);
+          $line->setAmount($adjustment->getAmount()->getNumber());
+          if ($isInvoice) {
+            $invoice->addInvoiceLine($line);
+          }
+          else {
+            $invoice->addListItem('SalesReceiptLineAdd', $line);
+          }
+          break;
+
         case 'tax':
           $line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
           $line->setItemName($adjustment->getLabel());
@@ -600,26 +772,23 @@ class SoapService implements SoapServiceInterface {
             $invoice->addInvoiceLine($line);
           }
           else {
-            $invoice->addSalesReceiptLine($line);
+            $invoice->addListItem('SalesReceiptLineAdd', $line);
           }
           break;
 
         case 'shipping':
           $line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
-          if (!empty($shipment)) {
-            $invoice->setShipMethodName($shipment->getShippingMethod()->getPlugin()->getLabel());
-            $line->setItemName($this->config->get('adjustments')['shipping_service']);
-            $line->setDescription($adjustment->getLabel());
-            $line->setQuantity(1);
-            $line->setAmount($adjustment->getAmount()->getNumber());
-            if ($isInvoice) {
-              $invoice->addInvoiceLine($line);
-            }
-            else {
-              $invoice->addSalesReceiptLine($line);
-            }
+          $shippingName = $this->config->get('adjustments')['shipping_service'];
+          $line->setItemName($shippingName);
+          $line->setDescription($adjustment->getLabel());
+          $line->setQuantity(1);
+          $line->setAmount($adjustment->getAmount()->getNumber());
+          if ($isInvoice) {
+            $invoice->addInvoiceLine($line);
+          }
+          else {
+            $invoice->addListItem('SalesReceiptLineAdd', $line);
           }
-
           break;
 
         case 'promotion':
@@ -628,13 +797,12 @@ class SoapService implements SoapServiceInterface {
           $line->setItemName($discountName);
           // TODO: https://www.drupal.org/project/commerce_qb_webconnect/issues/2953692
           $line->setDescription($adjustment->getLabel());
-          $line->setQuantity(1);
           $line->setAmount($adjustment->getAmount()->getNumber());
           if ($isInvoice) {
             $invoice->addInvoiceLine($line);
           }
           else {
-            $invoice->addSalesReceiptLine($line);
+            $invoice->addListItem('SalesReceiptLineAdd', $line);
           }
           break;
       }
@@ -643,6 +811,200 @@ class SoapService implements SoapServiceInterface {
     return $isInvoice ? $invoice->asQBXML(\QUICKBOOKS_ADD_INVOICE, \QuickBooks_XML::XML_DROP, '') : $invoice->asQBXML(\QUICKBOOKS_ADD_SALESRECEIPT, \QuickBooks_XML::XML_DROP, '');
   }
 
+  /**
+   * Parse order objects into XML if it's an update to an existing order.
+   *
+   * We are using templates to generate the XML here as the current PHP-SDK
+   * is removing the InvoiceMod/SalesReceipMod lines when an update call is
+   * made.
+   *
+   * @param array $qb_row_details
+   *  An array containing the list ID and edit sequence.
+   *
+   * @TODO: Figure out how to do this properly with the SDK, like we're doing
+   * for the other other entities.
+   *
+   * @return string
+   *   An xml export of order data.
+   */
+  protected function prepareOrderModExport($qb_row_details) {
+    $invoice = new \stdClass();
+
+    // Set the TxnID and Edit Sequence for this existing entity. This will map
+    // to the correct order in Quickbooks.
+    $orderId = $this->row->getSourceProperty('order_id');
+    /** @var \Drupal\commerce_order\Entity\Order $order */
+    $order = $this->entityTypeManager->getStorage('commerce_order')->load($orderId);
+
+    $invoice->quickbooks_order_txnid = $qb_row_details['list_id'];
+    // If this entity is being exported for the first time and was a result of
+    // an import from QB, the edit sequence will not exist, so let's use the
+    // changed time instead.
+    // @todo Figure out a better way to store/retrieve the edit sequence
+    $edit_sequence = isset($qb_row_details['edit_sequence'])
+    ? $qb_row_details['edit_sequence']
+    : $order->getChangedTime();
+    $invoice->quickbooks_order_edit_sequence = $edit_sequence;
+
+    $invoice->order_id = $orderId;
+
+    $customerListID = $this->row->getDestinationProperty('billing_profile_list_id');
+    if (!$customerListID || !is_string($customerListID)) {
+      $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+        'customer',
+        [
+          'profile_id' => $this->row->getSourceProperty('billing_profile__target_id'),
+          'revision_id' => NULL,
+        ]
+      );
+      $customerListID = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']) ? $qb_row_details['list_id'] : NULL;
+    }
+    if ($customerListID) {
+      $invoice->customer_quickbooks_listid = $customerListID;
+    }
+    /** @var \Drupal\commerce_payment\Entity\PaymentInterface[] $payments */
+    $payments = $this
+      ->entityTypeManager
+      ->getStorage('commerce_payment')
+      ->loadMultipleByOrder($order);
+    $orderPrefix = $this->config->get('id_prefixes')['po_number_prefix'];
+    $invoice->ref_number = $orderPrefix . $orderId;
+    $invoice->date = date("Y-m-d", $order->getCompletedTime());
+    /** @var \Drupal\profile\Entity\Profile $billingProfile */
+    $billingProfile = $order->getBillingProfile();
+    if ($billingProfile) {
+      /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+      $address = $billingProfile->address->get(0);
+      $invoice->first_name = $address->getGivenName();
+      $invoice->last_name = $address->getFamilyName();
+      $billing_address = [];
+      $billing_address['address1'] = $address->getAddressLine1();
+      $billing_address['address2'] = $address->getAddressLine2();
+      $billing_address['locality'] = $address->getDependentLocality();
+      $billing_address['city'] = $address->getLocality();
+      $billing_address['state'] = $address->getAdministrativeArea();
+      $billing_address['postal_code'] = $address->getPostalCode();
+      $billing_address['country'] = $address->getCountryCode();
+      $invoice->billing_address = $billing_address;
+    }
+
+    if ($order->hasField('shipments')) {
+      /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
+      foreach ($order->shipments->referencedEntities() as $shipment) {
+        /** @var \Drupal\profile\Entity\Profile $shipping_profile */
+        if ($shippingProfile = $shipment->getShippingProfile()) {
+          break;
+        }
+      }
+    }
+    if (!empty($shippingProfile)) {
+      /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+      $address = $shippingProfile->address->get(0);
+      $shipping_address = [];
+      $shipping_address['address1'] = $address->getAddressLine1();
+      $shipping_address['address2'] = $address->getAddressLine2();
+      $shipping_address['locality'] = $address->getDependentLocality();
+      $shipping_address['city'] = $address->getLocality();
+      $shipping_address['state'] = $address->getAdministrativeArea();
+      $shipping_address['postal_code'] = $address->getPostalCode();
+      $shipping_address['country'] = $address->getCountryCode();
+      $invoice->shipping_address = $shipping_address;
+    }
+    foreach ($payments as $payment) {
+      if ($gateway = $payment->getPaymentGateway()) {
+        $paymentMethod = $gateway->getPlugin()->getDisplayLabel();
+      }
+    }
+    if (!empty($paymentMethod)) {
+      $invoice->payment_method = $paymentMethod;
+    }
+    /** @var \Drupal\commerce_order\Entity\OrderItem $item */
+    $invoice->products = [];
+    foreach ($order->getItems() as $item) {
+      $product = [];
+
+      /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $purchasedItem */
+      if ($purchasedItem = $item->getPurchasedEntity()) {
+        $product['sku'] = $purchasedItem->getSku();
+        $product['title'] = $purchasedItem->label();
+
+        $langcode = \Drupal::service('language_manager')->getDefaultLanguage()->getId();
+        /** @var \Drupal\migrate\Plugin\Migration $variationMigration */
+        $variationMigration = $this->migrationPluginManager->createInstance('qb_webconnect_product_variation');
+        $filters = [
+          'variation_id' => $purchasedItem->id(),
+          'langcode' => $langcode,
+        ];
+        $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity('product_variation', $filters);
+        if ($variationMigration && $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id'])) {
+          $product['quickbooks_listid'] = $qb_row_details['list_id'];
+        }
+      }
+      else {
+        $product['sku'] = $item->getTitle();
+        $product['title'] = $item->getTitle();
+      }
+      $product['quantity'] = $item->getQuantity();
+      $product['price'] = sprintf('%0.2f', $item->getUnitPrice()->getNumber());
+
+      $invoice->products[] = $product;
+    }
+
+    /** @var \Drupal\commerce_order\Adjustment $adjustment */
+    $adjustments = [];
+    foreach ($order->getAdjustments() as $adjustment) {
+      $invoice_adjustment = [];
+      switch ($adjustment->getType()) {
+
+        case 'custom':
+          $invoice_adjustment['name'] = $adjustment->getLabel();
+          if (stripos($adjustment->getLabel(), 'ship') !== FALSE) {
+            $invoice_adjustment['name'] = $this->config->get('adjustments')['shipping_service'];
+          }
+          $invoice_adjustment['description'] = $adjustment->getLabel();
+          $invoice_adjustment['quantity'] = 1;
+          $invoice_adjustment['price'] = sprintf('%0.2f', $adjustment->getAmount()->getNumber());
+
+          break;
+
+        case 'tax':
+          $invoice_adjustment['name'] = $adjustment->getLabel();
+          $invoice_adjustment['description'] = $adjustment->getLabel();
+          $invoice_adjustment['quantity'] = 1;
+          $invoice_adjustment['price'] = sprintf('%0.2f', $adjustment->getAmount()->getNumber());
+
+          break;
+
+        case 'shipping':
+          $shippingName = $this->config->get('adjustments')['shipping_service'];
+          $invoice_adjustment['name'] = $shippingName;
+          $invoice_adjustment['description'] = $adjustment->getLabel();
+          $invoice_adjustment['quantity'] = 1;
+          $invoice_adjustment['price'] = sprintf('%0.2f', $adjustment->getAmount()->getNumber());
+
+          break;
+
+        case 'promotion':
+          $discountName = $this->config->get('adjustments')['discount_service'];
+          $invoice_adjustment['name'] = $discountName;
+          $invoice_adjustment['description'] = $adjustment->getLabel();
+          $invoice_adjustment['price'] = sprintf('%0.2f', $adjustment->getAmount()->getNumber());
+
+          break;
+      }
+      $invoice->adjustments[] = $invoice_adjustment;
+    }
+
+    // Call the appropriate QBXML template.
+    $isInvoice = $this->config->get('exportables')['order_type'] == 'invoices';
+    $qbxml_theme = [
+      '#theme' => $isInvoice ? 'mod_invoice_qbxml' : 'mod_sales_receipt_qbxml',
+      '#invoice' => $invoice,
+    ];
+
+    return $this->renderer->render($qbxml_theme, FALSE)->__toString();
+  }
+
   /**
    * Parse payment entities into a template-ready object.
    *
@@ -654,16 +1016,43 @@ class SoapService implements SoapServiceInterface {
 
     $paymentId = $this->row->getSourceProperty('payment_id');
     /** @var \Drupal\commerce_payment\Entity\Payment $payment */
-    $payment = $this->entityTypeManager->getStorage('commerce_payment')->load($paymentId);
+    $payment = $this
+      ->entityTypeManager
+      ->getStorage('commerce_payment')
+      ->load($paymentId);
     $orderId = $payment->getOrderId();
     /** @var \Drupal\commerce_order\Entity\Order $order */
-    $order = $this->entityTypeManager->getStorage('commerce_order')->load($orderId);
-    /** @var \Drupal\migrate\Plugin\Migration $customerMigration */
-    $customerMigration = $this->migrationPluginManager->createInstance('qb_webconnect_customer');
+    $order = $this
+      ->entityTypeManager
+      ->getStorage('commerce_order')
+      ->load($orderId);
     /** @var \Drupal\profile\Entity\Profile $billingProfile */
     $billingProfile = $order->getBillingProfile();
-    if ($db_row = $customerMigration->getIdMap()->getRowBySource(['profile_id' => $billingProfile->id()])) {
-      $receivePayment->setCustomerListID($db_row['destid1']);
+
+    // Check if this payment has already been synced with Quickbooks.
+    $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+      'payment',
+      ['payment_id' => $paymentId]
+    );
+    $is_update = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']);
+    if ($is_update) {
+      $receivePayment->setTxnID($qb_row_details['list_id']);
+      // If this entity is being exported for the first time and was a result of
+      // an import from QB, the edit sequence will not exist, so let's use the
+      // changed time instead.
+      // @todo Figure out a better way to store/retrieve the edit sequence
+      $edit_sequence = isset($qb_row_details['edit_sequence'])
+      ? $qb_row_details['edit_sequence']
+      : $payment->getCompletedTime();
+      $receivePayment->setEditSequence($edit_sequence);
+    }
+
+    $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+      'customer',
+      ['profile_id' => $billingProfile->id()]
+    );
+    if ($qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id'])) {
+      $receivePayment->setCustomerListID($qb_row_details['list_id']);
     }
     $paymentPrefix = $this->config->get('id_prefixes')['payment_prefix'];
     if ($paymentId = $payment->getRemoteId()) {
@@ -675,7 +1064,15 @@ class SoapService implements SoapServiceInterface {
     $receivePayment->setPaymentMethodFullName($payment->getPaymentGateway()->label());
     $receivePayment->setTransactionDate($payment->getCompletedTime());
     $transactionAdd = new \QuickBooks_QBXML_Object_ReceivePayment_AppliedToTxn();
-    if ($orderListId = $this->row->getDestinationProperty('order_list_id')) {
+    $orderListId = $this->row->getDestinationProperty('order_list_id');
+    if (!$orderListId || !is_string($orderListId)) {
+      $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+        'order',
+        ['order_id' => $this->row->getSourceProperty('order_id')]
+      );
+      $orderListId = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']) ? $qb_row_details['list_id'] : NULL;
+    }
+    if ($orderListId) {
       $transactionAdd->setTxnID($orderListId);
       $transactionAdd->setPaymentAmount($payment->getAmount()->getNumber());
       $receivePayment->addAppliedToTxn($transactionAdd);
@@ -684,6 +1081,9 @@ class SoapService implements SoapServiceInterface {
       $receivePayment->setIsAutoApply(TRUE);
     }
 
+    if ($is_update) {
+      return $receivePayment->asQBXML(\QUICKBOOKS_MOD_RECEIVE_PAYMENT, \QuickBooks_XML::XML_DROP, '');
+    }
     return $receivePayment->asQBXML(\QUICKBOOKS_ADD_RECEIVEPAYMENT, \QuickBooks_XML::XML_DROP, '');
   }
 
@@ -697,16 +1097,82 @@ class SoapService implements SoapServiceInterface {
     $inventoryItem = new \QuickBooks_QBXML_Object_InventoryItem();
     $variationId = $this->row->getSourceProperty('variation_id');
     /** @var \Drupal\commerce_product\Entity\ProductVariation $variation */
-    $variation = $this->entityTypeManager->getStorage('commerce_product_variation')->load($variationId);
-    $inventoryItem->setName($variation->getSku());
-    $inventoryItem->setSalesPrice($variation->getPrice()->getNumber());
-    $inventoryItem->setIncomeAccountFullName($this->config->get('accounts')['main_income_account']);
-    $inventoryItem->setCOGSAccountFullName($this->config->get('accounts')['cogs_account']);
-    $inventoryItem->setAssetAccountName($this->config->get('accounts')['assets_account']);
-    if ($productListId = $this->row->getDestinationProperty('product_list_id')) {
-      $inventoryItem->set('ParentRef ListID', $productListId);
+    $variation = $this->entityTypeManager
+      ->getStorage('commerce_product_variation')
+      ->load($variationId);
+    if ($variation->getProductId()) {
+      // Check if this variation has already been synced with Quickbooks.
+      $langcode = \Drupal::service('language_manager')->getDefaultLanguage()->getId();
+      $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+        'product_variation',
+        [
+          'variation_id' => $variationId,
+          'langcode' => $langcode,
+        ]
+      );
+      $is_update = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']);
+      if ($is_update) {
+        $inventoryItem->setListID($qb_row_details['list_id']);
+        // If this entity is being exported for the first time and was a result of
+        // an import from QB, the edit sequence will not exist, so let's use the
+        // changed time instead.
+        // @todo Figure out a better way to store/retrieve the edit sequence
+        $edit_sequence = isset($qb_row_details['edit_sequence'])
+        ? $qb_row_details['edit_sequence']
+        : $variation->getChangedTime();
+        $inventoryItem->setEditSequence($edit_sequence);
+      }
+
+      // Quickbooks doesn't accept colons in the name.
+      // Prepend the variation ID as Quickbooks won't let items have the same
+      // names.
+      $inventoryItem->setName(
+        str_replace(':', ' ', $variation->id())
+        . '-'
+        . str_replace(':', ' ', $variation->getSku())
+      );
+      // Use the SKU as the manufacturer part number for variations.
+      $inventoryItem->set('ManufacturerPartNumber', $variation->getSku());
+      $inventoryItem->setSalesPrice($variation->getPrice()->getNumber());
+      if ($variation->hasField('field_stock')) {
+        $inventoryItem->setQuantityOnHand(
+          $variation
+            ->get('field_stock')
+            ->first()
+            ->get('available_stock')
+            ->getValue()
+        );
+      }
+      $inventoryItem->setIncomeAccountFullName(
+        $this->config->get('accounts')['main_income_account']
+      );
+      $inventoryItem->setCOGSAccountFullName(
+        $this->config->get('accounts')['cogs_account']
+      );
+      $inventoryItem->setAssetAccountName(
+        $this->config->get('accounts')['assets_account']
+      );
+
+      $productListId = $this->row->getDestinationProperty('product_list_id');
+      if (!$productListId || !is_string($productListId)) {
+        $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+          'product',
+          [
+            'product_id' => $this->row->getSourceProperty('product_id'),
+            'langcode' => $langcode,
+          ]
+        );
+        $productListId = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']) ? $qb_row_details['list_id'] : NULL;
+      }
+      if ($productListId) {
+        $inventoryItem->set('ParentRef ListID', $productListId);
+      }
+
+      if ($is_update) {
+        return $inventoryItem->asQBXML(\QUICKBOOKS_MOD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
+      }
+      return $inventoryItem->asQBXML(\QUICKBOOKS_ADD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
     }
-    return $inventoryItem->asQBXML(\QUICKBOOKS_ADD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
   }
 
   /**
@@ -720,10 +1186,47 @@ class SoapService implements SoapServiceInterface {
     $productId = $this->row->getSourceProperty('product_id');
     /** @var \Drupal\commerce_product\Entity\Product $product */
     $product = $this->entityTypeManager->getStorage('commerce_product')->load($productId);
-    $inventoryItem->setName($product->label());
+
+    // Check if this product has already been synced with Quickbooks.
+    $langcode = \Drupal::service('language_manager')->getDefaultLanguage()->getId();
+    $qb_row_details = QbWebConnectUtilities::getQbDetailsForEntity(
+      'product',
+      [
+        'product_id' => $productId,
+        'langcode' => $langcode,
+      ]
+    );
+    $is_update = $qb_row_details && QbWebConnectUtilities::isQuickbooksIdentifier($qb_row_details['list_id']);
+    if ($is_update) {
+      $inventoryItem->setListID($qb_row_details['list_id']);
+      // If this entity is being exported for the first time and was a result of
+      // an import from QB, the edit sequence will not exist, so let's use the
+      // changed time instead.
+      // @todo Figure out a better way to store/retrieve the edit sequence
+      $edit_sequence = isset($qb_row_details['edit_sequence'])
+      ? $qb_row_details['edit_sequence']
+      : $product->getChangedTime();
+      $inventoryItem->setEditSequence($edit_sequence);
+    }
+
+    // Quickbooks doesn't accept colons in the name.
+    // Prepend the product ID as Quickbooks won't let items have the same names.
+    $inventoryItem->setName(
+      $product->id()
+      . '-'
+      . str_replace(':', ' ', $product->getTitle())
+    );
+    // As we don't have a SKU for the parent product, use the ID as the
+    // manufacturer part number.
+    // @todo Figure out a better identifier.
+    $inventoryItem->set('ManufacturerPartNumber', $product->id());
     $inventoryItem->setIncomeAccountFullName($this->config->get('accounts')['main_income_account']);
     $inventoryItem->setCOGSAccountFullName($this->config->get('accounts')['cogs_account']);
     $inventoryItem->setAssetAccountName($this->config->get('accounts')['assets_account']);
+
+    if ($is_update) {
+      return $inventoryItem->asQBXML(\QUICKBOOKS_MOD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
+    }
     return $inventoryItem->asQBXML(\QUICKBOOKS_ADD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
   }
 
diff --git a/templates/mod-invoice-qbxml.html.twig b/templates/mod-invoice-qbxml.html.twig
new file mode 100644
index 0000000..b627002
--- /dev/null
+++ b/templates/mod-invoice-qbxml.html.twig
@@ -0,0 +1,68 @@
+<InvoiceModRq>
+	<InvoiceMod>
+		<TxnID>{{ invoice.quickbooks_order_txnid }}</TxnID>
+		<EditSequence>{{ invoice.quickbooks_order_edit_sequence }}</EditSequence>
+		<CustomerRef>
+			{% if invoice.customer_quickbooks_listid is not empty %}
+				<ListID>{{ invoice.customer_quickbooks_listid }}</ListID>
+			{% endif %}
+			<FullName>{{ invoice.first_name }} {{ invoice.last_name}}</FullName>
+		</CustomerRef>
+		<TxnDate>{{ invoice.date }}</TxnDate>
+		<RefNumber>{{ invoice.ref_number }}</RefNumber>
+		<BillAddress>
+			<Addr1>{{ invoice.billing_address.address1 }}</Addr1>
+			<Addr2>{{ invoice.billing_address.address2 }}</Addr2>
+			<Addr3>{{ invoice.billing_address.locality }}</Addr3>
+			<City>{{ invoice.billing_address.city }}</City>
+			<State>{{ invoice.billing_address.state }}</State>
+			<PostalCode>{{ invoice.billing_address.postal_code }}</PostalCode>
+			<Country>{{ invoice.billing_address.country }}</Country>
+		</BillAddress>
+		{% if invoice.shipping_address is defined %}
+			<ShipAddress>
+				<Addr1>{{ invoice.shipping_address.address1 }}</Addr1>
+				<Addr2>{{ invoice.shipping_address.address2 }}</Addr2>
+				<Addr3>{{ invoice.shipping_address.locality }}</Addr3>
+				<City>{{ invoice.shipping_address.city }}</City>
+				<State>{{ invoice.shipping_address.state }}</State>
+				<PostalCode>{{ invoice.shipping_address.postal_code }}</PostalCode>
+				<Country>{{ invoice.shipping_address.country }}</Country>
+			</ShipAddress>
+		{% endif %}
+		{% if invoice.tax_type is defined %}
+			<ItemSalesTaxRef>
+				<FullName>{{ invoice.tax_type }}</FullName>
+			</ItemSalesTaxRef>
+		{% endif %}
+		{% if invoice.products is defined %}
+			{% for product in invoice.products %}
+				<InvoiceLineMod>
+					<TxnLineID>-1</TxnLineID>
+					<ItemRef>
+						{% if product.quickbooks_listid is not empty %}
+							<ListID>{{ product.quickbooks_listid }}</ListID>
+						{% endif %}
+						<FullName>{{ product.sku }}</FullName>
+					</ItemRef>
+					<Desc>{{ product.title }}</Desc>
+					<Quantity>{{ product.quantity }}</Quantity>
+					<Rate>{{ product.price }}</Rate>
+				</InvoiceLineMod>
+			{% endfor %}
+		{% endif %}
+		{% if invoice.adjustments is defined %}
+			{% for adjustment in invoice.adjustments %}
+				<InvoiceLineMod>
+					<TxnLineID>-1</TxnLineID>
+					<FullName>{{ adjustment.name }}</FullName>
+					<Desc>{{ adjustment.description }}</Desc>
+					{% if adjustment.quantity is defined %}
+						<Quantity>{{ adjustment.quantity }}</Quantity>
+					{% endif %}
+					<Amount>{{ adjustment.rate }}</Rate>
+				</InvoiceLineMod>
+			{% endfor %}
+		{% endif %}
+	</InvoiceMod>
+</InvoiceModRq>
diff --git a/templates/mod-sales-receipt-qbxml.html.twig b/templates/mod-sales-receipt-qbxml.html.twig
new file mode 100644
index 0000000..1091a5f
--- /dev/null
+++ b/templates/mod-sales-receipt-qbxml.html.twig
@@ -0,0 +1,75 @@
+<SalesReceiptModRq>
+	<SalesReceiptMod>
+		<TxnID>{{ invoice.quickbooks_order_txnid }}</TxnID>
+		<EditSequence>{{ invoice.quickbooks_order_edit_sequence }}</EditSequence>
+		<CustomerRef>
+			{% if invoice.customer_quickbooks_listid is not empty %}
+				<ListID>{{ invoice.customer_quickbooks_listid }}</ListID>
+			{% endif %}
+			<FullName>{{ invoice.first_name }} {{ invoice.last_name}}</FullName>
+		</CustomerRef>
+		<TxnDate>{{ invoice.date }}</TxnDate>
+		<RefNumber>{{ invoice.ref_number }}</RefNumber>
+		<BillAddress>
+			<Addr1>{{ invoice.billing_address.address1 }}</Addr1>
+			<Addr2>{{ invoice.billing_address.address2 }}</Addr2>
+			<Addr3>{{ invoice.billing_address.locality }}</Addr3>
+			<City>{{ invoice.billing_address.city }}</City>
+			<State>{{ invoice.billing_address.state }}</State>
+			<PostalCode>{{ invoice.billing_address.postal_code }}</PostalCode>
+			<Country>{{ invoice.billing_address.country }}</Country>
+		</BillAddress>
+		{% if invoice.shipping_address is defined %}
+			<ShipAddress>
+				<Addr1>{{ invoice.shipping_address.address1 }}</Addr1>
+				<Addr2>{{ invoice.shipping_address.address2 }}</Addr2>
+				<Addr3>{{ invoice.shipping_address.locality }}</Addr3>
+				<City>{{ invoice.shipping_address.city }}</City>
+				<State>{{ invoice.shipping_address.state }}</State>
+				<PostalCode>{{ invoice.shipping_address.postal_code }}</PostalCode>
+				<Country>{{ invoice.shipping_address.country }}</Country>
+			</ShipAddress>
+		{% endif %}
+		{% if invoice.payment_method is defined %}
+			<PaymentMethodRef>
+				<FullName>{{ invoice.payment_method }}</FullName>
+			</PaymentMethodRef>
+		{% endif %}
+		{% if invoice.tax_type is defined %}
+			<ItemSalesTaxRef>
+				<FullName>{{ invoice.tax_type }}</FullName>
+			</ItemSalesTaxRef>
+		{% endif %}
+		{% if invoice.products is defined %}
+			{% for product in invoice.products %}
+				<SalesReceiptLineMod>
+					<TxnLineID>-1</TxnLineID>
+					<ItemRef>
+						{% if product.quickbooks_listid is not empty %}
+							<ListID>{{ product.quickbooks_listid }}</ListID>
+						{% endif %}
+						<FullName>{{ product.sku }}</FullName>
+					</ItemRef>
+					<Desc>{{ product.title }}</Desc>
+					<Quantity>{{ product.quantity }}</Quantity>
+					<Rate>{{ product.price }}</Rate>
+				</SalesReceiptLineMod>
+			{% endfor %}
+		{% endif %}
+		{% if invoice.adjustments is defined %}
+			{% for adjustment in invoice.adjustments %}
+				<SalesReceiptLineMod>
+					<TxnLineID>-1</TxnLineID>
+					<ItemRef>
+						<FullName>{{ adjustment.name }}</FullName>
+					</ItemRef>
+					<Desc>{{ adjustment.description }}</Desc>
+					{% if adjustment.quantity is defined %}
+						<Quantity>{{ adjustment.quantity }}</Quantity>
+					{% endif %}
+					<Amount>{{ adjustment.price }}</Amount>
+				</SalesReceiptLineMod>
+			{% endfor %}
+		{% endif %}
+	</SalesReceiptMod>
+</SalesReceiptModRq>
