lib/usps_ex.ex

defmodule UspsEx do
  @moduledoc """
  Documentation for `UspsEx`.
  """

  require EEx
  import SweetXml

  alias UspsEx.Error
  alias UspsEx.Transaction

  import UspsEx.Util,
    only: [
      price_to_cents: 1,
      international?: 1,
      create_hash: 1,
      create_hash: 2,
      weight_in_ounces: 1,
      country: 1,
      container: 1,
      size: 1,
      strip_html: 1,
      price_to_dollars: 1
    ]

  alias UspsEx.Client
  alias UspsEx.Config
  alias UspsEx.Error
  alias UspsEx.Rate
  alias UspsEx.Service
  alias UspsEx.Package
  alias UspsEx.Util

  for f <-
        ~w(address cancel carrier_pickup_availability city_state_by_zipcode express_mail_commitments first_class_service_standards hold_for_pickup package_pickup_cancel package_pickup_change package_pickup_inquery package_pickup_schedule package_service_standardb priority_mail_service_standards proof_of_delivery return_label return_receipt label rate scan sdc_get_locations sunday_holiday track track_confirm_by_email po_locator track_fields validate_address zipcode)a do
    EEx.function_from_file(
      :defp,
      :"build_#{f}_request",
      __DIR__ <> "/usps_ex/requests/#{f}.eex",
      [
        :assigns
      ],
      trim: true
    )
  end

  defmacro with_response(response, do: block) do
    quote do
      case unquote(response) do
        {:ok, %{body: body}} ->
          case xpath(body, ~x"//Error//text()"s) do
            "" ->
              var!(body) = body
              unquote(block)

            error ->
              code = xpath(body, ~x"//Error//Number//text()"s)
              message = xpath(body, ~x"//Error//Description//text()"s)
              {:error, %{code: code, message: message}}
          end

        {:error, error} ->
          {:error, %{code: 1, message: "The USPS API is down.", extra: error}}
      end
    end
  end

  def sdc_get_locations(data) do
    sdc_get_locations(
      data.mail_class,
      data.origin_zip,
      data.destination_zip,
      data.accept_date,
      data.accept_time,
      data.non_emdetail,
      data.non_emorigin_type,
      data.non_emdest_type
    )
  end

  def sdc_get_locations(
        mail_class,
        origin_zip,
        destination_zip,
        accept_date,
        accept_time,
        non_emdetail,
        non_emorigin_type,
        non_emdest_type
      ) do
    api = "SDCGetLocations"
    # api = "CarrierPickupSchedule"
    xml =
      build_sdc_get_locations_request(
        mail_class: mail_class,
        origin_zip: origin_zip,
        destination_zip: destination_zip,
        accept_date: accept_date,
        accept_time: accept_time,
        non_emdetail: non_emdetail,
        non_emorigin_type: non_emorigin_type,
        non_emdest_type: non_emdest_type
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupScheduleResponse//Address",
         release: ~x"./FirstName//text()"s,
         caller_id: ~x"./CallerID//text()"s,
         source_id: ~x"./SourceID//text()"s,
         mail_class: [
           ~x"./MailClass//"l,
           origin_zip: ~x"./OriginZIP/text()",
           origin_city: ~x"./OriginCity/text()",
           origin_state: ~x"./OriginState/text()",
           dest_zip: ~x"./DestZIP/text()",
           dest_city: ~x"./DestCity/text()",
           dest_state: ~x"./DestState/text()",
           AcceptDate: ~x"./AcceptDate/text()",
           accept_time: ~x"./AcceptTime/text()",
           non_expedited_origin_type: ~x"./NonExpeditedOriginType/text()",
           expedited: [
             ~x"./Expedited//"l,
             ead: ~x"./EAD/text()",
             commitment: [
               ~x"./Commitment//"l,
               mail_class: ~x"./NonExpeditedOriginType/text()",
               commitment_name: ~x"./CommitmentName/text()",
               commitment_time: ~x"./CommitmentTime/text()",
               commitment_seq: ~x"./CommitmentSeq/text()",
               location: [
                 ~x"./Location//"l,
                 sdd: ~x"./SDD/text()",
                 cot: ~x"./COT/text()",
                 fac_type: ~x"./FacType/text()",
                 street: ~x"./Street/text()",
                 city: ~x"./City/text()",
                 state: ~x"./State/text()",
                 zip: ~x"./ZIP/text()",
                 is_guaranteed: ~x"./IsGuaranteed/text()"
               ]
             ]
           ]
         ]
       )}
    end
  end

  def package_pickup_schedule(data) do
    package_pickup_schedule(
      data.first_name,
      data.last_name,
      data.firm_name,
      data.suite_or_apt,
      data.address2,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.phone,
      data.extension,
      data.packages,
      data.estimated_weight,
      data.package_location,
      data.special_instructions,
      data.urbanization
    )
  end

  def package_pickup_schedule(
        first_name,
        last_name,
        firm_name,
        suite_or_apt,
        address2,
        city,
        state,
        zip5,
        zip4,
        phone,
        extension,
        packages \\ [],
        estimated_weight \\ 0,
        package_location \\ 0,
        special_instructions \\ 0,
        urbanization \\ nil
      ) do
    api = "CarrierPickupScheduleRequest"

    xml =
      build_package_pickup_schedule_request(
        first_name: first_name,
        last_name: last_name,
        firm_name: firm_name,
        suite_or_apt: suite_or_apt,
        address2: address2,
        city: city,
        state: state,
        zip5: zip5,
        zip4: zip4,
        phone: phone,
        extension: extension,
        packages: packages,
        estimated_weight: estimated_weight,
        package_location: package_location,
        special_instructions: special_instructions,
        urbanization: urbanization
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupScheduleResponse//Address",
         first_name: ~x"./FirstName//text()"s,
         last_name: ~x"./LastName//text()"s,
         firm_name: ~x"./FirmName//text()"s,
         suite_or_apt: ~x"./SuiteOrApt//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s,
         package: [
           ~x"./Package//"l,
           service_type: ~x"./ServiceType/text()",
           count: ~x"./count/text()"
         ],
         package_location: ~x"./PackageLocation//text()"s,
         estimated_weight: ~x"./EstimatedWeight//text()"s,
         confirmation_number: ~x"./ConfirmationNumber//text()"s,
         day_of_week: ~x"./DayOfWeek//text()"s,
         date: ~x"./Date//text()"s,
         carrier_route: ~x"./CarrierRoute//text()"s
       )}
    end
  end

  def package_pickup_cancel(data) do
    package_pickup_cancel(
      data.firm_name,
      data.address2,
      data.suite_or_apt,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.confirmation_number,
      data.urbanization
    )
  end

  def package_pickup_cancel(
        firm_name,
        address2,
        suite_or_apt,
        city,
        state,
        zip5,
        zip4,
        confirmation_number,
        urbanization \\ nil
      ) do
    api = "CarrierPickupCancel"

    xml =
      build_package_pickup_cancel_request(
        firm_name: firm_name,
        address2: address2,
        city: city,
        suite_or_apt: suite_or_apt,
        state: state,
        urbanization: urbanization,
        zip5: zip5,
        zip4: zip4,
        confirmation_number: confirmation_number
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupCancelResponse",
         firm_name: ~x"./FirmName//text()"s,
         suite_or_apt: ~x"./SuiteOrApt//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s,
         confirmation_number: ~x"./ConfirmationNumber//text()"s,
         status: ~x"./Status//text()"s
       )}
    end
  end

  def package_pickup_inquery(data) do
    package_pickup_inquery(
      data.first_name,
      data.last_name,
      data.firm_name,
      data.address2,
      data.suite_or_apt,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.confirmation_number,
      data.urbanization
    )
  end

  def package_pickup_inquery(
        first_name,
        last_name,
        firm_name,
        address2,
        suite_or_apt,
        city,
        state,
        zip5,
        zip4,
        confirmation_number,
        urbanization \\ nil
      ) do
    api = "CarrierPickupInquiry"

    xml =
      build_package_pickup_inquery_request(
        first_name: first_name,
        last_name: last_name,
        firm_name: firm_name,
        address2: address2,
        city: city,
        suite_or_apt: suite_or_apt,
        state: state,
        urbanization: urbanization,
        zip5: zip5,
        zip4: zip4,
        confirmation_number: confirmation_number
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupCancelResponse",
         first_name: ~x"./FirstName//text()"s,
         last_name: ~x"./LastName//text()"s,
         firm_name: ~x"./FirmName//text()"s,
         suite_or_appt: ~x"./SuiteOrApt//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s,
         phone: ~x"./Phone//text()"s,
         package: [
           ~x"./Package//"l,
           service_type: ~x"./ServiceType/text()",
           count: ~x"./count/text()"
         ],
         package_location: ~x"./PackageLocation//text()"s,
         estimated_weight: ~x"./EstimatedWeight//text()"s,
         special_instructions: ~x"./SpecialInstructions//text()"s,
         confirmation_number: ~x"./ConfirmationNumber//text()"s,
         day_of_week: ~x"./DayOfWeek//text()"s,
         date: ~x"./Date//text()"s
       )}
    end
  end

  def hold_for_pickup(zip) do
    api = "HFPFacilityInfo"

    xml = build_hold_for_pickup_request(zip: zip)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//HFPFacilityInfoResponse",
         pickup_city: ~x"./PickupCity//text()"s,
         pickup_state: ~x"./PickupState//text()"s,
         pickup_zip: ~x"./PickupZIP//text()"s,
         pickup_zip_4: ~x"./PickupZIP4//text()"s,
         facility: [
           ~x"./Facility//"l,
           facility_id: ~x"./FacilityID/text()",
           facility_name: ~x"./FacilityName/text()"
         ]
       )}
    end
  end

  def package_pickup_change(data) do
    package_pickup_availability(
      data.firm_name,
      data.suite_or_apt,
      data.address2,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.urbanization
    )
  end

  def package_pickup_change(
        firm_name,
        suite_or_apt,
        address2,
        city,
        state,
        zip5,
        zip4,
        urbanization \\ nil
      ) do
    api = "CarrierPickupChange"

    xml =
      build_package_pickup_change_request(
        firm_name: firm_name,
        suite_or_apt: suite_or_apt,
        address2: address2,
        city: city,
        state: state,
        urbanization: urbanization,
        zip5: zip5,
        zip4: zip4
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupChangeResponse",
         first_name: ~x"./FirstName//text()"s,
         last_name: ~x"./LastName//text()"s,
         firm_name: ~x"./FirmName//text()"s,
         suite_or_appt: ~x"./SuiteOrApt//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s,
         phone: ~x"./Phone//text()"s,
         extension: ~x"./Extension//text()"s,
         package: [
           ~x"./Package//"l,
           service_type: ~x"./ServiceType/text()",
           count: ~x"./count/text()"
         ],
         package_location: ~x"./PackageLocation//text()"s,
         estimated_weight: ~x"./EstimatedWeight//text()"s,
         special_instructions: ~x"./SpecialInstructions//text()"s,
         confirmation_number: ~x"./ConfirmationNumber//text()"s,
         day_of_week: ~x"./DayOfWeek//text()"s,
         date: ~x"./Date//text()"s,
         status: ~x"./Status//text()"s
       )}
    end
  end

  def package_pickup_availability(data) do
    package_pickup_availability(
      data.firm_name,
      data.suite_or_apt,
      data.address2,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.urbanization
    )
  end

  def package_pickup_availability(
        firm_name,
        suite_or_apt,
        address2,
        city,
        state,
        zip5,
        zip4,
        urbanization \\ nil
      ) do
    api = "CarrierPickupAvailability"

    xml =
      build_carrier_pickup_availability_request(
        firm_name: firm_name,
        suite_or_apt: suite_or_apt,
        address2: address2,
        city: city,
        state: state,
        urbanization: urbanization,
        zip5: zip5,
        zip4: zip4
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CarrierPickupAvailabilityResponse//Address",
         firm_name: ~x"./FirmName//text()"s,
         suite_or_apt: ~x"./SuiteOrApt//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s,
         day_of_week: ~x"./DayOfWeek//text()"s,
         date: ~x"./Date//text()"s,
         carrier_route: ~x"./CarrierRoute//text()"s
       )}
    end
  end

  def priority_mail_service_standards(data) do
    priority_mail_service_standards(data.origin_zip, data.destination_zip, data.mail_class)
  end

  def priority_mail_service_standards(origin_zip, destination_zip, mail_class) do
    api = "PriorityMail"

    xml =
      build_priority_mail_service_standards_request(
        origin_zip: origin_zip,
        destination_zip: destination_zip,
        mail_class: mail_class
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//PriorityMailResponse",
         origin_zip: ~x"./OriginZip//text()"s,
         destination_zip: ~x"./DestinationZip//text()"s,
         days: ~x"./Days//text()"l,
         effective_acceptance_date: ~x"./EffectiveAcceptanceDate//text()"s,
         scheduled_delivery_date: ~x"./ScheduledDeliveryDate//text()"s
       )}
    end
  end

  def package_service_standardb(data) do
    package_service_standardb(data.origin_zip, data.destination_zip, data.type, data.mail_class)
  end

  def package_service_standardb(origin_zip, destination_zip, type, mail_class) do
    api = "StandardB"

    xml =
      build_package_service_standardb_request(
        origin_zip: origin_zip,
        destination_zip: destination_zip,
        type: type,
        mail_class: mail_class
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//StandardBResponse",
         origin_zip: ~x"./OriginZip//text()"s,
         destination_zip: ~x"./DestinationZip//text()"s,
         days: ~x"./Days//text()"s,
         effective_acceptance_date: ~x"./EffectiveAcceptanceDate//text()"s,
         scheduled_delivery_date: ~x"./ScheduledDeliveryDate//text()"s
       )}
    end
  end

  def first_class_service_standards(data) do
    first_class_service_standards(
      data.origin_zip,
      data.destination_zip,
      data.type,
      data.mail_class
    )
  end

  def first_class_service_standards(origin_zip, destination_zip, type, mail_class) do
    api = "FirstClassMail"

    xml =
      build_first_class_service_standards_request(
        origin_zip: origin_zip,
        destination_zip: destination_zip,
        type: type,
        mail_class: mail_class
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//PriorityMailResponse",
         origin_zip: ~x"./OriginZip//text()"s,
         destination_zip: ~x"./DestinationZip//text()"s,
         mail_class: ~x"./MailClass//text()"s
       )}
    end
  end

  def express_mail_commitments(data) do
    express_mail_commitments(
      data.origin_zip,
      data.destination_zip,
      data.date,
      data.drop_off_time,
      data.return_dates,
      data.pm_guarantee
    )
  end

  def express_mail_commitments(
        origin_zip,
        destination_zip,
        date,
        drop_off_time,
        return_dates \\ "true",
        pm_guarantee \\ "N"
      ) do
    api = "ExpressMailCommitment"

    xml =
      build_express_mail_commitments_request(
        origin_zip: origin_zip,
        destination_zip: destination_zip,
        date: date,
        drop_off_time: drop_off_time,
        return_dates: return_dates,
        pm_guarantee: pm_guarantee
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//ExpressMailCommitmentResponse",
         message: ~x"./message//text()"s
       )}
    end
  end

  def fetch_rates(shipment) do
    rates =
      UspsEx.Service.services(shipment)
      |> Enum.map(fn x -> x.id end)
      |> Enum.map(fn x ->
        fetch_rate(shipment, x)
      end)

    {:ok, rates}
  end

  def machineable?(package, "Rectangular", true) do
    reply =
      package.length < 27 && package.height < 17 && package.height < 17 && package.weight < 28

    if reply == true do
      "TRUE"
    else
      "FALSE"
    end
  end

  def machineable?(package, "Nonrectangular", true) do
    reply =
      package.length < 27 && package.height < 17 && package.height < 17 && package.weight < 28

    if reply == true do
      "TRUE"
    else
      "FALSE"
    end
  end

  def machineable?(package, "Variable", true) do
    reply =
      package.length < 27 && package.height < 17 && package.height < 17 && package.weight < 28

    if reply == true do
      "TRUE"
    else
      "FALSE"
    end
  end

  def service_code(id), do: UspsEx.Service.service_code(id)
  def insurance_code(id), do: UspsEx.Insurance.code(id)

  def fetch_rate(shipment, service) do
    service =
      case service do
        %Service{} = service -> service
        s when is_atom(s) -> Service.get(s)
      end

    api =
      if international?(shipment) do
        "IntlRateV2"
      else
        "RateV4"
      end

    xml = build_rate_request(shipment: shipment, service: service)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      spec =
        extra_services_spec(shipment) ++
          if international?(shipment) do
            [
              name: ~x"./SvcDescription//text()"s,
              service: ~x"./SvcDescription//text()"s,
              rate: ~x"./Postage//text()"s
            ]
          else
            [
              name: ~x"./MailService//text()"s,
              service: ~x"./MailService//text()"s,
              rate: ~x"./Rate//text()"s
            ]
          end

      rates =
        if international?(shipment) do
          xpath(
            body,
            ~x"//IntlRateV2Response//Package//Service"l,
            spec
          )
        else
          xpath(
            body,
            ~x"//RateV4Response//Package//Postage"l,
            spec
          )
        end
        |> add_line_items(shipment, service)
        |> Enum.map(fn response ->
          total = response.line_items |> Enum.map(& &1.price) |> Enum.sum()

          %{
            name: strip_html(response.name),
            service: description_to_service(response.service),
            rate: total,
            line_items: response.line_items
          }
        end)
        |> Enum.map(fn %{name: description, service: service} = response ->
          service = %{service | description: description}

          rate = %UspsEx.Rate{
            service: service,
            price: response.rate,
            line_items: response.line_items
          }

          {:ok, rate}
        end)

      rates =
        if international?(shipment) do
          rates
          |> Enum.sort(fn {:ok, rate1}, {:ok, rate2} ->
            service = String.downcase(service.description)

            d1 = String.jaro_distance(String.downcase(rate1.service.description), service)
            d2 = String.jaro_distance(String.downcase(rate2.service.description), service)

            d1 > d2
          end)
        else
          rates
        end

      case rates do
        [] -> {:error, "Rate unavailable for service."}
        [rate] -> rate
        list when is_list(list) -> hd(list)
      end
    end
  end

  def sunday_holiday(data) do
    sunday_holiday(data.sunday_holiday, data.from_zip_code, data.to_zip_code)
  end

  def sunday_holiday(sunday_holiday, from_zip_code, to_zip_code) do
    api = "SundayHolidayAvailability"

    xml =
      build_sunday_holiday_request(
        sunday_holiday: sunday_holiday,
        from_zip_code: from_zip_code,
        to_zip_code: to_zip_code
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//SundayHolidayAvailabilityResponse",
         delivery_availability: ~x"./DeliveryAvailability//text()"s
       )}
    end
  end

  def scan(data) do
    scan(
      data.form,
      data.name,
      data.firm,
      data.address1,
      data.address2,
      data.city,
      data.state,
      data.zip5,
      data.zip4,
      data.pkg_bar_code,
      data.mail_date,
      data.mail_time,
      data.image_type,
      data.customer_ref_no
    )
  end

  def scan(
        form,
        name,
        firm,
        address1,
        address2,
        city,
        state,
        zip5,
        zip4,
        pkg_bar_code,
        mail_date,
        mail_time,
        image_type,
        customer_ref_no
      ) do
    api = "SCAN"

    xml =
      build_scan_request(
        form: form,
        name: name,
        firm: firm,
        address1: address1,
        address2: address2,
        city: city,
        state: state,
        zip5: zip5,
        zip4: zip4,
        pkg_bar_code: pkg_bar_code,
        mail_date: mail_date,
        mail_time: mail_time,
        image_type: image_type,
        customer_ref_no: customer_ref_no
      )

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//SCANResponse",
         scanform_number: [
           ~x"./SCANFormNumber//",
           scanform_image: ~x"./SCANFormImage//text()"s,
           entry_zip_code: ~x".//@EntryZipCode",
           ship_date: ~x".//@ShipDate",
           ship_date: ~x".//@ShipDate"
         ]
       )}
    end
  end

  def zipcode(address) do
    api = "ZipCodeLookup"
    xml = build_zipcode_request(address: address)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//ZipCodeLookupResponse//Address"l,
         firm_name: ~x"./FirmName//text()"s,
         address2: ~x"./Address2//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s,
         urbanization: ~x"./Urbanization//text()"s,
         zip5: ~x"./Zip5//text()"s,
         zip4: ~x"./Zip4//text()"s
       )}
    end
  end

  def fetch_city_state_by_zipcode(zipcode) do
    api = "CityStateLookup"
    xml = build_city_state_by_zipcode_request(zipcode: zipcode)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}, %{
                    "Content-Type" => "application/xml"
                  }) do
      {:ok,
       xpath(
         body,
         ~x"//CityStateLookupRequest//ZipCode",
         zip: ~x"./Zip5//text()"s,
         city: ~x"./City//text()"s,
         state: ~x"./State//text()"s
       )}
    end
  end

  defp description_to_service(description) do
    cond do
      description =~ ~r/priority mail express/i ->
        :usps_priority_express

      description =~ ~r/priority/i ->
        :usps_priority

      description =~ ~r/first[-\s]*class/i ->
        :usps_first_class

      description =~ ~r/retail ground/i ->
        :usps_retail_ground

      description =~ ~r/media mail/i ->
        :usps_media

      description =~ ~r/library mail/i ->
        :usps_library

      description =~ ~r/gxg/i ->
        :usps_gxg

      true ->
        :usps_retail_ground
    end
    |> UspsEx.Service.get()
  end

  def international_mail_type(%Package{container: nil}), do: "PACKAGE"

  def international_mail_type(%Package{container: container}) do
    container = "#{container}"

    cond do
      container =~ ~r/envelope/i -> "ENVELOPE"
      container =~ ~r/flat[-\s]*rate/i -> "FLATRATE"
      container =~ ~r/rectangular|variable/i -> "PACKAGE"
      true -> "ALL"
    end
  end

  def validate_address(address) do
    api = "Verify"
    xml = build_validate_address_request(address: address)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: xml}) do
      candidates =
        body
        |> xpath(
          ~x"//AddressValidateResponse//Address",
          address1: ~x"./Address1//text()"s,
          address2: ~x"./Address2//text()"s,
          city: ~x"./City//text()"s,
          city_abbreviation: ~x"./CityAbbreviation//text()"s,
          state: ~x"./State//text()"s,
          zip5: ~x"./Zip5//text()"s,
          zip4: ~x"./Zip4//text()"s,
          delivery_point: ~x"./DeliveryPoint//text()"s,
          carrier_route: ~x"./CarrierRoute//text()"s,
          footnotes: ~x"./Footnotes//text()"s,
          dpvconfirmation: ~x"./DPVConfirmation//text()"s,
          dpvcmra: ~x"./DPVCMRA//text()"s,
          dpvfootnotes: ~x"./DPVFootnotes//text()"s,
          business: ~x"./Business//text()"s,
          vacant: ~x"./Vacant//text()"s
        )
        |> Enum.map(fn candidate ->
          candidate
          |> Map.merge(Map.take(address, ~w(first_name last_name name company_name phone)a))
          |> UspsEx.Address.new!()
        end)

      {:ok, candidates}
    end
  end

  def create_transaction(shipment, service) when is_atom(service) do
    create_transaction(shipment, Service.get(service))
  end

  def create_transaction(shipment, service) do
    api =
      cond do
        not international?(shipment) ->
          "eVS"

        service.id == :usps_priority_express ->
          "eVSExpressMailIntl"

        service.id == :usps_priority ->
          "eVSPriorityMailIntl"

        service.id == :usps_first_class ->
          "eVSFirstClassMailIntl"

        true ->
          raise Error, message: "Only the Priority and Priority Express services are supported for
          international shipments at the moment. (Received :#{service.id}.)"
      end

    request = build_label_request(shipment: shipment, service: service, api: api)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      spec =
        if international?(shipment) do
          [insurance_fee: ~x"//InsuranceFee//text()"s]
        else
          extra_services_spec(shipment, "Extra")
        end ++
          [
            rate: ~x"//Postage//text()"s,
            tracking_number: ~x"//BarcodeNumber//text()"s,
            image: ~x"//LabelImage//text()"s
          ]

      response = xpath(body, ~x"//#{api}Response", spec) |> add_line_items(shipment, service)

      line_items = response.line_items
      price = line_items |> Enum.map(& &1.price) |> Enum.sum()

      rate = %Rate{service: service, price: price, line_items: line_items}
      image = String.replace(response.image, "\n", "")

      label = %Transaction.Label{
        tracking_number: response.tracking_number,
        format: :pdf,
        image: image
      }

      transaction = Transaction.new(shipment, rate, label)

      {:ok, transaction}
    end
  end

  def cancel_transaction(transaction) do
    cancel_transaction(transaction.shipment, transaction.label.tracking_number)
  end

  def cancel_transaction(shipment, tracking_number) do
    root =
      if international?(shipment) do
        "eVSI"
      else
        "eVS"
      end

    api = root <> "Cancel"

    request = build_cancel_request(root: root, tracking_number: tracking_number)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      data =
        xpath(
          body,
          ~x"//#{root}CancelResponse",
          status: ~x"//Status//text()"s,
          reason: ~x"//Reason//text()"s
        )

      status =
        if data.status =~ ~r/not cancel/i do
          :error
        else
          :ok
        end

      {status, data.reason}
    end
  end

  defp extra_services_spec(shipment, prefix \\ nil) do
    prefix =
      case prefix do
        nil ->
          if international?(shipment), do: "Extra", else: "Special"

        prefix ->
          prefix
      end

    [
      extra_services: [
        ~x"//#{prefix}Services//#{prefix}Service"l,
        id: ~x"./ServiceID//text()"s,
        name: ~x"./ServiceName//text()"s,
        available: ~x"./Available//text()"s |> transform_by(&String.downcase/1),
        price: ~x"./Price//text()"s
      ]
    ]
  end

  defp add_line_items(rates, shipment, service) when is_list(rates) do
    Enum.map(rates, fn rate -> add_line_items(rate, shipment, service) end)
  end

  defp add_line_items(rate, shipment, _service) do
    postage_line_item = %{name: "Postage", price: rate.rate}

    insurance_line_item =
      Enum.map(shipment.packages, fn package ->
        cond do
          is_nil(package.insurance) ->
            nil

          not is_nil(rate[:insurance_fee]) ->
            %{name: "Insurance", price: rate.insurance_fee}

          true ->
            insurance_code = Integer.to_string(package.insurance.code)

            rate.extra_services
            |> Enum.find(fn
              %{available: available, id: ^insurance_code} when available != "false" -> true
              _ -> false
            end)
            |> case do
              %{price: price} ->
                %{name: "Insurance", price: price}

              _ ->
                nil
            end
        end
      end)

    line_items =
      ([postage_line_item] ++ insurance_line_item)
      |> Enum.reject(&is_nil/1)
      |> Enum.map(fn %{price: price} = line_item ->
        %{line_item | price: price_to_cents(price)}
      end)

    Map.put(rate, :line_items, line_items)
  end

  def track_packages(tracking_number) when is_binary(tracking_number) do
    track_packages([tracking_number])
  end

  def track_packages(tracking_numbers) when is_list(tracking_numbers) do
    request = build_track_request(tracking_numbers: tracking_numbers)

    with_response Client.post("ShippingAPI.dll", %{API: "TrackV2", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//TrackResponse//TrackInfo"l,
         summary: ~x"./TrackSummary//text()"s,
         details: ~x"./TrackDetail//text()"l
       )}
    end
  end

  def return_label(data) do
    return_label(
      data.image_type,
      data.separate_receipt_page,
      data.customer_first_name,
      data.customer_last_name,
      data.customer_firm,
      data.customer_address2,
      data.customer_city,
      data.customer_state,
      data.customer_zip5,
      data.customer_zip4,
      data.po_zip_code,
      data.allow_non_cleansed_origin_addr,
      data.retailer_attn,
      data.retailer_firm,
      data.weight_in_ounces,
      data.service_type,
      data.width,
      data.length,
      data.height,
      data.girth,
      data.machinable,
      data.customer_ref_no,
      data.print_customer_ref_no,
      data.customer_ref_no2,
      data.print_customer_ref_no2,
      data.sender_name,
      data.sender_email,
      data.recipient_name,
      data.recipient_email,
      data.tracking_email_pdf,
      data.extra_services
    )
  end

  def return_label(
        image_type,
        separate_receipt_page,
        customer_first_name,
        customer_last_name,
        customer_firm,
        customer_address2,
        customer_city,
        customer_state,
        customer_zip5,
        customer_zip4,
        po_zip_code,
        allow_non_cleansed_origin_addr,
        retailer_attn,
        retailer_firm,
        weight_in_ounces,
        service_type,
        width,
        length,
        height,
        girth,
        machinable,
        customer_ref_no,
        print_customer_ref_no,
        customer_ref_no2,
        print_customer_ref_no2,
        sender_name,
        sender_email,
        recipient_name,
        recipient_email,
        tracking_email_pdf,
        extra_services
      ) do
    request =
      build_return_label_request(
        image_type: image_type,
        separate_receipt_page: separate_receipt_page,
        customer_first_name: customer_first_name,
        customer_last_name: customer_last_name,
        customer_firm: customer_firm,
        customer_address2: customer_address2,
        customer_city: customer_city,
        customer_state: customer_state,
        customer_zip5: customer_zip5,
        customer_zip4: customer_zip4,
        po_zip_code: po_zip_code,
        allow_non_cleansed_origin_addr: allow_non_cleansed_origin_addr,
        retailer_attn: retailer_attn,
        retailer_firm: retailer_firm,
        weight_in_ounces: weight_in_ounces,
        service_type: service_type,
        width: width,
        length: length,
        height: height,
        girth: girth,
        machinable: machinable,
        customer_ref_no: customer_ref_no,
        print_customer_ref_no: print_customer_ref_no,
        customer_ref_no2: customer_ref_no2,
        print_customer_ref_no2: print_customer_ref_no2,
        sender_name: sender_name,
        sender_email: sender_email,
        recipient_name: recipient_name,
        recipient_email: recipient_email,
        tracking_email_pdf: tracking_email_pdf,
        extra_services: extra_services
      )

    with_response Client.post("ShippingAPI.dll", %{API: "USPSReturnsLabel", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//USPSReturnsLabelResponse",
         barcode_number: ~x"./BarcodeNumber//text()"s,
         label_image: ~x"./LabelImage//text()"s,
         retailer_firm: ~x"./RetailerFirm//text()"s,
         retailer_address1: ~x"./RetailerAddress1//text()"s,
         retailer_address2: ~x"./RetailerAddress2//text()"s,
         retailer_city: ~x"./RetailerCity//text()"s,
         retailer_state: ~x"./RetailerState//text()"s,
         retailer_zip5: ~x"./RetailerZip5//text()"s,
         retailer_zip4: ~x"./RetailerZip4//text()"s,
         rdc: ~x"./RDC//text()"s,
         postage: ~x"./Postage//text()"s,
         extra_services: [
           ~x"./ExtraServices//ExtraService"l,
           service_id: ~x"./ServiceID/text()",
           service_name: ~x"./ServiceName/text()",
           price: ~x"./Price/text()"
         ],
         zone: ~x"./Zone//text()"s,
         carrier_route: ~x"./CarrierRoute//text()"s,
         fees: [
           ~x"./Fees//Fee"l,
           fee_type: ~x"./FeeType/text()",
           fee_price: ~x"./FeePrice/text()"
         ],
         attributes: [
           ~x"./Attributes"l,
           attribute: ~x"./Attribute/text()",
           key: ~x"./Attribute/@Key"
         ]
       )}
    end
  end

  def track_and_confirm_by_email(data) do
    track_and_confirm_by_email(
      data.tracking_number,
      data.company_name,
      data.mp_suffix,
      data.mp_date,
      data.request_type,
      data.first_name,
      data.last_name,
      data.email,
      data.email2,
      data.email3
    )
  end

  def track_and_confirm_by_email(
        tracking_number,
        company_name,
        mp_suffix,
        mp_date,
        request_type,
        first_name,
        last_name,
        email,
        email2 \\ nil,
        email3 \\ nil
      ) do
    request =
      build_track_confirm_by_email_request(
        tracking_number: tracking_number,
        company_name: company_name,
        mp_suffix: mp_suffix,
        mp_date: mp_date,
        request_type: request_type,
        first_name: first_name,
        last_name: last_name,
        email: email,
        email: email2,
        email: email3
      )

    with_response Client.post("ShippingAPI.dll", %{API: "TrackV2", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//PTSEMAILRESULT",
         result_text: ~x"./ResultText/text()",
         return_code: ~x"./ReturnCode/text()"
       )}
    end
  end

  def package_tracking_fields(tracking_numbers) when is_list(tracking_numbers) do
    request = build_track_fields_request(tracking_numbers: tracking_numbers)

    with_response Client.post("ShippingAPI.dll", %{API: "TrackV2", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//TrackResponse//TrackInfo"l,
         guaranteed_delivery_date: ~x"./GuaranteedDeliveryDate//text()"s,
         track_summary: [
           ~x"./TrackSummary",
           event_time: ~x"./EventTime/text()",
           event_date: ~x"./EventDate/text()",
           attribute: ~x"./Event/text()",
           event_city: ~x"./EventCity/text()",
           event_state: ~x"./EventState/text()",
           event_zipcode: ~x"./EventZIPCode/text()",
           event_country: ~x"./EventCountry/text()",
           firm_name: ~x"./FirmName/text()",
           name: ~x"./Name/text()",
           authorized_agent: ~x"./AuthorizedAgent/text()",
           delivery_attribute_code: ~x"./DeliveryAttributeCode/text()",
           gmt: ~x"./GMT/text()",
           gmtoffset: ~x"./GMTOffset/text()"
         ],
         track_detail: [
           ~x"./TrackDetail",
           event_time: ~x"./EventTime/text()",
           event_date: ~x"./EventDate/text()",
           attribute: ~x"./Event/text()",
           event_city: ~x"./EventCity/text()",
           event_state: ~x"./EventState/text()",
           event_zipcode: ~x"./EventZIPCode/text()",
           event_country: ~x"./EventCountry/text()",
           firm_name: ~x"./FirmName/text()",
           name: ~x"./Name/text()",
           authorized_agent: ~x"./AuthorizedAgent/text()",
           delivery_attribute_code: ~x"./DeliveryAttributeCode/text()",
           gmt: ~x"./GMT/text()",
           gmtoffset: ~x"./GMTOffset/text()"
         ]
       )}
    end
  end

  def proof_of_delivery(data) do
    proof_of_delivery(
      data.tracking_number,
      data.data.client_ip,
      data.company_name,
      data.mp_suffix,
      data.mp_date,
      data.request_type,
      data.email
    )
  end

  def proof_of_delivery(
        tracking_number,
        client_ip,
        company_name,
        mp_suffix,
        mp_date,
        request_type,
        email
      ) do
    request =
      build_proof_of_delivery_request(
        tracking_number: tracking_number,
        client_ip: client_ip,
        company_name: company_name,
        mp_suffix: mp_suffix,
        mp_date: mp_date,
        request_type: request_type,
        email: email
      )

    with_response Client.post("ShippingAPI.dll", %{API: "PTSPod", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//PTSPODRESULT",
         result_text: ~x"./ResultText/text()",
         return_code: ~x"./ReturnCode/text()"
       )}
    end
  end

  def return_receipt(data) do
    return_receipt(
      data.tracking_number,
      data.track_id,
      data.client_ip,
      data.company_name,
      data.mp_suffix,
      data.mp_date,
      data.request_type,
      data.email,
      data.table_code,
      data.cust_reg_id
    )
  end

  def return_receipt(
        tracking_number,
        track_id,
        client_ip,
        company_name,
        mp_suffix,
        mp_date,
        request_type,
        email,
        table_code,
        cust_reg_id
      ) do
    request =
      build_return_receipt_request(
        tracking_number: tracking_number,
        track_id: track_id,
        client_ip: client_ip,
        company_name: company_name,
        mp_suffix: mp_suffix,
        mp_date: mp_date,
        request_type: request_type,
        email: email,
        table_code: table_code,
        cust_reg_id: cust_reg_id
      )

    with_response Client.post("ShippingAPI.dll", %{API: "PTSRre", XML: request}) do
      {:ok,
       xpath(
         body,
         ~x"//PTSRRERESULT",
         result_text: ~x"./ResultText/text()",
         return_code: ~x"./ReturnCode/text()"
       )}
    end
  end

  def module_id() do
    :usps
  end

  def config() do
    with un when is_binary(un) <-
           Application.get_env(:usps_ex, :username, {:error, :not_found, :username}),
         pw when is_binary(pw) <-
           Application.get_env(:usps_ex, :password, {:error, :not_found, :password}) do
      %{
        username: un,
        password: pw
      }
    else
      {:error, :not_found, token} ->
        raise Error, message: "USPS config key missing: #{token}"

      {:error, :not_found} ->
        raise Error, message: "USPS config is either invalid or not found."
    end
  end

  defdelegate env(), to: Config

  @version Mix.Project.config()[:version]
  def version, do: @version
end