spree_fosdick_integration 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/Gemfile +21 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +76 -0
  6. data/Rakefile +15 -0
  7. data/app/controllers/spree/admin/fosdick_shipments_controller.rb +18 -0
  8. data/app/mailers/spree/fosdick_shipment_mailer.rb +13 -0
  9. data/app/models/spree/fosdick_exception.rb +15 -0
  10. data/app/models/spree/fosdick_shipment.rb +8 -0
  11. data/app/models/spree/shipment_decorator.rb +17 -0
  12. data/app/overrides/add_fosdick_panel.rb +8 -0
  13. data/app/serializers/address_serializer.rb +18 -0
  14. data/app/serializers/inventory_unit_serializer.rb +21 -0
  15. data/app/serializers/shipment_serializer.rb +104 -0
  16. data/app/services/exception_logger.rb +21 -0
  17. data/app/views/spree/admin/fosdick_shipments/_filters_bar.html.haml +36 -0
  18. data/app/views/spree/admin/fosdick_shipments/_fosdick_shipments_list.html.haml +28 -0
  19. data/app/views/spree/admin/fosdick_shipments/_fosdick_shipments_list_respond.js.erb +1 -0
  20. data/app/views/spree/admin/fosdick_shipments/_titles.html.haml +5 -0
  21. data/app/views/spree/admin/fosdick_shipments/index.html.haml +3 -0
  22. data/app/views/spree/admin/fosdick_shipments/index.js.haml +1 -0
  23. data/app/views/spree/fosdick_shipment_mailer/order_shipped.html.erb +135 -0
  24. data/config/locales/en.yml +14 -0
  25. data/config/locales/ru.yml +14 -0
  26. data/config/routes.rb +10 -0
  27. data/db/migrate/20151013153126_create_spree_fosdick_shipment.rb +17 -0
  28. data/db/migrate/20151015122038_create_fosdick_exceptions.rb +15 -0
  29. data/db/migrate/20151028105216_add_fosdick_atempt_and_state_spree_shipments.rb +6 -0
  30. data/lib/fosdick/documents/shipment.rb +89 -0
  31. data/lib/fosdick/processor.rb +64 -0
  32. data/lib/fosdick/receiver.rb +49 -0
  33. data/lib/fosdick/sender.rb +39 -0
  34. data/lib/generators/spree_fosdick_integration/install/install_generator.rb +32 -0
  35. data/lib/generators/spree_fosdick_integration/install/templates/fosdick.rb +6 -0
  36. data/lib/generators/spree_fosdick_integration/install/templates/fosdick.yml.template +18 -0
  37. data/lib/spree_fosdick_integration.rb +8 -0
  38. data/lib/spree_fosdick_integration/engine.rb +22 -0
  39. data/lib/spree_fosdick_integration/factories.rb +31 -0
  40. data/lib/spree_fosdick_integration/version.rb +3 -0
  41. data/script/rails +7 -0
  42. data/spec/controllers/admin/fosdick_shipments_controller_spec.rb +16 -0
  43. data/spec/models/spree/fosdick_exception_spec.rb +21 -0
  44. data/spec/models/spree/fosdick_shipment_spec.rb +9 -0
  45. data/spec/models/spree/shipment_spec.rb +50 -0
  46. data/spec/services/exception_logger_spec.rb +11 -0
  47. data/spec/services/fosdick/processor_spec.rb +94 -0
  48. data/spec/services/fosdick/receiver_spec.rb +43 -0
  49. data/spec/services/fosdick/sender_spec.rb +34 -0
  50. data/spec/spec_helper.rb +99 -0
  51. data/spree_fosdick_integration.gemspec +37 -0
  52. metadata +286 -0
