Skip to content

Instantly share code, notes, and snippets.

@heddn
Created January 31, 2018 19:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heddn/d8307aeae932575128153bc596683b1f to your computer and use it in GitHub Desktop.
Save heddn/d8307aeae932575128153bc596683b1f to your computer and use it in GitHub Desktop.
<?php
namespace Drupal\commerce_quickbooks_enterprise\SoapBundle\Services;
use Drupal\commerce_quickbooks_enterprise\QbeUtilities;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
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;
/**
* Handle SOAP requests and return a response.
*
* Class SoapService.
*
* @package Drupal\commerce_quickbooks_enterprise\SoapBundle\Services
*/
class SoapService implements SoapServiceInterface {
/**
* The row currently being migrated.
*
* @var \Drupal\migrate\Row
*/
protected $row;
/**
* 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_quickbooks_enterprise\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;
/**
* The default order in which to process exportable QB Items.
*
* @var array
*/
protected $itemPriorities = [
'add_customer',
'mod_customer',
'add_inventory_product',
'mod_inventory_product',
'add_non_inventory_product',
'mod_non_inventory_product',
'add_invoice',
'mod_invoice',
'add_sales_receipt',
'mod_sales_receipt',
'add_payment',
];
/**
* 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_quickbooks_enterprise\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.
*/
public function __construct(
MigrationPluginManager $migrationPluginManager,
MigratePluginManagerInterface $idMapPluginManager,
UserAuthInterface $userAuthInterface,
SoapSessionManager $sessionManager,
EntityTypeManagerInterface $entityTypeManager,
ConfigFactoryInterface $configFactory,
StateInterface $state
) {
$this->migrationPluginManager = $migrationPluginManager;
$this->idMapPluginManager = $idMapPluginManager;
$this->userAuthInterface = $userAuthInterface;
$this->sessionManager = $sessionManager;
$this->entityTypeManager = $entityTypeManager;
$this->config = $configFactory->get('commerce_quickbooks_enterprise.quickbooks_admin');
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function __call($method, array $data) {
\Drupal::logger('commerce_qbe')->info("QB SOAP service [$method] called. Incoming request: " . print_r($data, TRUE));
$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')) {
\Drupal::logger('commerce_qbe')->warning('User logged in successfully but didn\'t have Quickbooks SOAP Service access permissions.');
return $request;
}
}
else {
\Drupal::logger('commerce_qbe')->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() {
$done = 0;
$todo = 0;
foreach ($this->migrationPluginManager->createInstancesByTag('quickbooks_enterprise') as $id => $migration) {
$map = $migration->getIdMap();
$done += $map->importedCount();
$todo += $migration->getSourcePlugin()->count() - $map->processedCount();
}
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 exports id 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_qbe')->error("User Auth service couldn't be initialized.");
}
else {
$uid = $this->userAuthInterface->authenticate($strUserName, $strPassword);
if (!$uid) {
\Drupal::logger('commerce_qbe')->error("Invalid login credentials, aborting quickbooks SOAP service.");
}
else {
\Drupal::logger('commerce_qbe')->info("Quickbooks user $strUserName successfully connected! Commencing data exchange with client.");
$uuid = \Drupal::service('uuid')->generate();
$this->sessionManager->startSession($uuid, $uid);
$result = [$uuid, ''];
}
}
$request->authenticateResult = $result;
return $request;
}
/**
* {@inheritdoc}
*/
public function callSendRequestXML(\stdClass $request) {
\Drupal::logger('commerce_qbe')->info("Request received, searching for content to export.");
$migrations = $this->migrationPluginManager->createInstancesByTag('quickbooks_enterprise');
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
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) {
continue;
}
// Our MigrateSubscriber stops this migration after a single row.
(new MigrateExecutable($migration, new MigrateMessage()))->import();
$this->row = $this->state->get('qbe.current_row');
if ($migration->getIdMap()->errorCount()) {
$errors = iterator_to_array($migration->getIdMap()->getMessageIterator());
$error = array_shift($errors);
\Drupal::logger('commerce_qbe')->error($error->message);
return $request;
}
// Let's end the import for now and we'll continue next time.
break;
}
$callback = $this->row->getSourceProperty('static/send_callback');
if (is_callable([$this, $callback])) {
$qbxml = call_user_func([$this, $callback]);
$request->sendRequestXMLResult = $this->addXMLEnvelope($qbxml);
return $request;
}
\Drupal::logger('commerce_qbe')->error("Unable to prepare data for export. No method found for [$callback]");
$this->state->get('qbe.current_row');
return $request;
}
/**
* 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="2.0"?><QBXML><QBXMLMsgsRq onError="stopOnError">' . $qbxml . '</QBXMLMsgsRq></QBXML>';
}
/**
* {@inheritdoc}
*/
public function callReceiveResponseXML(\stdClass $request) {
$this->row = $this->state->get('qbe.current_row');
$retry = FALSE;
// Parse any errors if we have them to decide our next action.
if (!empty($request->response)) {
if ($code = QbeUtilities::extractStatusCode($request->response)) {
$error = [
'statusCode' => $code,
'statusMessage' => QbeUtilities::extractStatusMessage($request->response),
];
$error_msg = "Response error statusCode: " . print_r($error, TRUE);
\Drupal::logger('commerce_qbe_errors')->error($error_msg);
// 3180 is a temporary error with no clear reason. Just retry it.
if ($error['statusCode'] == "3180") {
$retry = TRUE;
}
}
}
$callback = $this->row->getSourceProperty('static/receive_callback');
if (!$retry && is_callable([$this, $callback])) {
call_user_func([$this, $callback], $request);
}
$request->receiveResponseXMLResult = $this->getCompletionProgress();
return $request;
}
/**
* Update identifiers.
*
* @param \stdClass $request
* The request.
*/
protected function updateIdentifier(\stdClass $request) {
$identifier = QbeUtilities::extractIdentifiers($request->response, $this->row->getSource()['entity_type']);
if ($identifier) {
/** @var \Drupal\migrate\Plugin\Migration $migration */
$migration = $this->migrationPluginManager->createInstance($this->state->get('qbe.current_migration'));
$migration->getIdMap()->saveIdMapping($this->row, ['uuid' => $identifier]);
}
}
/**
* {@inheritdoc}
*/
public function callGetLastError(\stdClass $request) {
$progress = $this->getCompletionProgress();
if ($progress == 100) {
$request->getLastErrorResult = 'No new exports remaining.';
}
else {
$request->getLastErrorResult = "$progress% remaining remaining.";
}
return $request;
}
/**
* {@inheritdoc}
*/
public function callCloseConnection(\stdClass $request) {
$this->sessionManager->closeSession();
$request->closeConnectionResult = 'OK';
return $request;
}
/**
* Parse profile entities into a template-ready object.
*/
protected function prepareCustomerExport() {
if ($this->row->getSourceProperty('bundle') == 'customer') {
$uuid = $this->row->getDestinationProperty('uuid');
$addresses = $this->row->getSourceProperty('address');
$address = reset($addresses);
$customer = new \QuickBooks_QBXML_Object_Customer();
if (QbeUtilities::isQuickbooksIdentifier($uuid)) {
$customer->setListID($uuid);
return $customer->asQBXML(QUICKBOOKS_QUERY_CUSTOMER);
}
$address1 = $address['address_line1'];
$address2 = $address['address_line2'];
$address3 = '';
$address4 = '';
$address5 = $address['dependent_locality'];
$city = $address['locality'];
$state = $address['administrative_area'];
$province = '';
$postal_code = $address['postal_code'];
$country = $address['country_code'];
$customer->setBillAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
$customer->setFirstName($address['given_name']);
$customer->setLastName($address['family_name']);
$user = $this->entityTypeManager->getStorage('user')->load($this->row->getSourceProperty('uid'));
$customer->setEmail($user->mail->value);
return $customer->asQBXML(QUICKBOOKS_ADD_CUSTOMER);
}
}
/**
* Parse Order entities into a template-ready object.
*
* @return string
* An xml export of order data.
*/
protected function prepareOrderExport() {
$isInvoice = $this->config->get('exportables')['order_type'] == 'invoices';
$invoice = new \QuickBooks_QBXML_Object_Invoice();
$orderId = $this->row->getSourceProperty('order_id');
/** @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('qbe_customer');
/** @var \Drupal\profile\Entity\Profile $billingProfile */
$billingProfile = $order->getBillingProfile();
if ($db_row = $customerMigration->getIdMap()->getRowBySource(['profile_id' => $billingProfile->id()])) {
$invoice->setCustomerListID($db_row['destid1']);
}
/** @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->setRefNumber($orderPrefix . $orderId);
$invoice->setTransactionDate($order->getCompletedTime());
if ($billingProfile) {
/** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
$address = $billingProfile->address->get(0);
$invoice->setCustomerFullName("{$address->getGivenName()} {$address->getFamilyName()}");
$address1 = $address->getAddressLine1();
$address2 = $address->getAddressLine2();
$address3 = '';
$address4 = '';
$address5 = $address->getDependentLocality();
$city = $address->getLocality();
$state = $address->getAdministrativeArea();
$province = '';
$postal_code = $address->getPostalCode();
$country = $address->getCountryCode();
$invoice->setBillAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
}
if ($order->hasField('shipments')) {
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);
$address1 = $address->getAddressLine1();
$address2 = $address->getAddressLine2();
$address3 = '';
$address4 = '';
$address5 = $address->getDependentLocality();
$city = $address->getLocality();
$state = $address->getAdministrativeArea();
$province = '';
$postal_code = $address->getPostalCode();
$country = $address->getCountryCode();
$invoice->setShipAddress($address1, $address2, $address3, $address4, $address5, $city, $state, $province, $postal_code, $country);
}
foreach ($payments as $payment) {
if ($gateway = $payment->getPaymentGateway()) {
$paymentMethod = $gateway->getPlugin()->getDisplayLabel();
}
}
if (!empty($paymentMethod)) {
$invoice->setPaymentMethodName($paymentMethod);
}
/** @var \Drupal\commerce_order\Entity\OrderItem $item */
foreach ($order->getItems() as $item) {
$line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
/** @var \Drupal\commerce_product\Entity\ProductVariationInterface $purchasedItem */
$purchasedItem = $item->getPurchasedEntity();
$line->setItemName($purchasedItem->getSku());
$line->setDescription($purchasedItem->label());
$line->setQuantity($item->getQuantity());
$line->setAmount($item->getUnitPrice()->getNumber());
if ($isInvoice) {
$invoice->addInvoiceLine($line);
}
else {
$invoice->addListItem('SalesReceiptLineAdd', $line);
}
}
/** @var \Drupal\commerce_order\Adjustment $adjustment */
foreach ($order->getAdjustments() as $adjustment) {
switch ($adjustment->getType()) {
case 'tax':
$taxName = $this->config->get('tax')['tax_name'];
$invoice->setSalesTaxItemFullName($taxName);
break;
case 'shipping':
$line = $isInvoice ? new \QuickBooks_QBXML_Object_Invoice_InvoiceLine() : new \QuickBooks_QBXML_Object_SalesReceipt_SalesReceiptLine();
$shippingName = $this->config->get('shipping')['shipping_service'];
$shippingDescription = $this->config->get('shipping')['shipping_service_description'];
$line->setItemName($shippingName);
$line->setDescription($shippingDescription);
$line->setQuantity(1);
$line->setAmount($adjustment->getAmount()->getNumber());
if ($isInvoice) {
$invoice->addInvoiceLine($line);
}
else {
$invoice->addListItem('SalesReceiptLineAdd', $line);
}
break;
}
}
return $isInvoice ? $invoice->asXML(\QUICKBOOKS_ADD_INVOICE)->asXML(\QuickBooks_XML::XML_DROP, '') : $invoice->asXML(QUICKBOOKS_ADD_SALESRECEIPT)->asXML(\QuickBooks_XML::XML_DROP, '');
}
/**
* Parse payment entities into a template-ready object.
*
* @return string
* An xml export of payment data.
*/
protected function preparePaymentExport() {
$receivePayment = new \QuickBooks_QBXML_Object_ReceivePayment();
$paymentId = $this->row->getSourceProperty('payment_id');
/** @var \Drupal\commerce_payment\Entity\Payment $payment */
$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('qbe_customer');
/** @var \Drupal\profile\Entity\Profile $billingProfile */
$billingProfile = $order->getBillingProfile();
if ($db_row = $customerMigration->getIdMap()->getRowBySource(['profile_id' => $billingProfile->id()])) {
$receivePayment->setCustomerListID($db_row['destid1']);
}
$paymentPrefix = $this->config->get('id_prefixes')['payment_prefix'];
if ($paymentId = $payment->getRemoteId()) {
$receivePayment->setRefNumber($paymentPrefix . $paymentId);
}
else {
$receivePayment->setRefNumber($paymentPrefix . $payment->id());
}
$receivePayment->setPaymentMethodFullName($payment->getPaymentGateway()->label());
$receivePayment->setTransactionDate($payment->getCompletedTime());
$transactionAdd = new \QuickBooks_QBXML_Object_ReceivePayment_AppliedToTxn();
/** @var \Drupal\migrate\Plugin\Migration $orderMigration */
$orderMigration = $this->migrationPluginManager->createInstance('qbe_order');
if ($db_row = $orderMigration->getIdMap()->getRowBySource(['order_id' => $order->id()])) {
$transactionAdd->setTxnID($db_row['destid1']);
$transactionAdd->setPaymentAmount($payment->getAmount()->getNumber());
$receivePayment->addAppliedToTxn($transactionAdd);
}
else {
$receivePayment->setIsAutoApply(TRUE);
}
return $receivePayment->asQBXML(\QUICKBOOKS_ADD_RECEIVEPAYMENT, \QuickBooks_XML::XML_DROP, '');
}
/**
* Parse product variation entities into a template-ready object.
*
* @return string
* An xml export of product variation data.
*/
protected function prepareProductVariationExport() {
$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->label());
$inventoryItem->setSalesPrice($variation->getPrice()->getNumber());
/** @var \Drupal\migrate\Plugin\Migration $productMigration */
$productMigration = $this->migrationPluginManager->createInstance('qbe_product');
/** @var \Drupal\Core\Language\LanguageManagerInterface $languageManager */
$languageManager = \Drupal::service('language_manager');
$langcode = $languageManager->getDefaultLanguage()->getId();
if ($db_row = $productMigration->getIdMap()->getRowBySource(['product_id' => $variation->getProductId(), 'langcode' => $langcode])) {
$inventoryItem->set('ParentRef ListID', $db_row['destid1']);
}
return $inventoryItem->asQBXML(\QUICKBOOKS_ADD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
}
/**
* Parse product entities into a template-ready object.
*
* @return string
* An xml export of product data.
*/
protected function prepareProductExport() {
$inventoryItem = new \QuickBooks_QBXML_Object_InventoryItem();
$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());
return $inventoryItem->asQBXML(\QUICKBOOKS_ADD_INVENTORYITEM, \QuickBooks_XML::XML_DROP, '');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment