spree_tradebyte 0.1.12.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.byebug_history +39 -0
  3. data/.gitignore +22 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +24 -0
  6. data/.travis.yml +31 -0
  7. data/Appraisals +29 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE +26 -0
  10. data/README.md +59 -0
  11. data/Rakefile +21 -0
  12. data/app/assets/javascripts/spree/backend/spree_tradebyte.js +2 -0
  13. data/app/assets/javascripts/spree/frontend/spree_tradebyte.js +2 -0
  14. data/app/assets/stylesheets/spree/backend/spree_tradebyte.css +4 -0
  15. data/app/assets/stylesheets/spree/frontend/spree_tradebyte.css +4 -0
  16. data/app/jobs/spree_tradebyte/application_job.rb +7 -0
  17. data/app/jobs/spree_tradebyte/export_products_job.rb +229 -0
  18. data/app/jobs/spree_tradebyte/export_stock_job.rb +39 -0
  19. data/app/jobs/spree_tradebyte/import_orders_job.rb +70 -0
  20. data/app/jobs/spree_tradebyte/order/processor.rb +158 -0
  21. data/app/mailers/application_mailer.rb +5 -0
  22. data/app/mailers/spree_tradebyte/error_notification_mailer.rb +9 -0
  23. data/app/models/spree/order/mail_defuser.rb +15 -0
  24. data/app/models/spree/order/tradebyte_message_builder.rb +133 -0
  25. data/app/models/spree/order_decorator.rb +14 -0
  26. data/app/models/spree/order_updater_decorator.rb +18 -0
  27. data/app/models/spree/payment_method/tradebyte.rb +7 -0
  28. data/app/models/spree/product_decorator.rb +15 -0
  29. data/app/models/spree/reimbursement/mail_defuser.rb +9 -0
  30. data/app/models/spree/reimbursement_decorator.rb +11 -0
  31. data/app/models/spree/shipment_decorator.rb +9 -0
  32. data/app/models/spree/shipment_handler/mail_defuser.rb +7 -0
  33. data/app/models/spree/shipment_handler_decorator.rb +5 -0
  34. data/app/models/spree/variant_decorator.rb +47 -0
  35. data/app/overrides/order_index.rb +13 -0
  36. data/app/overrides/tradebyte_settings.rb +13 -0
  37. data/app/views/layouts/spree_tradebyte/mailer.html.erb +13 -0
  38. data/app/views/layouts/spree_tradebyte/mailer.text.erb +1 -0
  39. data/app/views/spree/admin/orders/_index_tradebyte_state_override.html.erb +7 -0
  40. data/app/views/spree/admin/orders/_index_tradebyte_state_override_head.html.erb +1 -0
  41. data/app/views/spree/admin/products/_tradebyte_fields.html.erb +15 -0
  42. data/app/views/spree/admin/variants/_tradebyte_fields.html.erb +16 -0
  43. data/app/views/spree_tradebyte/error_notification_mailer/exception_mail.html.erb +12 -0
  44. data/bin/rails +8 -0
  45. data/config/locales/en.yml +5 -0
  46. data/config/routes.rb +3 -0
  47. data/db/migrate/20190301122602_add_tradebyte_exported_to_spree_orders.rb +5 -0
  48. data/db/migrate/20190305123644_add_columns_to_spree_orders.rb +8 -0
  49. data/db/migrate/20190307133555_add_tradebyte_id_to_spree_line_items.rb +5 -0
  50. data/db/migrate/20190315162630_add_tradebyte_sku_to_line_items.rb +5 -0
  51. data/db/migrate/20190529120919_add_tradebyte_fields_to_products.rb +7 -0
  52. data/db/migrate/20190529145337_add_tradebyte_settings_to_spree_variants.rb +5 -0
  53. data/db/migrate/20190611114706_create_shipping_methods_for_tradebyte.rb +17 -0
  54. data/db/migrate/20190611114720_create_payment_method_for_tradebyte.rb +5 -0
  55. data/lib/generators/spree_tradebyte/install/install_generator.rb +28 -0
  56. data/lib/generators/spree_tradebyte/templates/initializer.rb +42 -0
  57. data/lib/helpers/setup_configuration.rb +27 -0
  58. data/lib/spree_tradebyte.rb +26 -0
  59. data/lib/spree_tradebyte/engine.rb +25 -0
  60. data/lib/spree_tradebyte/factories.rb +6 -0
  61. data/lib/spree_tradebyte/tb_logger.rb +14 -0
  62. data/lib/spree_tradebyte/tradebyte_ftp.rb +98 -0
  63. data/lib/spree_tradebyte/version.rb +18 -0
  64. data/lib/tasks/variants.rake +13 -0
  65. data/spree_tradebyte.gemspec +48 -0
  66. metadata +427 -0
