getId() || !$track->getTrackNumber() || !$track->getParentId() || !$track->getOrderId()) { return; } $connection = $this->connection(); $existing = $connection->fetchRow($connection->select()->from($this->table()) ->where('shipment_track_id = ?', (int)$track->getId())->limit(1)); $number = trim((string)$track->getTrackNumber()); $data = [ 'shipment_track_id' => (int)$track->getId(), 'shipment_id' => (int)$track->getParentId(), 'order_id' => (int)$track->getOrderId(), 'tracking_number' => $number, ]; if ($existing && (string)$existing['tracking_number'] !== $number) { $data += [ 'carrier_code' => null, 'sync_status' => 'pending', 'error_code' => null, 'attempt_count' => 0, 'next_attempt_at' => null, 'package_status' => null, 'latest_event' => null, 'delivered_at' => null, 'last_error' => null, 'response_data' => null, 'last_synced_at' => null, ]; } $connection->insertOnDuplicate($this->table(), $data, array_keys($data)); } public function backfill(): void { $connection = $this->connection(); $track = $this->resource->getTableName('sales_shipment_track'); $select = $connection->select()->from(['track' => $track], [ 'shipment_track_id' => 'entity_id', 'shipment_id' => 'parent_id', 'order_id' => 'order_id', 'tracking_number' => 'track_number', ])->where('track.track_number IS NOT NULL')->where('track.track_number != ?', ''); $connection->query($connection->insertFromSelect( $select, $this->table(), ['shipment_track_id', 'shipment_id', 'order_id', 'tracking_number'], \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_IGNORE )); } public function getPending(int $limit): array { return $this->connection()->fetchAll($this->connection()->select()->from($this->table()) ->where('sync_status IN (?)', ['pending', 'error']) ->where('next_attempt_at IS NULL OR next_attempt_at <= UTC_TIMESTAMP()') ->order('updated_at ASC')->limit($limit)); } public function getTracking(int $limit): array { return $this->connection()->fetchAll($this->connection()->select()->from($this->table()) ->where('sync_status = ?', 'tracking')->where('package_status IS NULL OR package_status != ?', 'Delivered') ->where('next_attempt_at IS NULL OR next_attempt_at <= UTC_TIMESTAMP()') ->order('last_synced_at ASC')->limit($limit)); } public function markRegistered(string $number, ?int $carrier): void { $this->connection()->update($this->table(), [ 'sync_status' => 'tracking', 'carrier_code' => $carrier, 'error_code' => null, 'attempt_count' => 0, 'next_attempt_at' => null, 'last_error' => null, ], ['tracking_number = ?' => $number]); } public function markError(string $number, string $message, ?int $code, string $nextAttemptAt): void { $this->connection()->update($this->table(), [ 'sync_status' => 'error', 'error_code' => $code, 'attempt_count' => new \Zend_Db_Expr('attempt_count + 1'), 'next_attempt_at' => $nextAttemptAt, 'last_error' => mb_substr($message, 0, 2000), ], ['tracking_number = ?' => $number]); } public function markQueryError(string $number, string $message, ?int $code, string $nextAttemptAt): void { $this->connection()->update($this->table(), [ 'sync_status' => 'tracking', 'error_code' => $code, 'attempt_count' => new \Zend_Db_Expr('attempt_count + 1'), 'next_attempt_at' => $nextAttemptAt, 'last_error' => mb_substr($message, 0, 2000), ], ['tracking_number = ?' => $number]); } public function markPending(string $number, string $message, ?int $code, string $nextAttemptAt): void { $this->connection()->update($this->table(), [ 'sync_status' => 'pending', 'carrier_code' => null, 'error_code' => $code, 'attempt_count' => new \Zend_Db_Expr('attempt_count + 1'), 'next_attempt_at' => $nextAttemptAt, 'last_error' => mb_substr($message, 0, 2000), ], ['tracking_number = ?' => $number]); } public function markRejected(string $number, string $message, ?int $code): void { $this->connection()->update($this->table(), [ 'sync_status' => 'rejected', 'error_code' => $code, 'attempt_count' => new \Zend_Db_Expr('attempt_count + 1'), 'next_attempt_at' => null, 'last_error' => mb_substr($message, 0, 2000), ], ['tracking_number = ?' => $number]); } public function saveTrackingInfo(string $number, array $info, string $serialized): array { $trackInfo = (array)($info['track_info'] ?? []); $latestStatus = (array)($trackInfo['latest_status'] ?? []); $latestEvent = (array)($trackInfo['latest_event'] ?? []); $status = (string)($latestStatus['status'] ?? $info['package_status'] ?? ''); $event = (string)($latestEvent['description'] ?? $latestEvent['event_description'] ?? $info['latest_event_info'] ?? ''); $deliveredAt = null; if ($status === 'Delivered') { $deliveredAt = $trackInfo['time_metrics']['delivery_time'] ?? $trackInfo['time_metrics']['delievery_time'] ?? $latestEvent['time_utc'] ?? gmdate('Y-m-d H:i:s'); } $rows = $this->connection()->fetchAll($this->connection()->select()->from($this->table(), ['entity_id', 'order_id']) ->where('tracking_number = ?', $number)); $this->connection()->update($this->table(), [ 'carrier_code' => isset($info['carrier']) ? (int)$info['carrier'] : null, 'sync_status' => 'tracking', 'error_code' => null, 'attempt_count' => 0, 'next_attempt_at' => null, 'package_status' => $status !== '' ? $status : null, 'latest_event' => $event !== '' ? mb_substr($event, 0, 1024) : null, 'delivered_at' => $deliveredAt ? $this->toMysqlDate((string)$deliveredAt) : null, 'response_data' => $serialized, 'last_error' => null, 'last_synced_at' => gmdate('Y-m-d H:i:s'), ], ['tracking_number = ?' => $number]); return array_values(array_unique(array_map('intval', array_column($rows, 'order_id')))); } public function saveStoppedInfo(string $number, string $serialized): void { $this->connection()->update($this->table(), [ 'sync_status' => 'stopped', 'error_code' => null, 'next_attempt_at' => null, 'response_data' => $serialized, 'last_error' => null, 'last_synced_at' => gmdate('Y-m-d H:i:s'), ], ['tracking_number = ?' => $number]); } public function isOrderDelivered(int $orderId): bool { $select = $this->connection()->select()->from($this->table(), [ 'total' => new \Zend_Db_Expr('COUNT(*)'), 'delivered' => new \Zend_Db_Expr("SUM(package_status = 'Delivered')"), ])->where('order_id = ?', $orderId); $row = $this->connection()->fetchRow($select) ?: []; return (int)($row['total'] ?? 0) > 0 && (int)($row['total'] ?? 0) === (int)($row['delivered'] ?? 0); } private function toMysqlDate(string $value): string { try { return (new \DateTimeImmutable($value))->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); } catch (\Throwable) { return gmdate('Y-m-d H:i:s'); } } private function connection(): \Magento\Framework\DB\Adapter\AdapterInterface { return $this->resource->getConnection('sales'); } private function table(): string { return $this->resource->getTableName(self::TABLE); } }