rsr_group 1.0.0.pre → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c9bf20350d6ad63a809ab9b1ec14ed29dec0acb
4
- data.tar.gz: 0cdadb5c40579b6abe6d28fa55e68dd486f9e27d
3
+ metadata.gz: 30aec6e206ed332500ce1beac071246a0ae65ebc
4
+ data.tar.gz: ff5b079c4316ed16e702dc950bed30f2be80f23e
5
5
  SHA512:
6
- metadata.gz: d608e462128d6ab5e7dcef36f317c5b028e21ce9d47329c5b5b4eacff091a363028fa66b2b1afc505fb7faa7fabc83689848c6de0120d0061aaaa242d92e5d87
7
- data.tar.gz: 1ecd0f05f172f4a071df7e5c74c16e4367ad2cc9e269f67011cdf3bbb853e7f5d7a2f369d68b87370665d470f4c7f5b1f9d0b2f3f2db07a686e024e620c0807c
6
+ metadata.gz: f4957378cc596059428bcb1d4f53f14565471cbab324b58418c49e1b460256ecce39555c118d33aefe9dcecc8cc0f4cee44ecfaef79a912f97b7fb34ecba161b
7
+ data.tar.gz: 103f0c0119ef13e4a22a6feadb55533fc9925467c194d646483850732e16bcab6ae5abba216a67e878295caa03cbded6a4c2e8ecbe7cb3689ddb553353e83473
data/lib/rsr_group.rb CHANGED
@@ -5,12 +5,15 @@ require 'date'
5
5
  require 'net/ftp'
6
6
 
7
7
  require 'rsr_group/base'
8
+ require 'rsr_group/constants'
9
+ require 'rsr_group/data_row'
8
10
  require 'rsr_group/department'
9
11
  require 'rsr_group/inventory'
10
12
  require 'rsr_group/order'
11
13
  require 'rsr_group/order_detail'
12
14
  require 'rsr_group/order_ffl'
13
15
  require 'rsr_group/order_recipient'
16
+ require 'rsr_group/response_file'
14
17
  require 'rsr_group/user'
15
18
 
16
19
  module RsrGroup
@@ -32,11 +35,13 @@ module RsrGroup
32
35
  class Configuration
33
36
  attr_accessor :ftp_host
34
37
  attr_accessor :submission_dir
38
+ attr_accessor :response_dir
35
39
  attr_accessor :vendor_email
36
40
 
37
41
  def initialize
38
42
  @ftp_host ||= "ftp.rsrgroup.com"
39
43
  @submission_dir ||= File.join("eo", "incoming")
44
+ @response_dir ||= File.join("eo", "outgoing")
40
45
  @vendor_email ||= nil
41
46
  end
42
47
  end
@@ -1,24 +1,17 @@
1
1
  module RsrGroup
2
2
  class Base
3
3
 
4
- SHIPPING_CARRIERS = %w(UPS USPS)
5
-
6
- SHIPPING_METHODS = {
7
- "Grnd" => "Ground",
8
- "1Day" => "Next Day Air",
9
- "2Day" => "2nd Day Air",
10
- "3Day" => "3 Day Select",
11
- "NDam" => "Next Day Early AM",
12
- "NDAS" => "Next Day Air Saver",
13
- "PRIO" => "Priority"
14
- }
15
-
16
- protected
4
+ def self.connect(options = {})
5
+ requires!(options, :username, :password)
17
6
 
18
- def self.ftp_host
19
- RsrGroup.config.ftp_host
7
+ Net::FTP.open(RsrGroup.config.ftp_host, options[:username], options[:password]) do |ftp|
8
+ ftp.passive = true
9
+ yield ftp
10
+ end
20
11
  end
21
12
 
13
+ protected
14
+
22
15
  def self.requires!(hash, *params)
23
16
  params.each do |param|
24
17
  if param.is_a?(Array)
@@ -32,20 +25,14 @@ module RsrGroup
32
25
  end
33
26
  end
34
27
 
35
- def ftp_host
36
- self.class.ftp_host
37
- end
38
-
39
28
  # Wrapper to `self.requires!` that can be used as an instance method.
