rsr_group 1.0.0.pre → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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