Class: Feed

Inherits:
ApplicationRecord show all
Includes:
ERB::Util, Models::Auditable
Defined in:
app/models/feed.rb

Overview

== Schema Information

Table name: feeds
Database name: primary

id :integer not null, primary key
active :boolean default(TRUE), not null
cc_email :string
contact_info :text
custom1 :string(255)
custom2 :string(255)
custom3 :string(255)
custom_file_formatter :string
destination :integer default("ftp")
emails :string default([]), is an Array
encoding :string(25) default("UTF-8")
feed_type :integer default("inventory")
file_name_template :string(255)
frequency :integer default("hourly")
ftp_passive :boolean default(FALSE), not null
hostname :string(255)
name :string(255) not null
new_line_method :integer default("crlf")
next_attempt :datetime
output_format :integer default("txt")
output_layout :text not null
password :string(255)
remote_directory :string(255)
sender_email :string
username :string(255)
created_at :datetime
updated_at :datetime
creator_id :integer
customer_id :integer not null
updater_id :integer

Indexes

feeds_customer_id_idx (customer_id)

Foreign Keys

feeds_customer_id_fk (customer_id => parties.id) ON DELETE => cascade

Defined Under Namespace

Classes: ItemBaseGenerator, ItemInventoryGenerator, ItemListGenerator, ItemPresenter, ProductTypeGenerator, TransmitEmail, TransmitFtp, TransmitFtps, TransmitSftp

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#custom_file_formatterObject (readonly)



58
# File 'app/models/feed.rb', line 58

validates :custom_file_formatter, presence: { if: :custom_formatter? }

#destinationObject (readonly)



56
# File 'app/models/feed.rb', line 56

validates :name, :feed_type, :destination, presence: true

#encodingObject (readonly)



61
# File 'app/models/feed.rb', line 61

validates :encoding, presence: true

#feed_typeObject (readonly)



56
# File 'app/models/feed.rb', line 56

validates :name, :feed_type, :destination, presence: true

#hostnameObject (readonly)



59
# File 'app/models/feed.rb', line 59

validates :hostname, :username, :password, presence: { if: :ftp? }

#nameObject (readonly)



56
# File 'app/models/feed.rb', line 56

validates :name, :feed_type, :destination, presence: true

#output_formatObject (readonly)



57
# File 'app/models/feed.rb', line 57

validates :output_format, :output_layout, presence: { if: :requires_output_format? }

#output_layoutObject (readonly)



57
# File 'app/models/feed.rb', line 57

validates :output_format, :output_layout, presence: { if: :requires_output_format? }

#passwordObject (readonly)



59
# File 'app/models/feed.rb', line 59

validates :hostname, :username, :password, presence: { if: :ftp? }

#sender_emailObject (readonly)



60
# File 'app/models/feed.rb', line 60

validates :sender_email, presence: { if: :email? }

#usernameObject (readonly)



59
# File 'app/models/feed.rb', line 59

validates :hostname, :username, :password, presence: { if: :ftp? }

Class Method Details

.activeActiveRecord::Relation<Feed>

A relation of Feeds that are active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Feed>)

See Also:



68
# File 'app/models/feed.rb', line 68

scope :active, -> { where(active: true) }

.custom_file_formatter_for_selectObject



126
127
128
# File 'app/models/feed.rb', line 126

def self.custom_file_formatter_for_select
  %w[]
end

.destinations_for_selectObject



118
119
120
# File 'app/models/feed.rb', line 118

def self.destinations_for_select
  destinations.keys
end

.encoding_list_for_selectObject



217
218
219
# File 'app/models/feed.rb', line 217

def self.encoding_list_for_select
  Encoding.list.map(&:to_s)
end

.feed_types_for_selectObject



110
111
112
# File 'app/models/feed.rb', line 110

def self.feed_types_for_select
  feed_types.keys
end

.frequencies_for_selectObject



122
123
124
# File 'app/models/feed.rb', line 122

def self.frequencies_for_select
  frequencies.keys
end

.output_formats_for_selectObject



114
115
116
# File 'app/models/feed.rb', line 114

def self.output_formats_for_select
  output_formats.keys
end

.process_feedsObject



89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/feed.rb', line 89

def self.process_feeds
  feeds = {}
  Feed.active.timed.where('(next_attempt IS NULL or next_attempt <= ?)', Time.current).find_each do |f|
    if Rails.env.development?
      feeds[f.id] = f.run
    else
      res = FeedRunnerWorker.perform_async(f.id) # Queue individually as to prevent global failures
      feeds[f.id] = res ? :queued : :not_queued
    end
  end
  prune_feeds
  feeds
