Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
### Unreleased

* features
* Add product image management
* `ProductManagement::Image#delete`
* `ProductManagement::Image#sort`
* Add variation image management
* `ProductManagement::VariationImage#upload`
* `ProductManagement::VariationImage#upload_multiple`
* `ProductManagement::VariationImage#delete`
* `ProductManagement::VariationImage#sort`
* Add variation availability
* `ProductManagement::Availability#variation_availability`
* `ProductManagement::Availability#adjust_variation_stock_level`
* Add product attributes and variation properties
* `ProductManagement::Product#variation_properties`
* `ProductManagement::Product#assign_variation_differentiator`
* `ProductManagement::Product#create_custom_attribute`
* Add `Concerns::Connection#put_uri_list` for endpoints expecting a `text/uri-list` body

* bug-fixes
* Honour `paginated: false` in `ProductManagement::Image#all`,
`ProductManagement::Variation#all` and `ProductManagement::VariationImage#all`,
which previously returned only the first page
* Fix `AllPagesHandler` raising `NoMethodError` on an empty collection, which
carries no `_embedded` key
* Fix `Response#parsed_response` raising `NoMethodError` when the API answers
with a non-JSON body

### v0.24.2.pre

* features
Expand Down
10 changes: 7 additions & 3 deletions lib/beyond_api/all_pages_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ def initialize(session, url, params = {})

first_page_data = fetch_page(0)
@response = first_page_data
@resource_key = first_page_data[:embedded].keys.first
@total_pages = first_page_data.dig(:page, :total_pages)
@total_elements = first_page_data.dig(:page, :total_elements)
# An empty collection has no `_embedded` key at all, so there is no
# resource key to merge subsequent pages under.
@resource_key = first_page_data[:embedded]&.keys&.first
@total_pages = first_page_data.dig(:page, :total_pages).to_i
@total_elements = first_page_data.dig(:page, :total_elements).to_i
end

def call
return @response if @resource_key.nil?

(1..remaining_pages).each do |page|
process_page(page)
end
Expand Down
23 changes: 23 additions & 0 deletions lib/beyond_api/concerns/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ def upload_files(path, body, params = {})
end
end

# Issue a PUT with a `text/uri-list` body (newline-separated URIs).
# Used by image-sort endpoints which expect URIs rather than JSON.
# A dedicated connection is used because the standard `agent` always
# JSON-encodes the request body via the `:json` middleware.
def put_uri_list(path, uris)
handle_request do
connection = Faraday.new(url: @session.api_url, ssl: { verify: true }) do |faraday|
faraday.options.timeout = BeyondApi.configuration.timeout.to_i
faraday.options.open_timeout = BeyondApi.configuration.open_timeout.to_i
faraday.request :authorization, *authorization_config
faraday.headers['Accept'] = 'application/json'
faraday.request :retry, BeyondApi.configuration.retry_options
faraday.response :json, content_type: 'application/json'
faraday.response :logger, *logger_config { |logger| apply_filters(logger) }
end

connection.put(path) do |request|
request.headers['Content-Type'] = 'text/uri-list'
request.body = uris.join("\n")
end
end
end

private

def parse_request(hash)
Expand Down
3 changes: 3 additions & 0 deletions lib/beyond_api/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def parse

def parsed_response
return {} if body.blank?
# A non-JSON response (e.g. an upload endpoint answering `text/plain`) is
# left untouched by faraday's :json middleware, so `body` is a raw String.
return body unless body.respond_to?(:deep_transform_keys!)

body.deep_transform_keys! do |key|
key = remove_initial_underscore(key)
Expand Down
34 changes: 34 additions & 0 deletions lib/beyond_api/services/product_management/availability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ def enable_purchasability(id)
def disable_purchasability(id)
post("products/#{id}/availability/disable-purchasability")
end

# Retrieve the availability of a variation of a variation product.
#
# @see https://developer.epages.com/beyond-docs/#show_variation_availability_details
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
#
# @return [Hash]
#
# @example
# @client.variation_availability('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')
def variation_availability(product_id, variation_id)
get("products/#{product_id}/variations/#{variation_id}/availability")
end

