Created
August 19, 2020 17:55
-
-
Save machty/d555590fa1d7da4a8cdaf4f360a921b6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/app/models/checkout_location.rb b/app/models/checkout_location.rb | |
index 0a017d1ed..9b9fc0892 100644 | |
--- a/app/models/checkout_location.rb | |
+++ b/app/models/checkout_location.rb | |
@@ -156,7 +156,7 @@ class CheckoutLocation < ApplicationRecord | |
end | |
def alertable? | |
- shoppable? && !demo? | |
+ setup_statuses.none?(&:issue?) | |
end | |
alias_method :alertable, :alertable? | |
@@ -196,29 +196,24 @@ class CheckoutLocation < ApplicationRecord | |
demo_reasons.present? | |
end | |
- SHOPPABLE_STATUSES = %i[ready demo].freeze | |
def ready_for_shopping? | |
- setup_statuses.values.all? do |value| | |
- SHOPPABLE_STATUSES.include?(value) | |
- end | |
+ setup_statuses.all?(&:shoppable?) | |
end | |
def setup_statuses | |
- { | |
- inventory_adapter: inventory_adapter_setup_status, | |
- payment_adapters: payment_adapters_setup_status, | |
- visibility: visibility_status, | |
- maintenance_mode: maintenance_mode? ? true : :ready, | |
- discarded: discarded? ? true : :ready, | |
- } | |
+ stat = CheckoutLocation::SetupStatus | |
+ [ | |
+ inventory_adapter_setup_status, | |
+ payment_adapters_setup_status, | |
+ visibility_status, | |
+ stat.new(:maintenance_mode, maintenance_mode? ? stat::UNSHOPPABLE : stat::SHOPPABLE), | |
+ stat.new(:discarded, discarded? ? stat::UNSHOPPABLE : stat::SHOPPABLE), | |
+ ] | |
end | |
memoize :setup_statuses | |
def demo_reasons | |
- reasons = [] | |
- reasons.push('InventoryAdapter is a demo connection') if setup_statuses[:inventory_adapter] == :demo | |
- reasons.push('PaymentAdapter is a demo connection') if setup_statuses[:payment_adapters] == :demo | |
- reasons | |
+ setup_statuses.map(&:demo_reason).compact | |
end | |
def recent_user_sessions | |
diff --git a/app/models/checkout_location/adapter_connectable.rb b/app/models/checkout_location/adapter_connectable.rb | |
index 05b85671d..e9211a8d3 100644 | |
--- a/app/models/checkout_location/adapter_connectable.rb | |
+++ b/app/models/checkout_location/adapter_connectable.rb | |
@@ -116,17 +116,22 @@ class CheckoutLocation | |
end | |
def inventory_adapter_setup_status | |
- return :missing unless inventory_adapter_connection.present? | |
- return :demo if inventory_adapter_connection.demo? | |
- | |
- :ready | |
+ stat = CheckoutLocation::SetupStatus | |
+ if inventory_adapter_connection.present? | |
+ stat.new(:inventory_adapter, | |
+ inventory_adapter_connection.demo? ? stat::SHOPPABLE_DEMO : stat::SHOPPABLE) | |
+ else | |
+ stat.new(:inventory_adapter, stat::UNSHOPPABLE) | |
+ end | |
end | |
def payment_adapters_setup_status | |
- return :missing unless payment_adapter_connections.present? | |
- return :demo if payment_adapter_connections.any?(&:demo?) | |
- | |
- :ready | |
+ stat = CheckoutLocation::SetupStatus | |
+ if payment_adapter_connections.present? | |
+ stat.new(:payment_adapter, payment_adapter_connections.any?(&:demo?) ? stat::SHOPPABLE_DEMO : stat::SHOPPABLE) | |
+ else | |
+ stat.new(:payment_adapter, stat::UNSHOPPABLE) | |
+ end | |
end | |
end | |
end | |
diff --git a/app/models/checkout_location/setup_status.rb b/app/models/checkout_location/setup_status.rb | |
new file mode 100644 | |
index 000000000..e356b0e8a | |
--- /dev/null | |
+++ b/app/models/checkout_location/setup_status.rb | |
@@ -0,0 +1,32 @@ | |
+# frozen_string_literal: true | |
+ | |
+class CheckoutLocation | |
+ class SetupStatus | |
+ attr_reader :name, :state | |
+ | |
+ ENUM = [ | |
+ SHOPPABLE = :shoppable, | |
+ SHOPPABLE_DEMO = :shoppable_demo, | |
+ UNSHOPPABLE = :unshoppable, | |
+ ].freeze | |
+ | |
+ SHOPPABLE_STATES = [SHOPPABLE, SHOPPABLE_DEMO].freeze | |
+ | |
+ def initialize(name, state) | |
+ @name = name | |
+ @state = state | |
+ end | |
+ | |
+ def shoppable? | |
+ SHOPPABLE_STATES.include?(state) | |
+ end | |
+ | |
+ def demo_reason | |
+ "#{name} is demo" if state == SHOPPABLE_DEMO | |
+ end | |
+ | |
+ def issue? | |
+ state != SHOPPABLE | |
+ end | |
+ end | |
+end | |
diff --git a/app/models/checkout_location/visibility.rb b/app/models/checkout_location/visibility.rb | |
index cfe4d87ec..19fb19834 100644 | |
--- a/app/models/checkout_location/visibility.rb | |
+++ b/app/models/checkout_location/visibility.rb | |
@@ -27,9 +27,8 @@ class CheckoutLocation | |
included do | |
validates :visibility, inclusion: { in: ENUM } | |
- scope :visible_by_non_test_users, -> { where.not(visibility: TEST_USERS_ONLY) } | |
scope :visible_outside_radius, -> { where.not(visibility: WITHIN_RADIUS_ONLY) } | |
- scope :shoppable, -> { where(visibility: SHOPPABLE_VISIBILITIES) } | |
+ scope :geolocatable, -> { where(visibility: SHOPPABLE_VISIBILITIES) } | |
ENUM.each do |v| | |
scope v.to_sym, -> { where(visibility: v) } | |
@@ -44,12 +43,16 @@ class CheckoutLocation | |
end | |
end | |
- def shoppable? | |
- SHOPPABLE_VISIBILITIES.include?(visibility) | |
- end | |
- | |
def visibility_status | |
- open_to_public? ? :ready : visibility | |
+ stat = CheckoutLocation::SetupStatus | |
+ case visibility | |
+ when OPEN_TO_PUBLIC | |
+ stat.new(:visibility, stat::SHOPPABLE) | |
+ when WITHIN_RADIUS_ONLY, TEST_USERS_ONLY | |
+ stat.new(:visibility, stat::SHOPPABLE_DEMO) | |
+ when COMING_SOON | |
+ stat.new(:visibility, stat::UNSHOPPABLE) | |
+ end | |
end | |
end | |
end | |
diff --git a/app/models/fpr_cron_parser.rb b/app/models/fpr_cron_parser.rb | |
new file mode 100644 | |
index 000000000..51954d9fa | |
--- /dev/null | |
+++ b/app/models/fpr_cron_parser.rb | |
@@ -0,0 +1,30 @@ | |
+# frozen_string_literal: true | |
+ | |
+class FprCronParser | |
+ attr_reader :cron_string, :cron_parser, :time_zone | |
+ | |
+ def self.for(full_cron_string) | |
+ return nil unless full_cron_string.present? | |
+ | |
+ new(full_cron_string) | |
+ end | |
+ | |
+ def initialize(full_cron_string) | |
+ @cron_string, zone_str = full_cron_string.split('|').map(&:strip) | |
+ zone_str ||= "UTC" | |
+ @time_zone = ActiveSupport::TimeZone[zone_str] | |
+ @cron_parser = CronParser.new(@cron_string, time_zone) | |
+ end | |
+ | |
+ def next(time) | |
+ cron_parser.next(time.in_time_zone(time_zone)) | |
+ end | |
+ | |
+ def last(time) | |
+ cron_parser.last(time.in_time_zone(time_zone)) | |
+ end | |
+ | |
+ def summary | |
+ "#{Cronex::ExpressionDescriptor.new(cron_string).description} - #{time_zone}" | |
+ end | |
+end | |
diff --git a/app/models/http_utility.rb b/app/models/http_utility.rb | |
index 877396106..c6ea07686 100644 | |
--- a/app/models/http_utility.rb | |
+++ b/app/models/http_utility.rb | |
@@ -1,20 +1,35 @@ | |
# frozen_string_literal: true | |
class HttpUtility | |
- attr_accessor :log_user_events, :source, :redact, :httparty_options | |
+ attr_accessor :log_http_interactions, :source, :redact, :httparty_options | |
DEFAULT_TIMEOUT_SECONDS = 15 | |
class SSLCertificateExpiresSoon < BugsnaggableError; end | |
WARN_EXPIRING_CERTIFICATES_WITHIN = 3.weeks | |
- def initialize(source: nil, log_user_events: true, redact: nil, **httparty_options) | |
+ def initialize(source: nil, log_http_interactions: true, use_static_ip: false, redact: nil, **httparty_options) | |
self.redact = [redact].flatten.map(&:presence).compact | |
- self.log_user_events = log_user_events | |
+ self.log_http_interactions = log_http_interactions | |
self.source = source | |
self.httparty_options = { | |
timeout: DEFAULT_TIMEOUT_SECONDS, | |
- }.merge(httparty_options) | |
+ }.merge(static_ip_options(use_static_ip)) | |
+ .merge(httparty_options) | |
+ end | |
+ | |
+ def static_ip_options(use_static_ip) | |
+ if use_static_ip && proxy_url = ENV['FIXIE_URL'].presence | |
+ uri = URI.parse(proxy_url) | |
+ { | |
+ http_proxyaddr: uri.host, | |
+ http_proxyport: uri.port, | |
+ http_proxyuser: uri.user, | |
+ http_proxypass: uri.password, | |
+ } | |
+ else | |
+ {} | |
+ end | |
end | |
%i[get patch post put].each do |method| | |
@@ -26,7 +41,7 @@ class HttpUtility | |
end | |
end | |
- def log_user_event(url:, success:, duration:, post_body:, response_body:, code:, error:) | |
+ def log_http_interaction(url:, success:, duration:, post_body:, response_body:, code:, error:) | |
HttpInteraction.create_async( | |
source: source, | |
url: truncate_and_redact(url), | |
@@ -68,10 +83,10 @@ class HttpUtility | |
def log_result(url, options, response:, duration:, error:) | |
success = !!response&.success? | |
- return unless log_user_events | |
- return if success && log_user_events == :error | |
+ return unless log_http_interactions | |
+ return if success && log_http_interactions == :error | |
- log_user_event( | |
+ log_http_interaction( | |
url: url, | |
success: success, | |
duration: duration, | |
diff --git a/app/models/hubspot_integration.rb b/app/models/hubspot_integration.rb | |
index 7b733a1dc..10bd37ba5 100644 | |
--- a/app/models/hubspot_integration.rb | |
+++ b/app/models/hubspot_integration.rb | |
@@ -18,7 +18,8 @@ class HubspotIntegration < ApplicationRecord | |
"https://api.hubapi.com/contacts/v1/contact/?hapikey=#{api_key}" | |
end | |
- def find_and_update_or_create_customer(customer) | |
+ def find_and_update_or_create_customer(purchase) | |
+ customer = purchase.customer | |
resp = http_utility.get(profile_url(customer)).parsed_response | |
post_path = resp['status'] == 'error' ? create_contact_url : profile_url(customer) | |
properties = [ | |
@@ -28,6 +29,7 @@ class HubspotIntegration < ApplicationRecord | |
{ property: "phone", value: customer.phone_number }, | |
{ property: "used_mobile_checkout", value: true }, | |
{ property: "mobile_checkout_customer_id", value: customer.id }, | |
+ { property: "primary_branch", value: purchase.checkout_location.external_id }, | |
] | |
http_utility.post( | |
post_path, | |
diff --git a/app/models/inventory_adapter.rb b/app/models/inventory_adapter.rb | |
index 461d6d913..29fca9c20 100644 | |
--- a/app/models/inventory_adapter.rb | |
+++ b/app/models/inventory_adapter.rb | |
@@ -14,6 +14,7 @@ module InventoryAdapter | |
FPRAdapter, | |
RetailCloudAdapter, | |
ColruytAdapter, | |
+ ::Inventory, | |
] | |
end | |
diff --git a/app/models/inventory_adapter/fpr_adapter.rb b/app/models/inventory_adapter/fpr_adapter.rb | |
index 4a385661b..e723e4df4 100644 | |
--- a/app/models/inventory_adapter/fpr_adapter.rb | |
+++ b/app/models/inventory_adapter/fpr_adapter.rb | |
@@ -3,6 +3,7 @@ | |
module InventoryAdapter | |
class FPRAdapter < ApplicationRecord # rubocop:disable Metrics/ClassLength | |
include SharedAdapterMethods | |
+ include SharedFPRAdapterMethods | |
include LoyaltySupport | |
auto_strip_attributes :api_key | |
validates :api_key, presence: true, uniqueness: { scope: :inventory_adapter_fpr_adapter_host_id } | |
@@ -38,47 +39,6 @@ module InventoryAdapter | |
end | |
end | |
- def lookup_descriptor(desc) | |
- estimation = estimate(adds: [desc]) | |
- failure = estimation.failures.first | |
- | |
- if failure | |
- return nil unless failure['reason'] == 'needs_weight' | |
- | |
- line_item = extract_line_item_from_needs_weight_failure(failure) | |
- else | |
- line_item = estimation.line_item_collection.first | |
- end | |
- | |
- ItemData.new( | |
- name: line_item.name, | |
- code: line_item.upc, | |
- kind: determine_item_data_kind(line_item), | |
- price_per: (line_item.price_per unless line_item.price_per.zero?), | |
- weight: line_item.weight, | |
- requires: determine_item_data_requires(failure, line_item), | |
- allow_negative: allow_negative_adjustments?, | |
- ) | |
- end | |
- | |
- def determine_item_data_requires(failure, line_item) | |
- if failure | |
- "weight" | |
- elsif line_item.transaction_type == "department" | |
- "price" | |
- else | |
- "quantity" | |
- end | |
- end | |
- | |
- def determine_item_data_kind(line_item) | |
- if line_item.transaction_type == "department" | |
- "department" | |
- else | |
- "normal" | |
- end | |
- end | |
- | |
def allow_negative_adjustments? | |
# Fairway is the only client using department listing, and | |
# they requested negative adjustments to be disabled. | |
@@ -161,27 +121,14 @@ module InventoryAdapter | |
InventoryAdapter::FPRAdapter::Request.new(self) | |
end | |
- def extract_line_item_from_needs_weight_failure(failure_hash) | |
- FPRAdapter::CartSerializer.line_item_from_item_hash(failure_hash) | |
- end | |
- | |
def perform_remote_estimation(request) | |
- items_payload, translations = make_items_payload(request) | |
- params = { | |
- items: items_payload, | |
- loyalties: request.loyalties, | |
- external_id: request.external_id, | |
- source_type: request.source_type, | |
- source_id: request.source_id, | |
- started_at: request.started_at&.iso8601, | |
- } | |
- make_request.post( | |
- 'estimates', | |
- params, | |
- ).tap do |resp| | |
+ wrap_fpra_estimate(request) do |payload| | |
+ resp = make_request.post( | |
+ 'estimates', | |
+ payload, | |
+ ) | |
raise_estimation_error(StandardError.new(resp.body)) unless resp.success? | |
- end.as_json.tap do |response_payload| | |
- restore_original_scan_codes(response_payload, translations) | |
+ resp.as_json | |
end | |
end | |
@@ -213,84 +160,5 @@ module InventoryAdapter | |
response = http_utility.post(url, body: request.serialize(:oj)) | |
st::CONTAINER.deserialize(:oj, "FprAdapterEstimateResponse", response.body) | |
end | |
- | |
- def perform_estimation(request) | |
- resp = perform_remote_estimation(request) | |
- estimate_hash = resp.fetch('estimate') | |
- line_item_collection = FPRAdapter::CartSerializer.line_item_collection_from_adapter_payload( | |
- estimate_hash, | |
- ) | |
- failures = resp.fetch('failures') || [] | |
- estimation_attributes = { | |
- request: request, | |
- line_item_collection: line_item_collection, | |
- failures: failures, | |
- alerts: coupon_alerts(line_item_collection, request), | |
- tax_info: estimate_hash['tax_info'], | |
- tlog_data: estimate_hash['tlog_data'], | |
- external_id: estimate_hash['external_id'], | |
- possible_loyalty: failures.map { |f| f['found_loyalty'] }.compact.first, | |
- }.merge(loyalty_estimation_attributes(resp)) | |
- Estimation.new(estimation_attributes) | |
- rescue EOFError, SocketError => e | |
- raise_estimation_error(e) | |
- end | |
- | |
- def restore_original_scan_codes(response_payload, translations) | |
- response_payload['estimate']['items'].each do |item| | |
- client_id = item['client_id'] | |
- if original_code = translations[client_id] | |
- item['client_code'] = original_code | |
- end | |
- end | |
- end | |
- | |
- def coupon_alerts(line_item_collection, request) | |
- if (code = request.adds.first&.code) && | |
- !request.removes&.length.positive? && | |
- line_item_collection.coupons.find { |c| c.client_code == code } | |
- [AppAlert.find_by(key: 'shopper_added_coupon').as_json] | |
- else | |
- [] | |
- end | |
- end | |
- | |
- def raise_estimation_error(e) | |
- raise ::InventoryAdapter::EstimationError, "Bad Estimation Request: #{e.class} #{e.message}" | |
- end | |
- | |
- def make_items_payload(request) | |
- payload = [] | |
- translations = {} | |
- | |
- request.source_descriptors.each do |descriptor| | |
- payload << descriptor_as_json(descriptor, translations) | |
- end | |
- | |
- [payload, translations] | |
- end | |
- | |
- def translate_code(code) | |
- EpcPureIdentity.detect_from_scan(code)&.to_barcode_key | |
- end | |
- | |
- def descriptor_as_json(desc, translations) | |
- code = desc.code | |
- if translated_code = translate_code(code) | |
- translations[desc.client_id] = code | |
- code = translated_code | |
- end | |
- | |
- desc.lookup_params || { | |
- "code" => code, | |
- "client_id" => desc.client_id, | |
- "wgt" => desc.weight.as_json, | |
- }.tap do |h| | |
- if desc.item_type == 'department' | |
- h["amount"] = desc.amount.format | |
- h["type"] = 'department' | |
- end | |
- end | |
- end | |
end | |
end | |
diff --git a/app/models/inventory_adapter/fpr_adapter/loyalty_support.rb b/app/models/inventory_adapter/fpr_adapter/loyalty_support.rb | |
index 54403eabb..a1b8c02cc 100644 | |
--- a/app/models/inventory_adapter/fpr_adapter/loyalty_support.rb | |
+++ b/app/models/inventory_adapter/fpr_adapter/loyalty_support.rb | |
@@ -38,10 +38,6 @@ module InventoryAdapter | |
"fpr-adapter-host/#{inventory_adapter_fpr_adapter_host_id}/#{slug}" | |
end | |
- def loyalty_estimation_attributes(resp) | |
- { loyalties: resp.dig('estimate', 'loyalties') || [] } | |
- end | |
- | |
private | |
def make_loyalty_result(data) | |
diff --git a/app/models/inventory_adapter/shared_fpr_adapter_methods.rb b/app/models/inventory_adapter/shared_fpr_adapter_methods.rb | |
new file mode 100644 | |
index 000000000..0672a178b | |
--- /dev/null | |
+++ b/app/models/inventory_adapter/shared_fpr_adapter_methods.rb | |
@@ -0,0 +1,155 @@ | |
+# frozen_string_literal: true | |
+ | |
+module InventoryAdapter | |
+ module SharedFPRAdapterMethods | |
+ extend ActiveSupport::Concern | |
+ | |
+ def perform_estimation(request) | |
+ resp = perform_remote_estimation(request) | |
+ estimate_hash = resp.fetch('estimate') | |
+ line_item_collection = FPRAdapter::CartSerializer.line_item_collection_from_adapter_payload( | |
+ estimate_hash, | |
+ ) | |
+ failures = resp.fetch('failures') || [] | |
+ estimation_attributes = { | |
+ request: request, | |
+ line_item_collection: line_item_collection, | |
+ failures: failures, | |
+ alerts: coupon_alerts(line_item_collection, request), | |
+ tax_info: estimate_hash['tax_info'], | |
+ tlog_data: estimate_hash['tlog_data'], | |
+ external_id: estimate_hash['external_id'], | |
+ possible_loyalty: failures.map { |f| f['found_loyalty'] }.compact.first, | |
+ }.merge(loyalty_estimation_attributes(resp)) | |
+ Estimation.new(estimation_attributes) | |
+ rescue EOFError, SocketError => e | |
+ raise_estimation_error(e) | |
+ end | |
+ | |
+ def coupon_alerts(line_item_collection, request) | |
+ if (code = request.adds.first&.code) && | |
+ !request.removes&.length.positive? && | |
+ line_item_collection.coupons.find { |c| c.client_code == code } | |
+ [AppAlert.find_by(key: 'shopper_added_coupon').as_json] | |
+ else | |
+ [] | |
+ end | |
+ end | |
+ | |
+ def raise_estimation_error(e) | |
+ raise ::InventoryAdapter::EstimationError, "Bad Estimation Request: #{e.class} #{e.message}" | |
+ end | |
+ | |
+ def wrap_fpra_estimate(request) | |
+ payload, translations = make_remote_estimation_payload(request) | |
+ yield(payload).tap do |response_payload| | |
+ restore_original_scan_codes(response_payload, translations) | |
+ end | |
+ end | |
+ | |
+ def restore_original_scan_codes(response_payload, translations) | |
+ response_payload['estimate']['items'].each do |item| | |
+ client_id = item['client_id'] | |
+ if original_code = translations[client_id] | |
+ item['client_code'] = original_code | |
+ end | |
+ end | |
+ end | |
+ | |
+ def make_remote_estimation_payload(request) | |
+ items_payload, translations = make_items_payload(request) | |
+ payload = { | |
+ items: items_payload, | |
+ loyalties: request.loyalties, | |
+ external_id: request.external_id, | |
+ source_type: request.source_type, | |
+ source_id: request.source_id, | |
+ started_at: request.started_at&.iso8601, | |
+ } | |
+ [payload, translations] | |
+ end | |
+ | |
+ def make_items_payload(request) | |
+ payload = [] | |
+ translations = {} | |
+ | |
+ request.source_descriptors.each do |descriptor| | |
+ payload << descriptor_as_json(descriptor, translations) | |
+ end | |
+ | |
+ [payload, translations] | |
+ end | |
+ | |
+ def translate_code(code) | |
+ EpcPureIdentity.detect_from_scan(code)&.to_barcode_key | |
+ end | |
+ | |
+ def loyalty_estimation_attributes(resp) | |
+ { loyalties: resp.dig('estimate', 'loyalties') || [] } | |
+ end | |
+ | |
+ def descriptor_as_json(desc, translations) | |
+ code = desc.code | |
+ if translated_code = translate_code(code) | |
+ translations[desc.client_id] = code | |
+ code = translated_code | |
+ end | |
+ | |
+ desc.lookup_params || { | |
+ "code" => code, | |
+ "client_id" => desc.client_id, | |
+ "wgt" => desc.weight.as_json, | |
+ }.tap do |h| | |
+ if desc.item_type == 'department' | |
+ h["amount"] = desc.amount.format | |
+ h["type"] = 'department' | |
+ end | |
+ end | |
+ end | |
+ | |
+ def lookup_descriptor(desc) | |
+ estimation = estimate(adds: [desc]) | |
+ failure = estimation.failures.first | |
+ | |
+ if failure | |
+ return nil unless failure['reason'] == 'needs_weight' | |
+ | |
+ line_item = extract_line_item_from_needs_weight_failure(failure) | |
+ else | |
+ line_item = estimation.line_item_collection.first | |
+ end | |
+ | |
+ ItemData.new( | |
+ name: line_item.name, | |
+ code: line_item.upc, | |
+ kind: determine_item_data_kind(line_item), | |
+ price_per: (line_item.price_per unless line_item.price_per.zero?), | |
+ weight: line_item.weight, | |
+ requires: determine_item_data_requires(failure, line_item), | |
+ allow_negative: allow_negative_adjustments?, | |
+ ) | |
+ end | |
+ | |
+ def extract_line_item_from_needs_weight_failure(failure_hash) | |
+ FPRAdapter::CartSerializer.line_item_from_item_hash(failure_hash) | |
+ end | |
+ | |
+ def determine_item_data_requires(failure, line_item) | |
+ if failure | |
+ "weight" | |
+ elsif line_item.transaction_type == "department" | |
+ "price" | |
+ else | |
+ "quantity" | |
+ end | |
+ end | |
+ | |
+ def determine_item_data_kind(line_item) | |
+ if line_item.transaction_type == "department" | |
+ "department" | |
+ else | |
+ "normal" | |
+ end | |
+ end | |
+ end | |
+end | |
diff --git a/app/models/inventory_adapter/test_adapter.rb b/app/models/inventory_adapter/test_adapter.rb | |
index db537ae34..66a82154c 100644 | |
--- a/app/models/inventory_adapter/test_adapter.rb | |
+++ b/app/models/inventory_adapter/test_adapter.rb | |
@@ -88,10 +88,14 @@ module InventoryAdapter | |
end | |
def self.build_inventory | |
- Dir.glob("config/test_inventory/*.yml").map do |path| | |
- YAML.load_file(path).map do |item| | |
- item.symbolize_keys.merge(upc: item['upc'].gsub(/^0+/, '')) | |
- end | |
+ inventory_data.map do |item| | |
+ item.symbolize_keys.merge(upc: item['upc'].gsub(/^0+/, '')) | |
+ end | |
+ end | |
+ | |
+ def self.inventory_data | |
+ @inventory_data ||= Dir.glob("config/test_inventory/*.yml").map do |path| | |
+ YAML.load_file(path) | |
end.flatten | |
end | |
diff --git a/app/models/inventory_adapter/test_estimator.rb b/app/models/inventory_adapter/test_estimator.rb | |
index da48e2bff..204f83a97 100644 | |
--- a/app/models/inventory_adapter/test_estimator.rb | |
+++ b/app/models/inventory_adapter/test_estimator.rb | |
@@ -160,7 +160,7 @@ module InventoryAdapter | |
found_item.clone( | |
price: price, | |
prediscount_price: price, | |
- upc: nil, | |
+ upc: found_item.upc, | |
lookup_params: lookup.lookup_params || { | |
"type" => "department", | |
"code" => lookup.code, | |
diff --git a/app/models/slack_notifier.rb b/app/models/slack_notifier.rb | |
index 9d900b07d..e702f72b6 100644 | |
--- a/app/models/slack_notifier.rb | |
+++ b/app/models/slack_notifier.rb | |
@@ -76,7 +76,7 @@ class SlackNotifier | |
protected | |
def http_utility | |
- HttpUtility.new(log_user_events: :error, redact: WEBHOOKS) | |
+ HttpUtility.new(log_http_interactions: :error, redact: WEBHOOKS) | |
end | |
def fulfillment_app_notification(order_department, text) | |
diff --git a/app/models/transaction_log/item.rb b/app/models/transaction_log/item.rb | |
index f5e81be36..ace9f0a9a 100644 | |
--- a/app/models/transaction_log/item.rb | |
+++ b/app/models/transaction_log/item.rb | |
@@ -39,7 +39,17 @@ module TransactionLog | |
visaelectron | |
vpay | |
unionpay | |
- ] | |
+ ], _prefix: :subtype | |
+ | |
+ NON_CARD_PAYMENT_TYPES = %w[ | |
+ non_tender fpr_credit deferred unknown | |
+ ].freeze | |
+ | |
+ CARD_PAYMENT_TYPES = (TransactionLog::Item.transaction_subtypes.keys - NON_CARD_PAYMENT_TYPES).freeze | |
+ | |
+ def card_payment? | |
+ CARD_PAYMENT_TYPES.include?(transaction_subtype) | |
+ end | |
enum price_source: %i[unknown inventory scan_code], _prefix: :price_source | |
enum weight_source: %i[unknown user price scan_code], _prefix: :weight_source | |
@@ -82,6 +92,18 @@ module TransactionLog | |
end | |
end | |
+ def cart_item? | |
+ coded_item? || department? | |
+ end | |
+ | |
+ def item_id | |
+ line_number | |
+ end | |
+ | |
+ def parent_item_id | |
+ parent_ref | |
+ end | |
+ | |
def self.build_items_from_order(order) | |
[ | |
convert_cart_items_to_tlog_items_full(order), | |
diff --git a/app/models/transaction_log/order.rb b/app/models/transaction_log/order.rb | |
index 69068d1df..eb3be0bf0 100644 | |
--- a/app/models/transaction_log/order.rb | |
+++ b/app/models/transaction_log/order.rb | |
@@ -2,10 +2,18 @@ | |
module TransactionLog | |
class Order < ActiveRecord::Base | |
+ extend Memoist | |
has_many :transaction_log_items, class_name: "TransactionLog::Item", | |
foreign_key: :transaction_log_order_id, | |
inverse_of: :transaction_log_order, | |
dependent: :destroy | |
+ belongs_to :transaction_log_job, class_name: "TransactionLog::Job", | |
+ foreign_key: :transaction_log_job_id, | |
+ optional: true | |
+ belongs_to :transaction_log_upload_batch, | |
+ class_name: "TransactionLog::UploadBatch", | |
+ foreign_key: :transaction_log_upload_batch_id, | |
+ optional: true | |
belongs_to :checkout_location | |
belongs_to :customer, optional: true | |
@@ -35,12 +43,47 @@ module TransactionLog | |
transacted_at: purchasable.checkout_completed_at, | |
started_at: purchasable.created_at, | |
customer_birthdate: purchasable.customer_birthdate, | |
+ external_id: purchasable.external_id, | |
+ tlog_data: purchasable.tlog_data, | |
).tap do |order| | |
order.transaction_log_items = TransactionLog::Item.build_items_from_order(order) | |
order.assign_item_attrs | |
end | |
end | |
+ def receipt | |
+ ReceiptBuilder.new.single_receipt(purchasable).receipt unless purchasable.micropayment? | |
+ end | |
+ memoize :receipt | |
+ | |
+ def cart_items | |
+ transaction_log_items.select(&:cart_item?) | |
+ end | |
+ | |
+ def coupon_items | |
+ transaction_log_items.select(&:coupon?) | |
+ end | |
+ | |
+ def bottle_deposit_items | |
+ transaction_log_items.select(&:bottle_deposit?) | |
+ end | |
+ | |
+ def tax_items | |
+ transaction_log_items.select(&:tax?) | |
+ end | |
+ | |
+ def payment_items | |
+ transaction_log_items.select(&:payment?) | |
+ end | |
+ | |
+ def external_coupon_items | |
+ transaction_log_items.select(&:external_coupon?) | |
+ end | |
+ | |
+ def department_items | |
+ transaction_log_items.select(&:department?) | |
+ end | |
+ | |
def self.log_purchasable_idempotent(purchasable) | |
order = generate(purchasable) | |
maybe_insert_order_idempotent(purchasable, order) | |
diff --git a/app/models/transaction_log/upload_batch.rb b/app/models/transaction_log/upload_batch.rb | |
new file mode 100644 | |
index 000000000..03c7c59c1 | |
--- /dev/null | |
+++ b/app/models/transaction_log/upload_batch.rb | |
@@ -0,0 +1,17 @@ | |
+# frozen_string_literal: true | |
+ | |
+module TransactionLog | |
+ class UploadBatch < ApplicationRecord | |
+ belongs_to :s3_tlog_target | |
+ belongs_to :inventory | |
+ belongs_to :s3_tlog_target | |
+ enum state: %i[pending uploaded] | |
+ | |
+ has_many :transaction_log_orders, | |
+ class_name: "TransactionLog::Order", | |
+ foreign_key: :transaction_log_upload_batch_id, | |
+ inverse_of: :transaction_log_upload_batch | |
+ | |
+ validates :upload_scheduled_at, presence: true | |
+ end | |
+end | |
diff --git a/app/models/world/inventories.rb b/app/models/world/inventories.rb | |
new file mode 100644 | |
index 000000000..653749bbd | |
--- /dev/null | |
+++ b/app/models/world/inventories.rb | |
@@ -0,0 +1,103 @@ | |
+# frozen_string_literal: true | |
+ | |
+class World | |
+ module Inventories | |
+ extend ActiveSupport::Concern | |
+ | |
+ included do | |
+ define :inventory_group do |options| | |
+ attrs = { | |
+ name: "TestInventoryGroup", | |
+ price_order: options.fetch(:price_order, 0), | |
+ code_pattern_sources: "upc,ean13", | |
+ itemized_taxes: options.fetch(:itemized_taxes, true), | |
+ item_price_type: options.fetch(:item_price_type, "pre_tax_item_prices"), | |
+ ruleset_name: "fairway", | |
+ loyalty_discount_groups: { | |
+ "10": "0.1", | |
+ "12": "0.06", | |
+ "90": "0.2", | |
+ "91": "0.2", | |
+ }, | |
+ }.merge(options) | |
+ | |
+ InventoryGroup.create!( | |
+ attrs.merge(options), | |
+ ) | |
+ end | |
+ | |
+ define :inventory do |options| | |
+ Inventory.create!( | |
+ inventory_group: ensure_inventory_group, | |
+ name: 'TestInventory', | |
+ tax_rates: ensure_inventory_tax_rates_set, | |
+ additionals: ensure_inventory_additionals_set, | |
+ ).tap do |inventory| | |
+ ::InventoryAdapter::TestAdapter.inventory_data.each do |item_data| | |
+ import_from_yml(inventory, item_data) | |
+ end | |
+ end | |
+ end | |
+ | |
+ def import_from_yml(inventory, item_data) | |
+ if item_data['type'] == 'department' | |
+ import_department_from_yml(inventory, item_data) | |
+ else | |
+ import_item_from_yml(inventory, item_data) | |
+ end | |
+ end | |
+ | |
+ def import_department_from_yml(inventory, item_data) | |
+ inventory.departments.create!( | |
+ name: item_data['name'], | |
+ key: item_data['upc'], | |
+ discountable: !!item_data['discountable'], | |
+ tax_flags: item_data['tax_flags'], | |
+ department_code: item_data['upc'], | |
+ ) | |
+ end | |
+ | |
+ def import_item_from_yml(inventory, item_data) | |
+ price_per = (item_data['price_per'] || item_data['price']).to_money | |
+ normal_price_per = item_data['normal_price_per']&.to_money | |
+ | |
+ normal_price_calculator = PriceCalculator::BasicPricing.new(price_per) | |
+ if normal_price_per | |
+ special_price_calculator = normal_price_calculator | |
+ normal_price_calculator = PriceCalculator::BasicPricing.new(normal_price_per) | |
+ end | |
+ | |
+ inventory.items.create!( | |
+ name: item_data['name'], | |
+ key: item_data['upc'], | |
+ sold_by_weight: item_data['requires'] == 'weight', | |
+ normal_price_calculator: normal_price_calculator, | |
+ special_price_calculator: special_price_calculator || PriceCalculator::NullPricing.new, | |
+ discountable: !!item_data['discountable'], | |
+ tax_flags: item_data['tax_flags'], | |
+ department_code: item_data['department_code'], | |
+ tare_weight: (item_data['tare_weight'] || '0').to_d, | |
+ ) | |
+ end | |
+ | |
+ # TODO: this should probably be called `inventory_tax_rates` but it causes stack overflow | |
+ # issues with World's pluralization conventions | |
+ define :inventory_tax_rates_set do |_options| | |
+ [{ "name": "Sales Tax", "value": "0.08875", "tax_code": "A" }] | |
+ end | |
+ | |
+ define :inventory_additionals_set do |_options| | |
+ [ | |
+ { | |
+ 'name' => 'Bottle Deposit', | |
+ 'taxable' => false, | |
+ }, | |
+ { | |
+ 'name' => 'Eco Fee', | |
+ 'taxable' => true, | |
+ }, | |
+ ] | |
+ end | |
+ end | |
+ end | |
+end | |
diff --git a/app/models/world/inventory_adapter.rb b/app/models/world/inventory_adapter.rb | |
index a5e14d435..0ffafcb6f 100644 | |
--- a/app/models/world/inventory_adapter.rb | |
+++ b/app/models/world/inventory_adapter.rb | |
@@ -53,6 +53,14 @@ class World | |
cashier_id: '1', | |
) | |
end | |
+ | |
+ f.member :inline_inventory_adapter do |options| | |
+ if options.is_a?(::Inventory) | |
+ options | |
+ else | |
+ add(:inventory, options) | |
+ end | |
+ end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment