From 7c2847e8dfef7a4bcf7d4171d114251f4962bc78 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Fri, 8 May 2026 11:49:57 -0400 Subject: [PATCH] Introduce Product Resource The Core API supports retrieval of products by its Product ID. This PR introduces the new Product resource, along with child resources needed to build the main Product object. These child resources include Allergens, Ingredient, Dietary Tag. This change addresses the need by: * Introducing a CoreObject resource which can be used by all core (v2) objects * Introducing a Product resource --- lib/bls/core.rb | 7 +++ 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/object_v2.rb | 23 ++++++++++ lib/bls/core/resources/product.rb | 16 +++++++ lib/bls/core/resources/product_factory.rb | 45 ++++++++++++++++++++ lib/bls/core/testing/fake.rb | 35 +++++++++++++++ spec/core/resources/product_factory_spec.rb | 46 ++++++++++++++++++++ spec/core/resources/product_spec.rb | 30 +++++++++++++ spec/support/fixtures/product_response.json | 47 +++++++++++++++++++++ spec/support/helpers/api_helper.rb | 12 ++++++ 13 files changed, 289 insertions(+) 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/object_v2.rb create mode 100644 lib/bls/core/resources/product.rb create mode 100644 lib/bls/core/resources/product_factory.rb create mode 100644 spec/core/resources/product_factory_spec.rb create mode 100644 spec/core/resources/product_spec.rb create mode 100644 spec/support/fixtures/product_response.json diff --git a/lib/bls/core.rb b/lib/bls/core.rb index 52c5c75..453583a 100644 --- a/lib/bls/core.rb +++ b/lib/bls/core.rb @@ -17,13 +17,20 @@ require_relative "core/resources/api_error" require_relative "core/authorization/bearer" require_relative "core/resources/object" +require_relative "core/resources/object_v2" +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/product_factory" 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/object_v2.rb b/lib/bls/core/resources/object_v2.rb new file mode 100644 index 0000000..9e3c447 --- /dev/null +++ b/lib/bls/core/resources/object_v2.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..43a3200 --- /dev/null +++ b/lib/bls/core/resources/product.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Bls + module Core + class Product < Bls::Core::ObjectV2 + def object_name + "products" + end + + def self.build_from_response(data) + factory = ProductFactory.new(data) + factory.create + end + end + end +end diff --git a/lib/bls/core/resources/product_factory.rb b/lib/bls/core/resources/product_factory.rb new file mode 100644 index 0000000..7e01563 --- /dev/null +++ b/lib/bls/core/resources/product_factory.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Bls + module Core + class ProductFactory + attr_accessor :response + + def initialize(response) + @response = response + end + + def create + product = Product.build(response) + product.ingredients = ingredients + product.allergens = allergens + product.dietary_tags = dietary_tags + product.nutrition_facts = nutrition_facts + product + end + + protected + + def ingredients + Ingredient.build_from_array(response[:ingredients]) + end + + def allergens + Allergen.build_from_array(response[:allergens]) + end + + def dietary_tags + DietaryTag.build_from_array(response[:dietary_tags]) + end + + def nutrition_facts + as_packaged = NutritionFact. + build_from_array(response[:nutrition_facts][:as_packaged]) + as_cooked = NutritionFact. + build_from_array(response[:nutrition_facts]. + fetch(:as_cooked, [])) + { as_packaged: as_packaged, as_cooked: as_cooked } + end + end + end +end diff --git a/lib/bls/core/testing/fake.rb b/lib/bls/core/testing/fake.rb index 009adff..fa65b30 100644 --- a/lib/bls/core/testing/fake.rb +++ b/lib/bls/core/testing/fake.rb @@ -6,6 +6,26 @@ module Testing class Fake def self.configure(*); end + class Allergen + def self.create(code: "MILK", source: "ANCHOVY") + OpenStruct.new(code: code, source: source) + end + end + + class DietaryTag + def self.create(tag: "PESCATARIAN") + OpenStruct.new(tag: tag) + end + end + + class Ingredient + def self.create(code: "MILK", name: "ANCHOVY", quantity: 1, + unit: "serving") + OpenStruct.new(code: code, name: name, quantity: quantity, + unit: unit) + end + end + class InventoryLevel def self.create(sku: "SKU1", date: Date.today.to_s, distribution_center_name: "WEST_COAST", @@ -56,6 +76,12 @@ def initialize(menu_item) end end + class NutritionFact + def self.create(code: "CALORIES", amount: 0, unit: "serving") + OpenStruct.new(code: code, amount: amount, unit: unit) + 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, @@ -95,6 +121,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.create], + dietary_tags: [Fake::DietaryTag.create]) + end + end + def self.client Fake::Client.new end diff --git a/spec/core/resources/product_factory_spec.rb b/spec/core/resources/product_factory_spec.rb new file mode 100644 index 0000000..379beb3 --- /dev/null +++ b/spec/core/resources/product_factory_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::ProductFactory do + describe "#create" do + it "assigns the ingredients to the Product" do + response = build_product_response + ingredient = Bls::Core::Ingredient.build(response[:ingredients]) + factory = Bls::Core::ProductFactory.new(response) + + result = factory.create + + expect(result.ingredients).to match_array(ingredient) + end + + it "assigns the allergens to the Product" do + response = build_product_response + allergen = Bls::Core::Allergen.build_from_array(response[:allergens]) + factory = Bls::Core::ProductFactory.new(response) + + result = factory.create + + expect(result.allergens).to match_array(allergen) + end + + it "assigns the dietary tags to the Product" do + response = build_product_response + dietary_tags = Bls::Core::Allergen. + build_from_array(response[:dietary_tags]) + factory = Bls::Core::ProductFactory.new(response) + + result = factory.create + + expect(result.dietary_tags).to match_array(dietary_tags) + end + + it "assigns the nutrition facts to the Product" do + response = build_product_response + factory = Bls::Core::ProductFactory.new(response) + product = factory.create + + result = product.nutrition_facts + + expect(result.keys).to match_array([:as_packaged, :as_cooked]) + end + end +end diff --git a/spec/core/resources/product_spec.rb b/spec/core/resources/product_spec.rb new file mode 100644 index 0000000..30590ab --- /dev/null +++ b/spec/core/resources/product_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::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 = Bls::Core::Product.retrieve(product_id) + + expect(result.code).to eq(product_id) + end + end + + describe ".build_from_response" do + it "creates a product using the product factory" do + response = build_product_response + factory = Bls::Core::ProductFactory.new(response) + allow(Bls::Core::ProductFactory).to receive(:new).and_return(factory) + allow(factory).to receive(:create) + + Bls::Core::Product.build_from_response(response) + + expect(factory).to have_received(:create).once + 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..6a8e02e --- /dev/null +++ b/spec/support/fixtures/product_response.json @@ -0,0 +1,47 @@ +{ + "code": "string", + "name": "string", + "status": "DRAFT", + "refrigeration_type": "NONE", + "refrigeration_level": "STANDARD", + "unit_volume": { + "value": 0.1 + }, + "unit_slots": { + "value": 0.1 + }, + "ingredients": [ + { + "code": "string", + "name": "string", + "quantity": 0.1, + "unit": "string" + } + ], + "nutrition_facts": { + "as_packaged": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ], + "as_cooked": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ] + }, + "allergens": [ + { + "code": "MILK", + "source": "NONE" + } + ], + "dietary_tags": [ + "PESCATARIAN" + ] +} + diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index f7bb825..39e0f0d 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 } @@ -70,6 +72,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 +91,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]