diff --git a/Block/Customer/DataProviders/AdditionalConfig.php b/Block/Customer/DataProviders/AdditionalConfig.php
new file mode 100644
index 0000000..b1b2814
--- /dev/null
+++ b/Block/Customer/DataProviders/AdditionalConfig.php
@@ -0,0 +1,74 @@
+config = $config;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * Get districts
+ *
+ * @return array
+ */
+ private function getDistricts()
+ {
+ $districts = $this->config->getDistricts();
+ $data = [];
+ foreach ($districts as $district) {
+ $data[$district['region_id']][] = [
+ 'districtName' => $district['district_name'],
+ 'districtID' => $district['district_id']
+ ];
+ }
+ return $data;
+ }
+
+ /**
+ * @return string
+ */
+ public function getJsonData(): string
+ {
+ return $this->serializer->serialize([
+ 'districts' => $this->getDistricts()
+ ]);
+ }
+}
diff --git a/Console/GenerateRegionCommand.php b/Console/GenerateRegionCommand.php
new file mode 100644
index 0000000..2ac587a
--- /dev/null
+++ b/Console/GenerateRegionCommand.php
@@ -0,0 +1,155 @@
+resourceConnection = $resourceConnection;
+ $this->regionFactory = $regionFactory;
+ $this->commandPool = $commandPool;
+ parent::__construct($name);
+ }
+
+ protected function configure()
+ {
+ $this->setDescription('Generate region data.');
+ parent::configure();
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|void|null
+ * @throws LocalizedException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $commandResult = $this->commandPool->get('get_districts')->execute([]);
+ $data = SubjectReader::readDistricts($commandResult->get());
+
+ if ($data) {
+ $output->writeln('Generating data. Please wait...');
+ foreach ($data as $item) {
+ $provinceId = $item['ProvinceID'];
+ $districtId = $item['DistrictID'];
+ $region = $this->regionFactory->create()
+ ->loadByCode($provinceId, 'VN');
+
+ $this->insertData(
+ 'boolfly_giaohangnhanh_district',
+ [
+ 'district_id' => $districtId,
+ 'province_id' => $provinceId,
+ 'district_name' => $item['DistrictName']
+ ],
+ ['col' => 'district_id', 'val' => $districtId]
+ );
+
+ if (!$region->getId()) {
+ $this->insertData(
+ 'directory_country_region',
+ [
+ 'country_id' => 'VN',
+ 'code' => $provinceId,
+ 'default_name' => $item['ProvinceName']
+ ]
+ );
+ }
+ }
+ $output->writeln('Generate data successfully.');
+ } else {
+ $output->writeln('Generating data was interrupted. Please try again!');
+ }
+ }
+
+ /**
+ * @param string $tableName
+ * @param array $data
+ * @param array $pairOfColAndVal
+ */
+ private function insertData($tableName, $data, $pairOfColAndVal = [])
+ {
+ if (!$this->checkRecordExist($tableName, $pairOfColAndVal)) {
+ $this->resourceConnection->getConnection()->insert(
+ $this->resourceConnection->getTableName($tableName),
+ $data
+ );
+ }
+ }
+
+
+ /**
+ * @param string $tableName
+ * @param array $pairOfColAndVal
+ * @return bool
+ */
+ private function checkRecordExist($tableName, $pairOfColAndVal = [])
+ {
+ $checkingFlag = false;
+
+ if ($pairOfColAndVal) {
+ $connection = $this->resourceConnection->getConnection();
+ $sql = $connection->select()->from(
+ ['districtTable' => $this->resourceConnection->getTableName($tableName)],
+ $pairOfColAndVal['col']
+ )->where($pairOfColAndVal['col'] . ' = ?', $pairOfColAndVal['val']);
+
+ $rows = $connection->fetchAll($sql);
+
+ if (count($rows)) {
+ $checkingFlag = true;
+ }
+ }
+
+ return $checkingFlag;
+ }
+}
diff --git a/Controller/Customer/Address/FormPost.php b/Controller/Customer/Address/FormPost.php
new file mode 100644
index 0000000..95ddb43
--- /dev/null
+++ b/Controller/Customer/Address/FormPost.php
@@ -0,0 +1,67 @@
+_formKeyValidator->validate($this->getRequest())) {
+ return $this->resultRedirectFactory->create()->setPath('*/*/');
+ }
+
+ if (!$this->getRequest()->isPost()) {
+ $this->_getSession()->setAddressFormData($this->getRequest()->getPostValue());
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->_redirect->error($this->_buildUrl('*/*/edit'))
+ );
+ }
+
+ try {
+ $address = $this->_extractAddress();
+ /*custom code*/
+ $address->setCustomAttribute('district', $this->getRequest()->getParam('district_id'));
+ /*end*/
+ $this->_addressRepository->save($address);
+ $this->messageManager->addSuccessMessage(__('You saved the address.'));
+ $url = $this->_buildUrl('*/*/index', ['_secure' => true]);
+ return $this->resultRedirectFactory->create()->setUrl($this->_redirect->success($url));
+ } catch (InputException $e) {
+ $this->messageManager->addErrorMessage($e->getMessage());
+ foreach ($e->getErrors() as $error) {
+ $this->messageManager->addErrorMessage($error->getMessage());
+ }
+ } catch (\Exception $e) {
+ $redirectUrl = $this->_buildUrl('*/*/index');
+ $this->messageManager->addExceptionMessage($e, __('We can\'t save the address.'));
+ }
+
+ $url = $redirectUrl;
+ if (!$redirectUrl) {
+ $this->_getSession()->setAddressFormData($this->getRequest()->getPostValue());
+ $url = $this->_buildUrl('*/*/edit', ['id' => $this->getRequest()->getParam('id')]);
+ }
+
+ return $this->resultRedirectFactory->create()->setUrl($this->_redirect->error($url));
+ }
+}
diff --git a/Helper/Rate.php b/Helper/Rate.php
new file mode 100644
index 0000000..0d7cf81
--- /dev/null
+++ b/Helper/Rate.php
@@ -0,0 +1,121 @@
+storeManager = $storeManager;
+ $this->helperData = $helperData;
+ }
+
+ /**
+ * @param float $amount
+ * @return false|float
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ */
+ public function getAmountByStoreCurrency($amount)
+ {
+ if ($this->getDefaultCurrencyCode() == self::CURRENCY_CODE) {
+ return $amount;
+ } else {
+ try {
+ return round($this->helperData->currencyConvert(
+ $amount,
+ self::CURRENCY_CODE,
+ $this->getDefaultCurrencyCode()
+ ), 2);
+ } catch (\Exception $e) {
+ throw new LocalizedException(
+ __('We can\'t convert VND to store default currency. Please setup currency rates.')
+ );
+ }
+ }
+ }
+
+ /**
+ * @param Order $order
+ * @param $amount
+ * @return false|float
+ * @throws LocalizedException
+ */
+ public function getVndOrderAmount(Order $order, $amount)
+ {
+ if ($this->isVietnamDong($order)) {
+ return $amount;
+ } else {
+ try {
+ return round($this->helperData->currencyConvert(
+ $amount,
+ $order->getOrderCurrencyCode(),
+ self::CURRENCY_CODE
+ ));
+ } catch (\Exception $e) {
+ throw new LocalizedException(
+ __('We can\'t convert base currency to %1. Please setup currency rates.', self::CURRENCY_CODE)
+ );
+ }
+ }
+ }
+
+ /**
+ * @param Order $order
+ * @return boolean
+ */
+ private function isVietnamDong($order)
+ {
+ return $order->getOrderCurrencyCode() === self::CURRENCY_CODE;
+ }
+
+ /**
+ * @return mixed
+ * @throws NoSuchEntityException
+ */
+ private function getDefaultCurrencyCode()
+ {
+ return $this->storeManager->getStore()->getDefaultCurrencyCode();
+ }
+}
diff --git a/Model/Carrier/GHN.php b/Model/Carrier/GHN.php
new file mode 100644
index 0000000..58e7fb5
--- /dev/null
+++ b/Model/Carrier/GHN.php
@@ -0,0 +1,204 @@
+rateResultFactory = $rateResultFactory;
+ $this->rateMethodFactory = $rateMethodFactory;
+ $this->config = $config;
+ $this->helperRate = $helperRate;
+ $this->commandPool = $commandPool;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function collectRates(RateRequest $request)
+ {
+ if (!$this->getConfigFlag(Config::IS_ACTIVE)) {
+ return false;
+ }
+
+ if ($shippingCost = $this->estimateShippingCost($request)) {
+ /** @var Result $result */
+ $result = $this->rateResultFactory->create();
+ /** @var Method $method */
+ $method = $this->rateMethodFactory->create();
+ $method->setCarrier($this->_code);
+ $method->setCarrierTitle($this->getConfigData(Config::TITLE));
+ $method->setMethod($this->_code);
+ $method->setMethodTitle($this->getConfigData(Config::NAME));
+ $method->setPrice($shippingCost);
+ $method->setCost($shippingCost);
+
+ $result->append($method);
+
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAllowedMethods()
+ {
+ return [$this->_code => $this->getConfigData(Config::NAME)];
+ }
+
+
+ /**
+ * @param RateRequest $request
+ * @return string|null
+ */
+ protected function estimateShippingCost(RateRequest $request)
+ {
+ $shippingAddress = $request->getShippingAddress();
+ $shippingFee = 0;
+
+ try {
+ $this->prepareServices($request);
+ if ($serviceId = $this->getAvailableService()) {
+ $shippingAddress->setData('shipping_service_id', $serviceId);
+ $commandResult = $this->commandPool->get('calculate_rate')->execute(
+ [
+ 'rate_request' => $request,
+ 'service_id' => $serviceId
+ ]
+ );
+ $rate = SubjectReader::readRate($commandResult->get());
+ $shippingFee = SubjectReader::readCalculatedFee($rate);
+ }
+ return $this->helperRate->getAmountByStoreCurrency($shippingFee);
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+
+ /**
+ * @param RateRequest $request
+ * @throws CommandException
+ * @throws NotFoundException
+ */
+ protected function prepareServices($request)
+ {
+ $this->availableServices = SubjectReader::readServices(
+ $this->commandPool->get('get_services')->execute(['rate_request' => $request])->get()
+ );
+ }
+
+ /**
+ * @return mixed|null
+ */
+ protected function getAvailableService()
+ {
+ if (count($this->availableServices)) {
+ foreach ($this->availableServices as $serviceItem) {
+ if (is_array($serviceItem) && SubjectReader::readServiceName($serviceItem) == static::SERVICE_NAME) {
+ return $serviceItem[AbstractDataBuilder::SERVICE_ID];
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/Model/Carrier/GHN/Express.php b/Model/Carrier/GHN/Express.php
new file mode 100644
index 0000000..d5f6283
--- /dev/null
+++ b/Model/Carrier/GHN/Express.php
@@ -0,0 +1,27 @@
+storeManager = $storeManager;
+ $this->scopeConfig = $scopeConfig;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @return mixed
+ * @throws NoSuchEntityException
+ */
+ public function getWeightUnit()
+ {
+ return $this->getConfig(Data::XML_PATH_WEIGHT_UNIT);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDistricts()
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $sql = $connection->select()->from(
+ ['districtTable' => $this->resourceConnection->getTableName('boolfly_giaohangnhanh_district')]
+ )->joinLeft(
+ ['regionTable' => $this->resourceConnection->getTableName('directory_country_region')],
+ 'regionTable.code = districtTable.province_id',
+ 'regionTable.region_id as region_id'
+ );
+
+ return $connection->fetchAll($sql);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDistrictOptions()
+ {
+ $districts = $this->getDistricts();
+ $data = [];
+ foreach ($districts as $district) {
+ $districtName = $district['district_name'];
+ $data[] = [
+ 'title' => $districtName,
+ 'value' => $district['district_id'],
+ 'region_id' => $district['region_id'],
+ 'label' => $districtName
+ ];
+ }
+ return $data;
+ }
+
+ /**
+ * @param $path
+ * @return mixed
+ * @throws NoSuchEntityException
+ */
+ public function getConfig($path)
+ {
+ return $this->scopeConfig->getValue(
+ $path,
+ ScopeInterface::SCOPE_STORE,
+ $this->getStoreId()
+ );
+ }
+
+ /**
+ * @return int
+ * @throws NoSuchEntityException
+ */
+ private function getStoreId()
+ {
+ if (!$this->storeId) {
+ $this->storeId = $this->storeManager->getStore()->getStoreId();
+ }
+ return $this->storeId;
+ }
+}
diff --git a/Model/Config/Source/District.php b/Model/Config/Source/District.php
new file mode 100644
index 0000000..ecd864f
--- /dev/null
+++ b/Model/Config/Source/District.php
@@ -0,0 +1,79 @@
+storeInformation = $storeInformation;
+ $this->storeManager = $storeManager;
+ $this->config = $config;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toOptionArray()
+ {
+ $store = $this->storeManager->getStore();
+ $storeInfo = $this->storeInformation->getStoreInformationObject($store);
+ $districts = $this->config->getDistricts();
+ $data = [];
+ foreach ($districts as $district) {
+ if ($district['region_id'] == $storeInfo->getRegionId()) {
+ $data[] = [
+ 'label' => $district['district_name'],
+ 'value' => $district['district_id']
+ ];
+ }
+ }
+
+ if ($data) {
+ return $data;
+ }
+
+ return [['value' => '', 'label' => __('No district to select.')],];
+ }
+}
diff --git a/Model/Config/Source/NoteCode.php b/Model/Config/Source/NoteCode.php
new file mode 100644
index 0000000..b90a4ba
--- /dev/null
+++ b/Model/Config/Source/NoteCode.php
@@ -0,0 +1,36 @@
+ self::ALLOW_TRYING, 'label' => __('Allow trying item')],
+ ['value' => self::ALLOW_CHECKING_NOT_TRYING, 'label' => __('Allow checking item, but not trying')],
+ ['value' => self::NOT_ALLOW_CHECKING, 'label' => __('Don\'t allow checking item')]
+ ];
+ }
+}
diff --git a/Model/Config/Source/PaymentType.php b/Model/Config/Source/PaymentType.php
new file mode 100644
index 0000000..e51a534
--- /dev/null
+++ b/Model/Config/Source/PaymentType.php
@@ -0,0 +1,28 @@
+ 1, 'label' => __('Shop/Seller')], ['value' => 2, 'label' => __('Buyer/Consignee')]];
+ }
+}
diff --git a/Model/Logger/Logger.php b/Model/Logger/Logger.php
new file mode 100644
index 0000000..32f3aa4
--- /dev/null
+++ b/Model/Logger/Logger.php
@@ -0,0 +1,37 @@
+filterDebugData($debugData[$key], $debugReplacePrivateDataKeys);
+ }
+ }
+ return $debugData;
+ }
+}
diff --git a/Model/Service/Command/ServiceCommand.php b/Model/Service/Command/ServiceCommand.php
new file mode 100644
index 0000000..a5d1bff
--- /dev/null
+++ b/Model/Service/Command/ServiceCommand.php
@@ -0,0 +1,137 @@
+requestBuilder = $requestBuilder;
+ $this->transferFactory = $transferFactory;
+ $this->client = $client;
+ $this->handler = $handler;
+ $this->validator = $validator;
+ $this->logger = $logger;
+ $this->resultFactory = $resultFactory;
+ $this->resultKey = $resultKey;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $commandSubject)
+ {
+ $transferO = $this->transferFactory->create(
+ $this->requestBuilder->build($commandSubject)
+ );
+ $response = $this->client->request($transferO);
+ if ($this->validator !== null) {
+ $result = $this->validator->validate(
+ array_merge($commandSubject, ['response' => $response])
+ );
+ if (!$result->isValid()) {
+ throw new CommandException(
+ __(implode("\n", $result->getFailsDescription()))
+ );
+ }
+ }
+
+ if ($this->handler) {
+ $this->handler->handle(
+ $commandSubject,
+ $response
+ );
+ }
+
+ if ($this->resultKey) {
+ return $this->resultFactory->create(
+ [
+ 'array' => [
+ $this->resultKey => $response['data']
+ ]
+ ]
+ );
+ }
+ }
+}
diff --git a/Model/Service/Helper/Authorization.php b/Model/Service/Helper/Authorization.php
new file mode 100644
index 0000000..4fab435
--- /dev/null
+++ b/Model/Service/Helper/Authorization.php
@@ -0,0 +1,80 @@
+serializer = $serializer;
+ }
+
+ /**
+ * @return string
+ */
+ public function getParameter()
+ {
+ return $this->params;
+ }
+
+ /**
+ * @param $params
+ * @return $this
+ */
+ public function setParameter($params)
+ {
+ $this->params = $this->serializer->serialize($params);
+ return $this;
+ }
+
+ /**
+ * @param $params
+ * @return bool|string
+ */
+ public function getBody($params)
+ {
+ return $this->serializer->serialize($params);
+ }
+
+ /**
+ * Get Header
+ *
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return [
+ 'Content-Type: application/json',
+ 'Content-Length: ' . strlen($this->getParameter())
+ ];
+ }
+}
diff --git a/Model/Service/Helper/SubjectReader.php b/Model/Service/Helper/SubjectReader.php
new file mode 100644
index 0000000..aff2bfb
--- /dev/null
+++ b/Model/Service/Helper/SubjectReader.php
@@ -0,0 +1,251 @@
+clientFactory = $clientFactory;
+ $this->converter = $converter;
+ $this->logger = $logger;
+ }
+
+ public function request(TransferInterface $transferObject)
+ {
+ $log = [
+ 'request' => $this->converter ? $this->converter->convert($transferObject->getBody()) : $transferObject->getBody(),
+ 'request_uri' => $transferObject->getUri()
+ ];
+ $result = [];
+ /** @var ZendClient $client */
+ $client = $this->clientFactory->create();
+ $client->setConfig($transferObject->getClientConfig());
+ $client->setMethod($transferObject->getMethod());
+ $client->setRawData($transferObject->getBody());
+ $client->setHeaders($transferObject->getHeaders());
+ $client->setUrlEncodeBody($transferObject->shouldEncode());
+ $client->setUri($transferObject->getUri());
+
+ try {
+ $response = $client->request();
+ $result = $this->converter ? $this->converter->convert($response->getBody()) : [$response->getBody()];
+ $log['response'] = $result;
+ } catch (\Zend_Http_Client_Exception $e) {
+ throw new ClientException(
+ __($e->getMessage())
+ );
+ } catch (ConverterException $e) {
+ throw $e;
+ } finally {
+ $this->logger->debug($log);
+ }
+
+ return $result;
+ }
+}
diff --git a/Model/Service/Http/Converter/JsonToArray.php b/Model/Service/Http/Converter/JsonToArray.php
new file mode 100644
index 0000000..482bc0e
--- /dev/null
+++ b/Model/Service/Http/Converter/JsonToArray.php
@@ -0,0 +1,64 @@
+logger = $logger;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * Converts gateway response to array structure
+ *
+ * @param mixed $response
+ * @return array
+ * @throws ConverterException
+ */
+ public function convert($response)
+ {
+ try {
+ return $this->serializer->unserialize($response);
+ } catch (\Exception $e) {
+ $this->logger->critical('Can\'t read response from Giao Hang Nhanh');
+ throw new ConverterException(__('Can\'t read response from Giao Hang Nhanh'));
+ }
+ }
+}
diff --git a/Model/Service/Http/TransferFactory.php b/Model/Service/Http/TransferFactory.php
new file mode 100644
index 0000000..571525b
--- /dev/null
+++ b/Model/Service/Http/TransferFactory.php
@@ -0,0 +1,111 @@
+authorization = $authorization;
+ $this->transferBuilder = $transferBuilder;
+ $this->config = $config;
+ $this->path = $path;
+ }
+
+ /**
+ * Builds service transfer object
+ *
+ * @param array $request
+ * @return TransferInterface
+ */
+ public function create(array $request)
+ {
+ $header = $this->getAuthorization()
+ ->setParameter($request)
+ ->getHeaders();
+ $body = $this->getAuthorization()->getParameter();
+ return $this->transferBuilder
+ ->setMethod('POST')
+ ->setHeaders($header)
+ ->setBody($body)
+ ->setUri($this->getUri())
+ ->build();
+ }
+
+ /**
+ * @return Authorization
+ */
+ protected function getAuthorization()
+ {
+ return $this->authorization;
+ }
+
+ /**
+ * @return mixed|string
+ */
+ protected function getUri()
+ {
+ $baseUrl = $this->isSandboxMode() ? $this->config->getValue('giaohangnhanh_sandbox_url')
+ : $this->config->getValue('giaohangnhanh_url');
+
+ return $baseUrl . '/' . $this->config->getValue($this->path);
+ }
+
+ /**
+ * Whether sandbox mode is enabled in configuration
+ *
+ * @return bool
+ */
+ protected function isSandboxMode()
+ {
+ return $this->config && (bool)$this->config->getValue('sandbox_flag');
+ }
+}
diff --git a/Model/Service/Request/AbstractDataBuilder.php b/Model/Service/Request/AbstractDataBuilder.php
new file mode 100644
index 0000000..4f0a975
--- /dev/null
+++ b/Model/Service/Request/AbstractDataBuilder.php
@@ -0,0 +1,106 @@
+config = $config;
+ $this->storeManager = $storeManager;
+ $this->storeInformation = $storeInformation;
+ $this->addressFactory = $addressFactory;
+ $this->baseConfig = $baseConfig;
+ $this->helperRate = $helperRate;
+ }
+}
diff --git a/Model/Service/Request/GetDistrictsDataBuilder.php b/Model/Service/Request/GetDistrictsDataBuilder.php
new file mode 100644
index 0000000..847f74a
--- /dev/null
+++ b/Model/Service/Request/GetDistrictsDataBuilder.php
@@ -0,0 +1,23 @@
+ $this->config->getValue('api_token')];
+ }
+}
\ No newline at end of file
diff --git a/Model/Service/Request/OrderInfoDataBuilder.php b/Model/Service/Request/OrderInfoDataBuilder.php
new file mode 100644
index 0000000..b44be53
--- /dev/null
+++ b/Model/Service/Request/OrderInfoDataBuilder.php
@@ -0,0 +1,35 @@
+ $this->config->getValue('api_token'),
+ self::ORDER_CODE => $order->getData('tracking_code')
+ ];
+ }
+}
diff --git a/Model/Service/Request/ShippingDetailsDataBuilder.php b/Model/Service/Request/ShippingDetailsDataBuilder.php
new file mode 100644
index 0000000..95f0598
--- /dev/null
+++ b/Model/Service/Request/ShippingDetailsDataBuilder.php
@@ -0,0 +1,44 @@
+baseConfig->getWeightUnit() == self::DEFAULT_WEIGHT_UNIT ? Config::KGS_G : Config::LBS_G;
+ $data = [
+ self::TOKEN => $this->config->getValue('api_token'),
+ self::WEIGHT => $rateRequest->getPackageWeight() * $rate,
+ self::FROM_DISTRICT_ID => (int)$this->config->getValue('district'),
+ self::TO_DISTRICT_ID => (int)$rateRequest->getDistrict()
+ ];
+
+ if ($serviceId = SubjectReader::readServiceId($buildSubject)) {
+ $data[self::SERVICE_ID] = $serviceId;
+ }
+ return $data;
+ }
+}
diff --git a/Model/Service/Request/SynchronizeOrderDataBuilder.php b/Model/Service/Request/SynchronizeOrderDataBuilder.php
new file mode 100644
index 0000000..ef75e00
--- /dev/null
+++ b/Model/Service/Request/SynchronizeOrderDataBuilder.php
@@ -0,0 +1,65 @@
+baseConfig->getWeightUnit() == self::DEFAULT_WEIGHT_UNIT ? Config::KGS_G : Config::LBS_G;
+ $store = $this->storeManager->getStore();
+ $storeInfo = $this->storeInformation->getStoreInformationObject($store);
+ $storeFormattedAddress = $this->storeInformation->getFormattedAddress($store);
+ $storeDistrict = (int)$this->config->getValue('district');
+
+ return [
+ self::TOKEN => $this->config->getValue('api_token'),
+ self::PAYMENT_TYPE_ID => (int)$this->config->getValue('payment_type'),
+ self::FROM_DISTRICT_ID => $storeDistrict,
+ self::TO_DISTRICT_ID => (int)SubjectReader::readDistrict($buildSubject),
+ self::CLIENT_CONTACT_NAME => $storeInfo->getName(),
+ self::CLIENT_CONTACT_PHONE => $storeInfo->getPhone(),
+ self::CLIENT_ADDRESS => $storeFormattedAddress,
+ self::CUSTOMER_NAME => $order->getCustomerName(),
+ self::CUSTOMER_PHONE => $order->getShippingAddress()->getTelephone(),
+ self::SHIPPING_ADDRESS => $order->getShippingAddress()->getStreetLine(1),
+ self::NOTE_CODE => $this->config->getValue('note_code'),
+ self::SERVICE_ID => (int)SubjectReader::readShippingServiceId($buildSubject),
+ self::WEIGHT => $order->getWeight() * $weightRate,
+ self::LENGTH => (int)$this->config->getValue('default_length'),
+ self::WIDTH => (int)$this->config->getValue('default_width'),
+ self::HEIGHT => (int)$this->config->getValue('default_height'),
+ self::CO_D_AMOUNT => $this->helperRate->getVndOrderAmount($order, $order->getGrandTotal()),
+ self::RETURN_CONTACT_NAME => $storeInfo->getName(),
+ self::RETURN_CONTACT_PHONE => $storeInfo->getPhone(),
+ self::RETURN_ADDRESS => $storeFormattedAddress,
+ self::RETURN_DISTRICT_ID => $storeDistrict,
+ self::EXTERNAL_RETURN_CODE => $storeInfo->getName()
+ ];
+ }
+}
diff --git a/Model/Service/Response/CancelOrderHandler.php b/Model/Service/Response/CancelOrderHandler.php
new file mode 100644
index 0000000..6761429
--- /dev/null
+++ b/Model/Service/Response/CancelOrderHandler.php
@@ -0,0 +1,35 @@
+setData('ghn_canceling_status', self::GHN_SUCCESS_CANCELING_STATUS);
+ }
+}
diff --git a/Model/Service/Response/SynchronizeOrderHandler.php b/Model/Service/Response/SynchronizeOrderHandler.php
new file mode 100644
index 0000000..6c70a96
--- /dev/null
+++ b/Model/Service/Response/SynchronizeOrderHandler.php
@@ -0,0 +1,43 @@
+setData('ghn_status', self::GHN_STATUS_SUCCESS);
+ $order->setData('tracking_code', $trackingCode);
+ } else {
+ $order->setData('ghn_status', self::GHN_STATUS_FAIL);
+ }
+ }
+}
diff --git a/Model/Service/Validator/AbstractResponseValidator.php b/Model/Service/Validator/AbstractResponseValidator.php
new file mode 100644
index 0000000..4a9de9e
--- /dev/null
+++ b/Model/Service/Validator/AbstractResponseValidator.php
@@ -0,0 +1,52 @@
+config = $config;
+ }
+
+ /**
+ * @param array $response
+ * @return bool
+ */
+ protected function validateResponseMsg(array $response)
+ {
+ return isset($response[self::MSG]) && $response[self::MSG] === self::SUCCESS_MESSAGE;
+ }
+}
diff --git a/Model/Service/Validator/CancelOrderValidator.php b/Model/Service/Validator/CancelOrderValidator.php
new file mode 100644
index 0000000..3784671
--- /dev/null
+++ b/Model/Service/Validator/CancelOrderValidator.php
@@ -0,0 +1,38 @@
+validateResponseMsg($response);
+
+ if (!$validationResult) {
+ $errorMessages = [__('Something went wrong when cancel order.')];
+ }
+
+ return $this->createResult($validationResult, $errorMessages);
+ }
+}
diff --git a/Model/Service/Validator/SynchronizeOrderValidator.php b/Model/Service/Validator/SynchronizeOrderValidator.php
new file mode 100644
index 0000000..65f6985
--- /dev/null
+++ b/Model/Service/Validator/SynchronizeOrderValidator.php
@@ -0,0 +1,50 @@
+validateResponseMsg($response) && $this->validatePaymentTypeId($responseData);
+
+ if (!$validationResult) {
+ $errorMessages = [__('Something went wrong when synchronize order.')];
+ }
+
+ return $this->createResult($validationResult, $errorMessages);
+ }
+
+ /**
+ * @param array $responseData
+ * @return bool
+ */
+ private function validatePaymentTypeId(array $responseData)
+ {
+ return isset($responseData[AbstractDataBuilder::PAYMENT_TYPE_ID])
+ && $responseData[AbstractDataBuilder::PAYMENT_TYPE_ID] == $this->config->getValue('payment_type');
+ }
+}
diff --git a/Observer/SalesOrderCancelAfterObserver.php b/Observer/SalesOrderCancelAfterObserver.php
new file mode 100644
index 0000000..bdf2d59
--- /dev/null
+++ b/Observer/SalesOrderCancelAfterObserver.php
@@ -0,0 +1,65 @@
+commandPool = $commandPool;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param Observer $observer
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var Order $order */
+ $order = $observer->getEvent()->getOrder();
+
+ if ($order->getData('ghn_status') && $order->getData('tracking_code')) {
+ try {
+ $this->commandPool->get('cancel_order')->execute(['order' => $order]);
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+ }
+}
diff --git a/Observer/SalesOrderPlaceAfterObserver.php b/Observer/SalesOrderPlaceAfterObserver.php
new file mode 100644
index 0000000..3e1f0ed
--- /dev/null
+++ b/Observer/SalesOrderPlaceAfterObserver.php
@@ -0,0 +1,84 @@
+logger = $logger;
+ $this->quoteRepository = $quoteRepository;
+ $this->commandPool = $commandPool;
+ }
+
+ /**
+ * @param Observer $observer
+ * @throws NoSuchEntityException
+ * @throws Exception
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $observer->getEvent()->getOrder();
+
+ if (false !== strpos($order->getShippingMethod(), Config::GHN_CODE)) {
+ $quote = $this->quoteRepository->get($order->getQuoteId());
+ $shippingAddress = $quote->getShippingAddress();
+ try {
+ $this->commandPool->get('synchronize_order')->execute([
+ 'order' => $order,
+ 'district' => $shippingAddress->getDistrict(),
+ 'shipping_service_id' => $shippingAddress->getShippingServiceId()
+ ]);
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ throw new Exception(__('This shipping method isn\'t valid now. Please select another shipping method.'));
+ }
+ }
+ }
+}
diff --git a/Plugin/Checkout/Block/Checkout/Cart/LayoutProcessor.php b/Plugin/Checkout/Block/Checkout/Cart/LayoutProcessor.php
new file mode 100644
index 0000000..89c12de
--- /dev/null
+++ b/Plugin/Checkout/Block/Checkout/Cart/LayoutProcessor.php
@@ -0,0 +1,66 @@
+config = $config;
+ }
+
+ public function afterProcess(MageLayoutProcessor $subject, $jsLayout)
+ {
+ $jsLayout['components']['block-summary']['children']['block-shipping']
+ ['children']['address-fieldsets']['children'][AddressAttribute::DISTRICT] = [
+ 'component' => 'Boolfly_GiaoHangNhanh/js/view/cart/shipping/district',
+ 'dataScope' => 'shippingAddress.district',
+ 'provider' => 'checkoutProvider',
+ 'sortOrder' => 152,
+ 'config' => [
+ 'template' => 'ui/form/field',
+ 'elementTmpl' => 'ui/form/element/select'
+ ],
+ 'visible' => true,
+ 'formElement' => 'select',
+ 'label' => __('District'),
+ 'options' => [],
+ 'value' => null,
+ 'filterBy' => [
+ 'target' => '${ $.provider }:${ $.parentScope }.region_id',
+ 'field' => 'region_id'
+ ],
+ 'imports' => [
+ 'initialOptions' => 'index = checkoutProvider:dictionaries.district',
+ 'setOptions' => 'index = checkoutProvider:dictionaries.district'
+ ]
+ ];
+ $jsLayout['components']['checkoutProvider']['dictionaries'][AddressAttribute::DISTRICT] = $this->config->getDistrictOptions();
+ return $jsLayout;
+ }
+}
diff --git a/Plugin/Checkout/Block/Checkout/DirectoryDataProcessor.php b/Plugin/Checkout/Block/Checkout/DirectoryDataProcessor.php
new file mode 100644
index 0000000..8e02515
--- /dev/null
+++ b/Plugin/Checkout/Block/Checkout/DirectoryDataProcessor.php
@@ -0,0 +1,49 @@
+config = $config;
+ }
+
+ /**
+ * @param MageDirectoryDataProcessor $subject
+ * @param $result
+ * @return mixed
+ */
+ public function afterProcess(MageDirectoryDataProcessor $subject, $result)
+ {
+ $result['components']['checkoutProvider']['dictionaries'][AddressAttribute::DISTRICT] = $this->config->getDistrictOptions();
+
+ return $result;
+ }
+}
diff --git a/Plugin/Checkout/Block/Checkout/LayoutProcessor.php b/Plugin/Checkout/Block/Checkout/LayoutProcessor.php
new file mode 100644
index 0000000..93d54ee
--- /dev/null
+++ b/Plugin/Checkout/Block/Checkout/LayoutProcessor.php
@@ -0,0 +1,53 @@
+ 'Boolfly_GiaoHangNhanh/js/view/checkout/shipping/district',
+ 'config' => [
+ 'customScope' => 'shippingAddress.custom_attributes',
+ 'template' => 'ui/form/field',
+ 'elementTmpl' => 'ui/form/element/select',
+ 'id' => AddressAttribute::DISTRICT,
+ ],
+ 'dataScope' => 'shippingAddress.custom_attributes.district',
+ 'label' => __('District'),
+ 'provider' => 'checkoutProvider',
+ 'visible' => true,
+ 'validation' => ['required-entry' => false],
+ 'sortOrder' => 255,
+ 'id' => AddressAttribute::DISTRICT,
+ 'imports' => [
+ 'initialOptions' => 'index = checkoutProvider:dictionaries.district',
+ 'setOptions' => 'index = checkoutProvider:dictionaries.district'
+ ],
+ 'filterBy' => [
+ 'target' => 'checkoutProvider:shippingAddress.region_id',
+ 'field' => 'region_id'
+ ],
+ 'deps' => 'checkoutProvider'
+ ];
+
+ return $result;
+ }
+}
diff --git a/Plugin/Checkout/Model/ShippingInformationManagement.php b/Plugin/Checkout/Model/ShippingInformationManagement.php
new file mode 100644
index 0000000..9fbc642
--- /dev/null
+++ b/Plugin/Checkout/Model/ShippingInformationManagement.php
@@ -0,0 +1,78 @@
+quoteRepository = $quoteRepository;
+ $this->customerAddressFactory = $customerAddressFactory;
+ }
+
+ /**
+ * @param MageShippingInformationManagement $subject
+ * @param $cartId
+ * @param ShippingInformationInterface $addressInformation
+ * @throws NoSuchEntityException
+ */
+ public function beforeSaveAddressInformation(
+ MageShippingInformationManagement $subject,
+ $cartId,
+ ShippingInformationInterface $addressInformation
+ ) {
+ $quote = $this->quoteRepository->getActive($cartId);
+ $extensionAttributes = $addressInformation->getExtensionAttributes();
+ $shippingAddress = $quote->getShippingAddress();
+
+ if ($shippingAddress->getDistrict()) {
+ return;
+ }
+
+ if (!$extensionAttributes->getDistrict()) {
+ $customerAddressId = $addressInformation->getShippingAddress()->getCustomerAddressId();
+ $address = $this->customerAddressFactory->create()->load($customerAddressId);
+ $district = $address->getDistrict();
+ } else {
+ $district = $extensionAttributes->getDistrict();
+ }
+
+ $shippingAddress->setDistrict($district);
+ }
+}
diff --git a/Plugin/Checkout/Model/TotalsInformationManagement.php b/Plugin/Checkout/Model/TotalsInformationManagement.php
new file mode 100644
index 0000000..793722e
--- /dev/null
+++ b/Plugin/Checkout/Model/TotalsInformationManagement.php
@@ -0,0 +1,55 @@
+cartRepository = $cartRepository;
+ }
+
+ public function beforeCalculate(
+ MageTotalsInformationManagement $subject,
+ $cartId,
+ TotalsInformationInterface $addressInformation
+ ) {
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->cartRepository->get($cartId);
+ $shippingAddress = $quote->getShippingAddress();
+
+ if ($shippingAddress->getDistrict()) {
+ return;
+ }
+
+ $district = $addressInformation->getExtensionAttributes()->getDistrict();
+ $shippingAddress->setDistrict($district);
+ }
+}
diff --git a/Plugin/Quote/Model/EstimateByAddressIdBefore.php b/Plugin/Quote/Model/EstimateByAddressIdBefore.php
new file mode 100644
index 0000000..b5ed107
--- /dev/null
+++ b/Plugin/Quote/Model/EstimateByAddressIdBefore.php
@@ -0,0 +1,67 @@
+quoteRepository = $quoteRepository;
+ $this->customerAddressFactory = $customerAddressFactory;
+ }
+
+ /**
+ * @param MageShippingMethodManagement $subject
+ * @param $cartId
+ * @param $addressId
+ * @throws NoSuchEntityException
+ */
+ public function beforeEstimateByAddressId(
+ MageShippingMethodManagement $subject,
+ $cartId,
+ $addressId
+ ) {
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->quoteRepository->getActive($cartId);
+ $address = $this->customerAddressFactory->create()->load($addressId);
+
+ if ($address->getId()) {
+ $district = $address->getDistrict();
+ $quote->getShippingAddress()->setDistrict($district);
+ }
+ }
+}
diff --git a/Plugin/Quote/Model/EstimateByExtendedAddressBefore.php b/Plugin/Quote/Model/EstimateByExtendedAddressBefore.php
new file mode 100644
index 0000000..c9498c4
--- /dev/null
+++ b/Plugin/Quote/Model/EstimateByExtendedAddressBefore.php
@@ -0,0 +1,55 @@
+quoteRepository = $quoteRepository;
+ }
+
+
+ /**
+ * @param MageShippingMethodManagement $subject
+ * @param $cartId
+ * @param AddressInterface $address
+ * @throws NoSuchEntityException
+ */
+ public function beforeEstimateByExtendedAddress(
+ MageShippingMethodManagement $subject,
+ $cartId,
+ AddressInterface $address
+ ) {
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->quoteRepository->getActive($cartId);
+ $district = $address->getExtensionAttributes()->getDistrict();
+ $quote->getShippingAddress()->setDistrict($district);
+ }
+}
diff --git a/Plugin/Quote/Model/Quote/Address.php b/Plugin/Quote/Model/Quote/Address.php
new file mode 100644
index 0000000..632ab6a
--- /dev/null
+++ b/Plugin/Quote/Model/Quote/Address.php
@@ -0,0 +1,30 @@
+getDistrict()) {
+ $result->setCustomAttribute(AddressAttribute::DISTRICT, $district);
+ }
+
+ return $result;
+ }
+}
diff --git a/Plugin/Sales/Block/Adminhtml/Order/Create/Billing/Address.php b/Plugin/Sales/Block/Adminhtml/Order/Create/Billing/Address.php
new file mode 100644
index 0000000..7adf190
--- /dev/null
+++ b/Plugin/Sales/Block/Adminhtml/Order/Create/Billing/Address.php
@@ -0,0 +1,61 @@
+addressRepository = $addressRepository;
+ }
+
+ /**
+ * @param MageAddress $subject
+ * @throws LocalizedException
+ */
+ public function beforeGetFormValues(MageAddress $subject)
+ {
+ $customerAddressId = $subject->getAddressId();
+
+ if ($customerAddressId) {
+ try {
+ $customerAddress = $this->addressRepository->getById($customerAddressId);
+ $district = $customerAddress->getCustomAttribute(AddressAttribute::DISTRICT);
+
+ if ($district) {
+ $subject->getCreateOrderModel()->getBillingAddress()->setDistrict($district->getValue());
+ }
+ } catch (LocalizedException $e) {
+ throw new LocalizedException(
+ __("No such customer address with ID %1.", $customerAddressId)
+ );
+ }
+ }
+ }
+}
diff --git a/Plugin/Sales/Block/Adminhtml/Order/Create/Shipping/Address.php b/Plugin/Sales/Block/Adminhtml/Order/Create/Shipping/Address.php
new file mode 100644
index 0000000..3c941fc
--- /dev/null
+++ b/Plugin/Sales/Block/Adminhtml/Order/Create/Shipping/Address.php
@@ -0,0 +1,61 @@
+addressRepository = $addressRepository;
+ }
+
+ /**
+ * @param MageAddress $subject
+ * @throws LocalizedException
+ */
+ public function beforeGetFormValues(MageAddress $subject)
+ {
+ $customerAddressId = $subject->getAddressId();
+
+ if ($customerAddressId) {
+ try {
+ $customerAddress = $this->addressRepository->getById($customerAddressId);
+ $district = $customerAddress->getCustomAttribute(AddressAttribute::DISTRICT);
+
+ if ($district) {
+ $subject->getAddress()->setDistrict($district->getValue());
+ }
+ } catch (LocalizedException $e) {
+ throw new LocalizedException(
+ __("No such customer address with ID %1.", $customerAddressId)
+ );
+ }
+ }
+ }
+}
diff --git a/Plugin/Sales/Model/Order.php b/Plugin/Sales/Model/Order.php
new file mode 100644
index 0000000..dc24efb
--- /dev/null
+++ b/Plugin/Sales/Model/Order.php
@@ -0,0 +1,71 @@
+commandPool = $commandPool;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param MageOrder $subject
+ * @param $result
+ * @return bool
+ */
+ public function afterCanCancel(MageOrder $subject, $result)
+ {
+ if ($subject->getData('ghn_status') && $subject->getData('tracking_code')) {
+ try {
+ $commandResult = $this->commandPool->get('get_order_info')->execute(['order' => $subject]);
+ $orderInfo = SubjectReader::readInfo($commandResult->get());
+ if (SubjectReader::readCurrentOrderStatus($orderInfo) != self::DEFAULT_ORDER_STATUS) {
+ $result = false;
+ }
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/Plugin/Shipping/Model/Shipping.php b/Plugin/Shipping/Model/Shipping.php
new file mode 100644
index 0000000..9c60091
--- /dev/null
+++ b/Plugin/Shipping/Model/Shipping.php
@@ -0,0 +1,76 @@
+getShippingAddress($rateRequest);
+ $rateRequest->setShippingAddress($shippingAddress);
+
+ if ($district = $shippingAddress->getDistrict()) {
+ $rateRequest->setDistrict($district);
+ }
+ } catch (LocalizedException $exception) {
+ return;
+ }
+ }
+
+ /**
+ * Normalize rate request items. In rare cases they are not set at all.
+ *
+ * @param RateRequest $rateRequest
+ * @return AbstractItem[]
+ */
+ private function getItems(RateRequest $rateRequest)
+ {
+ if (!$rateRequest->getAllItems()) {
+ return [];
+ }
+
+ return $rateRequest->getAllItems();
+ }
+
+ /**
+ * Extract shipping address from rate request.
+ *
+ * @param RateRequest $rateRequest
+ * @return Address
+ * @throws LocalizedException
+ */
+ private function getShippingAddress(RateRequest $rateRequest)
+ {
+ $itemsToShip = $this->getItems($rateRequest);
+ $currentItem = current($itemsToShip);
+
+ if ($currentItem === false) {
+ throw new LocalizedException(__('No items to ship found in rates request.'));
+ }
+
+ return $currentItem->getAddress();
+ }
+}
diff --git a/README.md b/README.md
index 39af52c..a1ee9b9 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,90 @@
-# README #
+# GiaoHangNhanh
-This README would normally document whatever steps are necessary to get your application up and running.
-### What is this repository for? ###
+## Installation
-* Quick summary
-* Version
-* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo)
+##### Using Composer (we recommended)
-### How do I get set up? ###
+```
+composer require boolfly/module-giaohangnhanh
+```
-* Summary of set up
-* Configuration
-* Dependencies
-* Database configuration
-* How to run tests
-* Deployment instructions
+## Configuration
-### Contribution guidelines ###
+First of all, we need to run command-line: bin/magento region:generate
-* Writing tests
-* Code review
-* Other guidelines
+This command-line will import region information data into database (required).
-### Who do I talk to? ###
+### Setup Currency
+
+We need to make sure our website supporting Vietnamese Dong.
+
+Log in to Admin, **STORES > Configurations > GENERAL > Currency Setup > Currency Options > Allowed Currencies**. Make sure the Vietnamese Dong is selected.
+
+![GiaoHangNhanh currency](https://image.prntscr.com/image/8ED4t6WaQKKz8M52s1Geiw.png)
+
+Go to Currency Rates, **STORES > Currency > Currency Rates**
+
+![GiaoHangNhanh currency](https://image.prntscr.com/image/JTrmMIRsQE2MwJdk4rslsg.png)
+
+### Config Store Information
+GiaoHangNhanh extension supports Vietnam store only.
+
+Log in to Admin, **STORES > Configurations > GENERAL > General > Store Information**
+
+ - Country: Select Vietnam.
+
+### Config API
+Log in to Admin, **STORES > Configurations > SALES > Shipping Methods > Giao Hang Nhanh**
+
+![GiaoHangNhanh Configuration](https://image.prntscr.com/image/6SGprE_CTm2sGHwQI0NBXQ.png)
+
+![GiaoHangNhanh Configuration](https://image.prntscr.com/image/rwRqazVlQYOcF_PwwivkTQ.png)
+
+Read more here:
+
+- https://api.ghn.vn
+- https://api.ghn.vn/home/faq
+
+Create Giao Hang Nhanh account to get the api token.
+
+Configuration info to integrate with GiaoHangNhanh API.
+
+ - Sandbox Mode: when testing, we should enable this mode
+ - Api Token: Use the info above.
+ - Payment type: Choose who will pay the payment fee.
+ - Note Code: Rule for the shipping.
+ - Debug: Enable to allow writing log.
+
+GiaoHangNhanh extension consists of Giao Hang Nhanh Express and Giao Hang Nhanh Standard. We need to fill neccessary information for these solutions (Advanced Settings).
+
+
+## How does it work?
+
+### Checkout
+ After enabling this method, go to the checkout, we can see this method.
+
+ ![GiaoHangNhanh Checkout](https://image.prntscr.com/image/AT57c1bTSMyfSQ5D6ZfGdQ.png)
+
+Contribution
+---
+Want to contribute to this extension? The quickest way is to open a [pull request on GitHub](https://help.github.com/articles/using-pull-requests)
+
+Magento 2 Extensions
+---
+
+- [Ajax Wishlist](https://github.com/boolfly/ajax-wishlist)
+- [Quick View](https://github.com/boolfly/quick-view)
+- [Banner Slider](https://github.com/boolfly/banner-slider)
+- [Product Label](https://github.com/boolfly/product-label)
+- [ZaloPay](https://github.com/boolfly/zalo-pay)
+- [Momo](https://github.com/boolfly/momo-wallet)
+- [Blog](https://github.com/boolfly/blog)
+- [Brand](https://github.com/boolfly/brand)
+- [Product Question](https://github.com/boolfly/product-question)
+- [Sales Sequence](https://github.com/boolfly/sales-sequence)
+
+Support
+---
+Need help settings up or want to customize this extension to meet your business needs? Please email boolfly.inc@gmail.com and if we like your idea we will add this feature for free or at a discounted rate.
-* Repo owner or admin
-* Other community or team contact
\ No newline at end of file
diff --git a/Setup/Patch/Data/AddressAttribute.php b/Setup/Patch/Data/AddressAttribute.php
new file mode 100644
index 0000000..b452366
--- /dev/null
+++ b/Setup/Patch/Data/AddressAttribute.php
@@ -0,0 +1,104 @@
+eavConfig = $eavConfig;
+ $this->eavSetupFactory = $eavSetupFactory;
+ }
+
+ /**
+ * @return array|string[]
+ */
+ public static function getDependencies()
+ {
+ return [];
+ }
+
+ /**
+ * @return array|string[]
+ */
+ public function getAliases()
+ {
+ return [];
+ }
+
+ /**
+ * @return AddressAttribute|void
+ * @throws LocalizedException|Zend_Validate_Exception
+ */
+ public function apply()
+ {
+ $eavSetup = $this->eavSetupFactory->create();
+
+ $eavSetup->addAttribute(AddressMetadataInterface::ENTITY_TYPE_ADDRESS, self::DISTRICT, [
+ 'group' => 'General',
+ 'type' => 'varchar',
+ 'label' => 'District',
+ 'input' => 'text',
+ 'required' => false,
+ 'sort_order' => 110,
+ 'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
+ 'is_used_in_grid' => true,
+ 'is_visible_in_grid' => true,
+ 'is_filterable_in_grid' => false,
+ 'visible' => true,
+ 'user_defined' => true,
+ 'system' => 0,
+ 'is_html_allowed_on_front' => true,
+ 'visible_on_front' => true
+ ]);
+
+ $eavSetup->addAttributeToSet(
+ AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
+ AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS,
+ 1,
+ self::DISTRICT
+ );
+
+ $district = $this->eavConfig->getAttribute(AddressMetadataInterface::ENTITY_TYPE_ADDRESS, self::DISTRICT);
+ $district->setData(
+ 'used_in_forms',
+ ['adminhtml_customer_address','customer_address_edit','customer_register_address']
+ );
+ $district->save();
+ }
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..362d70c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "boolfly/module-giaohangnhanh",
+ "description": "Giao hang nhanh shipping method",
+ "require": {
+ "php": "~7.1.3||~7.2.0||~7.3.0",
+ "boolfly/module-integration-base": "^1.0.0"
+ },
+ "type": "magento2-module",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Boolfly\\GiaoHangNhanh\\": ""
+ }
+ }
+}
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
new file mode 100644
index 0000000..4030b4c
--- /dev/null
+++ b/etc/adminhtml/system.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+ 1
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ giaohangnhanh_setting/general/sandbox_flag
+
+
+
+ Magento\Config\Model\Config\Backend\Encrypted
+ Set token to access giaohangnhanh api.
+ giaohangnhanh_setting/general/api_token
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config\Source\PaymentType
+ giaohangnhanh_setting/general/payment_type
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config\Source\NoteCode
+ giaohangnhanh_setting/general/note_code
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config\Source\District
+ giaohangnhanh_setting/general/district
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ giaohangnhanh_setting/general/debug
+
+
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ carriers/giaohangnhanh_express/active
+
+
+
+ carriers/giaohangnhanh_express/name
+
+
+
+ carriers/giaohangnhanh_express/sort_order
+
+
+
+ carriers/giaohangnhanh_express/title
+
+
+
+ shipping-applicable-country
+ Magento\Shipping\Model\Config\Source\Allspecificcountries
+ carriers/giaohangnhanh_express/sallowspecific
+
+
+
+ Magento\Directory\Model\Config\Source\Country
+ 1
+ carriers/giaohangnhanh_express/specificcountry
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ shipping-skip-hide
+ carriers/giaohangnhanh_express/showmethod
+
+
+
+ carriers/giaohangnhanh_express/specificerrmsg
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ carriers/giaohangnhanh_standard/active
+
+
+
+ carriers/giaohangnhanh_standard/name
+
+
+
+ carriers/giaohangnhanh_standard/sort_order
+
+
+
+ carriers/giaohangnhanh_standard/title
+
+
+
+ shipping-applicable-country
+ Magento\Shipping\Model\Config\Source\Allspecificcountries
+ carriers/giaohangnhanh_standard/sallowspecific
+
+
+
+ Magento\Directory\Model\Config\Source\Country
+ 1
+ carriers/giaohangnhanh_standard/specificcountry
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ shipping-skip-hide
+ carriers/giaohangnhanh_standard/showmethod
+
+
+
+ carriers/giaohangnhanh_standard/specificerrmsg
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/config.xml b/etc/config.xml
new file mode 100644
index 0000000..3c221d9
--- /dev/null
+++ b/etc/config.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ 2
+ CalculateFee
+ CreateOrder
+ GetDistricts
+ CHOXEMHANGKHONGTHU
+ FindAvailableServices
+ OrderInfo
+ CancelOrder
+ 1
+ 1
+ 10
+ 10
+ 10
+ token
+ https://console.ghn.vn/api/v1/apiv3
+ https://dev-online-gateway.ghn.vn/apiv3-api/api/v1/apiv3
+
+
+
+
+ 1
+ 0
+ Boolfly\GiaoHangNhanh\Model\Carrier\GHN\Express
+ Giao Hang Nhanh(Express)
+ Giao Hang Nhanh
+ This shipping method is not available. To use this shipping method, please contact us.
+ F
+
+
+ 1
+ 0
+ Boolfly\GiaoHangNhanh\Model\Carrier\GHN\Standard
+ Giao Hang Nhanh(Standard)
+ Giao Hang Nhanh
+ This shipping method is not available. To use this shipping method, please contact us.
+ F
+
+
+
+
\ No newline at end of file
diff --git a/etc/db_schema.xml b/etc/db_schema.xml
new file mode 100644
index 0000000..2ad67a7
--- /dev/null
+++ b/etc/db_schema.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json
new file mode 100644
index 0000000..47183c1
--- /dev/null
+++ b/etc/db_schema_whitelist.json
@@ -0,0 +1,29 @@
+{
+ "boolfly_giaohangnhanh_district": {
+ "column": {
+ "entity_id": true,
+ "district_id": true,
+ "province_id": true,
+ "district_name": true
+ },
+ "constraint": {
+ "PRIMARY": true
+ }
+ },
+ "sales_order": {
+ "column": {
+ "ghn_status": true
+ }
+ },
+ "quote_address": {
+ "column": {
+ "district": true,
+ "shipping_service_id": true
+ }
+ },
+ "sales_order_grid": {
+ "column": {
+ "ghn_status": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/etc/di.xml b/etc/di.xml
new file mode 100644
index 0000000..8a24a86
--- /dev/null
+++ b/etc/di.xml
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+ region:generate
+ GhnCommandPool
+
+
+
+
+
+ -
+ Boolfly\GiaoHangNhanh\Console\GenerateRegionCommand
+
+
+
+
+
+
+
+ - sales_order.ghn_status
+ - sales_order.tracking_code
+ - sales_order.ghn_canceling_status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ GhnCommandPool
+
+
+
+
+ GhnCommandPool
+
+
+
+
+ GhnCommandPool
+
+
+
+
+ GhnCommandPool
+
+
+
+
+
+ - GetServicesCommand
+ - CalculateRateCommand
+ - CancelOrderCommand
+ - GetOrderInfoCommand
+ - SynchronizeOrderCommand
+ - GetDistrictsCommand
+
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\ShippingDetailsDataBuilder
+ GetServicesTransferFactory
+ GhnZendHttpClient
+ services
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\ShippingDetailsDataBuilder
+ CalculateRateTransferFactory
+ GhnZendHttpClient
+ rate
+
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\OrderInfoDataBuilder
+
+ CancelOrderTransferFactory
+ GhnZendHttpClient
+ Boolfly\GiaoHangNhanh\Model\Service\Validator\CancelOrderValidator
+ Boolfly\GiaoHangNhanh\Model\Service\Response\CancelOrderHandler
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\OrderInfoDataBuilder
+ GetOrderInfoTransferFactory
+ GhnZendHttpClient
+ order_info
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\SynchronizeOrderDataBuilder
+ SynchronizeOrderTransferFactory
+ GhnZendHttpClient
+ Boolfly\GiaoHangNhanh\Model\Service\Validator\SynchronizeOrderValidator
+ Boolfly\GiaoHangNhanh\Model\Service\Response\SynchronizeOrderHandler
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Service\Request\GetDistrictsDataBuilder
+ GetDistrictsTransferFactory
+ GhnZendHttpClient
+ districts
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::GETTING_SERVICES_URL
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::CALCULATING_FEE_URL
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::CANCELING_ORDER_URL
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::GETTING_ORDER_INFOR
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::SYNCHRONIZING_ORDER_URL
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::GETTING_DISTRICTS_URL
+
+
+
+
+
+ GhnConfig
+
+
+
+
+ /var/log/bf_giaohangnhanh.log
+
+
+
+
+
+ - Boolfly\GiaoHangNhanh\Model\Logger\VirtualDebug
+
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Logger\VirtualLogger
+
+
+
+
+
+ GhnLogger
+ Boolfly\GiaoHangNhanh\Model\Service\Http\Converter\JsonToArray
+
+
+
+
+ Boolfly\GiaoHangNhanh\Model\Config::INTEGRATION_TYPE
+ Boolfly\GiaoHangNhanh\Model\Config::DEFAULT_PATH_PATTERN
+
+
+
+
+ GhnConfig
+
+
+
+
+ GhnConfig
+
+
+
+
+ GhnConfig
+
+
+
diff --git a/etc/events.xml b/etc/events.xml
new file mode 100644
index 0000000..950c17d
--- /dev/null
+++ b/etc/events.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/extension_attributes.xml b/etc/extension_attributes.xml
new file mode 100644
index 0000000..a0a0e8c
--- /dev/null
+++ b/etc/extension_attributes.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml
new file mode 100644
index 0000000..c6f3581
--- /dev/null
+++ b/etc/frontend/di.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/etc/module.xml b/etc/module.xml
new file mode 100644
index 0000000..7d05a6b
--- /dev/null
+++ b/etc/module.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/registration.php b/registration.php
new file mode 100644
index 0000000..56d94d7
--- /dev/null
+++ b/registration.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+ Boolfly_GiaoHangNhanh::order/create/form/address.phtml
+
+
+
+
+ Boolfly\GiaoHangNhanh\Block\Customer\DataProviders\AdditionalConfig
+
+
+
+
+
+ Boolfly_GiaoHangNhanh::order/create/form/address.phtml
+
+
+
+
+ Boolfly\GiaoHangNhanh\Block\Customer\DataProviders\AdditionalConfig
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml b/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml
new file mode 100644
index 0000000..f6c54ad
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Boolfly_GiaoHangNhanh::order/create/form/address.phtml
+
+
+
+
+ Boolfly\GiaoHangNhanh\Block\Customer\DataProviders\AdditionalConfig
+
+
+
+
+
+ Boolfly_GiaoHangNhanh::order/create/form/address.phtml
+
+
+
+
+ Boolfly\GiaoHangNhanh\Block\Customer\DataProviders\AdditionalConfig
+
+
+
+
+
diff --git a/view/adminhtml/templates/order/create/form/address.phtml b/view/adminhtml/templates/order/create/form/address.phtml
new file mode 100644
index 0000000..60934f0
--- /dev/null
+++ b/view/adminhtml/templates/order/create/form/address.phtml
@@ -0,0 +1,146 @@
+getData('customerAddressCollection');
+
+$addressArray = [];
+if ($block->getCustomerId()) {
+ $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray();
+}
+
+/**
+ * @var \Magento\Sales\ViewModel\Customer\AddressFormatter $customerAddressFormatter
+ */
+$customerAddressFormatter = $block->getData('customerAddressFormatter');
+
+/**
+ * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block
+ */
+
+$defaultRegion = null;
+$defaultDistrict = null;
+
+if (!empty($block->getFormValues()['region_id'])) {
+ $defaultRegion = $block->getFormValues()['region_id'];
+}
+
+if (!empty($block->getFormValues()['district'])) {
+ $defaultDistrict = $block->getFormValues()['district'];
+}
+
+if ($block->getIsShipping()):
+ $_fieldsContainerId = 'order-shipping_address_fields';
+ $_addressChoiceContainerId = 'order-shipping_address_choice';
+ ?>
+
+
+
+
+
+
+
+
diff --git a/view/adminhtml/web/js/order/create/form/field/district.js b/view/adminhtml/web/js/order/create/form/field/district.js
new file mode 100644
index 0000000..1b0e60b
--- /dev/null
+++ b/view/adminhtml/web/js/order/create/form/field/district.js
@@ -0,0 +1,83 @@
+/* global AdminOrder */
+define([
+ 'jquery',
+ 'mage/translate',
+ 'domReady!'
+], function ($, $t) {
+ 'use strict';
+
+ $.widget('boolfly.adminDistrictUpdater', {
+ options: {
+ districtList: null,
+ defaultDistrict: null,
+ districtInput: '#order-shipping_address_district',
+ districtSelector: '#order-shipping_address_district_id',
+ regionSelector: '#order-shipping_address_region_id',
+ addressBox: '#order-shipping_address_fields',
+ districtSelectorID: 'order-shipping_address_district_id'
+ },
+
+ /**
+ *
+ * @private
+ */
+ _create: function () {
+ this.prepairAdditionalField();
+ this._bind();
+ },
+
+ _bind: function () {
+ let self = this,
+ districtInput = $(self.options.districtInput);
+
+ $(self.options.regionSelector).on('change', function () {
+ self.updateDistricts($(this).val());
+ $(self.options.districtSelector).trigger("change");
+ });
+
+ $(self.options.districtSelector).on('change', function () {
+ districtInput.val($(this).val());
+ districtInput.trigger("change");
+ });
+ },
+
+ prepairAdditionalField: function () {
+ let self = this,
+ defaultRegion = self.options.defaultRegion,
+ defaultDistrict = self.options.defaultDistrict;
+
+ $(self.options.districtInput).hide();
+
+ $(self.options.addressBox + " > .field-district > .control").append(
+ ''
+ );
+
+ if (defaultRegion) {
+ self.updateDistricts(defaultRegion);
+
+ if (defaultDistrict) {
+ if ($(self.options.districtSelector + " option[value=" + defaultDistrict + "]").length > 0) {
+ $(self.options.districtSelector).val(defaultDistrict);
+ }
+ }
+ }
+ },
+
+ updateDistricts: function (regionId) {
+ let self = this;
+ let districtSelector = $(self.options.districtSelector);
+ let districtList = self.options.jsonConfig.districts[parseInt(regionId)];
+
+ districtSelector.children('option:not(:first)').remove();
+
+ $.each(districtList, function (k, v) {
+ districtSelector.append(new Option(v.districtName, v.districtID));
+ });
+ }
+ });
+
+ return $.boolfly.adminDistrictUpdater;
+});
+
diff --git a/view/frontend/layout/checkout_cart_index.xml b/view/frontend/layout/checkout_cart_index.xml
new file mode 100644
index 0000000..c52d2d6
--- /dev/null
+++ b/view/frontend/layout/checkout_cart_index.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
- Boolfly_GiaoHangNhanh/js/view/shipping-rates-validation
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml
new file mode 100644
index 0000000..eb19b8c
--- /dev/null
+++ b/view/frontend/layout/checkout_index_index.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Boolfly_GiaoHangNhanh/js/view/shipping-rates-validation-express
+
+ -
+
- Boolfly_GiaoHangNhanh/js/view/shipping-rates-validation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/layout/customer_address_form.xml b/view/frontend/layout/customer_address_form.xml
new file mode 100644
index 0000000..e43c7b0
--- /dev/null
+++ b/view/frontend/layout/customer_address_form.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Boolfly_GiaoHangNhanh::address/edit.phtml
+
+
+
+
+ Boolfly\GiaoHangNhanh\Block\Customer\DataProviders\AdditionalConfig
+
+
+
+
+
diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js
new file mode 100644
index 0000000..65da46b
--- /dev/null
+++ b/view/frontend/requirejs-config.js
@@ -0,0 +1,10 @@
+var config = {
+ map: {
+ "*": {
+ 'Magento_Checkout/js/model/shipping-save-processor/default': 'Boolfly_GiaoHangNhanh/js/model/shipping-save-processor/default',
+ 'Magento_Checkout/js/model/shipping-rate-processor/new-address': 'Boolfly_GiaoHangNhanh/js/model/shipping-rate-processor/new-address',
+ 'Magento_Checkout/js/model/cart/totals-processor/default': 'Boolfly_GiaoHangNhanh/js/model/cart/totals-processor/default',
+ districtUpdater: 'Boolfly_GiaoHangNhanh/js/district-updater'
+ }
+ }
+};
\ No newline at end of file
diff --git a/view/frontend/templates/address/edit.phtml b/view/frontend/templates/address/edit.phtml
new file mode 100644
index 0000000..7292690
--- /dev/null
+++ b/view/frontend/templates/address/edit.phtml
@@ -0,0 +1,228 @@
+
+getLayout()->createBlock('Magento\Customer\Block\Widget\Company') ?>
+getLayout()->createBlock('Magento\Customer\Block\Widget\Telephone') ?>
+getLayout()->createBlock('Magento\Customer\Block\Widget\Fax') ?>
+
+getAddress()->getCustomAttribute('district');
+?>
+?>
+
diff --git a/view/frontend/web/js/district-updater.js b/view/frontend/web/js/district-updater.js
new file mode 100644
index 0000000..ce5938c
--- /dev/null
+++ b/view/frontend/web/js/district-updater.js
@@ -0,0 +1,58 @@
+define([
+ 'jquery',
+ 'underscore',
+ 'jquery/ui',
+ 'mage/validation',
+ 'domReady!'
+], function ($, _) {
+ 'use strict';
+
+ $.widget('boolfly.districtUpdater', {
+ options: {
+ isRegionRequired: true,
+ districtList: null,
+ defaultDistrict: ''
+ },
+
+ /**
+ *
+ * @private
+ */
+ _create: function () {
+ let self = this;
+ let defaultDistrict = self.options.defaultDistrict;
+ let districtSelector = $(self.options.districtListId);
+ let regionId = self.options.defaultRegion;
+
+ self.updateDistricts(regionId);
+
+ if (defaultDistrict) {
+ districtSelector.val(defaultDistrict);
+ }
+
+ self._bind();
+ },
+
+ _bind: function () {
+ let self = this;
+
+ self.element.on('change', function () {
+ self.updateDistricts($(this).val());
+ });
+ },
+
+ updateDistricts: function (regionId) {
+ let self = this;
+ let districtSelector = $(self.options.districtListId);
+ let districtList = self.options.jsonConfig.districts[regionId];
+
+ districtSelector.children('option:not(:first)').remove();
+
+ $.each(districtList, function (k, v) {
+ districtSelector.append(new Option(v.districtName, v.districtID));
+ });
+ }
+ });
+
+ return $.boolfly.districtUpdater;
+});
diff --git a/view/frontend/web/js/model/cart/totals-processor/default.js b/view/frontend/web/js/model/cart/totals-processor/default.js
new file mode 100644
index 0000000..bef23e3
--- /dev/null
+++ b/view/frontend/web/js/model/cart/totals-processor/default.js
@@ -0,0 +1,102 @@
+define([
+ 'underscore',
+ 'Magento_Checkout/js/model/resource-url-manager',
+ 'Magento_Checkout/js/model/quote',
+ 'mage/storage',
+ 'Magento_Checkout/js/model/totals',
+ 'Magento_Checkout/js/model/error-processor',
+ 'Magento_Checkout/js/model/cart/cache',
+ 'Magento_Customer/js/customer-data'
+], function (_, resourceUrlManager, quote, storage, totalsService, errorProcessor, cartCache, customerData) {
+ 'use strict';
+
+ /**
+ * Load data from server.
+ *
+ * @param {Object} address
+ */
+ var loadFromServer = function (address) {
+ var serviceUrl,
+ payload;
+
+ // Start loader for totals block
+ totalsService.isLoading(true);
+ serviceUrl = resourceUrlManager.getUrlForTotalsEstimationForNewAddress(quote);
+ let district = jQuery('[name="district"]').val();
+ payload = {
+ addressInformation: {
+ address: _.pick(address, cartCache.requiredFields),
+ extension_attributes: {
+ district: district
+ }
+ }
+ };
+
+ if (quote.shippingMethod() && quote.shippingMethod()['method_code']) {
+ payload.addressInformation['shipping_method_code'] = quote.shippingMethod()['method_code'];
+ payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code'];
+ }
+
+ return storage.post(
+ serviceUrl, JSON.stringify(payload), false
+ ).done(function (result) {
+ var data = {
+ totals: result,
+ address: address,
+ cartVersion: customerData.get('cart')()['data_id'],
+ shippingMethodCode: null,
+ shippingCarrierCode: null
+ };
+
+ if (quote.shippingMethod() && quote.shippingMethod()['method_code']) {
+ data.shippingMethodCode = quote.shippingMethod()['method_code'];
+ data.shippingCarrierCode = quote.shippingMethod()['carrier_code'];
+ }
+
+ quote.setTotals(result);
+ cartCache.set('cart-data', data);
+ }).fail(function (response) {
+ errorProcessor.process(response);
+ }).always(function () {
+ // Stop loader for totals block
+ totalsService.isLoading(false);
+ });
+ };
+
+ return {
+ /**
+ * Array of required address fields.
+ * @property {Array.String} requiredFields
+ * @deprecated Use cart cache.
+ */
+ requiredFields: cartCache.requiredFields,
+
+ /**
+ * Get shipping rates for specified address.
+ * @param {Object} address
+ */
+ estimateTotals: function (address) {
+ var data = {
+ shippingMethodCode: null,
+ shippingCarrierCode: null
+ };
+
+ if (quote.shippingMethod() && quote.shippingMethod()['method_code']) {
+ data.shippingMethodCode = quote.shippingMethod()['method_code'];
+ data.shippingCarrierCode = quote.shippingMethod()['carrier_code'];
+ }
+
+ if (!cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) &&
+ !cartCache.isChanged('shippingMethodCode', data.shippingMethodCode) &&
+ !cartCache.isChanged('shippingCarrierCode', data.shippingCarrierCode) &&
+ !cartCache.isChanged('address', address) &&
+ cartCache.get('totals') &&
+ !cartCache.isChanged('subtotal', parseFloat(quote.totals().subtotal))
+ ) {
+ quote.setTotals(cartCache.get('totals'));
+ } else {
+ return loadFromServer(address);
+ }
+ }
+ };
+});
diff --git a/view/frontend/web/js/model/shipping-rate-processor/new-address.js b/view/frontend/web/js/model/shipping-rate-processor/new-address.js
new file mode 100644
index 0000000..9331d5b
--- /dev/null
+++ b/view/frontend/web/js/model/shipping-rate-processor/new-address.js
@@ -0,0 +1,80 @@
+define([
+ 'Magento_Checkout/js/model/resource-url-manager',
+ 'Magento_Checkout/js/model/quote',
+ 'mage/storage',
+ 'Magento_Checkout/js/model/shipping-service',
+ 'Magento_Checkout/js/model/shipping-rate-registry',
+ 'Magento_Checkout/js/model/error-processor'
+], function (resourceUrlManager, quote, storage, shippingService, rateRegistry, errorProcessor) {
+ 'use strict';
+
+ return {
+ /**
+ * Get shipping rates for specified address.
+ * @param {Object} address
+ */
+ getRates: function (address) {
+ var cache, serviceUrl, payload;
+ var district = '';
+
+ shippingService.isLoading(true);
+ cache = rateRegistry.get(address.getCacheKey());
+ serviceUrl = resourceUrlManager.getUrlForEstimationShippingMethodsForNewAddress(quote);
+
+ if (typeof address.customAttributes === "undefined") {
+ district = jQuery('[name="district"]').val();
+ } else {
+ jQuery.each(address.customAttributes, function (k, v) {
+ if (v.attribute_code == 'district') {
+ district = v.value;
+ }
+ });
+ }
+
+ payload = JSON.stringify({
+ address: {
+ 'street': address.street,
+ 'city': address.city,
+ 'region_id': address.regionId,
+ 'region': address.region,
+ 'country_id': address.countryId,
+ 'postcode': address.postcode,
+ 'email': address.email,
+ 'customer_id': address.customerId,
+ 'firstname': address.firstname,
+ 'lastname': address.lastname,
+ 'middlename': address.middlename,
+ 'prefix': address.prefix,
+ 'suffix': address.suffix,
+ 'vat_id': address.vatId,
+ 'company': address.company,
+ 'telephone': address.telephone,
+ 'fax': address.fax,
+ 'custom_attributes': address.customAttributes,
+ 'extension_attributes': {
+ 'district': district
+ },
+ 'save_in_address_book': address.saveInAddressBook
+ }
+ }
+ );
+
+ if (cache) {
+ shippingService.setShippingRates(cache);
+ shippingService.isLoading(false);
+ } else {
+ storage.post(
+ serviceUrl, payload, false
+ ).done(function (result) {
+ rateRegistry.set(address.getCacheKey(), result);
+ shippingService.setShippingRates(result);
+ }).fail(function (response) {
+ shippingService.setShippingRates([]);
+ errorProcessor.process(response);
+ }).always(function () {
+ shippingService.isLoading(false);
+ });
+ }
+ }
+ };
+});
diff --git a/view/frontend/web/js/model/shipping-rates-validation-rules.js b/view/frontend/web/js/model/shipping-rates-validation-rules.js
new file mode 100644
index 0000000..6df9e81
--- /dev/null
+++ b/view/frontend/web/js/model/shipping-rates-validation-rules.js
@@ -0,0 +1,23 @@
+define([
+ 'Magento_Customer/js/model/customer'
+], function (customer) {
+ 'use strict';
+
+ return {
+ /**
+ * @return {Object}
+ */
+ getRules: function () {
+ let rules = {};
+ if (!customer.isLoggedIn()) {
+ rules = {
+ 'district': {
+ 'required': true
+ }
+ };
+ }
+
+ return rules;
+ }
+ };
+});
diff --git a/view/frontend/web/js/model/shipping-rates-validator.js b/view/frontend/web/js/model/shipping-rates-validator.js
new file mode 100644
index 0000000..06fa5d4
--- /dev/null
+++ b/view/frontend/web/js/model/shipping-rates-validator.js
@@ -0,0 +1,23 @@
+define([
+ 'jquery',
+ 'mageUtils',
+ 'Boolfly_GiaoHangNhanh/js/model/shipping-rates-validation-rules',
+ 'mage/translate'
+], function ($, utils, validationRules, $t) {
+ 'use strict';
+
+ return {
+ validationErrors: [],
+ validate: function(address) {
+ var self = this;
+ this.validationErrors = [];
+ $.each(validationRules.getRules(), function(field, rule) {
+ if (rule.required && utils.isEmpty(address[field])) {
+ var message = $t('Field ') + field + $t(' is required.');
+ self.validationErrors.push(message);
+ }
+ });
+ return !Boolean(this.validationErrors.length);
+ }
+ };
+});
diff --git a/view/frontend/web/js/model/shipping-save-processor/default.js b/view/frontend/web/js/model/shipping-save-processor/default.js
new file mode 100644
index 0000000..23ae7eb
--- /dev/null
+++ b/view/frontend/web/js/model/shipping-save-processor/default.js
@@ -0,0 +1,73 @@
+define(
+ [
+ 'jquery',
+ 'ko',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_Checkout/js/model/resource-url-manager',
+ 'mage/storage',
+ 'Magento_Checkout/js/model/payment-service',
+ 'Magento_Checkout/js/model/payment/method-converter',
+ 'Magento_Checkout/js/model/error-processor',
+ 'Magento_Checkout/js/model/full-screen-loader',
+ 'Magento_Checkout/js/action/select-billing-address'
+ ],
+ function (
+ $,
+ ko,
+ quote,
+ resourceUrlManager,
+ storage,
+ paymentService,
+ methodConverter,
+ errorProcessor,
+ fullScreenLoader,
+ selectBillingAddressAction
+ ) {
+ 'use strict';
+
+ return {
+ saveShippingInformation: function () {
+ var payload;
+
+ if (!quote.billingAddress()) {
+ selectBillingAddressAction(quote.shippingAddress());
+ }
+
+ var district = $('[name="custom_attributes[district]"]').val();
+
+ payload = {
+ addressInformation: {
+ shipping_address: quote.shippingAddress(),
+ billing_address: quote.billingAddress(),
+ shipping_method_code: quote.shippingMethod().method_code,
+ shipping_carrier_code: quote.shippingMethod().carrier_code,
+ extension_attributes:{
+ district: district
+
+ }
+ }
+ };
+
+ fullScreenLoader.startLoader();
+
+ return storage.post(
+ resourceUrlManager.getUrlForSetShippingInformation(quote),
+ JSON.stringify(payload)
+ ).done(
+ function (response) {
+ quote.setTotals(response.totals);
+ paymentService.setPaymentMethods(methodConverter(response.payment_methods));
+ fullScreenLoader.stopLoader();
+ }
+ ).fail(
+ function (response) {
+ errorProcessor.process(response);
+ fullScreenLoader.stopLoader();
+ }
+ );
+ }
+ };
+ }
+);
+
+
diff --git a/view/frontend/web/js/view/cart/shipping/district.js b/view/frontend/web/js/view/cart/shipping/district.js
new file mode 100644
index 0000000..03032e6
--- /dev/null
+++ b/view/frontend/web/js/view/cart/shipping/district.js
@@ -0,0 +1,14 @@
+define([
+ 'jquery',
+ 'Boolfly_GiaoHangNhanh/js/view/checkout/shipping/district'
+], function ($, Component) {
+ 'use strict';
+
+ return Component.extend({
+ defaults: {
+ imports: {
+ update: '${ $.parentName }.country_id:value'
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/view/frontend/web/js/view/checkout/shipping/district.js b/view/frontend/web/js/view/checkout/shipping/district.js
new file mode 100644
index 0000000..0737fb8
--- /dev/null
+++ b/view/frontend/web/js/view/checkout/shipping/district.js
@@ -0,0 +1,24 @@
+define([
+ 'jquery',
+ 'Magento_Ui/js/form/element/select'
+], function ($, Select) {
+ 'use strict';
+
+ return Select.extend({
+ defaults: {
+ imports: {
+ update: 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.country_id:value'
+ }
+ },
+
+ update: function (value) {
+ if (value === 'VN') {
+ this.validation['required-entry'] = true;
+ this.required(true);
+ this.setVisible(true);
+ } else {
+ this.setVisible(false);
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/view/frontend/web/js/view/shipping-rates-validation-express.js b/view/frontend/web/js/view/shipping-rates-validation-express.js
new file mode 100644
index 0000000..5253f8e
--- /dev/null
+++ b/view/frontend/web/js/view/shipping-rates-validation-express.js
@@ -0,0 +1,20 @@
+define([
+ 'uiComponent',
+ 'Magento_Checkout/js/model/shipping-rates-validator',
+ 'Magento_Checkout/js/model/shipping-rates-validation-rules',
+ 'Boolfly_GiaoHangNhanh/js/model/shipping-rates-validator',
+ 'Boolfly_GiaoHangNhanh/js/model/shipping-rates-validation-rules'
+], function (
+ Component,
+ defaultShippingRatesValidator,
+ defaultShippingRatesValidationRules,
+ ghnShippingRatesValidator,
+ ghnShippingRatesValidationRules
+) {
+ 'use strict';
+
+ defaultShippingRatesValidator.registerValidator('giaohangnhanh_express', ghnShippingRatesValidator);
+ defaultShippingRatesValidationRules.registerRules('giaohangnhanh_express', ghnShippingRatesValidationRules);
+
+ return Component;
+});
diff --git a/view/frontend/web/js/view/shipping-rates-validation.js b/view/frontend/web/js/view/shipping-rates-validation.js
new file mode 100644
index 0000000..a114af9
--- /dev/null
+++ b/view/frontend/web/js/view/shipping-rates-validation.js
@@ -0,0 +1,20 @@
+define([
+ 'uiComponent',
+ 'Magento_Checkout/js/model/shipping-rates-validator',
+ 'Magento_Checkout/js/model/shipping-rates-validation-rules',
+ 'Boolfly_GiaoHangNhanh/js/model/shipping-rates-validator',
+ 'Boolfly_GiaoHangNhanh/js/model/shipping-rates-validation-rules'
+], function (
+ Component,
+ defaultShippingRatesValidator,
+ defaultShippingRatesValidationRules,
+ ghnShippingRatesValidator,
+ ghnShippingRatesValidationRules
+) {
+ 'use strict';
+
+ defaultShippingRatesValidator.registerValidator('giaohangnhanh_standard', ghnShippingRatesValidator);
+ defaultShippingRatesValidationRules.registerRules('giaohangnhanh_standard', ghnShippingRatesValidationRules);
+
+ return Component;
+});