40
29
  def requires!(*args)
41
30
  self.class.requires!(*args)
42
31
  end
43
32
 
44
- def connect(options = {})
45
- requires!(options, :username, :password)
46
-
47
- Net::FTP.open(ftp_host, options[:username], options[:password]) do |ftp|
48
- ftp.passive = true
33
+ # Instance methods become class methods through inheritance
34
+ def connect(options)
35
+ self.class.connect(options) do |ftp|
49
36
  yield ftp
50
37
  end
51
38
  end
@@ -0,0 +1,89 @@
1
+ module RsrGroup
2
+
3
+ FILE_TYPES = {
4
+ "EERR" => "Error",
5
+ "ECONF" => "Confirmation",
6
+ "ESHIP" => "Shipping",
7
+ }
8
+
9
+ LINE_TYPES = {
10
+ "00" => :file_header,
11
+ "10" => :order_header,
12
+ "11" => :ffl_dealer,
13
+ "20" => :order_detail,
14
+ "30" => :confirmation_header,
15
+ "40" => :confirmation_detail,
16
+ "50" => :confirmation_trailer,
17
+ "60" => :shipping_header,
18
+ "70" => :shipping_detail,
19
+ "80" => :shipping_trailer,
20
+ "90" => :order_trailer,
21
+ "99" => :file_trailer,
22
+ }
23
+
24
+ SHIPPING_CARRIERS = %w(UPS USPS)
25
+
26
+ SHIPPING_METHODS = {
27
+ "Grnd" => "Ground",
28
+ "1Day" => "Next Day Air",
29
+ "2Day" => "2nd Day Air",
30
+ "3Day" => "3 Day Select",
31
+ "NDam" => "Next Day Early AM",
32
+ "NDAS" => "Next Day Air Saver",
33
+ "PRIO" => "Priority"
34
+ }
35
+
36
+ ERROR_CODES = {
37
+ "00001" => "File Header record missing",
38
+ "00002" => "Invalid date",
39
+ "00003" => "Duplicate file",
40
+ "00004" => "Invalid Etailer number",
41
+ "00005" => "Empty file",
42
+ "00006" => "Invalid Sequence number",
43
+ "10001" => "Order Header record missing",
44
+ "10002" => "Invalid ship‐to name",
45
+ "10003" => "Missing ship‐to address",
46
+ "10004" => "Invalid ship‐to city",
47
+ "10005" => "Invalid ship‐to state",
48
+ "10006" => "Invalid ship‐to zip",
49
+ "10007" => "Invalid phone",
50
+ "10008" => "Invalid email option",
51
+ "10009" => "Invalid email address",
52
+ "10010" => "Duplicate order",
53
+ "10011" => "No orders found ",
54
+ "10012" => "Invalid ship‐to address ",
55
+ "10013" => "Invalid ship‐to county ",
56
+ "10014" => "Order Cancelled",
57
+ "10099" => "No quantity available",
58
+ "10999" => "Miscellaneous",
59
+ "11000" => "FFL Dealer record missing",
60
+ "11001" => "Account is not setup for firearm shipments",
61
+ "11002" => "Dealer FFL not found",
62
+ "11003" => "Dealer zip code mismatch",
63
+ "11004" => "Dealer name mismatch",
64
+ "11005" => "Dealer state mismatch",
65
+ "11006" => "Dealer FFL expired",
66
+ "11007" => "Firearms combined with accessories",
67
+ "11008" => "Dealer deactivated",
68
+ "11999" => "Miscellaneous",
69
+ "20001" => "Order Detail record missing",
70
+ "20002" => "Invalid RSR stock number",
71
+ "20003" => "Invalid quantity ordered",
72
+ "20004" => "Invalid shipping carrier",
73
+ "20005" => "Invalid shipping method",
74
+ "20006" => "Duplicate item in order",
75
+ "20007" => "Item out of stock",
76
+ "20008" => "Item prohibited",
77
+ "20009" => "Phone number required",
78
+ "20010" => "RSR allocated",
79
+ "20011" => "Case quantity required",
80
+ "20999" => "Miscellaneous",
81
+ "30001" => "OrderTrailerrecordmissing",
82
+ "30002" => "Invalid total order amount",
83
+ "30003" => "Mismatch on number of items ordered and number specified on order trailer record",
84
+ "99001" => "File Trailer record missing",
85
+ "99002" => "Invalid quantity ordered",
86
+ "99003" => "Mismatch on number of orders found and total quantity specified on file trailer record",
87
+ }
88
+
89
+ end
@@ -0,0 +1,78 @@
1
+ module RsrGroup
2
+ class DataRow < Base
3
+
4
+ attr_reader :committed, :date_shipped, :error_code, :handling_fee,
5
+ :identifier, :licence_number, :line_type, :message, :name,
6
+ :ordered, :quantity, :stock_id, :ship_to_name, :shipped,
7
+ :shipping_carrier, :shipping_cost, :shipping_method,
8
+ :rsr_order_number, :tracking_number, :zip
9
+
10
+ def initialize(line, has_errors: false)
11
+ points = line.split(";").map { |point| point.chomp }
12
+
13
+ @identifier = points[0]
14
+ @line_type = LINE_TYPES[points[1]]
15
+
16
+ case @line_type
17
+ when :order_header
18
+ get_errors(points[-1]) if has_errors && points[-1] != "00000"
19
+ @ship_to_name = points[2]
20
+ @zip = points[8]
21
+ when :ffl_dealer # 11
22
+ get_errors(points[-1]) if has_errors && points[-1] != "00000"
23
+ @licence_number = points[2]
24
+ @name = points[3]
25
+ @zip = points[4]
26
+ when :order_detail # 20
27
+ get_errors(points[-1]) if has_errors && points[-1] != "00000"
28
+ @quantity = points[3].to_i
29
+ @stock_id = points[2]
30
+ @shipping_carrier = points[4]
31
+ @shipping_method = SHIPPING_METHODS[points[5]]
32
+ when :confirmation_header # 30
33
+ @rsr_order_number = points[2]
34
+ when :confirmation_detail # 40
35
+ @committed = points[4].to_i
36
+ @ordered = points[3].to_i
37
+ @stock_id = points[2]
38
+ when :confirmation_trailer # 50
39
+ @committed = points[3].to_i
40
+ @ordered = points[2].to_i
41
+ when :shipping_header # 60
42
+ @date_shipped = Time.parse(points[5])
43
+ @handling_fee = points[7].to_i.to_s
44
+ @rsr_order_number = points[4]
45
+ @ship_to_name = points[2]
46
+ @shipping_carrier = points[8]
47
+ @shipping_cost = points[6].to_i.to_s
48
+ @shipping_method = points[9]
49
+ @tracking_number = points[3]
50
+ when :shipping_detail # 70
51
+ @ordered = points[3].to_i
52
+ @shipped = points[4].to_i
53
+ @stock_id = points[2]
54
+ when :shipping_trailer # 80
55
+ @ordered = points[2].to_i
56
+ @shipped = points[3].to_i
57
+ when :order_trailer # 90
58
+ @quantity = points[2].to_i
59
+ end
60
+ end
61
+
62
+ def to_h
63
+ @to_h ||= Hash[
64
+ instance_variables.map do |name|
65
+ [name.to_s.gsub("@", "").to_sym, instance_variable_get(name)]
66
+ end
67
+ ]
68
+ end
69
+
70
+ private
71
+
72
+ def get_errors(code)
73
+ @error_code = code
74
+ @message = ERROR_CODES[code]
75
+ end
76
+
77
+ end
78
+ end
@@ -1,60 +1,76 @@
1
1
  module RsrGroup
