Class: Portage::Ucp::Resolver

Inherits:
Object
  • Object
show all
Defined in:
lib/portage/ucp/resolver.rb

Overview

Shared platform-detection + adapter-building logic for anything that needs to go from "a URL" to "a live Adapter for whatever commerce platform that URL runs on" — originally Check-only, now also used by portage buy's adapter-fallback step. Neither caller should duplicate the platform table; both go through here.

Defined Under Namespace

Classes: Platform

Constant Summary collapse

PLATFORMS =

One entry per adapter gem: how to recognize the platform from its homepage, which env vars its Client/Adapter need (mirroring each gem's exe/, see their READMEs), and how to build a live instance from those env vars.

[
  Platform.new(
    name: "Shopify", gem: "portage-ucp-shopify", require_path: "portage/ucp/shopify", namespace: "Shopify",
    markers: [/cdn\.shopify\.com/i, /Shopify\.theme/i, /\.myshopify\.com/i],
    env: { shop_domain: "SHOPIFY_SHOP_DOMAIN", admin_access_token: "SHOPIFY_ADMIN_ACCESS_TOKEN",
           storefront_access_token: "SHOPIFY_STOREFRONT_ACCESS_TOKEN" },
    required: i[shop_domain],
    build_client: lambda { |ns, env|
      ns::Client.new(shop_domain: env.fetch(:shop_domain), admin_access_token: env[:admin_access_token],
                     storefront_access_token: env[:storefront_access_token])
    },
    build_adapter: ->(ns, client, _env) { ns::Adapter.new(client: client) }
  ),
  Platform.new(
    name: "Wix", gem: "portage-ucp-wix", require_path: "portage/ucp/wix", namespace: "Wix",
    markers: [/static\.parastorage\.com/i, /wixstatic\.com/i, /X-Wix-Request-Id/i],
    env: { access_token: "WIX_ACCESS_TOKEN" },
    required: i[access_token],
    build_client: ->(ns, env) { ns::Client.new(access_token: env.fetch(:access_token)) },
    build_adapter: ->(ns, client, _env) { ns::Adapter.new(client: client) }
  ),
  Platform.new(
    name: "WooCommerce", gem: "portage-ucp-woocommerce", require_path: "portage/ucp/woocommerce",
    namespace: "WooCommerce",
    markers: [/woocommerce/i, %r{wp-content/plugins/woocommerce}i],
    env: { site_url: "WOOCOMMERCE_SITE_URL", consumer_key: "WOOCOMMERCE_CONSUMER_KEY",
           consumer_secret: "WOOCOMMERCE_CONSUMER_SECRET", currency: "WOOCOMMERCE_CURRENCY",
           payment_method: "WOOCOMMERCE_PAYMENT_METHOD", billing_address: "WOOCOMMERCE_BILLING_ADDRESS" },
    required: i[site_url consumer_key consumer_secret],
    build_client: lambda { |ns, env|
      ns::Client.new(site_url: env.fetch(:site_url), consumer_key: env.fetch(:consumer_key),
                     consumer_secret: env.fetch(:consumer_secret))
    },
    build_adapter: lambda { |ns, client, env|
      ns::Adapter.new(client: client, site_url: env.fetch(:site_url), currency: env.fetch(:currency, "USD"),
                      payment_method: env[:payment_method], billing_address: woocommerce_billing_address(env))
    }
  ),
  Platform.new(
    name: "BigCommerce", gem: "portage-ucp-bigcommerce", require_path: "portage/ucp/bigcommerce",
    namespace: "BigCommerce",
    markers: [/cdn11\.bigcommerce\.com/i, /bigcommerce/i],
    env: { store_hash: "BIGCOMMERCE_STORE_HASH", client_id: "BIGCOMMERCE_CLIENT_ID",
           access_token: "BIGCOMMERCE_ACCESS_TOKEN", site_url: "BIGCOMMERCE_SITE_URL",
           currency: "BIGCOMMERCE_CURRENCY" },
    required: i[store_hash client_id access_token site_url],
    build_client: lambda { |ns, env|
      ns::Client.new(store_hash: env.fetch(:store_hash), client_id: env.fetch(:client_id),
                     access_token: env.fetch(:access_token))
    },
    build_adapter: lambda { |ns, client, env|
      ns::Adapter.new(client: client, site_url: env.fetch(:site_url), currency: env.fetch(:currency, "USD"))
    }
  ),
  Platform.new(
    name: "Magento", gem: "portage-ucp-magento", require_path: "portage/ucp/magento", namespace: "Magento",
    markers: [/Mage\.Cookies/i, /Magento_/i],
    env: { base_url: "MAGENTO_BASE_URL", admin_token: "MAGENTO_ADMIN_TOKEN", currency: "MAGENTO_CURRENCY" },
    required: i[base_url],
    build_client: lambda { |ns, env|
      ns::Client.new(base_url: env.fetch(:base_url), admin_token: env[:admin_token])
    },
    build_adapter: lambda { |ns, client, env|
      ns::Adapter.new(client: client, site_url: env.fetch(:base_url), currency: env.fetch(:currency, "USD"))
    }
  ),
  Platform.new(
    name: "Etsy", gem: "portage-ucp-etsy", require_path: "portage/ucp/etsy", namespace: "Etsy",
    markers: [/etsy\.com/i],
    env: { access_token: "ETSY_ACCESS_TOKEN", api_key: "ETSY_API_KEY", shop_id: "ETSY_SHOP_ID" },
    required: i[access_token api_key shop_id],
    build_client: lambda { |ns, env|
      ns::Client.new(access_token: env.fetch(:access_token), api_key: env.fetch(:api_key))
    },
    build_adapter: ->(ns, client, env) { ns::Adapter.new(client: client, shop_id: env.fetch(:shop_id)) }
  ),
  Platform.new(
    name: "Instagram/Facebook Shops", gem: "portage-ucp-instagram", require_path: "portage/ucp/instagram",
    namespace: "Instagram",
    markers: [%r{instagram\.com/[^/]+/shop}i, %r{facebook\.com/.*/shop}i],
    env: { access_token: "META_ACCESS_TOKEN", catalog_id: "META_CATALOG_ID" },
    required: i[access_token catalog_id],
    build_client: ->(ns, env) { ns::Client.new(access_token: env.fetch(:access_token)) },
    build_adapter: ->(ns, client, env) { ns::Adapter.new(client: client, catalog_id: env.fetch(:catalog_id)) }
  )
].freeze

Class Method Summary collapse

Class Method Details

.build_adapter(platform, env) ⇒ Portage::Ucp::Adapter

Builds a live Adapter for the given platform from the given env hash (as returned by .env_for). Raises LoadError if the adapter gem isn't installed — callers decide how to handle that (e.g. Check reports it as a skipped probe rather than crashing).



129
130
131
132
133
134
# File 'lib/portage/ucp/resolver.rb', line 129

def self.build_adapter(platform, env)
  require platform.require_path
  namespace = Portage::Ucp.const_get(platform.namespace)
  client = platform.build_client.call(namespace, env)
  platform.build_adapter.call(namespace, client, env)
end

.detect_platform(body, headers) ⇒ Platform?

Returns the first platform whose markers match the given homepage body/headers, or nil if nothing recognizable was found.

Returns:

  • (Platform, nil) —

    the first platform whose markers match the given homepage body/headers, or nil if nothing recognizable was found.



107
108
109
110
# File 'lib/portage/ucp/resolver.rb', line 107

def self.detect_platform(body, headers)
  haystack = [body, headers.to_a.flatten.join(" ")].compact.join(" ")
  PLATFORMS.find { |platform| platform.markers.any? { |marker| haystack =~ marker } }
end

.env_for(platform) ⇒ Hash<Symbol, String, nil>

Returns the platform's env vars read from the process environment, keyed the same as Platform#env/#required.

Returns:

  • (Hash<Symbol, String, nil>) —

    the platform's env vars read from the process environment, keyed the same as Platform#env/#required.



114
115
116
# File 'lib/portage/ucp/resolver.rb', line 114

def self.env_for(platform)
  platform.env.transform_values { |var| ENV.fetch(var, nil) }
end

.missing_env(platform, env) ⇒ Array<String>

Returns the env var names still missing for this platform to be usable, or an empty array if all required vars are set.

Returns:

  • (Array<String>) —

    the env var names still missing for this platform to be usable, or an empty array if all required vars are set.



120
121
122
# File 'lib/portage/ucp/resolver.rb', line 120

def self.missing_env(platform, env)
  platform.required.select { |key| env[key].nil? }.map { |key| platform.env[key] }
end

.parse_woocommerce_billing_address(raw) ⇒ Object



156
157
158
159
160
# File 'lib/portage/ucp/resolver.rb', line 156

def self.parse_woocommerce_billing_address(raw)
  JSON.parse(raw)
rescue JSON::ParserError => e
  raise ArgumentError, "WOOCOMMERCE_BILLING_ADDRESS must be valid JSON: #{e.message}"
end

.woocommerce_billing_address(env) ⇒ Object

WOOCOMMERCE_BILLING_ADDRESS wins when set (raises ArgumentError, not a raw JSON::ParserError, on malformed JSON — callers building an adapter from env expect a config error to name what's wrong). Otherwise falls back to the same PORTAGE_SHIP_* env portage buy already reads for shipping (Portage::Cli::ShippingProfile) mapped to WooCommerce's Store API billing_address field names, so a shopper who already set a shipping profile doesn't also have to hand-build a separate JSON blob just for WooCommerce's stopgap billing_address (see Adapter's class-level CAVEAT #2). Returns nil when neither is configured — #submit_checkout already raises a clear error for that.



146
147
148
149
150
151
152
153
154
# File 'lib/portage/ucp/resolver.rb', line 146

def self.woocommerce_billing_address(env)
  return parse_woocommerce_billing_address(env[:billing_address]) if env[:billing_address]

  address = WOOCOMMERCE_BILLING_FROM_SHIP_ENV.filter_map do |var, key|
    value = ENV.fetch(var, nil)
    [key, value] if value && !value.empty?
  end.to_h
  address.empty? ? nil : address
end