@@ -0,0 +1,28 @@
1
+ = paginate fosdick_shipments
2
+ %table#listing_fosdick_shipments.index.sortable{'data-hook' => '', 'data-sortable-link' => update_positions_admin_fosdick_shipments_path}
3
+ %thead
4
+ %tr{'data-hook' => 'fosdick_shipments_header'}
5
+ %th.no-border
6
+ %th Fosdick order
7
+ %th External order
8
+ %th Tracking
9
+ %th Ship date
10
+ %th Created at
11
+ %th State
12
+ %th Errors
13
+ %tbody
14
+ - if fosdick_shipments.present?
15
+ - fosdick_shipments.each do |ship|
16
+ %tr{class: cycle('odd', 'even'), 'data-hook' => 'fosdick_shipments_row', id: ship.id}
17
+ %td.no-border
18
+ %span.handle
19
+ %td.align-center= ship.fosdick_order_num
20
+ %td.align-center= link_to ship.external_order_num, edit_admin_order_path(ship.shipment.order)
21
+ %td.align-center= ship.tracking_number
22
+ %td.align-center= ship.ship_date
23
+ %td.align-center= ship.created_at
24
+ %td.align-center= ship.state
25
+ - if ship.fosdick_exceptions.present? && ship.state != 'shipped'
26
+ %td{style: 'color: red;'}= ship.fosdick_exceptions.pluck(:message).uniq.join('. ')
27
+ - else
28
+ %td
@@ -0,0 +1 @@
1
+ $('table#listing_fosdick_shipments').html('<%= j(render('fosdick_shipments_list', fosdick_shipments: @fosdick_shipments))%>');
@@ -0,0 +1,5 @@
1
+ - content_for :page_title do
2
+ Fosdick integration panel
3
+ - content_for :page_actions do
4
+ %li
5
+ = button_link_to Spree.t(:back_to_orders_list), admin_orders_path, icon: 'arrow-left'
@@ -0,0 +1,3 @@
1
+ = render 'titles'
2
+ = render 'filters_bar'
3
+ = render 'fosdick_shipments_list', fosdick_shipments: @fosdick_shipments
@@ -0,0 +1 @@
1
+ = render 'fosdick_shipments_list_respond'
@@ -0,0 +1,135 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
5
+ <title><%= Spree.t('fosdick.order_shipped.subject') %></title>
6
+ <style type="text/css">
7
+ #outlook a{padding:0;} /* Force Outlook to provide a "view in browser" button. */
8
+ body{width:100% !important;} .ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
9
+ body{-webkit-text-size-adjust:none;} /* Prevent Webkit platforms from changing default text sizes. */
10
+
11
+ body{font-family:Arial;margin:0; padding:0;}
12
+ img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
13
+ table td{border-collapse:collapse;}
14
+ #backgroundTable{height:100% !important; margin:0; padding:0; width:100% !important;}
15
+
16
+ body, #backgroundTable{
17
+ background-color:#FAFAFA;
18
+ }
19
+
20
+ #templateContainer{
21
+ border: 1px solid #DDDDDD;
22
+ margin-top: 10px;
23
+ padding: 20px;
24
+ }
25
+
26
+ h1, .h1{
27
+ color:#202020;
28
+ display:block;
29
+ font-family:Arial;
30
+ font-size:34px;
31
+ font-weight:bold;
32
+ line-height:100%;
33
+ margin-top:0;
34
+ margin-right:0;
35
+ margin-bottom:10px;
36
+ margin-left:0;
37
+ text-align:left;
38
+ }
39
+
40
+ h2, .h2{
41
+ color:#202020;
42
+ display:block;
43
+ font-family:Arial;
44
+ font-size:30px;
45
+ font-weight:bold;
46
+ line-height:100%;
47
+ margin-top:0;
48
+ margin-right:0;
49
+ margin-bottom:10px;
50
+ margin-left:0;
51
+ text-align:left;
52
+ }
53
+
54
+ h3, .h3{
55
+ color:#202020;
56
+ display:block;
57
+ font-family:Arial;
58
+ font-size:26px;
59
+ font-weight:bold;
60
+ line-height:100%;
61
+ margin-top:0;
62
+ margin-right:0;
63
+ margin-bottom:10px;
64
+ margin-left:0;
65
+ text-align:left;
66
+ }
67
+
68
+ h4, .h4{
69
+ color:#202020;
70
+ display:block;
71
+ font-family:Arial;
72
+ font-size:22px;
73
+ font-weight:bold;
74
+ line-height:100%;
75
+ margin-top:0;
76
+ margin-right:0;
77
+ margin-bottom:10px;
78
+ margin-left:0;
79
+ text-align:left;
80
+ }
81
+
82
+ #templateContainer, .bodyContent{
83
+ background-color:#FFFFFF;
84
+ }
85
+
86
+ .bodyContent div{
87
+ color:#505050;
88
+ font-family:Arial;
89
+ font-size:14px;
90
+ line-height:150%;
91
+ text-align:left;
92
+ }
93
+
94
+ .bodyContent div a:link, .bodyContent div a:visited, /* Yahoo! Mail Override */ .bodyContent div a .yshortcuts /* Yahoo! Mail Override */{
95
+ color:#336699;
96
+ font-weight:normal;
97
+ text-decoration:underline;
98
+ }
99
+
100
+ .bodyContent img{
101
+ display:inline;
102
+ height:auto;
103
+ }
104
+
105
+ #items {
106
+ margin: 10px 0;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
111
+ <center>
112
+ <table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="backgroundTable">
113
+ <tr>
114
+ <td align="center" valign="top">
115
+ <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateContainer">
116
+ <tr>
117
+ <td align="center" valign="top">
118
+ <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateBody">
119
+ <tr>
120
+ <td valign="top" class="bodyContent">
121
+ <%= "Your order #{@order_number} has shipped!" %><br>
122
+ <%= "Your tracking number - #{@tracking}" %>
123
+ </td>
124
+ </tr>
125
+ </table>
126
+ </td>
127
+ </tr>
128
+ </table>
129
+ <br />
130
+ </td>
131
+ </tr>
132
+ </table>
133
+ </center>
134
+ </body>
135
+ </html>
@@ -0,0 +1,14 @@
1
+ en:
2
+ spree:
3
+ fosdick:
4
+ order_shipped:
5
+ subject: Oreder shipped
6
+ fosdick_num: Fosdick Number
7
+ shipment_num: External order
8
+ state_exception: Exception
9
+ state_sent: Successfully sent
10
+ state_shipped: Successfully shipped
11
+ integration_state: Integration state
12
+ date_range_created: Date range - Created
13
+ date_range_shipped: Date range - Shipped
14
+ select_integration_state: Select integration state
@@ -0,0 +1,14 @@
1
+ ru:
2
+ spree:
3
+ fosdick:
4
+ order_shipped:
5
+ subject: Заказ отправлен
6
+ fosdick_num: Fosdick номер
7
+ shipment_num: Номер доставки
8
+ state_exception: Исключение
9
+ state_sent: Успешно отправлено
10
+ state_shipped: Успешно поставляется
11
+ integration_state: Статус интеграции
12
+ date_range_created: Диапазон дат - Дата создания
13
+ date_range_shipped: Диапазон дат - Поставляется
14
+ select_integration_state: Выберите статус интеграции
@@ -0,0 +1,10 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ namespace :admin do
3
+ resources :fosdick_shipments, only: [:index] do
4
+ collection do
5
+ post :update_positions
6
+ end
7
+ end
8
+ end
9
+ get '123' => 'admin/fosdick_shipments#index'
10
+ end
@@ -0,0 +1,17 @@
1
+ class CreateSpreeFosdickShipment < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_fosdick_shipments do |t|
4
+ t.belongs_to :spree_shipment
5
+ t.string :fosdick_order_num
6
+ t.string :external_order_num
7
+ t.text :tracking_number
8
+ t.string :state
9
+ t.boolean :confirmation_sent, default: false, null: false
10
+
11
+ t.datetime :ship_date
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :spree_fosdick_shipments, :spree_shipment_id
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class CreateFosdickExceptions < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_fosdick_exceptions do |t|
4
+ t.belongs_to :spree_fosdick_shipment
5
+ t.string :error_code
6
+ t.string :message
7
+ t.string :state
8
+
9
+ t.datetime :happened_at
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :spree_fosdick_exceptions, :spree_fosdick_shipment_id
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ class AddFosdickAtemptAndStateSpreeShipments < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_shipments, :send_atempt, :integer, default: 0, null: false
4
+ add_column :spree_shipments, :fosdick_state, :string, default: 'ready', null: false
5
+ end
6
+ end
@@ -0,0 +1,89 @@
1
+ module Fosdick
2
+ module Documents
3
+ class Shipment
4
+ def initialize(shipment, config)
5
+ @shipment = shipment
6
+ @config = config
7
+ end
8
+
9
+ def to_xml
10
+ builder = Nokogiri::XML::Builder.new do |xml|
11
+ xml.UnitycartOrderPost('xml:lang' => 'en-US') {
12
+ xml.ClientCode(@config['client_code'])
13
+ xml.Test('Y') if test?
14
+ xml.TransactionID(SecureRandom.hex(15))
15
+ xml.Order {
16
+ xml.ShippingMethod(@shipment['shipping_method'])
17
+ xml.Subtotal(0)
18
+ xml.Total(0)
19
+ xml.ExternalID("#{@shipment['id']}")
20
+ xml.AdCode(@shipment['adcode'] || @config['adcode'])
21
+ xml.Prepaid('Y')
22
+ xml.PaymentType(5)
23
+ xml.ShipFirstname truncate_name
24
+ xml.ShipLastname(@shipment['shipping_address']['lastname'])
25
+ xml.ShipAddress1(@shipment['shipping_address']['address1'])
26
+ xml.ShipAddress2(@shipment['shipping_address']['address2'])
27
+ xml.ShipCity(truncate_city)
28
+
29
+ if (@shipment['shipping_address']['country'] != 'US')
30
+ xml.ShipStateOther(ship_state)
31
+ else
32
+ xml.ShipState(ship_state)
33
+ end
34
+
35
+ xml.ShipZip(@shipment['shipping_address']['zipcode'])
36
+ xml.ShipCountry(@shipment['shipping_address']['country'])
37
+ xml.ShipPhone(@shipment['shipping_address']['phone'])
38
+ xml.Email(@shipment['email'])
39
+ xml.Code(@shipment['shipping_method_code'])
40
+
41
+ (1..5).each do |i|
42
+ next unless @shipment.key? "custom#{i}"
43
+ xml.send("Custom#{i}", @shipment["custom#{i}"])
44
+ end
45
+
46
+ xml.Items {
47
+ @shipment['items'].each_with_index do |item, index|
48
+ xml.Item {
49
+ xml.Inv item['product_id']
50
+ xml.Qty item['quantity']
51
+ xml.PricePer 0
52
+ }
53
+ end
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ builder.to_xml
60
+ end
61
+
62
+ private
63
+
64
+ def truncate_name
65
+ if @shipment['shipping_address']['firstname'].present?
66
+ name = @shipment['shipping_address']['firstname']
67
+
68
+ (name.length > 15) ? name.slice(0..15) : name
69
+ end
70
+ end
71
+
72
+ def test?
73
+ @config['test']
74
+ end
75
+
76
+ def truncate_city
77
+ city = @shipment['shipping_address']['city']
78
+
79
+ (city.length > 12) ? city.slice(0..12) : city
80
+ end
81
+
82
+ def ship_state
83
+ state = @shipment['shipping_address']['state']
84
+
85
+ FOSDICK_STATES_EXCEPTIONS.has_key?(state) ? FOSDICK_STATES_EXCEPTIONS[state] : ModelUN.convert_state_name(state)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,64 @@
1
+ module Fosdick
2
+ class Processor
3
+ def self.send_shipment(shipment, config)
4
+ doc = Fosdick::Documents::Shipment.new(shipment, config).to_xml
5
+ res = Fosdick::Sender.send_doc(doc, config)
6
+
7
+ log_fosdick_shipment(shipment, res)
8
+ end
9
+
10
+ def self.receive_shipment(options = {}, end_point = 'shipmentdetail.json' )
11
+ res = Fosdick::Receiver.new(end_point, options).call_api(FOSDICK_CONFIG)
12
+
13
+ update_shipment_info(res, options)
14
+ end
15
+
16
+ private
17
+
18
+ def self.log_fosdick_shipment(shipment, fosdick_response)
19
+ spree_shipment = Spree::Shipment.find_by_number(shipment['id'])
20
+
21
+ if fosdick_response.is_a? String
22
+ Spree::FosdickShipment.where(fosdick_order_num: fosdick_response).first_or_create(
23
+ spree_shipment_id: spree_shipment.id,
24
+ external_order_num: shipment['id'],
25
+ state: 'sent')
26
+ spree_shipment.update(state: 'shipped', fosdick_state: 'success')
27
+
28
+ else
29
+ fosdick_shipment = Spree::FosdickShipment.create(
30
+ spree_shipment_id: spree_shipment.id,
31
+ external_order_num: shipment['id'],
32
+ state: 'exception')
33
+ Array(fosdick_response[:errors]).map {|exception| ExceptionLogger.new.log(fosdick_response[:code], exception.join(' - '), fosdick_shipment.id)}
34
+ end
35
+
36
+ spree_shipment.increment!(:send_atempt)
37
+ end
38
+
39
+ def self.update_shipment_info(fosdick_response, options)
40
+ shipment = Spree::Shipment.find_by_number(options[:external_order_num])
41
+ fosdick_shipment = Spree::FosdickShipment.where(external_order_num: options[:external_order_num], spree_shipment_id: shipment.id).first_or_create
42
+
43
+ if fosdick_response.is_a? Array
44
+ fosdick_response.each do |fos_shipment|
45
+ trackings = []
46
+ ship_date = fos_shipment.has_key?('ship_date') ? fos_shipment['ship_date'].to_date : nil
47
+
48
+ fos_shipment['trackings'].each {|tracking| trackings << tracking['tracking_num'] unless trackings.include?(tracking['tracking_num']) }
49
+ fosdick_shipment.update(tracking_number: trackings, ship_date: ship_date, fosdick_order_num: fos_shipment['fosdick_order_num'])
50
+ fosdick_shipment.update(state: 'shipped') unless ship_date.nil?
51
+ shipment.update(shipped_at: ship_date, state: 'shipped', tracking: trackings.join(', ')) if shipment.present?
52
+ shipment.order.update(shipment_state: 'shipped') if ship_date.present?
53
+
54
+ if fosdick_shipment.confirmation_sent == false && trackings.present?
55
+ Spree::FosdickShipmentMailer.order_shipped(shipment).deliver
56
+ fosdick_shipment.update(confirmation_sent: true)
57
+ end
58
+ end
59
+ elsif fosdick_response.is_a? Hash
60
+ ExceptionLogger.new.log('Error', fosdick_response['error'], fosdick_shipment.id)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ require 'open-uri'
2
+ require 'net/http'
3
+
4
+ module Fosdick
5
+ class Receiver
6
+
7
+ def initialize(end_point, request_options={})
8
+ @end_point = end_point
9
+ @request_options = request_options
10
+ end
11
+
12
+ def call_api(fosdick_options)
13
+ fetch_data(@end_point, fosdick_options)
14
+ end
15
+
16
+ private
17
+
18
+ def fetch_data(end_point, options={})
19
+ # required sleep 2 sec for Fosdick API
20
+ sleep 2
21
+ response = request(end_point, options)
22
+ # process only valid to JSON.parse data
23
+ begin
24
+ response.nil? ? nil : JSON.parse(response.body)
25
+ rescue JSON::ParserError
26
+ nil
27
+ end
28
+ end
29
+
30
+ def api_url(end_point)
31
+ "https://www.customerstatus.com/fosdickapi/#{end_point}?#{ { }.merge(@request_options).to_query }"
32
+ end
33
+
34
+ def request(end_point, options={})
35
+ uri = URI(api_url(end_point))
36
+
37
+ Net::HTTP.start(uri.host, uri.port,
38
+ use_ssl: uri.scheme == 'https',
39
+ verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
40
+
41
+ request = Net::HTTP::Get.new uri.request_uri
42
+ request['Accept'] = 'application/json'
43
+ request.basic_auth(options['basic_auth']['login'], options['basic_auth']['password']) if options['basic_auth'].present?
44
+
45
+ http.request request
46
+ end
47
+ end
48
+ end
49
+ end