2
+ # To submit an order:
3
+ #
4
+ # * Instantiate a new Order, passing in `:merchant_number`, `:username`, `:password`, `:sequence_number`, and `:identifier`
5
+ # * Call {#add_recipient}
6
+ # * Call {#add_item} for each item on the order
7
+ # * Call {#submit!} to send the order
2
8
  class Order < Base
3
9
 
4
- attr_reader :credentials
5
- attr_reader :identifier
6
- attr_reader :timestamp
7
- attr_reader :sequence_number
8
- attr_accessor :recipients
9
-
10
- LINE_TYPES = {
11
- "00" => "file_header",
12
- "10" => "order_header",
13
- "11" => "ffl_dealer",
14
- "20" => "order_detail",
15
- "90" => "order_trailer",
16
- "99" => "file_trailer"
17
- }
18
-
10
+ # @param [Hash] options
11
+ # @option options [String] :merchant_number *required*
12
+ # @option options [Integer] :sequence_number *required*
13
+ # @option options [String] :username *required*
14
+ # @option options [String] :password *required*
15
+ # @option options [String] :identifier *required*
19
16
  def initialize(options = {})
20
- requires!(options, :sequence_number, :username, :password, :identifier)
17
+ requires!(options, :merchant_number, :sequence_number, :username, :password, :identifier)
21
18
 
22
19
  @credentials = options.select { |k, v| [:username, :password].include?(k) }
23
20
  @identifier = options[:identifier]
21
+ @merchant_number = options[:merchant_number]
24
22
  @sequence_number = "%04d" % options[:sequence_number] # Leading zeros are required
25
- @timestamp = Time.now.strftime("%Y%m%e")
26
- @recipients = options[:recipients] || []
23
+ @timestamp = Time.now.strftime("%Y%m%d")
24
+ @items = []
27
25
  end
28
26
 
29
- def header
30
- ["FILEHEADER", LINE_TYPES.key("file_header"), customer_number, @timestamp, @sequence_number].join(";")
27
+ # @param [Hash] shipping_info
28
+ # @option shipping_info [String] :shipping_name *required*
29
+ # @option shipping_info [String] :attn
30
+ # @option shipping_info [String] :address_one *required*
31
+ # @option shipping_info [String] :address_two
32
+ # @option shipping_info [String] :city *required*
33
+ # @option shipping_info [String] :state *required*
34
+ # @option shipping_info [String] :zip *required*
35
+ # @option shipping_info [String] :phone
36
+ # @option shipping_info [String] :email
37
+ #
38
+ # @param [Hash] ffl_options optional
39
+ # @option ffl_options [String] :licence_number *required*
40
+ # @option ffl_options [String] :name *required*
41
+ # @option ffl_options [String] :zip *required*
42
+ def add_recipient(shipping_info, ffl_options = {})
43
+ @recipient = OrderRecipient.new(shipping_info.merge(order_identifier: @identifier))
44
+ @ffl = OrderFFL.new(ffl_options.merge(order_identifier: @identifier)) if ffl_options.any?
31
45
  end
32
46
 
33
- def footer
34
- ["FILETRAILER", LINE_TYPES.key("file_trailer"), ("%05d" % recipients.length)].join(";")
47
+ # @param [Hash] item
48
+ # @option item [String] :rsr_stock_number *required*
49
+ # @option item [Integer] :quantity *required*
50
+ # @option item [String] :shipping_carrier *required*
51
+ # @option item [String] :shipping_method *required*
52
+ def add_item(item)
53
+ @items << OrderDetail.new(item.merge(order_identifier: @identifier))
35
54
  end
36
55
 
37
56
  def filename
38
- name = ["EORD", customer_number, timestamp, sequence_number].join("-")
57
+ name = ["EORD", @merchant_number, @timestamp, @sequence_number].join("-")
39
58
  [name, ".txt"].join
40
59
  end
41
60
 
42
- def recipients
43
- @recipients ||= []
44
- end
45
-
46
61
  def to_txt
62
+ raise "Recipient is required!" unless @recipient
63
+ raise "Items are required!" unless @items.length > 0
64
+
47
65
  txt = header + "\n"
48
- recipients.each do |recipient|
49
- txt += (recipient.to_single_line + "\n")
50
- if recipient.ffl
51
- txt += (recipient.ffl.to_single_line + "\n")
52
- end
53
- recipient.items.each do |item|
54
- txt += (item.to_single_line + "\n")
55
- end
56
- txt += recipient.trailer + "\n"
66
+ txt += @recipient.to_single_line + "\n"
67
+ if @ffl
68
+ txt += (@ffl.to_single_line + "\n")
57
69
  end
