View source
<?php
namespace Drupal\commerce_avatax;
use Drupal\address\AddressInterface;
use Drupal\commerce_avatax\Resolver\ChainTaxCodeResolverInterface;
use Drupal\commerce_order\AdjustmentTypeManager;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_tax\Event\CustomerProfileEvent;
use Drupal\commerce_tax\Event\TaxEvents;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Variable;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Extension\ModuleHandlerInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class AvataxLib implements AvataxLibInterface {
protected $adjustmentTypeManager;
protected $chainTaxCodeResolver;
protected $client;
protected $config;
protected $eventDispatcher;
protected $logger;
protected $moduleHandler;
protected $cache;
public function __construct(AdjustmentTypeManager $adjustment_type_manager, ChainTaxCodeResolverInterface $chain_tax_code_resolver, ClientFactory $client_factory, ConfigFactoryInterface $config_factory, EventDispatcherInterface $event_dispatcher, LoggerInterface $logger, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
$this->adjustmentTypeManager = $adjustment_type_manager;
$this->chainTaxCodeResolver = $chain_tax_code_resolver;
$this->config = $config_factory
->get('commerce_avatax.settings');
$this->client = $client_factory
->createInstance($this->config
->get());
$this->eventDispatcher = $event_dispatcher;
$this->logger = $logger;
$this->moduleHandler = $module_handler;
$this->cache = $cache_backend;
}
public function transactionsCreate(OrderInterface $order, $type = 'SalesOrder') {
$request_body = $this
->prepareTransactionsCreate($order, $type);
if (empty($request_body['lines'])) {
return [];
}
$cid = 'transactions_create:' . $order
->id();
if ($cached = $this->cache
->get($cid)) {
$cached_data = $cached->data;
if (!empty($cached_data['response']) && isset($cached_data['request'])) {
$cached_data['request']['date'] = $request_body['date'];
if ($cached_data['request'] == $request_body) {
return $cached_data['response'];
}
}
}
$response_body = $this
->doRequest('POST', 'api/v2/transactions/create', [
'json' => $request_body,
]);
if (!empty($response_body)) {
$this->moduleHandler
->alter('commerce_avatax_order_response', $response_body, $order);
$expire = time() + 60 * 60 * 24;
$this->cache
->set($cid, [
'request' => $request_body,
'response' => $response_body,
], $expire);
}
return $response_body;
}
public function transactionsVoid(OrderInterface $order) {
$store = $order
->getStore();
if ($store
->get('avatax_company_code')
->isEmpty()) {
$company_code = $this->config
->get('company_code');
}
else {
$company_code = $store
->get('avatax_company_code')->value;
}
$transaction_code = 'DC-' . $order
->uuid();
return $this
->doRequest('POST', "api/v2/companies/{$company_code}/transactions/{$transaction_code}/void", [
'json' => [
'code' => 'DocVoided',
],
]);
}
public function prepareTransactionsCreate(OrderInterface $order, $type = 'SalesOrder') {
$store = $order
->getStore();
if ($store
->get('avatax_company_code')
->isEmpty()) {
$company_code = $this->config
->get('company_code');
}
else {
$company_code = $store
->get('avatax_company_code')->value;
}
$date = new DrupalDateTime();
$adjustment_types = array_keys($this->adjustmentTypeManager
->getDefinitions());
$customer = $order
->getCustomer();
$currency_code = $order
->getTotalPrice() ? $order
->getTotalPrice()
->getCurrencyCode() : $store
->getDefaultCurrencyCode();
$request_body = [
'type' => $type,
'companyCode' => $company_code,
'date' => $date
->format('c'),
'code' => 'DC-' . $order
->uuid(),
'currencyCode' => $currency_code,
'lines' => [],
];
if (!$customer
->isAnonymous()) {
if ($customer
->hasField('avatax_tax_exemption_number') && !$customer
->get('avatax_tax_exemption_number')
->isEmpty()) {
$request_body['ExemptionNo'] = $customer
->get('avatax_tax_exemption_number')->value;
}
if ($customer
->hasField('avatax_tax_exemption_type') && !$customer
->get('avatax_tax_exemption_type')
->isEmpty()) {
$request_body['CustomerUsageType'] = $customer
->get('avatax_tax_exemption_type')->value;
}
if ($customer
->hasField('avatax_customer_code') && !$customer
->get('avatax_customer_code')
->isEmpty()) {
$request_body['customerCode'] = $customer
->get('avatax_customer_code')->value;
}
else {
$customer_code_field = $this->config
->get('customer_code_field');
if ($order
->hasField($customer_code_field) && !$order
->get($customer_code_field)
->isEmpty()) {
$customer_code = $customer_code_field === 'mail' ? $order
->getEmail() : $order
->getCustomerId();
$request_body['customerCode'] = $customer_code;
}
}
}
if (!isset($request_body['customerCode'])) {
$request_body['customerCode'] = $order
->getEmail() ?: 'anonymous-' . $order
->id();
}
$has_shipments = $order
->hasField('shipments') && !$order
->get('shipments')
->isEmpty();
foreach ($order
->getItems() as $order_item) {
$profile = $this
->resolveCustomerProfile($order_item);
if (!$profile) {
continue;
}
$purchased_entity = $order_item
->getPurchasedEntity();
$address = $profile
->get('address')
->first();
$line_item = [
'number' => $order_item
->uuid(),
'quantity' => $order_item
->getQuantity(),
'amount' => $order_item
->getAdjustedTotalPrice(array_diff($adjustment_types, [
'tax',
]))
->getNumber(),
];
if ($purchased_entity instanceof ProductVariationInterface) {
$line_item['itemCode'] = $purchased_entity
->getSku();
}
if ($has_shipments) {
$line_item['addresses'] = [
'shipFrom' => self::formatAddress($store
->getAddress()),
'shipTo' => self::formatAddress($address),
];
}
else {
$line_item['addresses']['singleLocation'] = self::formatAddress($address);
}
$line_item['taxCode'] = $this->chainTaxCodeResolver
->resolve($order_item);
$request_body['lines'][] = $line_item;
}
if ($has_shipments) {
foreach ($order
->get('shipments')
->referencedEntities() as $shipment) {
if (is_null($shipment
->getAmount())) {
continue;
}
$request_body['lines'][] = [
'taxCode' => $this->config
->get('shipping_tax_code'),
'number' => $shipment
->uuid(),
'description' => $shipment
->label(),
'amount' => $shipment
->getAmount()
->getNumber(),
'quantity' => 1,
'addresses' => [
'shipFrom' => self::formatAddress($store
->getAddress()),
'shipTo' => self::formatAddress($shipment
->getShippingProfile()
->get('address')
->first()),
],
];
}
}
foreach ($order
->getAdjustments() as $adjustment) {
if (in_array($adjustment
->getType(), [
'shipping',
'fee',
'tax',
])) {
continue;
}
$line_item = [
'taxCode' => 'P0000000',
'description' => $adjustment
->getLabel(),
'amount' => $adjustment
->getAmount()
->getNumber(),
'quantity' => 1,
'addresses' => [
'shipFrom' => self::formatAddress($store
->getAddress()),
],
];
if (isset($request_body['lines'][0]['addresses']['shipTo'])) {
$line_item['addresses']['shipTo'] = $request_body['lines'][0]['addresses']['shipTo'];
$request_body['lines'][] = $line_item;
}
}
if ($request_body['type'] === 'SalesInvoice') {
$request_body['commit'] = TRUE;
}
$this->moduleHandler
->alter('commerce_avatax_order_request', $request_body, $order);
return $request_body;
}
public function resolveAddress(array $address) {
$request_data = [
'line1' => $address['address_line1'],
'line2' => $address['address_line2'],
'city' => $address['locality'],
'region' => $address['administrative_area'],
'country' => $address['country_code'],
'postalCode' => $address['postal_code'],
];
$cid = base64_encode(trim(serialize($request_data)));
if ($cache = $this->cache
->get($cid)) {
return $cache->data;
}
$request_data['textCase'] = 'Mixed';
$response_body = $this
->doRequest('POST', 'api/v2/addresses/resolve', [
'json' => $request_data,
]);
$this->cache
->set($cid, $response_body, time() + 86400);
return $response_body;
}
public function validateAddress(array $address) {
$validation = [
'valid' => FALSE,
'errors' => [],
'suggestion' => [],
'fields' => [],
'original' => [],
];
$avatax_response = $this
->resolveAddress($address);
if (empty($avatax_response['address'])) {
return $validation;
}
$original_address = $avatax_response['address'];
$validation['original'] = self::formatDrupalAddress($original_address);
if (!empty($avatax_response['messages'])) {
$error_fields_mapping = [
'Address.City' => 'locality',
'Address.Line0' => 'address_line1',
'Address.Line1' => 'address_line2',
'Address.PostalCode' => 'postal_code',
'Address.Region' => 'administrative_area',
];
$validation['errors'] = array_map(static function (array $message) use ($error_fields_mapping) {
return $error_fields_mapping[$message['refersTo']];
}, $avatax_response['messages']);
}
elseif (!empty($avatax_response['validatedAddresses'])) {
$validation['valid'] = TRUE;
$validatedAddress = end($avatax_response['validatedAddresses']);
unset($validatedAddress['addressType'], $validatedAddress['latitude'], $validatedAddress['longitude'], $validatedAddress['line_3']);
$suggestion = array_filter(array_diff($validatedAddress, $avatax_response['address']));
if (!empty($suggestion['postalCode']) && strlen($original_address['postalCode']) === 5) {
$postal_code_match = $this->config
->get('address_validation.postal_code_match');
if (!$postal_code_match && strpos($validatedAddress['postalCode'], $original_address['postalCode']) === 0) {
unset($suggestion['postalCode']);
}
}
if (!empty($suggestion)) {
$validation['fields'] = array_filter(self::formatDrupalAddress($suggestion));
$validation['suggestion'] = self::formatDrupalAddress($validatedAddress);
}
}
return $validation;
}
public static function formatDrupalAddress(array $address) {
return [
'address_line1' => $address['line1'] ?? '',
'address_line2' => $address['line2'] ?? '',
'locality' => $address['city'] ?? '',
'administrative_area' => $address['region'] ?? '',
'country_code' => $address['country'] ?? '',
'postal_code' => $address['postalCode'] ?? '',
];
}
protected static function formatAddress(AddressInterface $address) {
return [
'line1' => $address
->getAddressLine1(),
'line2' => $address
->getAddressLine2(),
'city' => $address
->getLocality(),
'region' => $address
->getAdministrativeArea(),
'country' => $address
->getCountryCode(),
'postalCode' => $address
->getPostalCode(),
];
}
protected function resolveCustomerProfile(OrderItemInterface $order_item) {
$order = $order_item
->getOrder();
$customer_profile = $order
->getBillingProfile();
$event = new CustomerProfileEvent($customer_profile, $order_item);
$this->eventDispatcher
->dispatch(TaxEvents::CUSTOMER_PROFILE, $event);
$customer_profile = $event
->getCustomerProfile();
return $customer_profile;
}
protected function doRequest($method, $path, array $parameters = []) {
$response_body = [];
try {
$response = $this->client
->request($method, $path, $parameters);
$response_body = Json::decode($response
->getBody()
->getContents());
} catch (ClientException $e) {
$body = $e
->getResponse()
->getBody()
->getContents();
$response_body = Json::decode($body);
$this->logger
->error('@method @path error: <pre>' . print_r($body, TRUE) . '</pre>', [
'@method' => $method,
'@path' => $path,
]);
} catch (\Exception $e) {
$this->logger
->error($e
->getMessage());
}
if ($this->config
->get('logging')) {
$url = $this->client
->getConfig('base_uri') . $path;
$this->logger
->info("URL: <pre>{$method} @url</pre>Headers: <pre>@headers</pre>Request: <pre>@request</pre>Response: <pre>@response</pre>", [
'@url' => $url,
'@headers' => Variable::export($this->client
->getConfig('headers')),
'@request' => Variable::export($parameters),
'@response' => Variable::export($response_body),
]);
}
return $response_body;
}
public function setClient(Client $client) {
$this->client = $client;
return $this;
}
}