# Adjust the available stock of a variation.
#
# @see https://developer.epages.com/beyond-docs/#adjust_variation_stock_level
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
# @param relative_amount [Integer] the relative amount to change the available stock by
#
# @return [Hash]
#
# @example
# @client.adjust_variation_stock_level('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', -1)
def adjust_variation_stock_level(product_id, variation_id, relative_amount)
post("products/#{product_id}/variations/#{variation_id}/availability/adjust-available-stock",
relative_amount:)
end
end
end
end
38 changes: 37 additions & 1 deletion lib/beyond_api/services/product_management/image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Image < BaseService
#
# @see https://developer.epages.com/beyond-docs/#list_product_images
#
# @option params [Boolean] :paginated
# @option params [Integer] :size the page size
# @option params [Integer] :page the page number
#
Expand All @@ -17,7 +18,7 @@ class Image < BaseService
# @example
# @client.all(size: 100, page: 0)
def all(id, params = {})
get("products/#{id}/images", params)
fetch_all_pages("products/#{id}/images", params)
end

# Upload an image and add it to a product. The body of the request must contain the content of the image.
Expand Down Expand Up @@ -80,6 +81,41 @@ def upload_external(product_id, uri, file_name = '')

post("products/#{product_id}/external-images", { data_uri: uri }, { file_name: })
end

# Delete a product image.
#
# @see https://developer.epages.com/beyond-docs/#delete_product_image
#
# @param product_id [String] the product UUID
# @param image_id [String] the image UUID
#
# @return [nil]
#
# @example
# @client.delete('4125b993-49fc-47c8-b9b3-76d8871e4e06', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')
def delete(product_id, image_id)
super("products/#{product_id}/images/#{image_id}") # Concerns::Connection delete method
end

# Sort the images of a product. The API expects a `text/uri-list` body
# with the absolute URIs of the images in the desired order.
#
# @see https://developer.epages.com/beyond-docs/#sort_product_images
#
# @param product_id [String] the product UUID
# @param image_ids [Array<String>] the image UUIDs in the desired order
#
# @return [Hash]
#
# @example
# @client.sort('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# ['a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'b2c3d4e5-f678-90ab-cdef-1234567890ab'])
def sort(product_id, image_ids)
uris = Array(image_ids).compact.map do |image_id|
"#{@session.api_url}/products/#{product_id}/images/#{image_id}"
end
put_uri_list("products/#{product_id}/images", uris)
end
end
end
end
46 changes: 46 additions & 0 deletions lib/beyond_api/services/product_management/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,52 @@ def remove_tags_from_products(ids, tags)
def update_variation_properties(id, body)
patch("products/#{id}/variation-properties", body)
end

# Retrieve the variation properties of a product.
#
# @see https://developer.epages.com/beyond-docs/#show_variation_properties
#
# @param id [String] the product UUID
#
# @return [Hash]
#
# @example
# @client.variation_properties('4125b993-49fc-47c8-b9b3-76d8871e4e06')
def variation_properties(id)
get("products/#{id}/variation-properties")
end

# Assign a variation attribute as the differentiator of a product.
#
# @see https://developer.epages.com/beyond-docs/#assign_variation_differentiator
#
# @param product_id [String] the product UUID
# @param variation_attribute_id [String] the variation attribute UUID
#
# @return [Hash]
#
# @example
# @client.assign_variation_differentiator('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')
def assign_variation_differentiator(product_id, variation_attribute_id)
post("products/#{product_id}/variation-attributes/#{variation_attribute_id}/make-differentiator")
end

# Create a custom attribute for a product.
#
# @see https://developer.epages.com/beyond-docs/#create_product_attribute
#
# @param product_id [String] the product UUID
# @param body [Hash] the request body containing the attribute definition
#
# @return [Hash]
#
# @example
# body = { type: 'material', value: 'cotton' }
# @client.create_custom_attribute('4125b993-49fc-47c8-b9b3-76d8871e4e06', body)
def create_custom_attribute(product_id, body)
post("products/#{product_id}/attributes", body)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/beyond_api/services/product_management/variation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Variation < BaseService
# @example
# @client.all(size: 100, page: 0)
def all(id, params = {})
get("products/#{id}/variations", params)
fetch_all_pages("products/#{id}/variations", params)
end

# Update a variation partially with json content type.
Expand Down
93 changes: 91 additions & 2 deletions lib/beyond_api/services/product_management/variation_image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class VariationImage < BaseService
#
# @example
# @client.all(size: 100, page: 0)
def all(product_id, variation_id, _params = {})
get("products/#{product_id}/variations/#{variation_id}/images")
def all(product_id, variation_id, params = {})
fetch_all_pages("products/#{product_id}/variations/#{variation_id}/images", params)
end