@@ -0,0 +1,39 @@
1
+ require 'nokogiri'
2
+
3
+ class SpreeTradebyte::ExportStockJob < ApplicationJob
4
+ queue_as :tradebyte
5
+
6
+ def perform(*args)
7
+ stock_items = get_stock_items
8
+ stock_xml = parse_stock_items(stock_items)
9
+ transport_stock(stock_xml)
10
+ end
11
+
12
+ def get_stock_items
13
+ products = Spree::Product.active
14
+ Spree::Variant.includes(:stock_items).where(product: products).collect do |v|
15
+ {
16
+ sku: v.sku,
17
+ stock_item: v.stock_items.first
18
+ }
19
+ end
20
+ end
21
+
22
+ def parse_stock_items(stock_items)
23
+ Nokogiri::XML::Builder.new do |xml|
24
+ xml.TBSTOCK do
25
+ stock_items.each do |stock_item|
26
+ xml.ARTICLE do
27
+ xml.A_EAN stock_item[:sku]
28
+ xml.A_STOCK stock_item[:stock_item].count_on_hand
29
+ end
30
+ end
31
+ end
32
+ end.to_xml
33
+ end
34
+
35
+ def transport_stock(stock_xml)
36
+ ftp = SpreeTradebyte::TradebyteFTP.new(SpreeTradebyte.ftp_user, SpreeTradebyte.ftp_password, SpreeTradebyte.ftp_server, SpreeTradebyte.ftp_port)
37
+ ftp.put_stock_file(stock_xml)
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ require 'nokogiri'
2
+ require 'spree_tradebyte/order/processor'
3
+ require 'spree_tradebyte/tb_logger'
4
+
5
+ class SpreeTradebyte::ImportOrdersJob < ApplicationJob
6
+ include SpreeTradebyte::Order::Processor
7
+
8
+ queue_as :tradebyte
9
+
10
+ def perform(*args)
11
+ ftp = SpreeTradebyte::TradebyteFTP.new(SpreeTradebyte.ftp_user, SpreeTradebyte.ftp_password, SpreeTradebyte.ftp_server, SpreeTradebyte.ftp_port)
12
+ files = ftp.get_order_files
13
+
14
+ orders_hash = parse_order_files(files)
15
+ orders_hash.map do |order_hash|
16
+ process_order(order_hash)
17
+ end
18
+
19
+ ftp.archive_files(files.map {|f| f[:file_name]})
20
+ rescue StandardError => e
21
+ SpreeTradebyte::TBLogger.debug("Something went wrong while processing ALL TB ORDERS")
22
+ SpreeTradebyte::TBLogger.debug(e)
23
+ SpreeTradebyte::TBLogger.debug(e.backtrace.join("\n"))
24
+ ErrorNotificationMailer.exception_mail(e).deliver_now
25
+ end
26
+
27
+ def parse_order_files(files)
28
+ files.each_with_object([]) do |file, list|
29
+ xml = Nokogiri::XML(file[:file_contents])
30
+ xml.search('//ORDER_LIST/ORDER').each do |order_xml|
31
+ list << {
32
+ tb_id: order_xml.search('./ORDER_DATA/TB_ID').text,
33
+ channel_id: order_xml.search('./ORDER_DATA/CHANNEL_ID').text,
34
+ channel_sign: order_xml.search('./ORDER_DATA/CHANNEL_SIGN').text,
35
+ paid: order_xml.search('./ORDER_DATA/PAID').text.to_i == 1,
36
+ approved: order_xml.search('./ORDER_DATA/APPROVED').text.to_i == 1,
37
+ email: order_xml.search('./SELL_TO/EMAIL').text,
38
+ bill_address: parse_address(order_xml.search('./SELL_TO')),
39
+ ship_address: parse_address(order_xml.search('./SHIP_TO')),
40
+ line_items: parse_line_items(order_xml.search('./ITEMS/ITEM')),
41
+ completed_at: DateTime.parse(order_xml.search('./ORDER_DATA/DATE_CREATED').text)
42
+ }
43
+ end
44
+ end
45
+ end
46
+
47
+ def parse_address(address_xml)
48
+ {
49
+ firstname: address_xml.search('./FIRSTNAME').text,
50
+ lastname: address_xml.search('./LASTNAME').text,
51
+ address1: address_xml.search('./STREET_NO').text,
52
+ zipcode: address_xml.search('./ZIP').text,
53
+ city: address_xml.search('./CITY').text,
54
+ country: Spree::Country.find_by(iso: address_xml.search('./COUNTRY').text),
55
+ }
56
+ end
57
+
58
+ def parse_line_items(items_xml)
59
+ items_xml.map do |item_xml|
60
+ {
61
+ tb_id: item_xml.search('./TB_ID').text,
62
+ sku: item_xml.search('./EAN').text,
63
+ tradebyte_sku: item_xml.search('./SKU').text,
64
+ quantity: item_xml.search('./QUANTITY').text,
65
+ price: item_xml.search('./ITEM_PRICE').text,
66
+ }
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,158 @@
1
+ require 'spree_tradebyte/tb_logger'
2
+
3
+ class TradebyteOrderInvalid < StandardError;
4
+ end
5
+
6
+ module SpreeTradebyte
7
+
8
+ module Order
9
+ module Processor
10
+
11
+ def process_order(order_hash)
12
+ order = Spree::Order.find_by_tradebyte_id(order_hash[:tb_id])
13
+ return order unless order_hash[:approved]
14
+ begin
15
+ if order.present?
16
+ update_order(order, order_hash)
17
+ else
18
+ order = create_order(order_hash)
19
+ end
20
+
21
+ if order.payments.none? && order.persisted?
22
+ order.update_with_updater!
23
+ payment_method = Spree::PaymentMethod.find_by_type!('Spree::PaymentMethod::Tradebyte')
24
+ payment = order.payments.create!(payment_method: payment_method, amount: order.total)
25
+ payment.capture!
26
+ order.state = 'complete'
27
+ order.completed_at = order_hash[:completed_at]
28
+ order.save!
29
+ order.reload
30
+ order.update_with_updater!
31
+ raise TradebyteOrderInvalid("Tradebyte order #{order.number} not completed. Tradebyte ID #{order.tradebyte_id}") unless order.complete?
32
+ order.tb_send_confirm_messages
33
+ end
34
+ rescue StandardError => e
35
+ SpreeTradebyte::TBLogger.debug("Something went wrong while processing tb order #{order_hash[:tb_id]}")
36
+ SpreeTradebyte::TBLogger.debug(e.message)
37
+ SpreeTradebyte::TBLogger.debug(e.backtrace.join("\n"))
38
+ ErrorNotificationMailer.exception_mail(e, order).deliver_now
39
+ end
40
+ order
41
+ end
42
+
43
+ def update_order(order, order_hash)
44
+ #Not implemented
45
+ end
46
+
47
+ def create_order(order_hash)
48
+ order_attibutes = {
49
+ tradebyte_id: order_hash[:tb_id],
50
+ tradebyte_channel_id: order_hash[:channel_id],
51
+ tradebyte_channel_sign: order_hash[:channel_sign],
52
+ is_tradebyte_order: true,
53
+ }.merge(build_order_attributes(order_hash))
54
+
55
+ order = Spree::Order.new(order_attibutes)
56
+ unavailable_line_items, order.line_items = order.line_items.partition do |line_item|
57
+ !line_item.variant.present? || !line_item.variant.available? || !line_item.variant.can_supply?(line_item.quantity)
58
+ end # split variants in array of unavailable and available line items
59
+
60
+ if unavailable_line_items.any?
61
+ order.tb_send_cancel_messages("Items are out of stock", unavailable_line_items) rescue nil
62
+ end
63
+
64
+ if order.line_items.any?
65
+ unless order.save
66
+ order.tb_send_cancel_messages(order.errors.full_messages) if order
67
+ SpreeTradebyte::TBLogger.debug("Cancelled order #{order.try(:tradebyte_id)} due to #{order.errors.full_messages}")
68
+ return order
69
+ end
70
+ elsif order.line_items.none?
71
+ return order
72
+ end
73
+
74
+ create_shipments(order_hash, order)
75
+
76
+ order.save!
77
+ order
78
+
79
+ rescue StandardError => e
80
+ SpreeTradebyte::TBLogger.debug("Something went wrong while creating tb order #{order_hash[:tb_id]}")
81
+ SpreeTradebyte::TBLogger.debug(e.message)
82
+ SpreeTradebyte::TBLogger.debug(e.backtrace.join("\n"))
83
+ ErrorNotificationMailer.exception_mail(e, order).deliver_now
84
+ end
85
+
86
+ def create_shipments(order_hash, order)
87
+ channel_shipping_method = Spree::ShippingMethod.where('code LIKE CONCAT(\'%\',?,\'%\')', order_hash[:channel_sign]).first
88
+
89
+ raise TradebyteOrderInvalid.new('Could not find a matching shipping method') unless channel_shipping_method.present?
90
+
91
+ inventory_units = Spree::Stock::InventoryUnitBuilder.new(order).units
92
+
93
+ shipment = order.shipments.build
94
+ shipment.stock_location = Spree::StockLocation.find_by(default: true)
95
+
96
+ order.line_items.each do |line_item|
97
+
98
+ inventory_unit = inventory_units.detect { |iu| iu.variant_id.to_i == line_item.variant_id }
99
+
100
+ if inventory_unit.present?
101
+ inventory_unit.shipment = shipment
102
+ inventory_unit.state = 'on_hand'
103
+ inventory_unit.save!
104
+
105
+ # Don't assign shipments to this inventory unit more than once
106
+ inventory_units.delete(inventory_unit)
107
+ end
108
+ end
109
+
110
+ shipment.state = 'ready'
111
+ shipment.save!
112
+
113
+ rate = shipment.shipping_rates.create!(shipping_method: channel_shipping_method, cost: 0.0)
114
+ shipment.selected_shipping_rate_id = rate.id
115
+
116
+ shipment.save!
117
+ shipment.update_amounts
118
+
119
+ rescue StandardError => e
120
+ SpreeTradebyte::TBLogger.debug("Something went wrong while creating tb shipment for order #{order_hash[:tb_id]}")
121
+ SpreeTradebyte::TBLogger.debug(e.message)
122
+ SpreeTradebyte::TBLogger.debug(e.backtrace.join("\n"))
123
+ ErrorNotificationMailer.exception_mail(e, order).deliver_now
124
+ end
125
+
126
+ def build_order_attributes(order_hash)
127
+ {
128
+ email: order_hash[:email],
129
+ line_items_attributes: order_hash[:line_items].map(&method(:build_line_item_attributes)).compact,
130
+ bill_address_attributes: build_address_attributes(order_hash[:bill_address]),
131
+ ship_address_attributes: build_address_attributes(order_hash[:ship_address])
132
+ }
133
+ end
134
+
135
+ def build_line_item_attributes(line_item_hash)
136
+ {
137
+ variant: Spree::Variant.find_by_sku(line_item_hash[:sku]),
138
+ quantity: line_item_hash[:quantity],
139
+ price: line_item_hash[:price],
140
+ tradebyte_item_id: line_item_hash[:tb_id],
141
+ tradebyte_sku: line_item_hash[:tradebyte_sku]
142
+ }
143
+ end
144
+
145
+ def build_address_attributes(address_hash)
146
+ {
147
+ firstname: address_hash[:firstname],
148
+ lastname: address_hash[:lastname],
149
+ address1: address_hash[:address1],
150
+ zipcode: address_hash[:zipcode],
151
+ city: address_hash[:city],
152
+ country: address_hash[:country],
153
+ }
154
+ end
155
+
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,5 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: 'from@example.com'
3
+ layout 'mailer'
4
+ end
5
+
@@ -0,0 +1,9 @@
1
+ class SpreeTradebyte::ErrorNotificationMailer < ApplicationMailer
2
+
3
+ def exception_mail(exception, order = nil)
4
+ @exception = exception
5
+ @order = order
6
+ mail(to: SpreeTradebyte.admin_email, subject: 'SpreeTradebyte: Something went wrong')
7
+ end
8
+
9
+ end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Order::MailDefuser
3
+ def deliver_order_confirmation_email
4
+ unless is_tradebyte_order?
5
+ super
6
+ end
7
+ end
8
+
9
+ def send_cancel_email
10
+ unless is_tradebyte_order?
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,133 @@
1
+ module Spree
2
+ class Order < Spree::Base
3
+ module TradebyteMessageBuilder
4
+ extend ActiveSupport::Concern
5
+
6
+ def tb_send_confirm_messages
7
+ return unless is_tradebyte_order?
8
+ line_item_message = Nokogiri::XML::Builder.new do |xml|
9
+ xml.MESSAGES_LIST do
10
+ line_items.map {|li| tb_confirm_message(xml, li)}
11
+ end
12
+ end
13
+
14
+ ftp = tb_ftp
15
+ ftp.put_message_file(line_item_message.to_xml)
16
+ ftp.close
17
+ end
18
+
19
+ def tb_send_cancel_messages(comment = nil, line_items = self.line_items)
20
+ return unless is_tradebyte_order?
21
+ line_item_message = Nokogiri::XML::Builder.new do |xml|
22
+ xml.MESSAGES_LIST do
23
+ line_items.map {|li| tb_cancel_message(xml, li, comment)}
24
+ end
25
+ end
26
+
27
+ ftp = tb_ftp
28
+ ftp.put_message_file(line_item_message.to_xml)
29
+ ftp.close
30
+ end
31
+
32
+ def tb_send_no_stock_messages(line_items)
33
+ return unless is_tradebyte_order?
34
+ line_item_message = Nokogiri::XML::Builder.new do |xml|
35
+ xml.MESSAGES_LIST do
36
+ line_items.map {|li| tb_no_stock_message(xml, li)}
37
+ end
38
+ end
39
+
40
+ ftp = tb_ftp
41
+ ftp.put_message_file(line_item_message.to_xml)
42
+ ftp.close
43
+ end
44
+
45
+ def tb_send_shipped_messages(inventory_units)
46
+ return unless is_tradebyte_order?
47
+ inventory_unit_message = Nokogiri::XML::Builder.new do |xml|
48
+ xml.MESSAGES_LIST do
49
+ inventory_units.map {|iu| tb_shipped_message(xml, iu)}
50
+ end
51
+ end
52
+
53
+ ftp = tb_ftp
54
+ ftp.put_message_file(inventory_unit_message.to_xml)
55
+ ftp.close
56
+ end
57
+
58
+ def tb_send_return_messages(return_items)
59
+ return unless is_tradebyte_order?
60
+ return_item_message = Nokogiri::XML::Builder.new do |xml|
61
+ xml.MESSAGES_LIST do
62
+ return_items.map {|ri| tb_return_message(xml, ri)}
63
+ end
64
+ end
65
+
66
+ ftp = tb_ftp
67
+ ftp.put_message_file(return_item_message.to_xml)
68
+ ftp.close
69
+ end
70
+
71
+ private
72
+
73
+ def tb_ftp
74
+ SpreeTradebyte::TradebyteFTP.new(SpreeTradebyte.ftp_user, SpreeTradebyte.ftp_password, SpreeTradebyte.ftp_server, SpreeTradebyte.ftp_port)
75
+ end
76
+
77
+ def tb_confirm_message(xml, line_item)
78
+ xml.MESSAGE do
79
+ xml.MESSAGE_TYPE 'ORDER_ACKNOWLEDGE'
80
+ xml.TB_ORDER_ID line_item.order.tradebyte_id
81
+ xml.TB_ORDER_ITEM_ID line_item.tradebyte_item_id
82
+ xml.SKU line_item.tradebyte_sku
83
+ xml.QUANTITY line_item.quantity
84
+ end
85
+ end
86
+
87
+ def tb_cancel_message(xml, line_item, comment = 'Cancelled')
88
+ xml.MESSAGE do
89
+ xml.MESSAGE_TYPE 'CUST_CANCEL'
90
+ xml.TB_ORDER_ID line_item.order.tradebyte_id
91
+ xml.TB_ORDER_ITEM_ID line_item.tradebyte_item_id
92
+ xml.SKU line_item.tradebyte_sku
93
+ xml.QUANTITY line_item.quantity
94
+ xml.COMMENT comment
95
+ end
96
+ end
97
+
98
+ def tb_no_stock_message(xml, line_item)
99
+ xml.MESSAGE do
100
+ xml.MESSAGE_TYPE 'NO_INVENTORY'
101
+ xml.TB_ORDER_ID self.tradebyte_id
102
+ xml.TB_ORDER_ITEM_ID line_item.tradebyte_item_id
103
+ xml.SKU line_item.tradebyte_sku
104
+ xml.QUANTITY line_item.quantity
105
+ end
106
+ end
107
+
108
+ def tb_shipped_message(xml, inventory_unit)
109
+ xml.MESSAGE do
110
+ xml.MESSAGE_TYPE 'SHIP'
111
+ xml.TB_ORDER_ID self.tradebyte_id
112
+ xml.TB_ORDER_ITEM_ID inventory_unit.line_item.tradebyte_item_id
113
+ xml.SKU inventory_unit.line_item.tradebyte_sku
114
+ xml.QUANTITY inventory_unit.quantity
115
+ xml.CARRIER_PARCEL_TYPE inventory_unit.shipment.shipping_method.send(SpreeTradebyte.parcel_type_method)
116
+ xml.IDCODE inventory_unit.shipment.tracking
117
+ end
118
+ end
119
+
120
+ def tb_return_message(xml, return_item)
121
+ xml.MESSAGE do
122
+ xml.MESSAGE_TYPE 'RETURN'
123
+ xml.TB_ORDER_ID self.tradebyte_id
124
+ xml.TB_ORDER_ITEM_ID return_item.inventory_unit.line_item.tradebyte_item_id
125
+ xml.SKU return_item.inventory_unit.line_item.tradebyte_sku
126
+ xml.QUANTITY return_item.inventory_unit.quantity
127
+ xml.RETURN_CAUSE return_item.return_authorization.try(:return_authorization_reason).try(:name)
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,14 @@
1
+ Spree::Order.class_eval do
2
+ include Spree::Order::TradebyteMessageBuilder
3
+
4
+ Spree::Order.state_machine.after_transition :to => :canceled, do: :cancel_tb_order
5
+
6
+ self.prepend(Spree::Order::MailDefuser)
7
+
8
+ private
9
+
10
+ def cancel_tb_order
11
+ tb_send_cancel_messages('The merchant cancelled the order')
12
+ end
13
+
14
+ end