# frozen_string_literal: true require 'net/http' require 'base64' require 'ox' require_relative 'http_client' require_relative 'encryption_service' module Tcat # Query class handles making requests to the Tcat system class Query DeliveryItem = Struct.new(:status, :status_code, :time, :office, :last_update, keyword_init: true) def initialize(tracking_number) @secret_string = Tcat.configuration.secret_string @secret_key = Tcat.configuration.secret_key validate_secrets! @tracking_number = tracking_number @http_client = HttpClient.new @encryption_service = EncryptionService.new(@secret_key) end # Get current delivery status code # @return [Symbol] Status code (:done, :delivering, :collected, :in_transit, :unknown) def status_code response_body = @http_client.post(data) parse_status_code(response_body) if response_body rescue HttpClient::RequestError => e # Log error or handle it appropriately nil end # Get complete delivery history # @return [Array] Array of delivery status items, sorted by time (newest first) def history response_body = @http_client.post(data) if response_body result = Ox.load(response_body, mode: :hash, with_cdata: true) return [] if result.dig(:Result, :Status) != '0' extract_delivery_history(result) end rescue StandardError => e Rails.logger.error("Error getting delivery history: #{e.message}") [] end # Get latest delivery status with details # @return [DeliveryItem, nil] Latest delivery status or nil if no history def latest_status items = history return nil if items.empty? items.first end private def validate_secrets! raise ArgumentError, 'SECRET_STRING must be configured' if @secret_string.nil? || @secret_string.empty? raise ArgumentError, 'SECRET_KEY must be configured' if @secret_key.nil? || @secret_key.empty? end def data { ConsignmentNo: @tracking_number, f: 5, isForeign: 'N', secret: generate_secret, SVC: 'TCATAPP' } end def date_string Time.now.utc.strftime('%Y%m%d%H%M%S') end def random rand(10_000..99_999).to_s end def source_string random + @secret_string + date_string end def generate_secret @encryption_service.encrypt(source_string) rescue EncryptionService::EncryptionError => e # Log error or handle it appropriately raise "Failed to generate secret: #{e.message}" end def parse_status_code(xml) begin result = Ox.load(xml, mode: :hash, with_cdata: true) return :error unless result.dig(:Result, :Status) == '0' delivery_items = result.dig(:Result, :Content, :Item) delivery_statuses = extract_delivery_statuses(delivery_items) parse_status_message(delivery_statuses.uniq) rescue StandardError => e Rails.logger.error("Error parsing T-Cat response: #{e.message}") :error end end def extract_delivery_history(result) items = result.dig(:Result, :Content, :Item) case items when Hash [parse_delivery_item(items)] when Array items.map { |item| parse_delivery_item(item) } else [] end.compact.sort_by(&:time).reverse rescue StandardError => e Rails.logger.error("Error parsing delivery history: #{e.message}") [] end def parse_delivery_item(item) return nil unless item[:Status] && item[:Time] DeliveryItem.new( status: item[:Status].to_s.force_encoding('UTF-8'), status_code: parse_status_message([item[:Status].to_s.force_encoding('UTF-8')]), time: parse_time(item[:Time]), office: item[:Office].to_s.force_encoding('UTF-8'), last_update: parse_last_update(item[:LastUPDateTime]) ) end def parse_time(time_str) return nil unless time_str Time.strptime(time_str.to_s, '%Y/%m/%d %H:%M') rescue nil end def parse_last_update(datetime_str) return nil unless datetime_str Time.strptime(datetime_str.to_s, '%Y%m%d%H%M%S') rescue nil end def extract_delivery_statuses(items) case items when Hash [items[:Status].force_encoding('UTF-8')] when Array items.map { |item| item[:Status].force_encoding('UTF-8') } else [] end end STATUS_MAPPINGS = { '順利送達' => :done, '配送中' => :delivering, '已集貨' => :collected, '轉運中' => :in_transit }.freeze def parse_status_message(statuses) STATUS_MAPPINGS.each do |status_text, status_symbol| return status_symbol if statuses.include?(status_text) end :unknown end end end