# A POST request is used to upload an image from an external resource and add it to a variation. The body of the request must contain a single URI of the image to be uploaded.
Expand All @@ -44,6 +44,95 @@ def upload_external(product_id, variation_id, uri, file_name = '')

post("products/#{product_id}/variations/#{variation_id}/external-images", { data_uri: uri }, { file_name: })
end

# Upload an image and add it to a variation. The body of the request must contain the content of the image.
# The maximum image size is 7000 x 7000 px, and the maximum file size per image is 8 MB.
#
# @see https://developer.epages.com/beyond-docs/#upload_variation_image
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
# @param image_path [String] the image path
# @param image_name [String] the image file name
#
# @return [Hash]
#
# @example
# @client.upload('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
# '/home/epages/file.png', 'file.png')
def upload(product_id, variation_id, image_path, image_name)
upload_file("products/#{product_id}/variations/#{variation_id}/images",
image_path,
Utils.file_content_type(image_path),
file_name: image_name)
end

# Upload multiple images and add them to a variation. Each image is uploaded
# in a separate request because the variation image endpoint accepts a single
# raw binary body per call.
#
# @see https://developer.epages.com/beyond-docs/#upload_variation_image
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
# @param image_paths [Array<String>] an array of image paths
# @param image_names [Array<String>] an array of image file names
#
# @return [Array<Hash>] one response per uploaded image
#
# @example
# image_paths = ['/home/epages/file1.png', '/home/epages/file2.png']
# image_names = ['file1.png', 'file2.png']
# @client.upload_multiple('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
# image_paths, image_names)
def upload_multiple(product_id, variation_id, image_paths, image_names)
image_paths.each_with_index.map do |path, index|
upload(product_id, variation_id, path, image_names[index])
end
end

# Delete a variation image.
#
# @see https://developer.epages.com/beyond-docs/#delete_variation_image
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
# @param image_id [String] the image UUID
#
# @return [nil]
#
# @example
# @client.delete('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
# 'b2c3d4e5-f678-90ab-cdef-1234567890ab')
def delete(product_id, variation_id, image_id)
# Concerns::Connection delete method
super("products/#{product_id}/variations/#{variation_id}/images/#{image_id}")
end

# Sort the images of a variation. The API expects a `text/uri-list` body
# with the absolute URIs of the images in the desired order.
#
# @see https://developer.epages.com/beyond-docs/#sort_variation_images
#
# @param product_id [String] the product UUID
# @param variation_id [String] the variation UUID
# @param image_ids [Array<String>] the image UUIDs in the desired order
#
# @return [Hash]
#
# @example
# @client.sort('4125b993-49fc-47c8-b9b3-76d8871e4e06',
# 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
# ['b2c3d4e5-f678-90ab-cdef-1234567890ab', 'c3d4e5f6-7890-abcd-ef1234567890ab'])
def sort(product_id, variation_id, image_ids)
uris = Array(image_ids).compact.map do |image_id|
"#{@session.api_url}/products/#{product_id}/variations/#{variation_id}/images/#{image_id}"
end
put_uri_list("products/#{product_id}/variations/#{variation_id}/images", uris)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

RSpec.describe BeyondApi::ProductManagement::Availability, :stubbed_requests do
let(:client) { stubbed_client }
let(:product_id) { '4125b993-49fc-47c8-b9b3-76d8871e4e06' }
let(:variation_id) { 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }

describe '.variation_availability' do
it 'GETs the variation availability resource' do
requests = record_requests

client.variation_availability(product_id, variation_id)

expect(requests.first.method).to eq(:get)
expect(requests.first.uri.path).to eq("/api/products/#{product_id}/variations/#{variation_id}/availability")
end
end

describe '.adjust_variation_stock_level' do
it 'POSTs the relative amount camelized' do
requests = record_requests

client.adjust_variation_stock_level(product_id, variation_id, -1)

request = requests.first
expect(request.method).to eq(:post)
expect(request.uri.path).to eq(
"/api/products/#{product_id}/variations/#{variation_id}/availability/adjust-available-stock"
)
expect(JSON.parse(request.body)).to eq('relativeAmount' => -1)
end
end
end
Loading