From 3adadd1b5337c00b4ec253936e2633fcb02d3003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:43:29 +0200 Subject: [PATCH] feat(inventory): add Inventory endpoint family Implements the new agentic:inventory OAuth-scoped endpoints: stock adjustments, atomic multi-variant reservations (create/get/commit/release), stock levels (get/set), and beta product knowledge (get/set/delete). Responses stay untyped Hash and errors surface via CheckoutApiException, consistent with every other domain in this SDK. --- lib/checkout_sdk.rb | 1 + lib/checkout_sdk/checkout_api.rb | 6 +- lib/checkout_sdk/inventory/inventory.rb | 9 ++ .../inventory/inventory_adjustment_request.rb | 24 +++ .../inventory/inventory_client.rb | 149 ++++++++++++++++++ lib/checkout_sdk/inventory/inventory_money.rb | 19 +++ .../inventory/inventory_reservation_item.rb | 20 +++ .../inventory_reservation_request.rb | 27 ++++ .../inventory/inventory_set_levels_request.rb | 22 +++ .../inventory_set_product_request.rb | 139 ++++++++++++++++ ...y_adjustment_request_serialization_spec.rb | 25 +++ .../inventory/inventory_client_spec.rb | 117 ++++++++++++++ ..._reservation_request_serialization_spec.rb | 47 ++++++ ...y_set_levels_request_serialization_spec.rb | 25 +++ ..._set_product_request_serialization_spec.rb | 123 +++++++++++++++ 15 files changed, 752 insertions(+), 1 deletion(-) create mode 100644 lib/checkout_sdk/inventory/inventory.rb create mode 100644 lib/checkout_sdk/inventory/inventory_adjustment_request.rb create mode 100644 lib/checkout_sdk/inventory/inventory_client.rb create mode 100644 lib/checkout_sdk/inventory/inventory_money.rb create mode 100644 lib/checkout_sdk/inventory/inventory_reservation_item.rb create mode 100644 lib/checkout_sdk/inventory/inventory_reservation_request.rb create mode 100644 lib/checkout_sdk/inventory/inventory_set_levels_request.rb create mode 100644 lib/checkout_sdk/inventory/inventory_set_product_request.rb create mode 100644 spec/checkout_sdk/inventory/inventory_adjustment_request_serialization_spec.rb create mode 100644 spec/checkout_sdk/inventory/inventory_client_spec.rb create mode 100644 spec/checkout_sdk/inventory/inventory_reservation_request_serialization_spec.rb create mode 100644 spec/checkout_sdk/inventory/inventory_set_levels_request_serialization_spec.rb create mode 100644 spec/checkout_sdk/inventory/inventory_set_product_request_serialization_spec.rb diff --git a/lib/checkout_sdk.rb b/lib/checkout_sdk.rb index 5358f88..2ebd05f 100644 --- a/lib/checkout_sdk.rb +++ b/lib/checkout_sdk.rb @@ -75,6 +75,7 @@ require 'checkout_sdk/network_tokens/network_tokens' require 'checkout_sdk/payment_methods/payment_methods' require 'checkout_sdk/identities/identities' +require 'checkout_sdk/inventory/inventory' # Checkout modules (previous) require 'checkout_sdk/sources/sources' diff --git a/lib/checkout_sdk/checkout_api.rb b/lib/checkout_sdk/checkout_api.rb index 78fb5a7..eb53456 100644 --- a/lib/checkout_sdk/checkout_api.rb +++ b/lib/checkout_sdk/checkout_api.rb @@ -79,6 +79,8 @@ module CheckoutSdk # @return [CheckoutSdk::Payments::ApplePayClient] # @!attribute google_pay # @return [CheckoutSdk::Payments::GooglePayClient] + # @!attribute inventory + # @return [CheckoutSdk::Inventory::InventoryClient] class CheckoutApi attr_reader :customers, :disputes, @@ -118,7 +120,8 @@ class CheckoutApi :identity_verification, :face_authentication, :apple_pay, - :google_pay + :google_pay, + :inventory # @param [CheckoutConfiguration] configuration def initialize(configuration) @@ -168,6 +171,7 @@ def initialize(configuration) CheckoutSdk::Identities::FaceAuthentication::FaceAuthenticationClient.new(api_client, configuration) @apple_pay = CheckoutSdk::Payments::ApplePayClient.new(api_client, configuration) @google_pay = CheckoutSdk::Payments::GooglePayClient.new(api_client, configuration) + @inventory = CheckoutSdk::Inventory::InventoryClient.new(api_client, configuration) end private diff --git a/lib/checkout_sdk/inventory/inventory.rb b/lib/checkout_sdk/inventory/inventory.rb new file mode 100644 index 0000000..521bcd6 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'checkout_sdk/inventory/inventory_money' +require 'checkout_sdk/inventory/inventory_adjustment_request' +require 'checkout_sdk/inventory/inventory_reservation_item' +require 'checkout_sdk/inventory/inventory_reservation_request' +require 'checkout_sdk/inventory/inventory_set_levels_request' +require 'checkout_sdk/inventory/inventory_set_product_request' +require 'checkout_sdk/inventory/inventory_client' diff --git a/lib/checkout_sdk/inventory/inventory_adjustment_request.rb b/lib/checkout_sdk/inventory/inventory_adjustment_request.rb new file mode 100644 index 0000000..903be4f --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_adjustment_request.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # Request body for POST /inventory/adjustments. Mirrors swagger + # `InventoryAdjustmentRequest`. + # + # @!attribute variant_id + # @return [String] The identifier of the variant to adjust. The variant must already + # exist. [Required] max 128 characters. + # @!attribute delta + # @return [Integer] The signed change to apply to on_hand. A negative delta that would + # drive on_hand below zero is rejected with a 409. Per spec, must be non-zero (not a + # formal schema constraint). [Required] + # @!attribute reason + # @return [String] A free-text reason recorded in the ledger. Must not contain personal + # data. [Required] min 1 character, max 256 characters. + class InventoryAdjustmentRequest + attr_accessor :variant_id, + :delta, + :reason + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_client.rb b/lib/checkout_sdk/inventory/inventory_client.rb new file mode 100644 index 0000000..6595b11 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_client.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # Client for the Inventory API: stock levels, atomic multi-variant reservations + # (hold/commit/release), stock adjustments, and per-variant "product knowledge" + # merchandising metadata for AI agents. + # + # All 10 operations require OAuth with the agentic:inventory scope (see + # {CheckoutSdk::OAuthScopes::AGENTIC_INVENTORY}); unlike most domains in this SDK, there is + # no ApiSecretKey/ApiPublicKey fallback, so this client always authorizes with + # {CheckoutSdk::AuthorizationType::OAUTH} (the same pattern already used by + # {CheckoutSdk::Payments::GooglePayClient}). + # + # Responses are returned as Hash (OpenStruct), per this SDK's convention (see e.g. + # {CheckoutSdk::Balances::BalancesClient}); there are no typed InventoryLevels, + # InventoryReservation or InventoryProductKnowledge response classes. Error responses + # (404/409/422) surface as {CheckoutSdk::CheckoutApiException}; this SDK has no per-domain + # typed error response classes, so `InventoryErrorResponse`'s fields + # (request_id, error_type, error_codes, variant_id, available) are available generically via + # `error.error_details`, the same as every other domain. + # + # getInventoryProduct, setInventoryProduct and deleteInventoryProduct are marked Beta in the + # specification. + class InventoryClient < Client + INVENTORY = 'inventory' + ADJUSTMENTS = 'adjustments' + RESERVATIONS = 'reservations' + COMMIT = 'commit' + RELEASE = 'release' + PRODUCT = 'product' + private_constant :INVENTORY, :ADJUSTMENTS, :RESERVATIONS, :COMMIT, :RELEASE, :PRODUCT + + # @param [ApiClient] api_client + # @param [CheckoutConfiguration] configuration + def initialize(api_client, configuration) + super(api_client, configuration, CheckoutSdk::AuthorizationType::OAUTH) + end + + # Apply a signed stock adjustment to a variant's on_hand quantity, recording the reason in + # the ledger. Returns 201, or 200 with a Cache-Control response header on an idempotent + # replay; both share the InventoryLevels response schema. + # + # @param [Hash, InventoryAdjustmentRequest] adjustment_request + # @param [String, nil] idempotency_key Optional. Cko-Idempotency-Key request header. + # @return [Hash] the InventoryLevels response + def adjust_inventory(adjustment_request, idempotency_key = nil) + api_client.invoke_post( + build_path(INVENTORY, ADJUSTMENTS), + sdk_authorization, + adjustment_request, + idempotency_key + ) + end + + # Create an atomic multi-variant hold. Returns 201, or 200 with a Cache-Control response + # header on an idempotent replay; both share the InventoryReservation response schema. + # + # @param [Hash, InventoryReservationRequest] reservation_request + # @param [String, nil] idempotency_key Optional. Cko-Idempotency-Key request header. + # @return [Hash] the InventoryReservation response + def create_inventory_reservation(reservation_request, idempotency_key = nil) + api_client.invoke_post( + build_path(INVENTORY, RESERVATIONS), + sdk_authorization, + reservation_request, + idempotency_key + ) + end + + # Retrieve a reservation by ID. + # + # @param [String] reservation_id + # @return [Hash] the InventoryReservation response + def get_inventory_reservation(reservation_id) + api_client.invoke_get(build_path(INVENTORY, RESERVATIONS, reservation_id), sdk_authorization) + end + + # Commit a held reservation, converting the hold into a permanent deduction. Takes no + # request body. + # + # @param [String] reservation_id + # @return [Hash] the InventoryReservation response + def commit_inventory_reservation(reservation_id) + api_client.invoke_post( + build_path(INVENTORY, RESERVATIONS, reservation_id, COMMIT), + sdk_authorization + ) + end + + # Release a held reservation, returning the reserved quantity to available stock. Takes + # no request body. + # + # @param [String] reservation_id + # @return [Hash] the InventoryReservation response + def release_inventory_reservation(reservation_id) + api_client.invoke_post( + build_path(INVENTORY, RESERVATIONS, reservation_id, RELEASE), + sdk_authorization + ) + end + + # Retrieve the stock levels for a variant. + # + # @param [String] variant_id + # @param [Boolean, nil] expand_product Optional. When true, adds `?expand=product` so the + # response embeds the variant's InventoryProductKnowledge (when it exists) under + # `product`. + # @return [Hash] the InventoryLevels response + def get_inventory_levels(variant_id, expand_product: false) + params = { expand: 'product' } if expand_product + api_client.invoke_get(build_path(INVENTORY, variant_id), sdk_authorization, params) + end + + # Create or replace the stock levels for a variant. + # + # @param [String] variant_id + # @param [Hash, InventorySetLevelsRequest] set_levels_request + # @return [Hash] the InventoryLevels response + def set_inventory_levels(variant_id, set_levels_request) + api_client.invoke_put(build_path(INVENTORY, variant_id), sdk_authorization, set_levels_request) + end + + # Retrieve the product knowledge (merchandising metadata) for a variant. Beta. + # + # @param [String] variant_id + # @return [Hash] the InventoryProductKnowledge response + def get_inventory_product(variant_id) + api_client.invoke_get(build_path(INVENTORY, variant_id, PRODUCT), sdk_authorization) + end + + # Create or replace the product knowledge for a variant. Beta. + # + # @param [String] variant_id + # @param [Hash, InventorySetProductRequest] set_product_request + # @return [Hash] the InventoryProductKnowledge response + def set_inventory_product(variant_id, set_product_request) + api_client.invoke_put(build_path(INVENTORY, variant_id, PRODUCT), sdk_authorization, set_product_request) + end + + # Delete the product knowledge for a variant. Beta. Returns 204 with no body. + # + # @param [String] variant_id + def delete_inventory_product(variant_id) + api_client.invoke_delete(build_path(INVENTORY, variant_id, PRODUCT), sdk_authorization) + end + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_money.rb b/lib/checkout_sdk/inventory/inventory_money.rb new file mode 100644 index 0000000..ff46430 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_money.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # A monetary amount in an inventory product's price or sale_price. Mirrors swagger + # `InventoryMoney`. Used by {InventorySetProductRequest#price} and + # {InventorySetProductRequest#sale_price}. + # + # @!attribute amount + # @return [Integer] The amount in the currency's minor unit. [Required] + # @!attribute currency + # @return [String] The 3-letter ISO 4217 currency code. [Required] min 3 characters, + # max 3 characters. + class InventoryMoney + attr_accessor :amount, + :currency + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_reservation_item.rb b/lib/checkout_sdk/inventory/inventory_reservation_item.rb new file mode 100644 index 0000000..2192733 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_reservation_item.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # A single line item within an inventory reservation. Mirrors swagger + # `InventoryReservationItem`. Used by {InventoryReservationRequest#items} and present in the + # `InventoryReservation` response's `items` array (returned as a Hash, not deserialized into + # this class, per this SDK's response convention). + # + # @!attribute variant_id + # @return [String] The identifier of the variant to reserve. The variant must already + # exist. [Required] max 128 characters. + # @!attribute quantity + # @return [Integer] The quantity to reserve. [Required] min 1. + class InventoryReservationItem + attr_accessor :variant_id, + :quantity + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_reservation_request.rb b/lib/checkout_sdk/inventory/inventory_reservation_request.rb new file mode 100644 index 0000000..1914a00 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_reservation_request.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # Request body for POST /inventory/reservations. Mirrors swagger + # `InventoryReservationRequest`. + # + # @!attribute owner_type + # @return [String] Free-text classification of the reservation's owner (e.g. an agent or + # order type). [Required] max 64 characters. + # @!attribute owner_reference + # @return [String] A reference identifying the specific owner, echoed back on the + # reservation. [Required] max 256 characters. + # @!attribute items + # @return [Array] The variants and quantities to hold. + # [Required] min 1 item, max 45 items. variant_id must be unique within the request. + # @!attribute ttl_seconds + # @return [Integer] How long the hold stays active before it expires. [Optional] + # min 60, max 3600. Default: 900. + class InventoryReservationRequest + attr_accessor :owner_type, + :owner_reference, + :items, + :ttl_seconds + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_set_levels_request.rb b/lib/checkout_sdk/inventory/inventory_set_levels_request.rb new file mode 100644 index 0000000..c327694 --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_set_levels_request.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # Request body for PUT /inventory/{variant_id}. Mirrors swagger + # `InventorySetLevelsRequest`. + # + # @!attribute on_hand + # @return [Integer] The physical stock to set for the variant. [Required] min 0. + # @!attribute safety_stock + # @return [Integer] The buffer withheld from sale. [Optional] min 0. Defaults to 0 on + # create; left unchanged on update if omitted. + # @!attribute reason + # @return [String] A free-text reason recorded in the ledger. Must not contain personal + # data. [Optional] max 256 characters. + class InventorySetLevelsRequest + attr_accessor :on_hand, + :safety_stock, + :reason + end + end +end diff --git a/lib/checkout_sdk/inventory/inventory_set_product_request.rb b/lib/checkout_sdk/inventory/inventory_set_product_request.rb new file mode 100644 index 0000000..a495d7b --- /dev/null +++ b/lib/checkout_sdk/inventory/inventory_set_product_request.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Inventory + # Request body for PUT /inventory/{variant_id}/product (Beta). Mirrors swagger + # `InventorySetProductRequest`, the "product knowledge" merchandising metadata used by AI + # agents. Shares its optional fields with the `InventoryProductKnowledge` response schema, + # which this SDK does not model as a class because responses are returned as Hash (see + # {InventoryClient}). + # + # @!attribute title + # @return [String] The product title. [Required] max 512 characters. + # @!attribute description + # @return [String] The product description. [Required] max 4000 characters. + # @!attribute product_url + # @return [String] The URL of the product page. [Required] max 2048 characters. + # @!attribute image_url + # @return [String] The URL of the primary product image. [Required] max 2048 characters. + # @!attribute additional_image_urls + # @return [Array] Additional product image URLs. [Optional] + # @!attribute video_url + # @return [String] The URL of a product video. [Optional] + # @!attribute model_3d_url + # @return [String] The URL of a 3D model of the product. [Optional] + # @!attribute sku + # @return [String] The merchant's SKU for the variant. [Optional] max 128 characters. + # @!attribute gtin + # @return [String] The Global Trade Item Number. [Optional] + # @!attribute mpn + # @return [String] The Manufacturer Part Number. [Optional] + # @!attribute brand + # @return [String] The product's brand. [Optional] + # @!attribute category + # @return [String] The product's category. [Optional] + # @!attribute price + # @return [InventoryMoney] The product's list price. [Optional] When set together with + # sale_price, sale_price must share its currency and be less than or equal to it. + # @!attribute sale_price + # @return [InventoryMoney] The product's discounted price. [Optional] Must share price's + # currency and be less than or equal to it. + # @!attribute sale_price_starts_at + # @return [Time] When the sale price becomes active. [Optional] Pairs with sale_price. + # Format: date-time (RFC 3339). + # @!attribute sale_price_ends_at + # @return [Time] When the sale price stops being active. [Optional] Pairs with + # sale_price. Format: date-time (RFC 3339). + # @!attribute group_id + # @return [String] Identifies a group of related variants (e.g. the same product in + # different colors/sizes). [Optional] Per spec (not a formal schema constraint), when + # set, color and size are both required. + # @!attribute group_title + # @return [String] The title shared by the variant group. [Optional] + # @!attribute color + # @return [String] The variant's color. [Optional] Required when group_id is set (spec + # text, not a formal constraint). + # @!attribute size + # @return [String] The variant's size. [Optional] Required when group_id is set (spec + # text, not a formal constraint). + # @!attribute size_system + # @return [String] The sizing system the size value uses. [Optional] + # @!attribute gender + # @return [String] The target gender for the product. [Optional] + # @!attribute condition + # @return [String] The product's condition. [Optional] Enum: "new" "used" "refurbished". + # Exact lowercase match. Default: "new". + # @!attribute material + # @return [String] The product's material. [Optional] + # @!attribute age_group + # @return [String] The target age group for the product. [Optional] + # @!attribute length + # @return [Float] The product's length. [Optional] + # @!attribute width + # @return [Float] The product's width. [Optional] + # @!attribute height + # @return [Float] The product's height. [Optional] + # @!attribute dimension_unit + # @return [String] The unit used for length, width and height. [Optional] + # @!attribute weight + # @return [Float] The product's weight. [Optional] + # @!attribute weight_unit + # @return [String] The unit used for weight. [Optional] + # @!attribute expiration_date + # @return [Time] The product's expiration date. [Optional] Format: date-time + # (RFC 3339). + # @!attribute harmonized_system_code + # @return [String] The Harmonized System code used for customs. [Optional] + # @!attribute country_of_origin + # @return [String] The product's country of origin. [Optional] 2-letter ISO 3166-1 + # alpha-2 code. + # @!attribute seller_name + # @return [String] The seller's name. [Optional] + # @!attribute seller_url + # @return [String] The seller's URL. [Optional] + # @!attribute seller_privacy_policy + # @return [String] The URL of the seller's privacy policy. [Optional] + # @!attribute seller_tos + # @return [String] The URL of the seller's terms of service. [Optional] + class InventorySetProductRequest + attr_accessor :title, + :description, + :product_url, + :image_url, + :additional_image_urls, + :video_url, + :model_3d_url, + :sku, + :gtin, + :mpn, + :brand, + :category, + :price, + :sale_price, + :sale_price_starts_at, + :sale_price_ends_at, + :group_id, + :group_title, + :color, + :size, + :size_system, + :gender, + :condition, + :material, + :age_group, + :length, + :width, + :height, + :dimension_unit, + :weight, + :weight_unit, + :expiration_date, + :harmonized_system_code, + :country_of_origin, + :seller_name, + :seller_url, + :seller_privacy_policy, + :seller_tos + end + end +end diff --git a/spec/checkout_sdk/inventory/inventory_adjustment_request_serialization_spec.rb b/spec/checkout_sdk/inventory/inventory_adjustment_request_serialization_spec.rb new file mode 100644 index 0000000..7a442fe --- /dev/null +++ b/spec/checkout_sdk/inventory/inventory_adjustment_request_serialization_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe 'InventoryAdjustmentRequest serialization' do + it 'serializes all properties (roundtrip)' do + request = CheckoutSdk::Inventory::InventoryAdjustmentRequest.new + request.variant_id = 'var_1' + request.delta = -3 + request.reason = 'damaged in transit' + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(request) + + expect(hash['variant_id']).to eq('var_1') + expect(hash['delta']).to eq(-3) + expect(hash['reason']).to eq('damaged in transit') + end + + it 'deserializes the swagger example' do + json = '{"variant_id":"var_1","delta":-3,"reason":"damaged in transit"}' + parsed = JSON.parse(json, object_class: OpenStruct) + + expect(parsed.variant_id).to eq('var_1') + expect(parsed.delta).to eq(-3) + expect(parsed.reason).to eq('damaged in transit') + end +end diff --git a/spec/checkout_sdk/inventory/inventory_client_spec.rb b/spec/checkout_sdk/inventory/inventory_client_spec.rb new file mode 100644 index 0000000..d675f36 --- /dev/null +++ b/spec/checkout_sdk/inventory/inventory_client_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +RSpec.describe CheckoutSdk::Inventory do + let(:credentials_mock) { double('credentials') } + let(:api_client_mock) { double('api_client') } + let(:configuration_mock) { double('configuration') } + let(:client) { CheckoutSdk::Inventory::InventoryClient.new(api_client_mock, configuration_mock) } + + before do + allow(configuration_mock).to receive(:credentials).and_return(credentials_mock) + allow(credentials_mock).to receive(:get_authorization).and_return('oauth') + end + + describe '#adjust_inventory' do + it 'POSTs typed DTO to inventory/adjustments with idempotency_key' do + request = CheckoutSdk::Inventory::InventoryAdjustmentRequest.new + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/adjustments', 'oauth', request, 'idem-1').and_return('r') + expect(client.adjust_inventory(request, 'idem-1')).to eq('r') + end + + it 'POSTs without idempotency_key when omitted' do + request = CheckoutSdk::Inventory::InventoryAdjustmentRequest.new + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/adjustments', 'oauth', request, nil).and_return('r') + expect(client.adjust_inventory(request)).to eq('r') + end + + it 'also accepts a raw Hash' do + hash_request = { 'variant_id' => 'var_1', 'delta' => -1, 'reason' => 'damaged' } + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/adjustments', 'oauth', hash_request, nil).and_return('r') + expect(client.adjust_inventory(hash_request)).to eq('r') + end + end + + describe '#create_inventory_reservation' do + it 'POSTs typed DTO to inventory/reservations with idempotency_key' do + request = CheckoutSdk::Inventory::InventoryReservationRequest.new + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/reservations', 'oauth', request, 'idem-2').and_return('r') + expect(client.create_inventory_reservation(request, 'idem-2')).to eq('r') + end + end + + describe '#get_inventory_reservation' do + it 'GETs inventory/reservations/{id}' do + expect(api_client_mock).to receive(:invoke_get) + .with('inventory/reservations/rsv_1', 'oauth').and_return('r') + expect(client.get_inventory_reservation('rsv_1')).to eq('r') + end + end + + describe '#commit_inventory_reservation' do + it 'POSTs to inventory/reservations/{id}/commit with no body' do + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/reservations/rsv_1/commit', 'oauth').and_return('r') + expect(client.commit_inventory_reservation('rsv_1')).to eq('r') + end + end + + describe '#release_inventory_reservation' do + it 'POSTs to inventory/reservations/{id}/release with no body' do + expect(api_client_mock).to receive(:invoke_post) + .with('inventory/reservations/rsv_1/release', 'oauth').and_return('r') + expect(client.release_inventory_reservation('rsv_1')).to eq('r') + end + end + + describe '#get_inventory_levels' do + it 'GETs inventory/{variant_id} with no params by default' do + expect(api_client_mock).to receive(:invoke_get) + .with('inventory/var_1', 'oauth', nil).and_return('r') + expect(client.get_inventory_levels('var_1')).to eq('r') + end + + it 'GETs inventory/{variant_id} with expand=product when requested' do + expect(api_client_mock).to receive(:invoke_get) + .with('inventory/var_1', 'oauth', { expand: 'product' }).and_return('r') + expect(client.get_inventory_levels('var_1', expand_product: true)).to eq('r') + end + end + + describe '#set_inventory_levels' do + it 'PUTs typed DTO to inventory/{variant_id}' do + request = CheckoutSdk::Inventory::InventorySetLevelsRequest.new + expect(api_client_mock).to receive(:invoke_put) + .with('inventory/var_1', 'oauth', request).and_return('r') + expect(client.set_inventory_levels('var_1', request)).to eq('r') + end + end + + describe '#get_inventory_product' do + it 'GETs inventory/{variant_id}/product' do + expect(api_client_mock).to receive(:invoke_get) + .with('inventory/var_1/product', 'oauth').and_return('r') + expect(client.get_inventory_product('var_1')).to eq('r') + end + end + + describe '#set_inventory_product' do + it 'PUTs typed DTO to inventory/{variant_id}/product' do + request = CheckoutSdk::Inventory::InventorySetProductRequest.new + expect(api_client_mock).to receive(:invoke_put) + .with('inventory/var_1/product', 'oauth', request).and_return('r') + expect(client.set_inventory_product('var_1', request)).to eq('r') + end + end + + describe '#delete_inventory_product' do + it 'DELETEs inventory/{variant_id}/product' do + expect(api_client_mock).to receive(:invoke_delete) + .with('inventory/var_1/product', 'oauth').and_return('r') + expect(client.delete_inventory_product('var_1')).to eq('r') + end + end +end diff --git a/spec/checkout_sdk/inventory/inventory_reservation_request_serialization_spec.rb b/spec/checkout_sdk/inventory/inventory_reservation_request_serialization_spec.rb new file mode 100644 index 0000000..d13f5fa --- /dev/null +++ b/spec/checkout_sdk/inventory/inventory_reservation_request_serialization_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.describe 'InventoryReservationRequest serialization' do + it 'serializes all properties, including nested items (roundtrip)' do + item = CheckoutSdk::Inventory::InventoryReservationItem.new + item.variant_id = 'var_1' + item.quantity = 2 + + request = CheckoutSdk::Inventory::InventoryReservationRequest.new + request.owner_type = 'agent' + request.owner_reference = 'order_123' + request.items = [item] + request.ttl_seconds = 300 + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(request) + + expect(hash['owner_type']).to eq('agent') + expect(hash['owner_reference']).to eq('order_123') + expect(hash['items']).to eq([{ 'variant_id' => 'var_1', 'quantity' => 2 }]) + expect(hash['ttl_seconds']).to eq(300) + end + + it 'deserializes the swagger example' do + json = '{"owner_type":"agent","owner_reference":"order_123",' \ + '"items":[{"variant_id":"var_1","quantity":2}],"ttl_seconds":300}' + parsed = JSON.parse(json, object_class: OpenStruct) + + expect(parsed.owner_type).to eq('agent') + expect(parsed.owner_reference).to eq('order_123') + expect(parsed.items.first.variant_id).to eq('var_1') + expect(parsed.items.first.quantity).to eq(2) + expect(parsed.ttl_seconds).to eq(300) + end +end + +RSpec.describe 'InventoryReservationItem serialization' do + it 'serializes all properties (roundtrip)' do + item = CheckoutSdk::Inventory::InventoryReservationItem.new + item.variant_id = 'var_1' + item.quantity = 5 + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(item) + + expect(hash['variant_id']).to eq('var_1') + expect(hash['quantity']).to eq(5) + end +end diff --git a/spec/checkout_sdk/inventory/inventory_set_levels_request_serialization_spec.rb b/spec/checkout_sdk/inventory/inventory_set_levels_request_serialization_spec.rb new file mode 100644 index 0000000..c72da2a --- /dev/null +++ b/spec/checkout_sdk/inventory/inventory_set_levels_request_serialization_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe 'InventorySetLevelsRequest serialization' do + it 'serializes all properties (roundtrip)' do + request = CheckoutSdk::Inventory::InventorySetLevelsRequest.new + request.on_hand = 100 + request.safety_stock = 10 + request.reason = 'restock' + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(request) + + expect(hash['on_hand']).to eq(100) + expect(hash['safety_stock']).to eq(10) + expect(hash['reason']).to eq('restock') + end + + it 'deserializes the swagger example' do + json = '{"on_hand":100,"safety_stock":10,"reason":"restock"}' + parsed = JSON.parse(json, object_class: OpenStruct) + + expect(parsed.on_hand).to eq(100) + expect(parsed.safety_stock).to eq(10) + expect(parsed.reason).to eq('restock') + end +end diff --git a/spec/checkout_sdk/inventory/inventory_set_product_request_serialization_spec.rb b/spec/checkout_sdk/inventory/inventory_set_product_request_serialization_spec.rb new file mode 100644 index 0000000..bb925e5 --- /dev/null +++ b/spec/checkout_sdk/inventory/inventory_set_product_request_serialization_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.describe 'InventoryMoney serialization' do + it 'serializes all properties (roundtrip)' do + money = CheckoutSdk::Inventory::InventoryMoney.new + money.amount = 1999 + money.currency = 'USD' + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(money) + + expect(hash['amount']).to eq(1999) + expect(hash['currency']).to eq('USD') + end +end + +RSpec.describe 'InventorySetProductRequest serialization' do + it 'serializes all properties, including nested price/sale_price (roundtrip)' do + price = CheckoutSdk::Inventory::InventoryMoney.new + price.amount = 2999 + price.currency = 'USD' + + sale_price = CheckoutSdk::Inventory::InventoryMoney.new + sale_price.amount = 1999 + sale_price.currency = 'USD' + + request = CheckoutSdk::Inventory::InventorySetProductRequest.new + request.title = 'Running shoes' + request.description = 'Lightweight running shoes' + request.product_url = 'https://example.com/products/running-shoes' + request.image_url = 'https://example.com/images/running-shoes.png' + request.additional_image_urls = ['https://example.com/images/running-shoes-2.png'] + request.video_url = 'https://example.com/videos/running-shoes.mp4' + request.model_3d_url = 'https://example.com/models/running-shoes.glb' + request.sku = 'SKU-1' + request.gtin = '00012345678905' + request.mpn = 'MPN-1' + request.brand = 'Acme' + request.category = 'Footwear' + request.price = price + request.sale_price = sale_price + request.sale_price_starts_at = Time.parse('2026-01-01T00:00:00Z') + request.sale_price_ends_at = Time.parse('2026-01-31T23:59:59Z') + request.group_id = 'grp_1' + request.group_title = 'Running shoes group' + request.color = 'red' + request.size = 'M' + request.size_system = 'US' + request.gender = 'unisex' + request.condition = 'new' + request.material = 'mesh' + request.age_group = 'adult' + request.length = 30.5 + request.width = 12.0 + request.height = 10.0 + request.dimension_unit = 'cm' + request.weight = 0.8 + request.weight_unit = 'kg' + request.expiration_date = Time.parse('2027-01-01T00:00:00Z') + request.harmonized_system_code = '6404.11' + request.country_of_origin = 'US' + request.seller_name = 'Acme Store' + request.seller_url = 'https://example.com' + request.seller_privacy_policy = 'https://example.com/privacy' + request.seller_tos = 'https://example.com/tos' + + hash = CheckoutSdk::JsonSerializer.to_custom_hash(request) + + expect(hash['title']).to eq('Running shoes') + expect(hash['description']).to eq('Lightweight running shoes') + expect(hash['product_url']).to eq('https://example.com/products/running-shoes') + expect(hash['image_url']).to eq('https://example.com/images/running-shoes.png') + expect(hash['additional_image_urls']).to eq(['https://example.com/images/running-shoes-2.png']) + expect(hash['video_url']).to eq('https://example.com/videos/running-shoes.mp4') + expect(hash['model_3d_url']).to eq('https://example.com/models/running-shoes.glb') + expect(hash['sku']).to eq('SKU-1') + expect(hash['gtin']).to eq('00012345678905') + expect(hash['mpn']).to eq('MPN-1') + expect(hash['brand']).to eq('Acme') + expect(hash['category']).to eq('Footwear') + expect(hash['price']).to eq({ 'amount' => 2999, 'currency' => 'USD' }) + expect(hash['sale_price']).to eq({ 'amount' => 1999, 'currency' => 'USD' }) + expect(hash['sale_price_starts_at']).to eq(Time.parse('2026-01-01T00:00:00Z')) + expect(hash['sale_price_ends_at']).to eq(Time.parse('2026-01-31T23:59:59Z')) + expect(hash['group_id']).to eq('grp_1') + expect(hash['group_title']).to eq('Running shoes group') + expect(hash['color']).to eq('red') + expect(hash['size']).to eq('M') + expect(hash['size_system']).to eq('US') + expect(hash['gender']).to eq('unisex') + expect(hash['condition']).to eq('new') + expect(hash['material']).to eq('mesh') + expect(hash['age_group']).to eq('adult') + expect(hash['length']).to eq(30.5) + expect(hash['width']).to eq(12.0) + expect(hash['height']).to eq(10.0) + expect(hash['dimension_unit']).to eq('cm') + expect(hash['weight']).to eq(0.8) + expect(hash['weight_unit']).to eq('kg') + expect(hash['expiration_date']).to eq(Time.parse('2027-01-01T00:00:00Z')) + expect(hash['harmonized_system_code']).to eq('6404.11') + expect(hash['country_of_origin']).to eq('US') + expect(hash['seller_name']).to eq('Acme Store') + expect(hash['seller_url']).to eq('https://example.com') + expect(hash['seller_privacy_policy']).to eq('https://example.com/privacy') + expect(hash['seller_tos']).to eq('https://example.com/tos') + end + + it 'deserializes the swagger example' do + json = '{"title":"Running shoes","description":"Lightweight running shoes",' \ + '"product_url":"https://example.com/products/running-shoes",' \ + '"image_url":"https://example.com/images/running-shoes.png",' \ + '"condition":"new","price":{"amount":2999,"currency":"USD"}}' + parsed = JSON.parse(json, object_class: OpenStruct) + + expect(parsed.title).to eq('Running shoes') + expect(parsed.description).to eq('Lightweight running shoes') + expect(parsed.product_url).to eq('https://example.com/products/running-shoes') + expect(parsed.image_url).to eq('https://example.com/images/running-shoes.png') + expect(parsed.condition).to eq('new') + expect(parsed.price.amount).to eq(2999) + expect(parsed.price.currency).to eq('USD') + end +end