70
+ @items.each do |item|
71
+ txt += (item.to_single_line + "\n")
72
+ end
73
+ txt += order_trailer + "\n"
58
74
  txt += footer
59
75
  end
60
76
 
@@ -68,16 +84,23 @@ module RsrGroup
68
84
  io.close
69
85
  end
70
86
  end
87
+ true
71
88
  end
72
89
 
73
90
  private
74
91
 
75
- def customer_number
76
- credentials[:username]
92
+ def header
93
+ ["FILEHEADER", LINE_TYPES.key(:file_header), @merchant_number, @timestamp, @sequence_number].join(";")
94
+ end
95
+
96
+ def footer
97
+ # NOTE: The 'Number of Orders in File' datum is hard-coded to 1
98
+ # For our purposes, only 1 recipient can ever be allowed in an RSR order
99
+ ["FILETRAILER", LINE_TYPES.key(:file_trailer), '00001'].join(";")
77
100
  end
78
101
 
79
- def items
80
- recipients.map(&:items).flatten.compact
102
+ def order_trailer
103
+ [@identifier, LINE_TYPES.key(:order_trailer), ("%07d" % @items.length)].join(";")
81
104
  end
82
105
 
83
106
  end
@@ -17,7 +17,7 @@ module RsrGroup
17
17
  def to_single_line
