# frozen_string_literal: true require 'json' require 'retryable' require 'go_puff/tax_service/errors' require 'go_puff/tax_service/response/get' require 'go_puff/tax_service/response/commit' require 'go_puff/tax_service/response/cancel' module GoPuff module TaxService class Tax TIMEOUT = 5 FEES_KINDS = %w[all non_alcohol alcohol none].freeze ALCOHOL_FEE_ID = 'ALCOHOL_FEE' attr_reader :order def initialize(order, address: nil) @order = order @address = address @fees_kind = 'all' end def call raise Errors::FeesKindInvalid, @fees_kind unless FEES_KINDS.include?(@fees_kind) request_tax_service return create_response_object(@response.body) if @response.success? order.tax_service_validation_failed = true response_for_error end private def configuration GoPuff::TaxService.configuration end def endpoint raise NotImplementedError end def body_params raise NotImplementedError end def request_tax_service url = "#{configuration.base_url}#{endpoint}" Retryable.retryable(**retryable_config) do @response = GoPuff::Http::Client.new(url, timeout: TIMEOUT).post(headers: headers, params: body_params) end rescue *configuration.requests.retryable_exceptions => e raise Errors::RequestFailed.new(e, retryable_config[:tries]) end def retryable_config { on: configuration.requests.retryable_exceptions, tries: configuration.requests.max_retries, sleep: configuration.requests.retry_wait_time } end def headers { 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{configuration.token_provider.call}" } end def empty_response { success: false, error: nil, data: {} } end def response_class_name "GoPuff::TaxService::Response::#{self.class.name.demodulize}" end def create_response_object(response) response_class_name.constantize.new(response) rescue NameError response end def response_for_error case @response.status.to_i when 401 configuration.on_unauthorized.call raise Errors::Unauthorized when 400 raise Errors::ValidationFailed.new(@response.body, body_params) else create_response_object(empty_response) end end def serialize_purchase(purchase) { productId: purchase.product_id, quantity: purchase.amount, price: purchase.price.to_f, priceIncludesTax: price_includes_tax? } end def purchases_to_tax @purchases_to_tax ||= order.purchases end def products @products ||= purchases_to_tax.map { |purchase| serialize_purchase(purchase) } + fees_line_items end def alcohol_fees { ALCOHOL_FEE_ID => order.alcohol_fee.to_f } end def non_alcohol_fees { 'DELIVERY_FEE' => order.delivery.to_f, 'SERVICE_FEE' => order.service_fee.to_f, 'SMALL_ORDER_FEE' => order.small_order_fee.to_f, 'PRIORITY_FEE' => order.priority_fee.to_f } end def fees_line_items case @fees_kind when 'all' non_alcohol_fees.merge(alcohol_fees) when 'non_alcohol' non_alcohol_fees when 'alcohol' alcohol_fees else {} end.map do |key, value| next unless value.positive? { productId: key, quantity: 1, price: value, priceIncludesTax: price_includes_tax? } end.compact end def price_includes_tax? !['us', :us].include?(order.country_code) end def delivery_zone_address @delivery_zone_address ||= order&.delivery_zone&.delivery_zone_address end def delivery_address return {} unless @address get_address_for(@address, fallback: delivery_zone_address) end def delivery_address_empty? delivery_address[:line1].blank? end def warehouse_address return {} unless delivery_zone_address get_address_for(delivery_zone_address, fallback: @address) end def get_address_for(resource, fallback:) { line1: resource.address.presence || fallback&.address, city: resource.city.presence || fallback&.city, state: resource.state.presence || fallback&.state, country: order.country_code&.upcase, postalCode: resource.zip.presence || fallback&.zip }.compact end def user_identifier return 'backoffice' if order.third_party? return 'external' unless order.gopuff? (order&.user&.gim_id.presence || order&.user_id).to_s end end end end