mge_wholesale 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 886679bf2b3c05ded7fc01419e2e8ac27bdf66ce7d7901e2a4e2b30873545854
4
+ data.tar.gz: f4f51d632d13087d1e9a37875f566a70be0004a766e0b630ee5f04483d528cb6
5
+ SHA512:
6
+ metadata.gz: f0f804a38963dfdd52c082a862d013ea582af0686709de1c5962933b5a7a7935a6338133446881f11b04c850d91b1a0baf7550d30376ade28f50d80b3bb5e34c
7
+ data.tar.gz: 824e81b8deb279ebb36a99333aef106dd228da1e89d1ed4feaf37b6f7b671bb40868fc302f8180b72def4835edff9e8d09fe2cc87e37e796fe151644b12e4e92
@@ -0,0 +1,41 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+ jobs:
7
+ build:
8
+ docker:
9
+ # specify the version you desire here
10
+ - image: circleci/ruby:2.4.1-node-browsers
11
+
12
+ working_directory: ~/repo
13
+
14
+ steps:
15
+ - checkout
16
+
17
+ # Download and cache dependencies
18
+ - restore_cache:
19
+ keys:
20
+ - v1-dependencies-{{ checksum "mge_wholesale.gemspec" }}
21
+ # fallback to using the latest cache if no exact match is found
22
+ - v1-dependencies-
23
+
24
+ - run:
25
+ name: install dependencies
26
+ command: |
27
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
28
+
29
+ - save_cache:
30
+ paths:
31
+ - ./vendor/bundle
32
+ key: v1-dependencies-{{ checksum "Gemfile.lock" }}
33
+
34
+ # run tests!
35
+ - run:
36
+ name: run tests
37
+ command: |
38
+ mkdir /tmp/test-results
39
+ TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
40
+
41
+ bundle exec rspec --format documentation $TEST_FILES
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ script/*
11
+
data/.rbenv-gemsets ADDED
@@ -0,0 +1 @@
1
+ mge_wholesale
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ mge_wholesale
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mge_wholesale.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Ken Ebling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # MGE Wholesale
2
+
3
+ Ruby library for MGE Wholesale.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mge_wholesale'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install mge_wholesale
20
+
21
+
22
+
23
+ #TODO: Update info below
24
+
25
+
26
+
27
+ ## Usage
28
+
29
+ **Note:** Nearly all methods require `:username` and `:password` keys in the options hash.
30
+
31
+ ```ruby
32
+ options = {
33
+ username: 'dealer@example.com',
34
+ password: 'sekret-passwd'
35
+ }
36
+ ```
37
+
38
+ ### MgeWholesale::Catalog
39
+
40
+ To get all items in the catalog:
41
+
42
+ ```ruby
43
+ catalog = []
44
+ MgeWholesale::Catalog.new(options).all do |i|
45
+ catalog << i
46
+ end
47
+ ```
48
+
49
+ See `MgeWholesale::Catalog` for the response structure.
50
+
51
+ ### MgeWholesale::Inventory
52
+
53
+ To get your inventory details (availability, price, etc.):
54
+
55
+ ```ruby
56
+ inventory = []
57
+ MgeWholesale::Inventory.new(options).all do |i|
58
+ inventory << i
59
+ end
60
+ ```
61
+
62
+ See `MgeWholesale::Inventory` for the response structure.
63
+
64
+ ### MgeWholesale::Category
65
+
66
+ Returns an array of category codes and descriptions.
67
+
68
+ ```ruby
69
+ categories = MgeWholesale::Category.all(options)
70
+
71
+ # [
72
+ # {:code=>"H648", :description=>"AIRGUNS"},
73
+ # {:code=>"H610", :description=>"AMMUNITION"},
74
+ # ...,
75
+ # ]
76
+ ```
77
+
78
+ ### MgeWholesale::Order
79
+
80
+ To build and submit an order, the basic steps are: 1) instantiate an Order object, 2) add header
81
+ information, 3) add item information (multiple items if needed), 4) submit the order.
82
+
83
+ ```ruby
84
+ # Instantiate the Order instance, passing in your :username and :password
85
+ order = MgeWholesale::Order.new(options)
86
+
87
+ # Add header information:
88
+ header_opts = {
89
+ customer: '...', # customer number
90
+ purchase_order: '...', # application specific purchase order
91
+ ffl: '...', # your FFL number
92
+ shipping: { # shipping information (all fields except :address_2 are required)
93
+ name: '...',
94
+ address_1: '...',
95
+ address_2: '...',
96
+ city: '...',
97
+ state: '...',
98
+ zip: '...',
99
+ },
100
+
101
+ # Optional fields:
102
+ shipping_method: '...',
103
+ notes: '...',
104
+ }
105
+ order.add_header(header_opts)
106
+
107
+ # Add item information:
108
+ item_opts = {
109
+ item_number: '...', # MGE Wholesale item number
110
+ description: '...',
111
+ quantity: 1,
112
+ price: '123.45', # Decimal formatted price, without currency sign
113
+ }
114
+ order.add_item(item_opts) # Multiple items may be added, just call #add_item for each one.
115
+
116
+ # Submit the order (returns true on success, raises an exception on failure):
117
+ order.submit!
118
+ ```
119
+
120
+ See `MgeWholesale::Order` for details on required options.
121
+
122
+ ## Development
123
+
124
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
125
+
126
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
127
+
128
+ ## Contributing
129
+
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ammoready/mge_wholesale.
131
+
132
+
133
+ ## License
134
+
135
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mge_wholesale"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,60 @@
1
+ require 'mge_wholesale/version'
2
+
3
+ require 'net/ftp'
4
+ require 'tempfile'
5
+
6
+ # these can be commented out when the gem is bundled into a Rails app
7
+ require 'nokogiri'
8
+ require 'active_support/all'
9
+
10
+ require 'mge_wholesale/base'
11
+ require 'mge_wholesale/ftp'
12
+ require 'mge_wholesale/catalog'
13
+ require 'mge_wholesale/category'
14
+ require 'mge_wholesale/inventory'
15
+ #require 'mge_wholesale/order'
16
+ #require 'mge_wholesale/response_file'
17
+ require 'mge_wholesale/user'
18
+ #require 'mge_wholesale/brand_converter'
19
+
20
+ module MgeWholesale
21
+ #TODO: handle orders
22
+ class InvalidOrder < StandardError; end
23
+ class NotAuthenticated < StandardError; end
24
+
25
+ class << self
26
+ attr_accessor :config
27
+ end
28
+
29
+ def self.config
30
+ @config ||= Configuration.new
31
+ end
32
+
33
+ def self.configure
34
+ yield(config)
35
+ end
36
+
37
+ class Configuration
38
+ attr_accessor :debug_mode
39
+ attr_accessor :ftp_host
40
+ attr_accessor :response_dir
41
+ attr_accessor :submission_dir
42
+ attr_accessor :top_level_dir
43
+
44
+ def initialize
45
+ @debug_mode ||= false
46
+ @ftp_host ||= "ftp.mgegroup.com"
47
+ @top_level_dir ||= "ffldealer"
48
+ @submission_dir ||= ""
49
+ @response_dir ||= ""
50
+ end
51
+
52
+ def full_submission_dir
53
+ File.join(@top_level_dir, @submission_dir)
54
+ end
55
+
56
+ def full_response_dir
57
+ File.join(@top_level_dir, @response_dir)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,77 @@
1
+ module MgeWholesale
2
+ class Base
3
+
4
+ def self.connect(options = {})
5
+ requires!(options, :username, :password)
6
+
7
+ Net::FTP.open(MgeWholesale.config.ftp_host, options[:username], options[:password]) do |ftp|
8
+ begin
9
+ ftp.debug_mode = MgeWholesale.config.debug_mode
10
+ ftp.passive = true
11
+ yield ftp
12
+ ensure
13
+ ftp.close
14
+ end
15
+ end
16
+ rescue Net::FTPPermError
17
+ raise MgeWholesale::NotAuthenticated
18
+ end
19
+
20
+ protected
21
+
22
+ # Wrapper to `self.requires!` that can be used as an instance method.
23
+ def requires!(*args)
24
+ self.class.requires!(*args)
25
+ end
26
+
27
+ def self.requires!(hash, *params)
28
+ params.each do |param|
29
+ if param.is_a?(Array)
30
+ raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
31
+
32
+ valid_options = param[1..-1]
33
+ raise ArgumentError.new("Parameter: #{param.first} must be one of: #{valid_options.join(', ')}") unless valid_options.include?(hash[param.first])
34
+ else
35
+ raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
36
+ end
37
+ end
38
+ end
39
+
40
+ # Instance methods become class methods through inheritance
41
+ def connect(options)
42
+ self.class.connect(options) do |ftp|
43
+ begin
44
+ yield ftp
45
+ ensure
46
+ ftp.close
47
+ end
48
+ end
49
+ end
50
+
51
+ def get_file(filename)
52
+ connect(@options) do |ftp|
53
+ begin
54
+ tempfile = Tempfile.new
55
+
56
+ ftp.chdir(MgeWholesale.config.top_level_dir)
57
+ ftp.getbinaryfile(filename, tempfile.path)
58
+
59
+ tempfile
60
+ ensure
61
+ ftp.close
62
+ end
63
+ end
64
+ end
65
+
66
+ def content_for(xml_doc, field)
67
+ node = xml_doc.css(field).first
68
+
69
+ if node.nil?
70
+ nil
71
+ else
72
+ node.content.try(:strip)
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,125 @@
1
+ module MgeWholesale
2
+ class Catalog < Base
3
+
4
+ CATALOG_FILENAME = 'vendorname_items.xml'
5
+ ITEM_NODE_NAME = 'item'
6
+
7
+ PERMITTED_FEATURES = [
8
+ 'Action',
9
+ 'Barrel_Length',
10
+ 'Blade_Edge',
11
+ 'Blade_Finish',
12
+ 'Blade_Length',
13
+ 'Blade_Style',
14
+ 'Box_Qty',
15
+ 'Bullet_Type',
16
+ 'Caliber',
17
+ 'Caliber_Gauge',
18
+ 'Capacity',
19
+ 'Carry_Type',
20
+ 'Color',
21
+ 'Cutting_Edge',
22
+ 'Diameter',
23
+ 'Edge',
24
+ 'Finish',
25
+ 'Gauge',
26
+ 'Grain',
27
+ 'Grit',
28
+ 'Gun_Manufacturer',
29
+ 'Gun_Model',
30
+ 'L_x_W',
31
+ 'Magnifactaion',
32
+ 'Material',
33
+ 'Model',
34
+ 'OAL',
35
+ 'Reticle',
36
+ 'Shell_Length',
37
+ 'Shot',
38
+ 'Tube_Diameter',
39
+ 'Type'
40
+ ]
41
+
42
+ def initialize(options = {})
43
+ requires!(options, :username, :password)
44
+ @options = options
45
+ end
46
+
47
+ def self.all(options = {}, &block)
48
+ requires!(options, :username, :password)
49
+ new(options).all(&block)
50
+ end
51
+
52
+ def all(&block)
53
+ tempfile = get_file(CATALOG_FILENAME)
54
+
55
+ Nokogiri::XML::Reader.from_io(tempfile).each do |node|
56
+ next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
57
+ next unless node.name == ITEM_NODE_NAME
58
+
59
+ yield map_hash(Nokogiri::XML::DocumentFragment.parse(node.inner_xml))
60
+ end
61
+
62
+ tempfile.close
63
+ tempfile.unlink
64
+ end
65
+
66
+ protected
67
+
68
+ def map_hash(node)
69
+ features = map_features(node)
70
+
71
+ category = content_for(node, 'category')
72
+ subcategory = content_for(node, 'subCategory')
73
+
74
+ case category
75
+ when 'Firearms'
76
+ case subcategory
77
+ when 'Pistol', 'Revolver'
78
+ product_type = :handgun
79
+ when 'Rifle', 'Rifle Frame', 'Shotgun', 'Short Barrel Rifle'
80
+ product_type = :rifle
81
+ end
82
+ when 'NFA - Class 3'
83
+ case subcategory
84
+ when 'Suppressors'
85
+ product_type = :suppressor
86
+ end
87
+ end
88
+
89
+ {
90
+ name: content_for(node, 'name'),
91
+ upc: content_for(node, 'barcod'),
92
+ item_identifier: content_for(node, 'id'),
93
+ quantity: content_for(node, 'qty').to_i,
94
+ price: content_for(node, 'price'),
95
+ short_description: content_for(node, 'description'),
96
+ product_type: product_type,
97
+ category: category,
98
+ subcategory: subcategory,
99
+ mfg_number: content_for(node, 'vendorItemNo'),
100
+ weight: content_for(node, 'Weight'),
101
+ brand: content_for(node, 'Brand'),
102
+ features: features
103
+ }
104
+ end
105
+
106
+ def map_features(node)
107
+ features = Hash.new
108
+
109
+ node.elements.each do |n|
110
+ if PERMITTED_FEATURES.include?(n.name.strip)
111
+ # fix their typo
112
+ n.name = "Magnification" if n.name.strip == "Magnifactaion"
113
+
114
+ features[n.name.strip] = n.text.gsub("_", " ").strip
115
+ end
116
+ end
117
+
118
+ features.delete_if { |k, v| v.to_s.blank? }
119
+ features.transform_keys! { |k| k.downcase }
120
+ features.transform_keys! { |k| k.gsub(/[^0-9A-Za-z\_]/, '') }
121
+ features.symbolize_keys!
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,42 @@
1
+ module MgeWholesale
2
+ # Category item response structure:
3
+ #
4
+ # {
5
+ # code: "...", # ':category_code' in Catalog response.
6
+ # description: "..." # ':category_description' in Catalog response.
7
+ # }
8
+ class Category < Base
9
+
10
+ def initialize(options = {})
11
+ requires!(options, :username, :password)
12
+ @options = options
13
+ end
14
+
15
+ def self.all(options = {}, &block)
16
+ requires!(options, :username, :password)
17
+ new(options).all(&block)
18
+ end
19
+
20
+ # Returns an array of hashes with category details.
21
+ def all(&block)
22
+ categories = []
23
+
24
+ # Categories are listed in catalog xml, so fetch that.
25
+ catalog = Catalog.new(@options)
26
+ catalog.all do |item|
27
+ categories << {
28
+ code: item[:subcategory].gsub("&amp;", "").gsub(/[^A-Za-z0-9]/, "_").squeeze("_").downcase,
29
+ description: item[:subcategory].gsub("&amp;", "&")
30
+ }
31
+ end
32
+
33
+ categories.uniq! { |c| c[:code] }
34
+ categories.sort_by! { |c| c[:code] }
35
+
36
+ categories.each do |category|
37
+ yield category
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module MgeWholesale
2
+ class FTP
3
+
4
+ attr_reader :connection
5
+
6
+ def initialize(credentials)
7
+ @connection ||= Net::FTP.new(MgeWholesale.config.ftp_host)
8
+ @connection.passive = true
9
+ self.login(credentials[:username], credentials[:password])
10
+ end
11
+
12
+ def login(username, password)
13
+ @connection.login(username, password)
14
+ rescue Net::FTPPermError
15
+ raise MgeWholesale::NotAuthenticated
16
+ end
17
+
18
+ def close
19
+ @connection.close
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,64 @@
1
+ module MgeWholesale
2
+ class Inventory < Base
3
+
4
+ INVENTORY_FILENAME = 'exportXML_cq_barcodeFFL.xml'
5
+
6
+ def initialize(options = {})
7
+ requires!(options, :username, :password)
8
+ @options = options
9
+ end
10
+
11
+ def self.all(options = {}, &block)
12
+ requires!(options, :username, :password)
13
+ new(options).all(&block)
14
+ end
15
+
16
+ def self.get_quantity_file(options = {})
17
+ requires!(options, :username, :password)
18
+ new(options).get_quantity_file
19
+ end
20
+
21
+ def self.quantity(options = {}, &block)
22
+ requires!(options, :username, :password)
23
+ new(options).all(&block)
24
+ end
25
+
26
+ def all(&block)
27
+ tempfile = get_file(INVENTORY_FILENAME)
28
+
29
+ Nokogiri::XML(tempfile).xpath('//item').each do |item|
30
+ yield map_hash(item)
31
+ end
32
+
33
+ tempfile.close
34
+ tempfile.unlink
35
+ end
36
+
37
+ def get_quantity_file
38
+ inventory_tempfile = get_file(INVENTORY_FILENAME)
39
+ tempfile = Tempfile.new
40
+
41
+ Nokogiri::XML(inventory_tempfile).xpath('//item').each do |item|
42
+ tempfile.puts("#{content_for(item, 'id')},#{content_for(item, 'qty')}")
43
+ end
44
+
45
+ inventory_tempfile.close
46
+ inventory_tempfile.unlink
47
+ tempfile.close
48
+ tempfile.path
49
+ end
50
+
51
+ alias quantity all
52
+
53
+ private
54
+
55
+ def map_hash(node)
56
+ {
57
+ item_identifier: content_for(node, 'id'),
58
+ quantity: content_for(node, 'qty').to_i,
59
+ price: content_for(node, 'cost')
60
+ }
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,127 @@
1
+ module MgeWholesale
2
+ # To submit an order:
3
+ #
4
+ # * Instantiate a new Order, passing in `:username` and `:password`
5
+ # * Call {#add_header}
6
+ # * Call {#add_item} for each item on the order
7
+ # * Call {#submit!} to send the order
8
+ #
9
+ # See each method for a list of required options.
10
+ class Order < Base
11
+
12
+ # @option options [String] :username *required*
13
+ # @option options [String] :password *required*
14
+ def initialize(options = {})
15
+ requires!(options, :username, :password)
16
+ @options = options
17
+ @items = []
18
+ end
19
+
20
+ # @param [Hash] header
21
+ # * :customer [String] *required*
22
+ # * :purchase_order [String] *required*
23
+ # * :ffl [String] *required*
24
+ # * :shipping [Hash] *required*
25
+ # * :name [String] *required*
26
+ # * :address_1 [String] *required*
27
+ # * :address_2 [String] optional
28
+ # * :city [String] *required*
29
+ # * :state [String] *required*
30
+ # * :zip [String] *required*
31
+ def add_header(header = {})
32
+ requires!(header, :customer, :purchase_order, :ffl, :shipping)
33
+ requires!(header[:shipping], :name, :address_1, :city, :state, :zip)
34
+ @header = header
35
+ # Ensure that address_2 is not an empty string
36
+ if @header[:shipping][:address_2] && @header[:shipping][:address_2].empty?
37
+ @header[:shipping][:address_2] = nil
38
+ end
39
+ end
40
+
41
+ # @option item [String] :item_number *required*
42
+ # @option item [Integer] :quantity *required*
43
+ # @option item [String] :price *required* - Decimal formatted price, without currency sign
44
+ # @option item [String] :description optional
45
+ def add_item(item = {})
46
+ requires!(item, :item_number, :quantity, :price)
47
+ @items << item
48
+ end
49
+
50
+ def filename
51
+ "#{@header[:purchase_order]}-order.txt"
52
+ end
53
+
54
+ def submit!
55
+ raise MgeWholesale::InvalidOrder.new("Must call #add_header before submitting") if @header.nil?
56
+ raise MgeWholesale::InvalidOrder.new("Must add items with #add_item before submitting") if @items.empty?
57
+
58
+ @order_file = Tempfile.new(filename)
59
+ begin
60
+ CSV.open(@order_file.path, 'w+', col_sep: "\t") do |csv|
61
+ csv << header_names
62
+ csv << header_fields
63
+ csv << items_header
64
+ @items.each do |item|
65
+ csv << item_fields(item)
66
+ end
67
+ end
68
+
69
+ upload!
70
+ ensure
71
+ # Close and delete (unlink) file.
72
+ @order_file.close
73
+ @order_file.unlink
74
+ end
75
+
76
+ # TODO: Find some way of returning a meaningful true/false. Currently, if there's a problem, an exception is raised.
77
+ true
78
+ end
79
+
80
+ private
81
+
82
+ def header_names
83
+ ['HL', 'Customer#', 'Ship to#', 'Ship to Name1', 'Address 1', 'Address 2', 'city', 'state', 'zip', 'cust po', 'ship method', 'notes', 'FFL#']
84
+ end
85
+
86
+ def header_fields
87
+ [
88
+ 'H',
89
+ @header[:customer],
90
+ (@header[:shipping][:ship_to_number] || '0000'),
91
+ @header[:shipping][:name],
92
+ @header[:shipping][:address_1],
93
+ @header[:shipping][:address_2],
94
+ @header[:shipping][:city],
95
+ @header[:shipping][:state],
96
+ @header[:shipping][:zip],
97
+ @header[:purchase_order],
98
+ @header[:shipping_method],
99
+ @header[:notes],
100
+ @header[:ffl],
101
+ 'END',
102
+ ]
103
+ end
104
+
105
+ def items_header
106
+ ['LL', 'Item', 'Description', 'Qty', 'Price']
107
+ end
108
+
109
+ def item_fields(item)
110
+ [
111
+ 'L',
112
+ item[:item_number],
113
+ item[:description],
114
+ item[:quantity],
115
+ item[:price],
116
+ ]
117
+ end
118
+
119
+ def upload!
120
+ connect(@options) do |ftp|
121
+ ftp.chdir(MgeWholesale.config.full_submission_dir)
122
+ ftp.puttextfile(@order_file.path, filename)
123
+ end
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,71 @@
1
+ module MgeWholesale
2
+ class ResponseFile < Base
3
+
4
+ attr_reader :credentials
5
+ attr_reader :filename
6
+
7
+ # @option options [String] :username *required*
8
+ # @option options [String] :password *required*
9
+ # @option options [String] :filename *required*
10
+ def initialize(ftp, options = {})
11
+ requires!(options, :username, :password, :filename)
12
+
13
+ @credentials = options.select { |k, v| [:username, :password].include?(k) }
14
+ @filename = options[:filename]
15
+ @ftp = ftp
16
+ end
17
+
18
+ # Return list of '855 Purchase Order Acknowledgement' files
19
+ # @option options [String] :username *required*
20
+ # @option options [String] :password *required*
21
+ def self.all(ftp)
22
+ ftp.nlst("*.txt")
23
+ end
24
+
25
+ # Is the file a '855 Purchase Order Acknowledgement'?
26
+ def ack?
27
+ filename.downcase.start_with?("ack")
28
+ end
29
+
30
+ # Is the file a '856 Advance Shipping Notice'?
31
+ def asn?
32
+ filename.downcase.start_with?("asn")
33
+ end
34
+
35
+ # Use '#gettextfile' to read file contents as a string
36
+ def content
37
+ return @content if @content
38
+
39
+ @content = @ftp.gettextfile(@filename, nil)
40
+
41
+ @content
42
+ end
43
+
44
+ # Convert to easily readable key-value pairs
45
+ def to_json
46
+ if corrupt_asn?
47
+ CSV.parse(content.gsub("Price|", ""), headers: true, col_sep: "|").
48
+ map { |x| x.to_h }.
49
+ group_by { |x| x["PO Number"] }
50
+ else
51
+ CSV.parse(content, headers: true, col_sep: "|").
52
+ map { |x| x.to_h }.
53
+ group_by { |x| x["PO Number"] }
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def corrupt_asn?
60
+ return false if ack?
61
+ lines = content.lines.map(&:chomp)
62
+ if lines[0].split("|").length != lines[1].split("|").length
63
+ puts "Notice: ASN file is malformed! (#{filename})"
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ module MgeWholesale
2
+ class User < Base
3
+
4
+ def initialize(options = {})
5
+ requires!(options, :username, :password)
6
+ @options = options
7
+ end
8
+
9
+ def authenticated?
10
+ connect(@options) { |ftp| ftp.pwd }
11
+ true
12
+ rescue MgeWholesale::NotAuthenticated
13
+ false
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module MgeWholesale
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mge_wholesale/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mge_wholesale"
8
+ spec.version = MgeWholesale::VERSION
9
+ spec.authors = ["Ken Ebling"]
10
+ spec.email = ["kenebling@gmail.com"]
11
+
12
+ spec.summary = %q{Ruby library for MGE Wholesale}
13
+ spec.description = %q{}
14
+ spec.homepage = ""
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "nokogiri", "~> 1.8"
25
+ spec.add_runtime_dependency "activesupport", "~> 5"
26
+
27
+ spec.add_development_dependency "bundler", ">= 1.15"
28
+ spec.add_development_dependency "rake", "~> 12.3.0"
29
+ spec.add_development_dependency "rspec", "~> 3.7"
30
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mge_wholesale
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ken Ebling
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 12.3.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 12.3.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.7'
83
+ description: ''
84
+ email:
85
+ - kenebling@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".circleci/config.yml"
91
+ - ".gitignore"
92
+ - ".rbenv-gemsets"
93
+ - ".rspec"
94
+ - ".ruby-gemset"
95
+ - ".ruby-version"
96
+ - Gemfile
97
+ - LICENSE.txt
98
+ - README.md
99
+ - Rakefile
100
+ - bin/console
101
+ - bin/setup
102
+ - lib/mge_wholesale.rb
103
+ - lib/mge_wholesale/base.rb
104
+ - lib/mge_wholesale/catalog.rb
105
+ - lib/mge_wholesale/category.rb
106
+ - lib/mge_wholesale/ftp.rb
107
+ - lib/mge_wholesale/inventory.rb
108
+ - lib/mge_wholesale/order.rb
109
+ - lib/mge_wholesale/response_file.rb
110
+ - lib/mge_wholesale/user.rb
111
+ - lib/mge_wholesale/version.rb
112
+ - mge_wholesale.gemspec
113
+ homepage: ''
114
+ licenses:
115
+ - MIT
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 2.7.7
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Ruby library for MGE Wholesale
137
+ test_files: []