18
18
  [
19
19
  order_identifier,
20
- Order::LINE_TYPES.key("order_detail"),
20
+ LINE_TYPES.key(:order_detail),
21
21
  @rsr_stock_number,
22
22
  @quantity,
23
23
  @shipping_carrier,
@@ -13,7 +13,7 @@ module RsrGroup
13
13
  def to_single_line
14
14
  [
15
15
  order_identifier,
16
- Order::LINE_TYPES.key("ffl_dealer"),
16
+ LINE_TYPES.key(:ffl_dealer),
17
17
  @options[:licence_number],
18
18
  @options[:name],
19
19
  @options[:zip],
@@ -1,27 +1,17 @@
1
1
  module RsrGroup
2
2
  class OrderRecipient < Base
3
3
 
4
- attr_reader :order_identifier
5
- attr_accessor :ffl
6
- attr_accessor :items
7
-
8
4
  def initialize(options = {})
9
5
  requires!(options, :order_identifier, :shipping_name, :address_one, :city, :state, :zip)
10
6
 
11
- @ffl = options[:ffl]
12
- @items = options[:items] || []
13
7
  @options = options
14
8
  @order_identifier = options[:order_identifier]
15
9
  end
16
10
 
17
- def items
18
- @items ||= []
19
- end
20
-
21
11
  def to_single_line
22
12
  [
23
- order_identifier,
24
- Order::LINE_TYPES.key("order_header"),
13
+ @order_identifier,
14
+ LINE_TYPES.key(:order_header),
25
15
  @options[:shipping_name],
26
16
  @options[:attn],
27
17
  @options[:address_one],
@@ -29,27 +19,13 @@ module RsrGroup
29
19
  @options[:city],
30
20
  @options[:state],
31
21
  @options[:zip],
32
- @options[:phone],
33
- (@options[:email].nil? ? "N" : "Y"),
22
+ (@options[:phone].nil? ? '' : @options[:phone].gsub(/\D/, '')),
23
+ (@options[:email].nil? ? 'N' : 'Y'),
34
24
  @options[:email],
35
25
  RsrGroup.config.vendor_email,
36
26
  nil
37
27
  ].join(";")
38
28
  end
39
29
 
40
- def trailer
41
- [
42
- order_identifier,
43
- Order::LINE_TYPES.key("order_trailer"),
44
- ("%07d" % quantity_sum)
45
- ].join(";")
46
- end
47
-
48
- private
49
-
50
- def quantity_sum
51
- items.map(&:quantity).map(&:to_i).inject(:+)
52
- end
53
-
54
30
  end
55
31
  end
@@ -0,0 +1,89 @@
1
+ module RsrGroup
2
+ class ResponseFile < Base
3
+
4
+ attr_reader :filename
5
+
6
+ def initialize(options = {})
7
+ requires!(options, :username, :password, :filename)
8
+
9
+ @credentials = options.select { |k, v| [:username, :password].include?(k) }
10
+ @filename = options[:filename]
11
+ end
12
+
13
+ FILE_TYPES.each do |key, value|
14
+ define_method("#{value.downcase}?".to_sym) do
15
+ response_type == value
16
+ end
17
+ end
18
+
19
+ def self.all(options = {})
20
+ requires!(options, :username, :password)
21
+
22
+ Base.connect(options) do |ftp|
23
+ ftp.chdir(RsrGroup.config.response_dir)
24
+ ftp.nlst("*.txt")
25
+ end
26
+ end
27
+
28
+ def content
29
+ return @content if @content
30
+ connect(@credentials) do |ftp|
31
+ ftp.chdir(RsrGroup.config.response_dir)
32
+ @content = ftp.gettextfile(@filename, nil)
33
+ end
34
+ end
35
+ alias get_content content
36
+
37
+ def response_type
38
+ FILE_TYPES[@filename.split("-").first]
39
+ end
40
+
41
+ def to_json
42
+ get_content
43
+
44
+ @json = {
45
+ response_type: response_type,
46
+ identifier: @content.lines[1].split(";")[0]
47
+ }
48
+
49
+ return parse_eerr if error?
50
+ return parse_econf if confirmation?
51
+ return parse_eship if shipping?
52
+ end
53
+
54
+ private
55
+
56
+ def parse_eerr
57
+ errors = @content.lines[1..-2].map do |line|
58
+ DataRow.new(line, has_errors: true).to_h
59
+ end.compact
60
+
61
+ errors.select! { |e| !e[:error_code].nil? }
62
+
63
+ @json.merge!(errors: errors)
64
+ end
65
+
66
+ def parse_econf
67
+ details = @content.lines[2..-3].map do |line|
68
+ DataRow.new(line).to_h
69
+ end.compact
70
+
71
+ @json.merge!({
72
+ rsr_order_number: @content.lines[1].split(";")[2].chomp,
73
+ details: details
74
+ })
75
+ end
76
+
77
+ def parse_eship
78
+ details = @content.lines[1..-3].map do |line|
79
+ DataRow.new(line).to_h
80
+ end.compact
81
+
82
+ @json.merge!({
83
+ rsr_order_number: @content.lines[0].split(";")[3].chomp,
84
+ details: details
85
+ })
86
+ end
87
+
88
+ end
89
+ end
@@ -1,3 +1,3 @@
1
1
  module RsrGroup
2
- VERSION = "1.0.0.pre"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsr_group
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dale Campbell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-17 00:00:00.000000000 Z
11
+ date: 2017-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -87,12 +87,15 @@ files:
87
87
  - config.rb.example
88
88
  - lib/rsr_group.rb
89
89
  - lib/rsr_group/base.rb
90
+ - lib/rsr_group/constants.rb
91
+ - lib/rsr_group/data_row.rb
90
92
  - lib/rsr_group/department.rb
91
93
  - lib/rsr_group/inventory.rb
92
94
  - lib/rsr_group/order.rb
93
95
  - lib/rsr_group/order_detail.rb
94
96
  - lib/rsr_group/order_ffl.rb
95
97
  - lib/rsr_group/order_recipient.rb
98
+ - lib/rsr_group/response_file.rb
96
99
  - lib/rsr_group/user.rb
97
100
  - lib/rsr_group/version.rb
98
101
  - rsr_group.gemspec
@@ -111,9 +114,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
114
  version: '2.0'
112
115
  required_rubygems_version: !ruby/object:Gem::Requirement
113
116
  requirements:
114
- - - ">"
117
+ - - ">="
115
118
  - !ruby/object:Gem::Version
116
- version: 1.3.1
119
+ version: '0'
117
120
  requirements: []
118
121
  rubyforge_project:
119
122
  rubygems_version: 2.6.7