From 5a15f1d43aa0cde87c1f732c7e902bb4d0307da4 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Fri, 8 May 2026 11:49:57 -0400 Subject: [PATCH 01/11] Introduce CORE Api Authentication A new version of the API (CORE) is now available and will be used in the near future for new resources. This new API utilizes a basic authentication scheme. This PR lays the groundwork for future PR's to be able to interact with the CORE api, and adds the plumbing necessary to successfully configure the gem, and authenticate with CORE. This change addresses the need by: * Introducing a v2 authenticator * Introducing a v2 api * Introducing a CoreObject resource which can be used by all core (v2) objects --- lib/bls/core.rb | 5 +- lib/bls/core/api/v1/authenticator.rb | 2 +- lib/bls/core/api/v2.rb | 70 ++++++++++++++ lib/bls/core/api/v2/authenticator.rb | 42 ++++++++ lib/bls/core/authorization/basic.rb | 4 +- lib/bls/core/resources/api_error.rb | 4 +- spec/core/api/v2/authenticator_spec.rb | 58 +++++++++++ spec/core/api/v2_spec.rb | 129 +++++++++++++++++++++++++ spec/support/helpers/api_helper.rb | 6 ++ 9 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 lib/bls/core/api/v2.rb create mode 100644 lib/bls/core/api/v2/authenticator.rb create mode 100644 spec/core/api/v2/authenticator_spec.rb create mode 100644 spec/core/api/v2_spec.rb diff --git a/lib/bls/core.rb b/lib/bls/core.rb index f672ea4..52c5c75 100644 --- a/lib/bls/core.rb +++ b/lib/bls/core.rb @@ -7,7 +7,9 @@ require_relative "core/api_client" require_relative "core/api/v1" +require_relative "core/api/v2" require_relative "core/api/v1/authenticator" +require_relative "core/api/v2/authenticator" require_relative "core/api_operations/retrieve" require_relative "core/authorization/base" require_relative "core/authorization/basic" @@ -32,7 +34,8 @@ module Core class Error < StandardError; end class << self - attr_accessor :client_id, :client_secret, :environment + attr_accessor :client_id, :client_secret, :environment, :username, + :password def configure yield self diff --git a/lib/bls/core/api/v1/authenticator.rb b/lib/bls/core/api/v1/authenticator.rb index 77c9e17..6490b18 100644 --- a/lib/bls/core/api/v1/authenticator.rb +++ b/lib/bls/core/api/v1/authenticator.rb @@ -15,7 +15,7 @@ def save! path = "#{base_url}/token" body = { client_id: client_id, client_secret: client_secret } auth = Authorization::Basic. - factory(client_id: client_id, client_secret: client_secret) + factory(username: client_id, password: client_secret) client = Core::ApiClient.new(authentication: auth) response = client.post(path: path, body: body) handle_authentication_response!(response: response.data) diff --git a/lib/bls/core/api/v2.rb b/lib/bls/core/api/v2.rb new file mode 100644 index 0000000..3f6dd40 --- /dev/null +++ b/lib/bls/core/api/v2.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Bls + module Core + class V2 + attr_accessor :authentication + attr_reader :environment, :username, :password + + PRODUCTION = "production" + STAGING = "staging" + + def initialize(username: nil, password: nil, authentication: nil, + environment: STAGING) + @authentication = authentication + @environment = environment + @username = username + @password = password + end + + def base_url + "https://core.batchlinesolutions.#{tld}" + end + + def authenticate! + return if authenticated? + + authenticator = V2::Authenticator.build(self) + @authentication = authenticator.save! + end + + def client + build_api_client! + end + + def authenticated? + authentication.present? + end + + def reset_authentication + @authentication = nil + self + end + + def self.build(config: Bls::Core) + new(environment: config.environment, + username: config.username, + password: config.password) + end + + protected + + def build_api_client! + authenticate! + Bls::Core::ApiClient.build(self) + end + + def tld + @tld ||= determine_tld + end + + def determine_tld + if environment == PRODUCTION + "com" + else + "dev" + end + end + end + end +end diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb new file mode 100644 index 0000000..af9f157 --- /dev/null +++ b/lib/bls/core/api/v2/authenticator.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Bls + module Core + class V2::Authenticator + attr_accessor :base_url, :authentication + + def initialize(base_url:, authentication:) + @base_url = base_url + @authentication = authentication + end + + def save! + path = "#{base_url}/auth/info" + client = Core::ApiClient.new(authentication: authentication) + response = client.get(path: path) + handle_authentication_response!(response: response) + end + + def self.build(api) + authentication = Authorization::Basic. + factory(username: api.username, + password: api.password) + new(base_url: api.base_url, authentication: authentication) + end + + protected + + def handle_authentication_response!(response:) + if response.success? + authentication + else + handle_errors! + end + end + + def handle_errors! + raise AuthenticationError.new(status: 401, title: "Authentication Failed") + end + end + end +end diff --git a/lib/bls/core/authorization/basic.rb b/lib/bls/core/authorization/basic.rb index 010c8ea..dd6d422 100644 --- a/lib/bls/core/authorization/basic.rb +++ b/lib/bls/core/authorization/basic.rb @@ -8,8 +8,8 @@ def header "Basic #{token}" end - def self.factory(client_id:, client_secret:) - encoded_token = Base64.strict_encode64("#{client_id}:#{client_secret}") + def self.factory(username:, password:) + encoded_token = Base64.strict_encode64("#{username}:#{password}") new(token: encoded_token) end end diff --git a/lib/bls/core/resources/api_error.rb b/lib/bls/core/resources/api_error.rb index 1a7475f..07ba116 100644 --- a/lib/bls/core/resources/api_error.rb +++ b/lib/bls/core/resources/api_error.rb @@ -22,7 +22,9 @@ def message def self.build(response) data = response.data - data => { status:, title:, details: } + status = data.fetch(:status, data[:type]) + title = data.fetch(:title, data[:message]) + details = data[:details] new(status: status, title: title, details: details) end diff --git a/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb new file mode 100644 index 0000000..e4b2cfc --- /dev/null +++ b/spec/core/api/v2/authenticator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2::Authenticator do + describe "#save!" do + it "returns a authorization scheme" do + basic = Bls::Core::Authorization::Basic.new(token: "test") + auth = Bls::Core::V2::Authenticator. + new(base_url: "http://test.com", authentication: basic) + stub_authentication + + result = auth.save! + + expect(result).to respond_to(:header) + end + + context "when authentication fails" do + it "raises a AuthenticationError" do + basic = Bls::Core::Authorization::Basic.new(token: "test") + auth = Bls::Core::V2::Authenticator. + new(base_url: "http://test.com", authentication: basic) + response_body = { error: "Authentication Failed" } + response = error_response(body: response_body) + stub_api_response response: response + + expect { + auth.save! + }.to raise_error(Bls::Core::AuthenticationError) + end + end + end + + describe ".build" do + it "sets the base_url" do + api = Bls::Core::V2.new + + result = Bls::Core::V2::Authenticator.build(api) + + expect(result.base_url).to eq(api.base_url) + end + + it "sets the authentication" do + api = Bls::Core::V2.new + basic = Bls::Core::Authorization::Basic.new(token: "test") + allow(Bls::Core::Authorization::Basic).to receive(:factory). + and_return(basic) + + result = Bls::Core::V2::Authenticator.build(api) + + expect(result.authentication).to eq(basic) + end + end + + def build_config(environment: "staging", client_id: "cid_123", + client_secret: "cs_123") + OpenStruct.new(environment: environment, client_id: client_id, + client_secret: client_secret) + end +end diff --git a/spec/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb new file mode 100644 index 0000000..80915dc --- /dev/null +++ b/spec/core/api/v2_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2 do + describe "#base_url" do + context "when the environment is production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "production") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.com") + end + end + + context "when the environment is not production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "staging") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.dev") + end + end + end + + describe "#client" do + it "returns a api client" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + result = api.client + + expect(result).to respond_to(:post) + end + end + + describe "#authenticate" do + it "builds a authenticator" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(Bls::Core::V2::Authenticator).to have_received(:build).once + end + + it "authenticates" do + api = Bls::Core::V2.new + authenticator = build_stubbed_authenticator(api) + + api.authenticate! + + expect(authenticator).to have_received(:save!).once + end + + context "when the authentication is present" do + it "is authenticated" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(api).to be_authenticated + end + end + end + + describe "reset authentication" do + it "removes the present authentication" do + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result.authentication).to be_blank + end + + it "is not authenticated" do + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result).not_to be_authenticated + end + end + + describe ".build" do + it "sets the environment" do + config = build_config(environment: "preview") + + result = Bls::Core::V2.build(config: config) + + expect(result.environment).to eq("preview") + end + + it "sets the username" do + config = build_config(username: "secretUsername") + + result = Bls::Core::V2.build(config: config) + + expect(result.username).to eq("secretUsername") + end + + it "sets the password" do + config = build_config(password: "secretPassword") + + result = Bls::Core::V2.build(config: config) + + expect(result.password).to eq("secretPassword") + end + end + + def build_config(environment: "staging", username: "string", + password: "string") + OpenStruct.new(environment: environment, username: username, + password: password) + end + + def build_stubbed_authenticator(api) + authenticator = Bls::Core::V2::Authenticator.build(api) + authentication = authenticator.authentication + client = Bls::Core::ApiClient.new(authentication: authentication) + allow(Bls::Core::V2::Authenticator).to receive(:build). + and_return(authenticator) + allow(authenticator).to receive(:save!).and_return(client) + authenticator + end +end diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index e64195b..f7bb825 100644 --- a/spec/support/helpers/api_helper.rb +++ b/spec/support/helpers/api_helper.rb @@ -28,6 +28,12 @@ def stub_api_v1_authentication(authenticated: true) allow(api).to receive(:authenticated?).and_return(authenticated) end + def stub_api_v2_authentication(authenticated: true) + api = Bls::Core::V2.build + allow(Bls::Core::V2).to receive(:new).and_return(api) + allow(api).to receive(:authenticated?).and_return(authenticated) + end + def stub_api_response(response: OpenStruct.new(body: "{}", code: 200), http: double) allow(Net::HTTP).to receive(:start).and_yield(http) From bb56b5c41c6b77428d726bada13dba2b5e0acf3f Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Wed, 12 Aug 2026 10:26:08 -0400 Subject: [PATCH 02/11] WIP remove auth/info call --- lib/bls/core/api/v2/authenticator.rb | 19 +------------------ spec/core/api/v2/authenticator_spec.rb | 15 --------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb index af9f157..2f78191 100644 --- a/lib/bls/core/api/v2/authenticator.rb +++ b/lib/bls/core/api/v2/authenticator.rb @@ -11,10 +11,7 @@ def initialize(base_url:, authentication:) end def save! - path = "#{base_url}/auth/info" - client = Core::ApiClient.new(authentication: authentication) - response = client.get(path: path) - handle_authentication_response!(response: response) + authentication end def self.build(api) @@ -23,20 +20,6 @@ def self.build(api) password: api.password) new(base_url: api.base_url, authentication: authentication) end - - protected - - def handle_authentication_response!(response:) - if response.success? - authentication - else - handle_errors! - end - end - - def handle_errors! - raise AuthenticationError.new(status: 401, title: "Authentication Failed") - end end end end diff --git a/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb index e4b2cfc..61e874f 100644 --- a/spec/core/api/v2/authenticator_spec.rb +++ b/spec/core/api/v2/authenticator_spec.rb @@ -12,21 +12,6 @@ expect(result).to respond_to(:header) end - - context "when authentication fails" do - it "raises a AuthenticationError" do - basic = Bls::Core::Authorization::Basic.new(token: "test") - auth = Bls::Core::V2::Authenticator. - new(base_url: "http://test.com", authentication: basic) - response_body = { error: "Authentication Failed" } - response = error_response(body: response_body) - stub_api_response response: response - - expect { - auth.save! - }.to raise_error(Bls::Core::AuthenticationError) - end - end end describe ".build" do From 5e9fed0e9fda4dfb48d1b32aad73c0845485ec84 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Wed, 12 Aug 2026 10:31:24 -0400 Subject: [PATCH 03/11] WIP --- lib/bls/core/api/v2/authenticator.rb | 7 +++---- spec/core/api/v2/authenticator_spec.rb | 12 +----------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb index 2f78191..ea2c316 100644 --- a/lib/bls/core/api/v2/authenticator.rb +++ b/lib/bls/core/api/v2/authenticator.rb @@ -3,10 +3,9 @@ module Bls module Core class V2::Authenticator - attr_accessor :base_url, :authentication + attr_accessor :authentication - def initialize(base_url:, authentication:) - @base_url = base_url + def initialize(authentication:) @authentication = authentication end @@ -18,7 +17,7 @@ def self.build(api) authentication = Authorization::Basic. factory(username: api.username, password: api.password) - new(base_url: api.base_url, authentication: authentication) + new(authentication: authentication) end end end diff --git a/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb index 61e874f..39e7779 100644 --- a/spec/core/api/v2/authenticator_spec.rb +++ b/spec/core/api/v2/authenticator_spec.rb @@ -4,9 +4,7 @@ describe "#save!" do it "returns a authorization scheme" do basic = Bls::Core::Authorization::Basic.new(token: "test") - auth = Bls::Core::V2::Authenticator. - new(base_url: "http://test.com", authentication: basic) - stub_authentication + auth = Bls::Core::V2::Authenticator.new(authentication: basic) result = auth.save! @@ -15,14 +13,6 @@ end describe ".build" do - it "sets the base_url" do - api = Bls::Core::V2.new - - result = Bls::Core::V2::Authenticator.build(api) - - expect(result.base_url).to eq(api.base_url) - end - it "sets the authentication" do api = Bls::Core::V2.new basic = Bls::Core::Authorization::Basic.new(token: "test") From 64841b4daf5c4e6749bcf8ecc80275079c33ce34 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Fri, 8 May 2026 11:49:57 -0400 Subject: [PATCH 04/11] Introduce Product Resource Version 2 of the api is now available, and introduces the Product resource. We'd like to expose this via the gem, allowing our clients to consume this new resource. The Product resource will be retrieved via ID using a different api, version 2. This new API utilizes a basic authentication scheme. Since this is the first resource to be consumed via the v2 API this PR not only introduces Products, it also adds all the plumbing required to interact with the new api version. This change addresses the need by: * Introducing a v2 authenticator * Introducing a v2 api * Introducing a CoreObject resource which can be used by all core (v2) objects * Introducing a Product resource --- lib/intelligent_foods/api/v2.rb | 68 +++++++++ lib/intelligent_foods/api/v2/authenticator.rb | 44 ++++++ lib/intelligent_foods/resources/allergen.rb | 5 + .../resources/core_object.rb | 17 +++ .../resources/dietary_tag.rb | 5 + lib/intelligent_foods/resources/ingredient.rb | 5 + .../resources/nutrition_fact.rb | 5 + lib/intelligent_foods/resources/product.rb | 27 ++++ lib/intelligent_foods/testing/fake.rb | 142 ++++++++++++++++++ spec/core/api/v2_spec.rb | 61 ++++++++ spec/core/resources/product_spec.rb | 17 +++ spec/support/fixtures/product_response.json | 44 ++++++ spec/support/helpers/api_helper.rb | 18 +++ 13 files changed, 458 insertions(+) create mode 100644 lib/intelligent_foods/api/v2.rb create mode 100644 lib/intelligent_foods/api/v2/authenticator.rb create mode 100644 lib/intelligent_foods/resources/allergen.rb create mode 100644 lib/intelligent_foods/resources/core_object.rb create mode 100644 lib/intelligent_foods/resources/dietary_tag.rb create mode 100644 lib/intelligent_foods/resources/ingredient.rb create mode 100644 lib/intelligent_foods/resources/nutrition_fact.rb create mode 100644 lib/intelligent_foods/resources/product.rb create mode 100644 lib/intelligent_foods/testing/fake.rb create mode 100644 spec/core/resources/product_spec.rb create mode 100644 spec/support/fixtures/product_response.json diff --git a/lib/intelligent_foods/api/v2.rb b/lib/intelligent_foods/api/v2.rb new file mode 100644 index 0000000..61f1a2b --- /dev/null +++ b/lib/intelligent_foods/api/v2.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module IntelligentFoods + class V2 + attr_accessor :authentication + attr_reader :environment, :username, :password + + PRODUCTION = "production" + STAGING = "staging" + + def initialize(username: nil, password: nil, authentication: nil, + environment: STAGING) + @authentication = authentication + @environment = environment + @username = username + @password = password + end + + def base_url + "https://core.intelligentfoods.#{tld}" + end + + def authenticate! + return if authenticated? + + authenticator = V2::Authenticator.build(self) + @authentication = authenticator.save! + end + + def client + build_api_client! + end + + def authenticated? + authentication.present? + end + + def reset_authentication + @authentication = nil + self + end + + def self.build(config: IntelligentFoods) + new(environment: config.environment, + username: config.username, + password: config.password) + end + + protected + + def build_api_client! + authenticate! + IntelligentFoods::ApiClient.build(self) + end + + def tld + @tld ||= determine_tld + end + + def determine_tld + if environment == PRODUCTION + "com" + else + "dev" + end + end + end +end diff --git a/lib/intelligent_foods/api/v2/authenticator.rb b/lib/intelligent_foods/api/v2/authenticator.rb new file mode 100644 index 0000000..57dc998 --- /dev/null +++ b/lib/intelligent_foods/api/v2/authenticator.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module IntelligentFoods + class V2::Authenticator + attr_accessor :base_url, :authentication + + def initialize(base_url:, authentication:) + @base_url = base_url + @authentication = authentication + end + + def save! + path = "#{base_url}/auth/info" + client = IntelligentFoods::ApiClient.new(authentication: authentication) + response = client.get(path: path) + handle_authentication_response!(response: response) + end + + def self.build(api) + authentication = Authorization::Basic. + factory(client_id: api.username, + client_secret: api.password) + new(base_url: api.base_url, authentication: authentication) + end + + protected + + def handle_authentication_response!(response:) + if response.success? + authentication + else + handle_errors! + end + end + + def response_has_errors?(response) + response.has_key?(:error) + end + + def handle_errors! + raise AuthenticationError.new(status: 401, title: "Authentication Failed") + end + end +end diff --git a/lib/intelligent_foods/resources/allergen.rb b/lib/intelligent_foods/resources/allergen.rb new file mode 100644 index 0000000..5fb9938 --- /dev/null +++ b/lib/intelligent_foods/resources/allergen.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IntelligentFoods + class Allergen < IntelligentFoods::CoreObject; end +end diff --git a/lib/intelligent_foods/resources/core_object.rb b/lib/intelligent_foods/resources/core_object.rb new file mode 100644 index 0000000..822cf8c --- /dev/null +++ b/lib/intelligent_foods/resources/core_object.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module IntelligentFoods + class CoreObject < IntelligentFoods::Object + def resources_path + "#{api.base_url}/api/#{object_name.downcase}" + end + + def resource_path + "#{api.base_url}/api/#{object_name.downcase}/#{id}" + end + + def api + @api ||= IntelligentFoods::V2.build + end + end +end diff --git a/lib/intelligent_foods/resources/dietary_tag.rb b/lib/intelligent_foods/resources/dietary_tag.rb new file mode 100644 index 0000000..f152ce2 --- /dev/null +++ b/lib/intelligent_foods/resources/dietary_tag.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IntelligentFoods + class DietaryTag < IntelligentFoods::CoreObject; end +end diff --git a/lib/intelligent_foods/resources/ingredient.rb b/lib/intelligent_foods/resources/ingredient.rb new file mode 100644 index 0000000..7283d98 --- /dev/null +++ b/lib/intelligent_foods/resources/ingredient.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IntelligentFoods + class Ingredient < IntelligentFoods::CoreObject; end +end diff --git a/lib/intelligent_foods/resources/nutrition_fact.rb b/lib/intelligent_foods/resources/nutrition_fact.rb new file mode 100644 index 0000000..0437c11 --- /dev/null +++ b/lib/intelligent_foods/resources/nutrition_fact.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IntelligentFoods + class NutritionFact < IntelligentFoods::CoreObject; end +end diff --git a/lib/intelligent_foods/resources/product.rb b/lib/intelligent_foods/resources/product.rb new file mode 100644 index 0000000..3ff1684 --- /dev/null +++ b/lib/intelligent_foods/resources/product.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module IntelligentFoods + class Product < IntelligentFoods::CoreObject + def initialize(args = {}) + super + end + + def object_name + "products" + end + + def self.build_from_response(data) + product = build(data) + product.ingredients = Ingredient.build_from_array(data[:ingredients]) + product.nutrition_facts = NutritionFact. + build_from_array(data[:nutrition_facts]) + product.nutrition_facts_cooked = NutritionFact. + build_from_array( + data.fetch(:nutrition_facts_cooked, []) + ) + product.allergens = Allergen.build_from_array(data[:allergens]) + product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) + product + end + end +end diff --git a/lib/intelligent_foods/testing/fake.rb b/lib/intelligent_foods/testing/fake.rb new file mode 100644 index 0000000..eac89d5 --- /dev/null +++ b/lib/intelligent_foods/testing/fake.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +module IntelligentFoods + module Testing + class Fake + def self.configure(*); end + + class Allergen + def self.new(code: "MILK", source: "ANCHOVY") + OpenStruct.new(code: code, source: source) + end + end + + class DistributionCenterInventory + def self.create(name: "WEST_COAST", quantity: 100) + OpenStruct.new(distribution_center: name, quantity: quantity) + end + end + + class InventoryLevel + def self.create(sku: "SKU1", date: Date.today.to_s, + dc_inventory: [DistributionCenterInventory.create]) + inventory_level = OpenStruct.new( + date: date, + inventory_by_distribution_center: dc_inventory + ) + OpenStruct.new(sku: sku, inventory_levels: [inventory_level]) + end + end + + class MenuItem + def self.create(id: SecureRandom.uuid, sku: "SKU1", + name: "FakeMenuItem") + OpenStruct.new(id: id, sku: sku, name: name) + end + end + + class Menu + def self.new(id: Date.today.to_s) + items = [Fake::MenuItem.create] + OpenStruct.new(id: id, deadline: Time.zone.now, + shipping_fee: 9.99, items: items) + end + + def self.retrieve(id) + items = [Fake::MenuItem.create] + OpenStruct.new(id: id, deadline: Time.zone.now, + shipping_fee: 9.99, items: items) + end + end + + class MenuItemSerializer + def initialize(menu_item) + OpenStruct.new(id: menu_item.id, sku: menu_item.sku, + name: menu_item.name) + end + end + + class Recipient + def self.new(name: nil, street1: nil, street2: nil, city: nil, + state: nil, zip: nil, zip4: nil, email: nil, phone: nil, + delivery_instructions: nil) + OpenStruct.new(name: name, street1: street1, street2: street2, + city: city, state: state, zip: zip, zip4: zip4, + email: email, phone: phone, + delivery_instructions: delivery_instructions) + end + end + + class Order + def initialize(*) + self + end + + def id + @id ||= SecureRandom.uuid + end + + def create! + items = [Fake::MenuItem.create] + ship_to = Fake::Recipient.new + OpenStruct.new(id: SecureRandom.uuid, items: items, + ship_to: ship_to) + end + + def cancel! + OpenStruct.new(id: SecureRandom.uuid, + status: "CANCELLED") + end + end + + class OrderItem + def self.new(sku:, quantity:, protein_sku:) + OpenStruct.new(sku: sku, quantity: quantity, protein_sku: protein_sku) + end + end + + class Product + def self.new(code:, name:, status: "DRAFT", nutrition_facts: []) + OpenStruct.new(code: code, name: name, status: status, + nutrition_facts: nutrition_facts, + allergens: [Fake::Allergen.new], + dietary_tags: ["NUTS"]) + end + end + + def self.client + Fake::Client.new + end + + class ApiClient + def self.new(*) + Fake::Client.new + end + end + + class ApiClientSerializer + def self.serialize(*) + OpenStruct.new(id: nil, secret: nil).to_h + end + end + + class Client + def initialize(*) + self + end + + def authenticated? + false + end + + def authenticate!; end + end + + class OrderNotCreatedError < StandardError; end + + class OrderNotCancelledError < StandardError; end + + class AuthenticationError < StandardError; end + end + end +end diff --git a/spec/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb index 80915dc..a6972ad 100644 --- a/spec/core/api/v2_spec.rb +++ b/spec/core/api/v2_spec.rb @@ -1,10 +1,18 @@ # frozen_string_literal: true +<<<<<<< HEAD:spec/core/api/v2_spec.rb RSpec.describe Bls::Core::V2 do describe "#base_url" do context "when the environment is production" do it "returns the correct url" do api = Bls::Core::V2.new(environment: "production") +======= +RSpec.describe IntelligentFoods::V2 do + describe "#base_url" do + context "when the environment is production" do + it "returns the correct url" do + api = IntelligentFoods::V2.new(environment: "production") +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.base_url @@ -14,7 +22,11 @@ context "when the environment is not production" do it "returns the correct url" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new(environment: "staging") +======= + api = IntelligentFoods::V2.new(environment: "staging") +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.base_url @@ -25,7 +37,11 @@ describe "#client" do it "returns a api client" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new +======= + api = IntelligentFoods::V2.new +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) result = api.client @@ -36,16 +52,28 @@ describe "#authenticate" do it "builds a authenticator" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new +======= + api = IntelligentFoods::V2.new +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) api.authenticate! +<<<<<<< HEAD:spec/core/api/v2_spec.rb expect(Bls::Core::V2::Authenticator).to have_received(:build).once end it "authenticates" do api = Bls::Core::V2.new +======= + expect(IntelligentFoods::V2::Authenticator).to have_received(:build).once + end + + it "authenticates" do + api = IntelligentFoods::V2.new +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb authenticator = build_stubbed_authenticator(api) api.authenticate! @@ -55,7 +83,11 @@ context "when the authentication is present" do it "is authenticated" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new +======= + api = IntelligentFoods::V2.new +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) api.authenticate! @@ -67,8 +99,13 @@ describe "reset authentication" do it "removes the present authentication" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb auth = Bls::Core::Authorization::Basic.new(token: "1234") api = Bls::Core::V2.new(authentication: auth) +======= + auth = IntelligentFoods::Authorization::Basic.new(token: "1234") + api = IntelligentFoods::V2.new(authentication: auth) +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.reset_authentication @@ -76,8 +113,13 @@ end it "is not authenticated" do +<<<<<<< HEAD:spec/core/api/v2_spec.rb auth = Bls::Core::Authorization::Basic.new(token: "1234") api = Bls::Core::V2.new(authentication: auth) +======= + auth = IntelligentFoods::Authorization::Basic.new(token: "1234") + api = IntelligentFoods::V2.new(authentication: auth) +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.reset_authentication @@ -89,7 +131,11 @@ it "sets the environment" do config = build_config(environment: "preview") +<<<<<<< HEAD:spec/core/api/v2_spec.rb result = Bls::Core::V2.build(config: config) +======= + result = IntelligentFoods::V2.build(config: config) +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.environment).to eq("preview") end @@ -97,7 +143,11 @@ it "sets the username" do config = build_config(username: "secretUsername") +<<<<<<< HEAD:spec/core/api/v2_spec.rb result = Bls::Core::V2.build(config: config) +======= + result = IntelligentFoods::V2.build(config: config) +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.username).to eq("secretUsername") end @@ -105,7 +155,11 @@ it "sets the password" do config = build_config(password: "secretPassword") +<<<<<<< HEAD:spec/core/api/v2_spec.rb result = Bls::Core::V2.build(config: config) +======= + result = IntelligentFoods::V2.build(config: config) +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.password).to eq("secretPassword") end @@ -118,10 +172,17 @@ def build_config(environment: "staging", username: "string", end def build_stubbed_authenticator(api) +<<<<<<< HEAD:spec/core/api/v2_spec.rb authenticator = Bls::Core::V2::Authenticator.build(api) authentication = authenticator.authentication client = Bls::Core::ApiClient.new(authentication: authentication) allow(Bls::Core::V2::Authenticator).to receive(:build). +======= + authenticator = IntelligentFoods::V2::Authenticator.build(api) + authentication = authenticator.authentication + client = IntelligentFoods::ApiClient.new(authentication: authentication) + allow(IntelligentFoods::V2::Authenticator).to receive(:build). +>>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb and_return(authenticator) allow(authenticator).to receive(:save!).and_return(client) authenticator diff --git a/spec/core/resources/product_spec.rb b/spec/core/resources/product_spec.rb new file mode 100644 index 0000000..908e17e --- /dev/null +++ b/spec/core/resources/product_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe IntelligentFoods::Product do + describe ".retrieve" do + it "returns the product" do + product_id = "MP0527" + body = build_product_response(product_id: product_id) + response = build_response(body: body) + stub_api_v2_authentication + stub_api_response response: response + + result = IntelligentFoods::Product.retrieve(product_id) + + expect(result.code).to eq(product_id) + end + end +end diff --git a/spec/support/fixtures/product_response.json b/spec/support/fixtures/product_response.json new file mode 100644 index 0000000..c9c4fa7 --- /dev/null +++ b/spec/support/fixtures/product_response.json @@ -0,0 +1,44 @@ +{ + "code": "string", + "name": "string", + "status": "DRAFT", + "refrigeration_type": "NONE", + "refrigeration_level": "STANDARD", + "unit_volume": { + "value": 0.1 + }, + "unit_slots": { + "value": 0.1 + }, + "ingredients": [ + { + "inventory_code": "string", + "name": "string", + "quantity": 0.1, + "unit": "string" + } + ], + "nutrition_facts": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ], + "nutrition_facts_cooked": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ], + "allergens": [ + { + "code": "MILK", + "source": "ANCHOVY" + } + ], + "dietary_tags": [ + "PESCATARIAN" + ] +} diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index f7bb825..f0061ea 100644 --- a/spec/support/helpers/api_helper.rb +++ b/spec/support/helpers/api_helper.rb @@ -4,6 +4,8 @@ module ApiHelper ERROR_API_RESPONSE = "spec/support/fixtures/error_response.json".freeze INVENTORY_LEVELS_API_RESPONSE = "spec/support/fixtures/inventory_levels_response.json".freeze + PRODUCT_API_RESPONSE = + "spec/support/fixtures/product_response.json".freeze def authentication_response(access_token:) build_response body: { access_token: access_token } @@ -34,6 +36,12 @@ def stub_api_v2_authentication(authenticated: true) allow(api).to receive(:authenticated?).and_return(authenticated) end + def stub_api_v2_authentication(authenticated: true) + api = IntelligentFoods::V2.build + allow(IntelligentFoods::V2).to receive(:new).and_return(api) + allow(api).to receive(:authenticated?).and_return(authenticated) + end + def stub_api_response(response: OpenStruct.new(body: "{}", code: 200), http: double) allow(Net::HTTP).to receive(:start).and_yield(http) @@ -70,6 +78,10 @@ def read_inventory_levels_api_response parse_json_file(INVENTORY_LEVELS_API_RESPONSE) end + def read_product_api_response + parse_json_file(PRODUCT_API_RESPONSE) + end + def build_menu_response(menu_id: "2023-01-01", menu_items: []) stubbed_response = read_menu_api_response stubbed_response[:id] = menu_id @@ -85,6 +97,12 @@ def build_inventory_levels_response read_inventory_levels_api_response end + def build_product_response(product_id: "string") + stubbed_response = read_product_api_response + stubbed_response[:code] = product_id + stubbed_response + end + def stubbed_menu_items response = read_menu_api_response response[:items] From 2e87fae200349d1c7d186e1661440db25f50ea67 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Mon, 10 Aug 2026 14:11:49 -0400 Subject: [PATCH 05/11] WIP --- lib/intelligent_foods/api/v2.rb | 68 ------------------- lib/intelligent_foods/api/v2/authenticator.rb | 44 ------------ .../resources/core_object.rb | 17 ----- 3 files changed, 129 deletions(-) delete mode 100644 lib/intelligent_foods/api/v2.rb delete mode 100644 lib/intelligent_foods/api/v2/authenticator.rb delete mode 100644 lib/intelligent_foods/resources/core_object.rb diff --git a/lib/intelligent_foods/api/v2.rb b/lib/intelligent_foods/api/v2.rb deleted file mode 100644 index 61f1a2b..0000000 --- a/lib/intelligent_foods/api/v2.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class V2 - attr_accessor :authentication - attr_reader :environment, :username, :password - - PRODUCTION = "production" - STAGING = "staging" - - def initialize(username: nil, password: nil, authentication: nil, - environment: STAGING) - @authentication = authentication - @environment = environment - @username = username - @password = password - end - - def base_url - "https://core.intelligentfoods.#{tld}" - end - - def authenticate! - return if authenticated? - - authenticator = V2::Authenticator.build(self) - @authentication = authenticator.save! - end - - def client - build_api_client! - end - - def authenticated? - authentication.present? - end - - def reset_authentication - @authentication = nil - self - end - - def self.build(config: IntelligentFoods) - new(environment: config.environment, - username: config.username, - password: config.password) - end - - protected - - def build_api_client! - authenticate! - IntelligentFoods::ApiClient.build(self) - end - - def tld - @tld ||= determine_tld - end - - def determine_tld - if environment == PRODUCTION - "com" - else - "dev" - end - end - end -end diff --git a/lib/intelligent_foods/api/v2/authenticator.rb b/lib/intelligent_foods/api/v2/authenticator.rb deleted file mode 100644 index 57dc998..0000000 --- a/lib/intelligent_foods/api/v2/authenticator.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class V2::Authenticator - attr_accessor :base_url, :authentication - - def initialize(base_url:, authentication:) - @base_url = base_url - @authentication = authentication - end - - def save! - path = "#{base_url}/auth/info" - client = IntelligentFoods::ApiClient.new(authentication: authentication) - response = client.get(path: path) - handle_authentication_response!(response: response) - end - - def self.build(api) - authentication = Authorization::Basic. - factory(client_id: api.username, - client_secret: api.password) - new(base_url: api.base_url, authentication: authentication) - end - - protected - - def handle_authentication_response!(response:) - if response.success? - authentication - else - handle_errors! - end - end - - def response_has_errors?(response) - response.has_key?(:error) - end - - def handle_errors! - raise AuthenticationError.new(status: 401, title: "Authentication Failed") - end - end -end diff --git a/lib/intelligent_foods/resources/core_object.rb b/lib/intelligent_foods/resources/core_object.rb deleted file mode 100644 index 822cf8c..0000000 --- a/lib/intelligent_foods/resources/core_object.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class CoreObject < IntelligentFoods::Object - def resources_path - "#{api.base_url}/api/#{object_name.downcase}" - end - - def resource_path - "#{api.base_url}/api/#{object_name.downcase}/#{id}" - end - - def api - @api ||= IntelligentFoods::V2.build - end - end -end From 57726cfe96595776f1bfcc3b0dc1b0f803ca02e8 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Mon, 10 Aug 2026 14:29:47 -0400 Subject: [PATCH 06/11] WIP --- lib/bls/core.rb | 6 + lib/bls/core/resources/allergen.rb | 7 + lib/bls/core/resources/dietary_tag.rb | 7 + lib/bls/core/resources/ingredient.rb | 7 + lib/bls/core/resources/nutrition_fact.rb | 7 + lib/bls/core/resources/objectv2.rb | 23 +++ lib/bls/core/resources/product.rb | 29 ++++ lib/bls/core/testing/fake.rb | 15 ++ lib/intelligent_foods/resources/allergen.rb | 5 - .../resources/dietary_tag.rb | 5 - lib/intelligent_foods/resources/ingredient.rb | 5 - .../resources/nutrition_fact.rb | 5 - lib/intelligent_foods/resources/product.rb | 27 ---- lib/intelligent_foods/testing/fake.rb | 142 ------------------ spec/core/resources/product_spec.rb | 4 +- spec/support/helpers/api_helper.rb | 6 - 16 files changed, 103 insertions(+), 197 deletions(-) create mode 100644 lib/bls/core/resources/allergen.rb create mode 100644 lib/bls/core/resources/dietary_tag.rb create mode 100644 lib/bls/core/resources/ingredient.rb create mode 100644 lib/bls/core/resources/nutrition_fact.rb create mode 100644 lib/bls/core/resources/objectv2.rb create mode 100644 lib/bls/core/resources/product.rb delete mode 100644 lib/intelligent_foods/resources/allergen.rb delete mode 100644 lib/intelligent_foods/resources/dietary_tag.rb delete mode 100644 lib/intelligent_foods/resources/ingredient.rb delete mode 100644 lib/intelligent_foods/resources/nutrition_fact.rb delete mode 100644 lib/intelligent_foods/resources/product.rb delete mode 100644 lib/intelligent_foods/testing/fake.rb diff --git a/lib/bls/core.rb b/lib/bls/core.rb index 52c5c75..4a3a3db 100644 --- a/lib/bls/core.rb +++ b/lib/bls/core.rb @@ -17,13 +17,19 @@ require_relative "core/resources/api_error" require_relative "core/authorization/bearer" require_relative "core/resources/object" +require_relative "core/resources/objectv2" +require_relative "core/resources/allergen" +require_relative "core/resources/dietary_tag" +require_relative "core/resources/ingredient" require_relative "core/resources/inventory_level" +require_relative "core/resources/nutrition_fact" require_relative "core/resources/order" require_relative "core/resources/order_item" require_relative "core/serializers/order_item_serializer" require_relative "core/resources/menu" require_relative "core/resources/menu_item" require_relative "core/serializers/menu_item_serializer" +require_relative "core/resources/product" require_relative "core/resources/recipient" require_relative "core/serializers/recipient_serializer" require_relative "core/version" diff --git a/lib/bls/core/resources/allergen.rb b/lib/bls/core/resources/allergen.rb new file mode 100644 index 0000000..6601376 --- /dev/null +++ b/lib/bls/core/resources/allergen.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class Allergen < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/dietary_tag.rb b/lib/bls/core/resources/dietary_tag.rb new file mode 100644 index 0000000..db080fd --- /dev/null +++ b/lib/bls/core/resources/dietary_tag.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class DietaryTag < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/ingredient.rb b/lib/bls/core/resources/ingredient.rb new file mode 100644 index 0000000..c82fbc5 --- /dev/null +++ b/lib/bls/core/resources/ingredient.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class Ingredient < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/nutrition_fact.rb b/lib/bls/core/resources/nutrition_fact.rb new file mode 100644 index 0000000..77e3de7 --- /dev/null +++ b/lib/bls/core/resources/nutrition_fact.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class NutritionFact < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/objectv2.rb b/lib/bls/core/resources/objectv2.rb new file mode 100644 index 0000000..9e3c447 --- /dev/null +++ b/lib/bls/core/resources/objectv2.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Bls + module Core + class ObjectV2 < Bls::Core::Object + def self.build_from_array(array) + array.map { |object| build(object) } + end + + def resources_path + "#{api.base_url}/api/#{object_name.downcase}" + end + + def resource_path + "#{api.base_url}/api/#{object_name.downcase}/#{id}" + end + + def api + @api ||= Bls::Core::V2.build + end + end + end +end diff --git a/lib/bls/core/resources/product.rb b/lib/bls/core/resources/product.rb new file mode 100644 index 0000000..a8d8f7f --- /dev/null +++ b/lib/bls/core/resources/product.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Bls + module Core + class Product < Bls::Core::ObjectV2 + def initialize(args = {}) + super + end + + def object_name + "products" + end + + def self.build_from_response(data) + product = build(data) + product.ingredients = Ingredient.build_from_array(data[:ingredients]) + product.nutrition_facts = NutritionFact. + build_from_array(data[:nutrition_facts]) + product.nutrition_facts_cooked = NutritionFact. + build_from_array( + data.fetch(:nutrition_facts_cooked, []) + ) + product.allergens = Allergen.build_from_array(data[:allergens]) + product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) + product + end + end + end +end diff --git a/lib/bls/core/testing/fake.rb b/lib/bls/core/testing/fake.rb index 5c03dd3..2f2d05b 100644 --- a/lib/bls/core/testing/fake.rb +++ b/lib/bls/core/testing/fake.rb @@ -6,6 +6,12 @@ module Testing class Fake def self.configure(*); end + class Allergen + def self.new(code: "MILK", source: "ANCHOVY") + OpenStruct.new(code: code, source: source) + end + end + class DistributionCenterInventory def self.create(name: "WEST_COAST", quantity: 100) OpenStruct.new(distribution_center: name, quantity: quantity) @@ -90,6 +96,15 @@ def self.new(sku:, quantity:, protein_sku:) end end + class Product + def self.new(code:, name:, status: "DRAFT", nutrition_facts: []) + OpenStruct.new(code: code, name: name, status: status, + nutrition_facts: nutrition_facts, + allergens: [Fake::Allergen.new], + dietary_tags: ["NUTS"]) + end + end + def self.client Fake::Client.new end diff --git a/lib/intelligent_foods/resources/allergen.rb b/lib/intelligent_foods/resources/allergen.rb deleted file mode 100644 index 5fb9938..0000000 --- a/lib/intelligent_foods/resources/allergen.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class Allergen < IntelligentFoods::CoreObject; end -end diff --git a/lib/intelligent_foods/resources/dietary_tag.rb b/lib/intelligent_foods/resources/dietary_tag.rb deleted file mode 100644 index f152ce2..0000000 --- a/lib/intelligent_foods/resources/dietary_tag.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class DietaryTag < IntelligentFoods::CoreObject; end -end diff --git a/lib/intelligent_foods/resources/ingredient.rb b/lib/intelligent_foods/resources/ingredient.rb deleted file mode 100644 index 7283d98..0000000 --- a/lib/intelligent_foods/resources/ingredient.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class Ingredient < IntelligentFoods::CoreObject; end -end diff --git a/lib/intelligent_foods/resources/nutrition_fact.rb b/lib/intelligent_foods/resources/nutrition_fact.rb deleted file mode 100644 index 0437c11..0000000 --- a/lib/intelligent_foods/resources/nutrition_fact.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class NutritionFact < IntelligentFoods::CoreObject; end -end diff --git a/lib/intelligent_foods/resources/product.rb b/lib/intelligent_foods/resources/product.rb deleted file mode 100644 index 3ff1684..0000000 --- a/lib/intelligent_foods/resources/product.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - class Product < IntelligentFoods::CoreObject - def initialize(args = {}) - super - end - - def object_name - "products" - end - - def self.build_from_response(data) - product = build(data) - product.ingredients = Ingredient.build_from_array(data[:ingredients]) - product.nutrition_facts = NutritionFact. - build_from_array(data[:nutrition_facts]) - product.nutrition_facts_cooked = NutritionFact. - build_from_array( - data.fetch(:nutrition_facts_cooked, []) - ) - product.allergens = Allergen.build_from_array(data[:allergens]) - product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) - product - end - end -end diff --git a/lib/intelligent_foods/testing/fake.rb b/lib/intelligent_foods/testing/fake.rb deleted file mode 100644 index eac89d5..0000000 --- a/lib/intelligent_foods/testing/fake.rb +++ /dev/null @@ -1,142 +0,0 @@ -# frozen_string_literal: true - -module IntelligentFoods - module Testing - class Fake - def self.configure(*); end - - class Allergen - def self.new(code: "MILK", source: "ANCHOVY") - OpenStruct.new(code: code, source: source) - end - end - - class DistributionCenterInventory - def self.create(name: "WEST_COAST", quantity: 100) - OpenStruct.new(distribution_center: name, quantity: quantity) - end - end - - class InventoryLevel - def self.create(sku: "SKU1", date: Date.today.to_s, - dc_inventory: [DistributionCenterInventory.create]) - inventory_level = OpenStruct.new( - date: date, - inventory_by_distribution_center: dc_inventory - ) - OpenStruct.new(sku: sku, inventory_levels: [inventory_level]) - end - end - - class MenuItem - def self.create(id: SecureRandom.uuid, sku: "SKU1", - name: "FakeMenuItem") - OpenStruct.new(id: id, sku: sku, name: name) - end - end - - class Menu - def self.new(id: Date.today.to_s) - items = [Fake::MenuItem.create] - OpenStruct.new(id: id, deadline: Time.zone.now, - shipping_fee: 9.99, items: items) - end - - def self.retrieve(id) - items = [Fake::MenuItem.create] - OpenStruct.new(id: id, deadline: Time.zone.now, - shipping_fee: 9.99, items: items) - end - end - - class MenuItemSerializer - def initialize(menu_item) - OpenStruct.new(id: menu_item.id, sku: menu_item.sku, - name: menu_item.name) - end - end - - class Recipient - def self.new(name: nil, street1: nil, street2: nil, city: nil, - state: nil, zip: nil, zip4: nil, email: nil, phone: nil, - delivery_instructions: nil) - OpenStruct.new(name: name, street1: street1, street2: street2, - city: city, state: state, zip: zip, zip4: zip4, - email: email, phone: phone, - delivery_instructions: delivery_instructions) - end - end - - class Order - def initialize(*) - self - end - - def id - @id ||= SecureRandom.uuid - end - - def create! - items = [Fake::MenuItem.create] - ship_to = Fake::Recipient.new - OpenStruct.new(id: SecureRandom.uuid, items: items, - ship_to: ship_to) - end - - def cancel! - OpenStruct.new(id: SecureRandom.uuid, - status: "CANCELLED") - end - end - - class OrderItem - def self.new(sku:, quantity:, protein_sku:) - OpenStruct.new(sku: sku, quantity: quantity, protein_sku: protein_sku) - end - end - - class Product - def self.new(code:, name:, status: "DRAFT", nutrition_facts: []) - OpenStruct.new(code: code, name: name, status: status, - nutrition_facts: nutrition_facts, - allergens: [Fake::Allergen.new], - dietary_tags: ["NUTS"]) - end - end - - def self.client - Fake::Client.new - end - - class ApiClient - def self.new(*) - Fake::Client.new - end - end - - class ApiClientSerializer - def self.serialize(*) - OpenStruct.new(id: nil, secret: nil).to_h - end - end - - class Client - def initialize(*) - self - end - - def authenticated? - false - end - - def authenticate!; end - end - - class OrderNotCreatedError < StandardError; end - - class OrderNotCancelledError < StandardError; end - - class AuthenticationError < StandardError; end - end - end -end diff --git a/spec/core/resources/product_spec.rb b/spec/core/resources/product_spec.rb index 908e17e..f394ffd 100644 --- a/spec/core/resources/product_spec.rb +++ b/spec/core/resources/product_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe IntelligentFoods::Product do +RSpec.describe Bls::Core::Product do describe ".retrieve" do it "returns the product" do product_id = "MP0527" @@ -9,7 +9,7 @@ stub_api_v2_authentication stub_api_response response: response - result = IntelligentFoods::Product.retrieve(product_id) + result = Bls::Core::Product.retrieve(product_id) expect(result.code).to eq(product_id) end diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index f0061ea..39e0f0d 100644 --- a/spec/support/helpers/api_helper.rb +++ b/spec/support/helpers/api_helper.rb @@ -36,12 +36,6 @@ def stub_api_v2_authentication(authenticated: true) allow(api).to receive(:authenticated?).and_return(authenticated) end - def stub_api_v2_authentication(authenticated: true) - api = IntelligentFoods::V2.build - allow(IntelligentFoods::V2).to receive(:new).and_return(api) - allow(api).to receive(:authenticated?).and_return(authenticated) - end - def stub_api_response(response: OpenStruct.new(body: "{}", code: 200), http: double) allow(Net::HTTP).to receive(:start).and_yield(http) From a4fa6a9027223b1537f868ae1b9ad3cf0166e5a0 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Mon, 10 Aug 2026 15:28:55 -0400 Subject: [PATCH 07/11] WIP --- spec/core/api/v2_spec.rb | 61 ---------------------------------------- 1 file changed, 61 deletions(-) diff --git a/spec/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb index a6972ad..d00717e 100644 --- a/spec/core/api/v2_spec.rb +++ b/spec/core/api/v2_spec.rb @@ -1,18 +1,10 @@ # frozen_string_literal: true -<<<<<<< HEAD:spec/core/api/v2_spec.rb RSpec.describe Bls::Core::V2 do describe "#base_url" do context "when the environment is production" do it "returns the correct url" do api = Bls::Core::V2.new(environment: "production") -======= -RSpec.describe IntelligentFoods::V2 do - describe "#base_url" do - context "when the environment is production" do - it "returns the correct url" do - api = IntelligentFoods::V2.new(environment: "production") ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.base_url @@ -22,11 +14,7 @@ context "when the environment is not production" do it "returns the correct url" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new(environment: "staging") -======= - api = IntelligentFoods::V2.new(environment: "staging") ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.base_url @@ -37,11 +25,7 @@ describe "#client" do it "returns a api client" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new -======= - api = IntelligentFoods::V2.new ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) result = api.client @@ -52,28 +36,16 @@ describe "#authenticate" do it "builds a authenticator" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new -======= - api = IntelligentFoods::V2.new ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) api.authenticate! -<<<<<<< HEAD:spec/core/api/v2_spec.rb expect(Bls::Core::V2::Authenticator).to have_received(:build).once end it "authenticates" do api = Bls::Core::V2.new -======= - expect(IntelligentFoods::V2::Authenticator).to have_received(:build).once - end - - it "authenticates" do - api = IntelligentFoods::V2.new ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb authenticator = build_stubbed_authenticator(api) api.authenticate! @@ -83,11 +55,7 @@ context "when the authentication is present" do it "is authenticated" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb api = Bls::Core::V2.new -======= - api = IntelligentFoods::V2.new ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb build_stubbed_authenticator(api) api.authenticate! @@ -99,13 +67,8 @@ describe "reset authentication" do it "removes the present authentication" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb - auth = Bls::Core::Authorization::Basic.new(token: "1234") - api = Bls::Core::V2.new(authentication: auth) -======= auth = IntelligentFoods::Authorization::Basic.new(token: "1234") api = IntelligentFoods::V2.new(authentication: auth) ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.reset_authentication @@ -113,13 +76,8 @@ end it "is not authenticated" do -<<<<<<< HEAD:spec/core/api/v2_spec.rb - auth = Bls::Core::Authorization::Basic.new(token: "1234") - api = Bls::Core::V2.new(authentication: auth) -======= auth = IntelligentFoods::Authorization::Basic.new(token: "1234") api = IntelligentFoods::V2.new(authentication: auth) ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb result = api.reset_authentication @@ -131,11 +89,7 @@ it "sets the environment" do config = build_config(environment: "preview") -<<<<<<< HEAD:spec/core/api/v2_spec.rb - result = Bls::Core::V2.build(config: config) -======= result = IntelligentFoods::V2.build(config: config) ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.environment).to eq("preview") end @@ -143,11 +97,7 @@ it "sets the username" do config = build_config(username: "secretUsername") -<<<<<<< HEAD:spec/core/api/v2_spec.rb - result = Bls::Core::V2.build(config: config) -======= result = IntelligentFoods::V2.build(config: config) ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.username).to eq("secretUsername") end @@ -155,11 +105,7 @@ it "sets the password" do config = build_config(password: "secretPassword") -<<<<<<< HEAD:spec/core/api/v2_spec.rb - result = Bls::Core::V2.build(config: config) -======= result = IntelligentFoods::V2.build(config: config) ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb expect(result.password).to eq("secretPassword") end @@ -172,17 +118,10 @@ def build_config(environment: "staging", username: "string", end def build_stubbed_authenticator(api) -<<<<<<< HEAD:spec/core/api/v2_spec.rb - authenticator = Bls::Core::V2::Authenticator.build(api) - authentication = authenticator.authentication - client = Bls::Core::ApiClient.new(authentication: authentication) - allow(Bls::Core::V2::Authenticator).to receive(:build). -======= authenticator = IntelligentFoods::V2::Authenticator.build(api) authentication = authenticator.authentication client = IntelligentFoods::ApiClient.new(authentication: authentication) allow(IntelligentFoods::V2::Authenticator).to receive(:build). ->>>>>>> c99e65f (Introduce Product Resource):spec/intelligent_foods/api/v2_spec.rb and_return(authenticator) allow(authenticator).to receive(:save!).and_return(client) authenticator From f952db0becd1b84e87137f51972460f8ab9b1e5d Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Tue, 11 Aug 2026 09:25:10 -0400 Subject: [PATCH 08/11] WIP updated product response JSON --- lib/bls/core/resources/product.rb | 12 +++---- spec/support/fixtures/product_response.json | 36 +++++++++++---------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/bls/core/resources/product.rb b/lib/bls/core/resources/product.rb index a8d8f7f..66886af 100644 --- a/lib/bls/core/resources/product.rb +++ b/lib/bls/core/resources/product.rb @@ -14,14 +14,14 @@ def object_name def self.build_from_response(data) product = build(data) product.ingredients = Ingredient.build_from_array(data[:ingredients]) - product.nutrition_facts = NutritionFact. - build_from_array(data[:nutrition_facts]) - product.nutrition_facts_cooked = NutritionFact. - build_from_array( - data.fetch(:nutrition_facts_cooked, []) - ) + as_packaged = NutritionFact. + build_from_array(data[:nutrition_facts][:as_packaged]) + as_cooked = NutritionFact. + build_from_array(data[:nutrition_facts][:as_cooked]) product.allergens = Allergen.build_from_array(data[:allergens]) product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) + product.nutrition_facts = { as_packaged: as_packaged, + as_cooked: as_cooked } product end end diff --git a/spec/support/fixtures/product_response.json b/spec/support/fixtures/product_response.json index c9c4fa7..89eba23 100644 --- a/spec/support/fixtures/product_response.json +++ b/spec/support/fixtures/product_response.json @@ -12,33 +12,35 @@ }, "ingredients": [ { - "inventory_code": "string", + "code": "string", "name": "string", "quantity": 0.1, "unit": "string" } ], - "nutrition_facts": [ - { - "code": "CALORIES", - "amount": 0, - "unit": "string" - } - ], - "nutrition_facts_cooked": [ - { - "code": "CALORIES", - "amount": 0, - "unit": "string" - } - ], + "nutrition_facts": { + "as_packaged": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ], + "as_cooked": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ] + }, "allergens": [ { "code": "MILK", - "source": "ANCHOVY" + "source": "NONE" } ], "dietary_tags": [ "PESCATARIAN" ] -} +} \ No newline at end of file From 8db3d7c4973c63c24b6bcf4c1660834bd4530cad Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Tue, 11 Aug 2026 09:34:48 -0400 Subject: [PATCH 09/11] WIP --- spec/core/resources/product_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/core/resources/product_spec.rb b/spec/core/resources/product_spec.rb index f394ffd..2aad42f 100644 --- a/spec/core/resources/product_spec.rb +++ b/spec/core/resources/product_spec.rb @@ -13,5 +13,18 @@ expect(result.code).to eq(product_id) end + + it "returns packaged and cooked nutrition facts" do + body = build_product_response + response = build_response(body: body) + stub_api_v2_authentication + stub_api_response response: response + + result = Bls::Core::Product.retrieve("TEST123") + + expect(result.nutrition_facts.keys).to match_array( + [:as_packaged, :as_cooked] + ) + end end end From 7b3591d880ab7809db5211dfdefb3ad635a0214f Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Wed, 12 Aug 2026 10:34:02 -0400 Subject: [PATCH 10/11] WIP --- spec/core/api/v2_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb index d00717e..80915dc 100644 --- a/spec/core/api/v2_spec.rb +++ b/spec/core/api/v2_spec.rb @@ -67,8 +67,8 @@ describe "reset authentication" do it "removes the present authentication" do - auth = IntelligentFoods::Authorization::Basic.new(token: "1234") - api = IntelligentFoods::V2.new(authentication: auth) + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) result = api.reset_authentication @@ -76,8 +76,8 @@ end it "is not authenticated" do - auth = IntelligentFoods::Authorization::Basic.new(token: "1234") - api = IntelligentFoods::V2.new(authentication: auth) + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) result = api.reset_authentication @@ -89,7 +89,7 @@ it "sets the environment" do config = build_config(environment: "preview") - result = IntelligentFoods::V2.build(config: config) + result = Bls::Core::V2.build(config: config) expect(result.environment).to eq("preview") end @@ -97,7 +97,7 @@ it "sets the username" do config = build_config(username: "secretUsername") - result = IntelligentFoods::V2.build(config: config) + result = Bls::Core::V2.build(config: config) expect(result.username).to eq("secretUsername") end @@ -105,7 +105,7 @@ it "sets the password" do config = build_config(password: "secretPassword") - result = IntelligentFoods::V2.build(config: config) + result = Bls::Core::V2.build(config: config) expect(result.password).to eq("secretPassword") end @@ -118,10 +118,10 @@ def build_config(environment: "staging", username: "string", end def build_stubbed_authenticator(api) - authenticator = IntelligentFoods::V2::Authenticator.build(api) + authenticator = Bls::Core::V2::Authenticator.build(api) authentication = authenticator.authentication - client = IntelligentFoods::ApiClient.new(authentication: authentication) - allow(IntelligentFoods::V2::Authenticator).to receive(:build). + client = Bls::Core::ApiClient.new(authentication: authentication) + allow(Bls::Core::V2::Authenticator).to receive(:build). and_return(authenticator) allow(authenticator).to receive(:save!).and_return(client) authenticator From dd091b3c8fef81391bb98e49315bbe286cbadf2f Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Wed, 12 Aug 2026 10:40:44 -0400 Subject: [PATCH 11/11] WIP as_cooked may not exist --- lib/bls/core/resources/product.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/bls/core/resources/product.rb b/lib/bls/core/resources/product.rb index 66886af..e3725eb 100644 --- a/lib/bls/core/resources/product.rb +++ b/lib/bls/core/resources/product.rb @@ -17,7 +17,8 @@ def self.build_from_response(data) as_packaged = NutritionFact. build_from_array(data[:nutrition_facts][:as_packaged]) as_cooked = NutritionFact. - build_from_array(data[:nutrition_facts][:as_cooked]) + build_from_array(data[:nutrition_facts]. + fetch(:as_cooked, [])) product.allergens = Allergen.build_from_array(data[:allergens]) product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) product.nutrition_facts = { as_packaged: as_packaged,