diff --git a/README.md b/README.md index b5b09fc..bb485a7 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,14 @@ report = Fulfil::Report.new(client: fulfil, report_name: 'account.tax.summary.ir report.execute(start_date: Date.new(2020, 12, 1), end_date: Date.new(2020, 12, 31)) ``` +## Date Ranges +Fulfil expects an ISO8601 date when retrieving a date range. The GEM will take a `date` or `datetime` and parse it into the object Fulfil expects. + +``` +datetime = DateTime.new(2024, 1, 1, 12, 0, 0) +fulfil_model.search(domain: [['create_date', '>=', datetime]]) +``` + ## Rate limits Fulfil's API applies rate limits to the API requests that it receives. Every request is subject to throttling under the general limits. In addition, there are resource-based rate limits and throttles. diff --git a/lib/fulfil/client.rb b/lib/fulfil/client.rb index 15aacb1..ce2d264 100644 --- a/lib/fulfil/client.rb +++ b/lib/fulfil/client.rb @@ -3,6 +3,7 @@ require 'http' require 'logger' require 'fulfil/response_parser' +require 'fulfil/domain_parser' module Fulfil SUBDOMAIN = ENV.fetch('FULFIL_SUBDOMAIN', nil) @@ -69,7 +70,8 @@ def find_many(model:, ids:, fields: nil) def search(model:, domain:, offset: nil, limit: 100, sort: nil, fields: %w[id]) uri = URI("#{model_url(model: model)}/search_read") - body = [domain, offset, limit, sort, fields] + parsed_domain = Fulfil::DomainParser.new(domain).parsed + body = [parsed_domain, offset, limit, sort, fields] results = request(verb: :put, endpoint: uri, json: body) parse(results: results) @@ -77,7 +79,8 @@ def search(model:, domain:, offset: nil, limit: 100, sort: nil, fields: %w[id]) def count(model:, domain:) uri = URI("#{model_url(model: model)}/search_count") - body = [domain] + parsed_domain = Fulfil::DomainParser.new(domain).parsed + body = [parsed_domain] request(verb: :put, endpoint: uri, json: body) end diff --git a/lib/fulfil/converter.rb b/lib/fulfil/converter.rb new file mode 100644 index 0000000..ae80a1f --- /dev/null +++ b/lib/fulfil/converter.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Fulfil + # The Fulfil::Conversions module provides utility methods for converting + # Date and DateTime objects into a standardized hash format. + class Converter + class << self + def date_or_datetime_as_object(date_or_datetime) + case date_or_datetime + when Date + date_as_object(date_or_datetime) + when DateTime + datetime_as_object(date_or_datetime) + end + end + + def datetime_as_object(datetime) + { + __class__: 'datetime', + iso_string: datetime.new_offset(0).iso8601 + } + end + + def date_as_object(date) + datetime_as_object(date.to_datetime) + end + end + end +end diff --git a/lib/fulfil/domain_parser.rb b/lib/fulfil/domain_parser.rb new file mode 100644 index 0000000..e9705c4 --- /dev/null +++ b/lib/fulfil/domain_parser.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'fulfil/converter' + +module Fulfil + # The Fulfil::DomainParser module provides utility methods for converting + # Date and DateTime objects into a standardized hash format. + # The module iterates over given parameters, identifies + # Date and DateTime objects, and converts them into a hash with a class descriptor + # and an ISO 8601 formatted string. + class DomainParser + attr_reader :domain + + def initialize(domain) + @domain = domain + + disable_escape_html_entities + end + + def parsed + new_domain = domain.map do |values| + update_values(values) + end + + enable_escape_html_entities + new_domain + end + + def update_values(values) + values.map do |value| + case value.class.name + when 'Date' + date_as_object(value) + when 'DateTime' + datetime_as_object(value) + else + value + end + end + end + + private + + def date_as_object(date) + Converter.date_as_object(date) + end + + def datetime_as_object(datetime) + Converter.datetime_as_object(datetime) + end + + def disable_escape_html_entities + return unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:escape_html_entities_in_json=) + + ActiveSupport.escape_html_entities_in_json = false + end + + def enable_escape_html_entities + return unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:escape_html_entities_in_json=) + + ActiveSupport.escape_html_entities_in_json = true + end + end +end diff --git a/lib/fulfil/model.rb b/lib/fulfil/model.rb index 1e6acf2..0c36d61 100644 --- a/lib/fulfil/model.rb +++ b/lib/fulfil/model.rb @@ -28,6 +28,7 @@ def search( offset: nil, sort: nil ) + @client.search( model: model, domain: domain, diff --git a/lib/fulfil/query.rb b/lib/fulfil/query.rb index 2218e91..54435a9 100644 --- a/lib/fulfil/query.rb +++ b/lib/fulfil/query.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'date' +require 'fulfil/converter' + module Fulfil class Query def initialize @@ -86,11 +89,25 @@ def build_search_term(field:, value:, options:, prefix: nil) [[key, 'ilike', value]] end when 'Hash' + handle_hash(field, key, value, options) + when 'Date', 'DateTime' + [[key, '=', Converter.date_or_datetime_as_object(value)]] + else + raise "Unhandled value type: #{value} (#{value.class.name})" + end + end + + def handle_hash(field, key, value, options) + if %i[gte gt lte lt].any? { |op| value.key?(op) } + value.map do |operator, val| + op_map = { gte: '>=', gt: '>', lte: '<=', lt: '<' } + converted_value = Converter.date_or_datetime_as_object(val) + [key, op_map[operator], converted_value] + end + else value.flat_map do |nested_field, nested_value| build_search_term(prefix: field, field: nested_field, value: nested_value, options: options) end - else - raise "Unhandled value type: #{value} (#{value.class.name})" end end diff --git a/test/fulfil/client_test.rb b/test/fulfil/client_test.rb index 17ff694..7992fa6 100644 --- a/test/fulfil/client_test.rb +++ b/test/fulfil/client_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'test_helper' +require 'date' module Fulfil class ClientTest < Minitest::Test @@ -68,5 +69,81 @@ def test_do_not_retry_when_retry_is_disabled end end end + + def test_date_conversion_in_search + date = Date.new(2022, 1, 1) + datetime = date.to_datetime + utc_datetime = datetime.new_offset(0) + expected_formatted_date = { + __class__: 'datetime', + iso_string: utc_datetime.iso8601 + } + + # Convert the expected hash's symbol keys to strings for comparison + expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s) + + stub_request(:put, fulfil_url_for('sale.sale/search_read')) + .to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' }) + + client = Fulfil::Client.new + fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale') + fulfil_model.search(domain: [['create_date', '>=', date]]) + + assert_requested(:put, fulfil_url_for('sale.sale/search_read'), times: 1) do |request| + actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2) + + assert_equal expected_formatted_date_with_string_keys, actual_formatted_date + end + end + + def test_datetime_conversion_in_search + datetime = DateTime.new(2022, 1, 1, 12, 0, 0) + utc_datetime = datetime.new_offset(0) + expected_formatted_date = { + __class__: 'datetime', + iso_string: utc_datetime.iso8601 + } + + # Convert the expected hash's symbol keys to strings for comparison + expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s) + + stub_request(:put, fulfil_url_for('sale.sale/search_read')) + .to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' }) + + client = Fulfil::Client.new + fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale') + fulfil_model.search(domain: [['create_date', '>=', datetime]]) + + assert_requested(:put, fulfil_url_for('sale.sale/search_read'), times: 1) do |request| + actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2) + + assert_equal expected_formatted_date_with_string_keys, actual_formatted_date + end + end + + def test_datetime_conversion_in_count + datetime = DateTime.new(2022, 1, 1, 12, 0, 0) + utc_datetime = datetime.new_offset(0) + expected_formatted_date = { + __class__: 'datetime', + iso_string: utc_datetime.iso8601 + } + + # Convert the expected hash's symbol keys to strings for comparison + expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s) + + stub_request(:put, fulfil_url_for('sale.sale/search_count')) + .to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' }) + + client = Fulfil::Client.new + fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale') + fulfil_model.count(domain: [['create_date', '>=', datetime]]) + + assert_requested(:put, fulfil_url_for('sale.sale/search_count'), times: 1) do |request| + actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2) + + assert_equal expected_formatted_date_with_string_keys, actual_formatted_date + end + end end end diff --git a/test/fulfil/query_test.rb b/test/fulfil/query_test.rb index de7bc28..231e58e 100644 --- a/test/fulfil/query_test.rb +++ b/test/fulfil/query_test.rb @@ -2,7 +2,10 @@ require 'minitest/autorun' require 'fulfil/query' +require 'fulfil/converter' +require 'date' +# rubocop:disable Metrics/ClassLength module Fulfil class QueryTest < Minitest::Test def setup @@ -47,6 +50,32 @@ def test_nested_queries assert_equal [['sale.id', 'in', [123]]], @query.query end + def test_dates_are_converted_to_iso8601 + date = Date.new(2019, 1, 1) + expected_date_format = Converter.date_as_object(date) + + @query.search(date: date) + + assert_equal [['date', '=', expected_date_format]], @query.query + end + + def test_date_greater_than_or_equal_to_converted_to_iso8601 + date = Date.new(2019, 1, 1) + expected_date_format = Converter.date_as_object(date) + + @query.search(date: { gte: date }) + + assert_equal [['date', '>=', expected_date_format]], @query.query + end + + def test_datetime_less_than_converted_to_iso8601 + datetime = DateTime.new(2019, 1, 1, 0, 0, 0) + expected_datetime_format = Converter.datetime_as_object(datetime) + + @query.search(datetime: { lt: datetime }) + + assert_equal [['datetime', '<', expected_datetime_format]], @query.query + end # -- #exclude ----------------------- def test_equals_exclude @@ -183,3 +212,4 @@ def test_chaining end end end +# rubocop:enable Metrics/ClassLength