end

.prune_feedsObject



103
104
105
106
107
108
# File 'app/models/feed.rb', line 103

def self.prune_feeds
  # Delete all feed histories older than 30 days
  old_feeds = FeedHistory.where('created_at < ?', 30.days.ago)
  Rails.logger.info "#{old_feeds.length} old feeds will be deleted. "
  FeedHistory.where('created_at < ?', 30.days.ago).destroy_all
end

.timedActiveRecord::Relation<Feed>

A relation of Feeds that are timed. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Feed>)

See Also:



69
# File 'app/models/feed.rb', line 69

scope :timed, -> { where.not(frequency: nil) }

Instance Method Details

#all_to_emailsObject



140
141
142
# File 'app/models/feed.rb', line 140

def all_to_emails
  contact_points.map(&:detail) + (emails || [])
end

#build_transmitterObject

Builds a transmitter for ftp or email destination



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'app/models/feed.rb', line 268

def build_transmitter
  case destination.to_sym
  when :ftp
    transmitter = Feed::TransmitFtp.new(
      logger:,
      hostname:,
      ftp_passive:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :ftps
    transmitter = Feed::TransmitFtps.new(
      logger:,
      hostname:,
      ftp_passive:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :sftp
    transmitter = Feed::TransmitSftp.new(
      logger:,
      hostname:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :email
    transmitter = Feed::TransmitEmail.new(
      logger:,
      recipient_contact_points: contact_points,
      sender_email:,
      cc_email:,
      extra_emails: emails,
      feed_id: id
    )
  else
    raise 'Unsupported Destination, cannot resolve Transmitter Type'
  end
  transmitter
end

#calculate_next_attemptObject



238
239
240
241
242
243
244
245
246
247
248
# File 'app/models/feed.rb', line 238

def calculate_next_attempt
  # Set next attempt based on frequency
  case frequency
  when 'hourly'
    1.hour.from_now
  when 'weekly'
    1.week.from_now
  else
    1.day.from_now
  end
end

#catalogObject



160
161
162
# File 'app/models/feed.rb', line 160

def catalog
  customer&.catalog
end

#catalog_itemsObject



164
165
166
167
168
169
170
# File 'app/models/feed.rb', line 164

def catalog_items
  return CatalogItem.none unless catalog

  catalog_item_query = catalog.catalog_items.for_edi_feeds
  catalog_item_query = catalog_item_query.where('third_party_sku ~ ?', catalog.third_party_sku_filter_regex) if catalog.is_active_third_party_sku_filter
  catalog_item_query
end

#catalog_items_sizeObject

Alias for Catalog_items#size

Returns:

  • (Object)

    Catalog_items#catalog_items_size

See Also:



50
# File 'app/models/feed.rb', line 50

delegate :size, to: :catalog_items, prefix: true

#contact_pointsActiveRecord::Relation<ContactPoint>

Returns:

See Also:



54
# File 'app/models/feed.rb', line 54

has_and_belongs_to_many :contact_points, inverse_of: :feeds

#contact_points_for_selectObject



130
131
132
# File 'app/models/feed.rb', line 130

def contact_points_for_select
  customer.all_contact_points.emails.map { |cp| ["#{cp.party.full_name} - #{cp.detail}", cp.id] }.sort_by { |cp| cp[0] }
end

#custom_formatter_classObject

Raises:

  • (NameError)


187
188
189
190
191
192
# File 'app/models/feed.rb', line 187

def custom_formatter_class
  klass = "Edi::FileFormat::#{custom_file_formatter}".safe_constantize
  raise NameError, "Custom file formatter class Edi::FileFormat::#{custom_file_formatter} not found for Feed ##{id}" unless klass

  klass
end

#customerCustomer

Returns:

See Also:



52
# File 'app/models/feed.rb', line 52

belongs_to :customer, inverse_of: :feeds, optional: true

#deep_dupObject



80
81
82
83
84
85
86
87
# File 'app/models/feed.rb', line 80

def deep_dup
  deep_clone do |original, copy|
    if copy.is_a?(Feed)
      copy.name = "#{original.name} (Copy)"
      copy.active = false
    end
  end
end

#encoding_to_useObject



207
208
209
210
211
212
213
214
215
# File 'app/models/feed.rb', line 207

def encoding_to_use
  enc = begin
    Encoding.find(encoding)
  rescue StandardError
    nil
  end
  enc ||= Encoding.find('UTF-8')
  enc.to_s
end

#feed_attributesObject

Returns relevant instance variables to be used in template merge



150
151
152
153
154
155
156
157
158
# File 'app/models/feed.rb', line 150

def feed_attributes
  res = {}
  case feed_type.to_sym
  when :inventory
    res[:catalog_items] = catalog_items
  end
  res[:unique_id] = Time.current.to_i
  res
end

#feed_historiesActiveRecord::Relation<FeedHistory>

Returns:

See Also:



53
# File 'app/models/feed.rb', line 53

has_many :feed_histories, inverse_of: :feed

#file_nameObject



225
226
227
228
229
230
231
232
233
234
235
236
# File 'app/models/feed.rb', line 225

def file_name
  @file_name ||= ERB.new(file_name_template).result(binding) if file_name_template.present?
  if @file_name.nil? && custom_formatter?
    begin
      @file_name = custom_formatter_class.file_name
    rescue NameError => e
      Rails.logger.error("[Feed#file_name] #{e.message} — falling back to default name")
    end
  end
  @file_name ||= "#{feed_type}_#{id}_#{customer_id}_#{Time.current.strftime('%y%m%d%H%M')}.#{output_format}"
  @file_name
end

#last_feedObject



341
342
343
# File 'app/models/feed.rb', line 341

def last_feed
  feed_histories.all.order('id desc').try(:first)
end

#last_run_dateObject



345
346
347
# File 'app/models/feed.rb', line 345

def last_run_date
  last_feed.try(:created_at)
end

#last_run_resultObject



349
350
351
# File 'app/models/feed.rb', line 349

def last_run_result
  last_feed.try(:result)
end

#output_to_fileObject



314
315
316
317
318
# File 'app/models/feed.rb', line 314

def output_to_file
  local_file_path = Rails.application.config.x.temp_storage_path.join(file_name)
  render_to_file local_file_path
  local_file_path
end

#renderObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'app/models/feed.rb', line 172

def render
  # Create some bindings for specific types
  if product_data?
    res = Feed::ItemListGenerator.new.process
    output = res.output
  else # ERB style render
    @date = Time.current
    feed_attributes.each { |k, v| instance_variable_set(:"@#{k}", v) }
    @feed = self
    output = ERB.new(output_layout).result(binding)
  end
  output = output.gsub("\r\n", "\n") if universal?
  output.encode(encoding_to_use, invalid: :replace, undef: :replace)
end

#render_to_file(file_path) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
# File 'app/models/feed.rb', line 194

def render_to_file(file_path)
  if custom_formatter?
    formatter = custom_formatter_class.new(output_location: file_path)
    formatter.process feed_attributes
  else
    File.open(file_path, "w+:#{encoding_to_use}") do |f|
      f.write(render)
      f.flush
      f.fsync
    end
  end
end

#runObject



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'app/models/feed.rb', line 250

def run
  return unless active

  # Only one method for now
  Rails.logger.info "Running feed #{id}"
  res = transmit

  if res.transmitted?
    update_column(:next_attempt, calculate_next_attempt) # Don't require validation
  else
    Rails.logger.error " ** Error running feed #{id} #{res.message}"
    update_column(:next_attempt, 1.hour.from_now) # Don't require validation
  end
  Rails.logger.info "Feed id #{id} complete, next run scheduled for #{next_attempt}"
  res
end

#sender_email_options_for_selectObject



134
135
136
137
138
# File 'app/models/feed.rb', line 134

def sender_email_options_for_select
  ['ediadmin@warmlyyours.com'] +
    ['orders@warmlyyours.com'] +
    Employee.active_employees.map(&:email).sort
end

#ssc(str) ⇒ Object

Strip Special Characters



145
146
147
# File 'app/models/feed.rb', line 145

def ssc(str)
  str.gsub(%r{[^0-9A-Za-z.-/]}, ' ').squish
end

#to_sObject



221
222
223
# File 'app/models/feed.rb', line 221

def to_s
  "Feed #{id}"
end

#transmitObject



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/models/feed.rb', line 320

def transmit
  local_file_path = output_to_file

  raise "Feed #{local_file_path} not found" unless File.exist? local_file_path

  upload = Upload.uploadify(local_file_path.to_s, 'Feed')
  logger.info "Job output uploaded to upload id #{upload.id}"

  transmitter = build_transmitter

  res = transmitter.process(upload)

  fh = feed_histories.create(result: res.status,
                             file_name: upload.try(:attachment_name),
                             notes: res.message,
                             communication: res.try(:communication),
                             upload:)

  res
end