gemgento 2.8.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.
Files changed (429) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +8 -0
  5. data/README.md +9 -0
  6. data/Rakefile +21 -0
  7. data/app/adapters/gemgento/magento/address_adapter.rb +37 -0
  8. data/app/adapters/gemgento/magento/customer_adapter.rb +24 -0
  9. data/app/adapters/gemgento/magento/line_item_adapter.rb +41 -0
  10. data/app/adapters/gemgento/magento/order_adapter.rb +97 -0
  11. data/app/adapters/gemgento/magento/order_status_adapter.rb +24 -0
  12. data/app/adapters/gemgento/magento/payment_adapter.rb +38 -0
  13. data/app/adapters/gemgento/magento/product_adapater.rb +24 -0
  14. data/app/adapters/gemgento/magento/quote_adapter.rb +17 -0
  15. data/app/adapters/gemgento/magento/shipment_adapter.rb +74 -0
  16. data/app/adapters/gemgento/magento/shipment_item_adapter.rb +39 -0
  17. data/app/adapters/gemgento/magento/shipment_track_adapter.rb +31 -0
  18. data/app/adapters/gemgento/magento_adapter.rb +17 -0
  19. data/app/assets/fonts/FuturaStd-Bold.otf +0 -0
  20. data/app/assets/fonts/FuturaStd-Book.otf +0 -0
  21. data/app/assets/fonts/FuturaStd-Light.otf +0 -0
  22. data/app/assets/fonts/FuturaStd-Medium.otf +0 -0
  23. data/app/assets/fonts/Verlag-Black.otf +0 -0
  24. data/app/assets/fonts/Verlag-BlackItalic.otf +0 -0
  25. data/app/assets/fonts/Verlag-Bold.otf +0 -0
  26. data/app/assets/fonts/Verlag-BoldItalic.otf +0 -0
  27. data/app/assets/fonts/Verlag-Book.otf +0 -0
  28. data/app/assets/fonts/Verlag-BookItalic-2.otf +0 -0
  29. data/app/assets/fonts/Verlag-BookItalic-3.otf +0 -0
  30. data/app/assets/fonts/Verlag-BookItalic.otf +0 -0
  31. data/app/assets/fonts/Verlag-Light.otf +0 -0
  32. data/app/assets/fonts/Verlag-LightItalic.otf +0 -0
  33. data/app/assets/fonts/Verlag-XLight.otf +0 -0
  34. data/app/assets/fonts/Verlag-XLightItalic.otf +0 -0
  35. data/app/assets/images/about_store.jpg +0 -0
  36. data/app/assets/images/gemgento-icon-bolt.png +0 -0
  37. data/app/assets/javascripts/gemgento/active_admin.js.coffee +42 -0
  38. data/app/assets/javascripts/gemgento/checkout/address.coffee +23 -0
  39. data/app/assets/javascripts/gemgento/gemgento.js +1 -0
  40. data/app/assets/javascripts/gemgento/global.js.coffee +31 -0
  41. data/app/assets/stylesheets/gemgento/active_admin.css.scss +549 -0
  42. data/app/assets/stylesheets/gemgento/default-views/_address.scss +11 -0
  43. data/app/assets/stylesheets/gemgento/default-views/_category.scss +54 -0
  44. data/app/assets/stylesheets/gemgento/default-views/_checkout.scss +28 -0
  45. data/app/assets/stylesheets/gemgento/default-views/_footer.scss +13 -0
  46. data/app/assets/stylesheets/gemgento/default-views/_form.scss +127 -0
  47. data/app/assets/stylesheets/gemgento/default-views/_header.scss +32 -0
  48. data/app/assets/stylesheets/gemgento/default-views/_layout.css.scss +157 -0
  49. data/app/assets/stylesheets/gemgento/default-views/_pages.css.scss +83 -0
  50. data/app/assets/stylesheets/gemgento/default-views/_product.scss +57 -0
  51. data/app/assets/stylesheets/gemgento/default-views/_shopping_bag.scss +87 -0
  52. data/app/assets/stylesheets/gemgento/default-views/_subheader.css.scss +30 -0
  53. data/app/assets/stylesheets/gemgento/default-views/application.css.scss +1 -0
  54. data/app/controllers/concerns/gemgento/api/v1/base.rb +15 -0
  55. data/app/controllers/concerns/gemgento/api/v1/product_filters.rb +33 -0
  56. data/app/controllers/gemgento/addresses_controller.rb +14 -0
  57. data/app/controllers/gemgento/api/v1/categories_controller.rb +13 -0
  58. data/app/controllers/gemgento/api/v1/products_controller.rb +23 -0
  59. data/app/controllers/gemgento/api/v1/search_controller.rb +14 -0
  60. data/app/controllers/gemgento/application_controller.rb +5 -0
  61. data/app/controllers/gemgento/cart_controller.rb +99 -0
  62. data/app/controllers/gemgento/categories_controller.rb +37 -0
  63. data/app/controllers/gemgento/checkout/address_controller.rb +48 -0
  64. data/app/controllers/gemgento/checkout/confirm_controller.rb +72 -0
  65. data/app/controllers/gemgento/checkout/coupons_controller.rb +42 -0
  66. data/app/controllers/gemgento/checkout/gift_card_controller.rb +39 -0
  67. data/app/controllers/gemgento/checkout/gift_controller.rb +32 -0
  68. data/app/controllers/gemgento/checkout/login_controller.rb +123 -0
  69. data/app/controllers/gemgento/checkout/payment_controller.rb +70 -0
  70. data/app/controllers/gemgento/checkout/paypal_controller.rb +23 -0
  71. data/app/controllers/gemgento/checkout/shipping_controller.rb +46 -0
  72. data/app/controllers/gemgento/checkout/shipping_payment_controller.rb +58 -0
  73. data/app/controllers/gemgento/checkout/thank_you_controller.rb +45 -0
  74. data/app/controllers/gemgento/checkout_controller.rb +120 -0
  75. data/app/controllers/gemgento/countries_controller.rb +17 -0
  76. data/app/controllers/gemgento/errors_controller.rb +7 -0
  77. data/app/controllers/gemgento/footer_items_controller.rb +16 -0
  78. data/app/controllers/gemgento/home_controller.rb +7 -0
  79. data/app/controllers/gemgento/magento/addresses_controller.rb +61 -0
  80. data/app/controllers/gemgento/magento/base_controller.rb +18 -0
  81. data/app/controllers/gemgento/magento/categories_controller.rb +103 -0
  82. data/app/controllers/gemgento/magento/email/sales/credit_memos_controller.rb +21 -0
  83. data/app/controllers/gemgento/magento/email/sales/invoices_controller.rb +21 -0
  84. data/app/controllers/gemgento/magento/email/sales/orders_controller.rb +20 -0
  85. data/app/controllers/gemgento/magento/email/sales/shipments_controller.rb +25 -0
  86. data/app/controllers/gemgento/magento/emails_controller.rb +11 -0
  87. data/app/controllers/gemgento/magento/inventory_controller.rb +30 -0
  88. data/app/controllers/gemgento/magento/orders_controller.rb +12 -0
  89. data/app/controllers/gemgento/magento/price_rules_controller.rb +22 -0
  90. data/app/controllers/gemgento/magento/product_attribute_sets_controller.rb +37 -0
  91. data/app/controllers/gemgento/magento/product_attributes_controller.rb +70 -0
  92. data/app/controllers/gemgento/magento/products_controller.rb +240 -0
  93. data/app/controllers/gemgento/magento/recurring_profiles_controller.rb +42 -0
  94. data/app/controllers/gemgento/magento/stores_controller.rb +21 -0
  95. data/app/controllers/gemgento/magento/user_groups_controller.rb +23 -0
  96. data/app/controllers/gemgento/magento/users_controller.rb +63 -0
  97. data/app/controllers/gemgento/order_export_controller.rb +21 -0
  98. data/app/controllers/gemgento/orders_controller.rb +12 -0
  99. data/app/controllers/gemgento/products_controller.rb +12 -0
  100. data/app/controllers/gemgento/regions_controller.rb +18 -0
  101. data/app/controllers/gemgento/search_controller.rb +16 -0
  102. data/app/controllers/gemgento/shipments_controller.rb +5 -0
  103. data/app/controllers/gemgento/stock_notifications_controller.rb +22 -0
  104. data/app/controllers/gemgento/subscribers_controller.rb +35 -0
  105. data/app/controllers/gemgento/sync_controller.rb +50 -0
  106. data/app/controllers/gemgento/user/addresses_controller.rb +79 -0
  107. data/app/controllers/gemgento/user/base_controller.rb +14 -0
  108. data/app/controllers/gemgento/user/home_controller.rb +9 -0
  109. data/app/controllers/gemgento/user/orders_controller.rb +17 -0
  110. data/app/controllers/gemgento/user/passwords_controller.rb +18 -0
  111. data/app/controllers/gemgento/user/recurring_profiles_controller.rb +31 -0
  112. data/app/controllers/gemgento/user/registration_session_controller.rb +103 -0
  113. data/app/controllers/gemgento/user/registrations_controller.rb +28 -0
  114. data/app/controllers/gemgento/user/saved_credit_cards_controller.rb +56 -0
  115. data/app/controllers/gemgento/user/sessions_controller.rb +50 -0
  116. data/app/controllers/gemgento/user/wishlist_items_controller.rb +55 -0
  117. data/app/controllers/gemgento/users_controller.rb +36 -0
  118. data/app/helpers/gemgento/application_helper.rb +125 -0
  119. data/app/helpers/gemgento/categories_helper.rb +39 -0
  120. data/app/helpers/gemgento/checkout/payment_helper.rb +5 -0
  121. data/app/helpers/gemgento/products_helper.rb +23 -0
  122. data/app/jobs/gemgento/import_job.rb +12 -0
  123. data/app/mailers/gemgento/sales_mailer.rb +80 -0
  124. data/app/models/admin_user.rb +7 -0
  125. data/app/models/concerns/gemgento/product_touches.rb +26 -0
  126. data/app/models/gemgento.rb +5 -0
  127. data/app/models/gemgento/adapter/sellect/customer.rb +34 -0
  128. data/app/models/gemgento/adapter/sellect/order.rb +378 -0
  129. data/app/models/gemgento/adapter/sellect/product.rb +250 -0
  130. data/app/models/gemgento/adapter/sellect_adapter.rb +16 -0
  131. data/app/models/gemgento/adapter/shopify/address.rb +40 -0
  132. data/app/models/gemgento/adapter/shopify/category.rb +51 -0
  133. data/app/models/gemgento/adapter/shopify/customer.rb +66 -0
  134. data/app/models/gemgento/adapter/shopify/order.rb +334 -0
  135. data/app/models/gemgento/adapter/shopify/product.rb +377 -0
  136. data/app/models/gemgento/adapter/shopify/product_category_position.rb +58 -0
  137. data/app/models/gemgento/adapter/shopify_adapter.rb +63 -0
  138. data/app/models/gemgento/address.rb +206 -0
  139. data/app/models/gemgento/api_job.rb +68 -0
  140. data/app/models/gemgento/asset.rb +185 -0
  141. data/app/models/gemgento/asset_file.rb +57 -0
  142. data/app/models/gemgento/asset_type.rb +8 -0
  143. data/app/models/gemgento/bundle.rb +5 -0
  144. data/app/models/gemgento/bundle/item.rb +17 -0
  145. data/app/models/gemgento/bundle/option.rb +20 -0
  146. data/app/models/gemgento/category.rb +235 -0
  147. data/app/models/gemgento/country.rb +21 -0
  148. data/app/models/gemgento/gift_message.rb +8 -0
  149. data/app/models/gemgento/image_import.rb +141 -0
  150. data/app/models/gemgento/import.rb +125 -0
  151. data/app/models/gemgento/inventory.rb +60 -0
  152. data/app/models/gemgento/inventory_import.rb +51 -0
  153. data/app/models/gemgento/line_item.rb +142 -0
  154. data/app/models/gemgento/line_item_option.rb +10 -0
  155. data/app/models/gemgento/magento_db.rb +35 -0
  156. data/app/models/gemgento/magento_response.rb +10 -0
  157. data/app/models/gemgento/order.rb +56 -0
  158. data/app/models/gemgento/order_status.rb +7 -0
  159. data/app/models/gemgento/payment.rb +50 -0
  160. data/app/models/gemgento/price_rule.rb +258 -0
  161. data/app/models/gemgento/price_tier.rb +36 -0
  162. data/app/models/gemgento/product.rb +607 -0
  163. data/app/models/gemgento/product_attribute.rb +48 -0
  164. data/app/models/gemgento/product_attribute_option.rb +39 -0
  165. data/app/models/gemgento/product_attribute_set.rb +22 -0
  166. data/app/models/gemgento/product_attribute_value.rb +26 -0
  167. data/app/models/gemgento/product_category.rb +34 -0
  168. data/app/models/gemgento/product_import.rb +359 -0
  169. data/app/models/gemgento/quote.rb +586 -0
  170. data/app/models/gemgento/recurring_profile.rb +28 -0
  171. data/app/models/gemgento/region.rb +14 -0
  172. data/app/models/gemgento/relation.rb +10 -0
  173. data/app/models/gemgento/relation_type.rb +7 -0
  174. data/app/models/gemgento/saved_credit_card.rb +45 -0
  175. data/app/models/gemgento/search.rb +14 -0
  176. data/app/models/gemgento/session.rb +49 -0
  177. data/app/models/gemgento/shipment.rb +38 -0
  178. data/app/models/gemgento/shipment_comment.rb +19 -0
  179. data/app/models/gemgento/shipment_item.rb +11 -0
  180. data/app/models/gemgento/shipment_track.rb +21 -0
  181. data/app/models/gemgento/stock_notification.rb +17 -0
  182. data/app/models/gemgento/store.rb +31 -0
  183. data/app/models/gemgento/store_tag.rb +8 -0
  184. data/app/models/gemgento/subscriber.rb +30 -0
  185. data/app/models/gemgento/swatch.rb +9 -0
  186. data/app/models/gemgento/sync.rb +134 -0
  187. data/app/models/gemgento/tag.rb +64 -0
  188. data/app/models/gemgento/user.rb +161 -0
  189. data/app/models/gemgento/user_group.rb +13 -0
  190. data/app/models/gemgento/wishlist_item.rb +11 -0
  191. data/app/services/gemgento/magento_api.rb +173 -0
  192. data/app/services/gemgento/price.rb +67 -0
  193. data/app/validators/gemgento/credit_card_validator.rb +50 -0
  194. data/app/validators/gemgento/image_import_validator.rb +100 -0
  195. data/app/validators/gemgento/inventory_import_validator.rb +73 -0
  196. data/app/validators/gemgento/inventory_validator.rb +14 -0
  197. data/app/validators/gemgento/product_import_validator.rb +163 -0
  198. data/app/views/admin/product_positions/_index.html.haml +14 -0
  199. data/app/views/devise/registrations/edit.haml +37 -0
  200. data/app/views/gemgento/addresses/_address.json.jbuilder +6 -0
  201. data/app/views/gemgento/addresses/region_options.html.haml +1 -0
  202. data/app/views/gemgento/api/v1/categories/_category.json.jbuilder +52 -0
  203. data/app/views/gemgento/api/v1/categories/index.json.jbuilder +8 -0
  204. data/app/views/gemgento/api/v1/categories/show.json.jbuilder +7 -0
  205. data/app/views/gemgento/api/v1/products/_attributes.json.jbuilder +9 -0
  206. data/app/views/gemgento/api/v1/products/_product.json.jbuilder +18 -0
  207. data/app/views/gemgento/api/v1/products/_relationships.json.jbuilder +48 -0
  208. data/app/views/gemgento/api/v1/products/index.json.jbuilder +8 -0
  209. data/app/views/gemgento/api/v1/products/show.json.jbuilder +7 -0
  210. data/app/views/gemgento/api/v1/search/index.json.jbuilder +8 -0
  211. data/app/views/gemgento/api/v1/search/show.json.jbuilder +8 -0
  212. data/app/views/gemgento/assets/_asset.json.jbuilder +13 -0
  213. data/app/views/gemgento/cart/show.html.haml +67 -0
  214. data/app/views/gemgento/categories/_category.json.jbuilder +9 -0
  215. data/app/views/gemgento/categories/_product_tile.html.haml +16 -0
  216. data/app/views/gemgento/categories/index.html.haml +10 -0
  217. data/app/views/gemgento/categories/index.json.jbuilder +1 -0
  218. data/app/views/gemgento/categories/show.html.haml +16 -0
  219. data/app/views/gemgento/categories/show.json.jbuilder +1 -0
  220. data/app/views/gemgento/checkout/_checkout_header.html.haml +17 -0
  221. data/app/views/gemgento/checkout/address/error.js.erb +1 -0
  222. data/app/views/gemgento/checkout/address/show.html.haml +88 -0
  223. data/app/views/gemgento/checkout/address/success.js.erb +1 -0
  224. data/app/views/gemgento/checkout/confirm/error.js.erb +0 -0
  225. data/app/views/gemgento/checkout/confirm/show.html.haml +60 -0
  226. data/app/views/gemgento/checkout/confirm/success.js.erb +1 -0
  227. data/app/views/gemgento/checkout/login/login_fail.js.erb +1 -0
  228. data/app/views/gemgento/checkout/login/login_guest_fail.js.erb +1 -0
  229. data/app/views/gemgento/checkout/login/login_guest_success.js.erb +1 -0
  230. data/app/views/gemgento/checkout/login/login_success.js.erb +1 -0
  231. data/app/views/gemgento/checkout/login/registration_fail.js.erb +1 -0
  232. data/app/views/gemgento/checkout/login/registration_success.js.erb +1 -0
  233. data/app/views/gemgento/checkout/login/show.html.haml +50 -0
  234. data/app/views/gemgento/checkout/payment/error.js.erb +1 -0
  235. data/app/views/gemgento/checkout/payment/show.html.haml +56 -0
  236. data/app/views/gemgento/checkout/payment/success.js.erb +1 -0
  237. data/app/views/gemgento/checkout/shipping/error.js.erb +0 -0
  238. data/app/views/gemgento/checkout/shipping/show.html.haml +26 -0
  239. data/app/views/gemgento/checkout/shipping/success.js.erb +1 -0
  240. data/app/views/gemgento/checkout/shipping_payment/show.haml +52 -0
  241. data/app/views/gemgento/checkout/thank_you/show.html.haml +6 -0
  242. data/app/views/gemgento/countries/_country.json.jbuilder +4 -0
  243. data/app/views/gemgento/countries/index.json.jbuilder +3 -0
  244. data/app/views/gemgento/countries/show.json.jbuilder +1 -0
  245. data/app/views/gemgento/home/index.html.haml +1 -0
  246. data/app/views/gemgento/order/add_item.js.erb +0 -0
  247. data/app/views/gemgento/order/no_inventory.js.erb +1 -0
  248. data/app/views/gemgento/order/remove_item.js.erb +0 -0
  249. data/app/views/gemgento/order/update_item.js.erb +0 -0
  250. data/app/views/gemgento/order_export/index.html.haml +2 -0
  251. data/app/views/gemgento/order_items/_order_item.json.jbuilder +7 -0
  252. data/app/views/gemgento/orders/_order.json.jbuilder +29 -0
  253. data/app/views/gemgento/products/_product.json.jbuilder +40 -0
  254. data/app/views/gemgento/products/index.html.haml +19 -0
  255. data/app/views/gemgento/products/show.html.haml +67 -0
  256. data/app/views/gemgento/products/show.json.jbuilder +1 -0
  257. data/app/views/gemgento/sales_mailer/credit_memo_email.html.haml +1 -0
  258. data/app/views/gemgento/sales_mailer/invoice_email.html.haml +1 -0
  259. data/app/views/gemgento/sales_mailer/order_email.html.haml +1 -0
  260. data/app/views/gemgento/sales_mailer/shipment_email.html.haml +1 -0
  261. data/app/views/gemgento/search/index.html.haml +8 -0
  262. data/app/views/gemgento/shared/_asset.json.jbuilder +35 -0
  263. data/app/views/gemgento/shared/_asset_file.json.jbuilder +19 -0
  264. data/app/views/gemgento/shared/_asset_type.json.jbuilder +13 -0
  265. data/app/views/gemgento/shared/_error_messages.haml +7 -0
  266. data/app/views/gemgento/shared/_flash_messages.haml +4 -0
  267. data/app/views/gemgento/shared/_footer.haml +7 -0
  268. data/app/views/gemgento/shared/_header.haml +23 -0
  269. data/app/views/gemgento/shared/_mini_bag.haml +36 -0
  270. data/app/views/gemgento/shared/_product_image.haml +8 -0
  271. data/app/views/gemgento/shipments/_shipment.json.jbuilder +12 -0
  272. data/app/views/gemgento/subscribers/create.js.erb +2 -0
  273. data/app/views/gemgento/subscribers/errors.js.erb +1 -0
  274. data/app/views/gemgento/user/_user.json.jbuilder +2 -0
  275. data/app/views/gemgento/user/addresses/_form.haml +39 -0
  276. data/app/views/gemgento/user/addresses/errors.js.erb +1 -0
  277. data/app/views/gemgento/user/addresses/index.haml +33 -0
  278. data/app/views/gemgento/user/addresses/new.haml +11 -0
  279. data/app/views/gemgento/user/addresses/success.js.erb +1 -0
  280. data/app/views/gemgento/user/confirmations/new.haml +9 -0
  281. data/app/views/gemgento/user/home/index.haml +8 -0
  282. data/app/views/gemgento/user/mailer/confirmation_instructions.html.erb +5 -0
  283. data/app/views/gemgento/user/mailer/reset_password_instructions.html.erb +8 -0
  284. data/app/views/gemgento/user/mailer/unlock_instructions.html.erb +7 -0
  285. data/app/views/gemgento/user/orders/index.html.haml +29 -0
  286. data/app/views/gemgento/user/orders/show.html.haml +87 -0
  287. data/app/views/gemgento/user/passwords/edit.html.erb +18 -0
  288. data/app/views/gemgento/user/passwords/new.html.haml +21 -0
  289. data/app/views/gemgento/user/registration_session/new.haml +67 -0
  290. data/app/views/gemgento/user/registrations/new.html.haml +28 -0
  291. data/app/views/gemgento/user/saved_credit_cards/_form.haml +69 -0
  292. data/app/views/gemgento/user/saved_credit_cards/index.haml +25 -0
  293. data/app/views/gemgento/user/saved_credit_cards/new.haml +13 -0
  294. data/app/views/gemgento/user/sessions/new.html.haml +29 -0
  295. data/app/views/gemgento/user/shared/_links.erb +25 -0
  296. data/app/views/gemgento/user/shared/_user_header.haml +8 -0
  297. data/app/views/gemgento/user/wishlist_items/index.html.haml +20 -0
  298. data/app/views/kaminari/_first_page.html.haml +9 -0
  299. data/app/views/kaminari/_gap.html.haml +8 -0
  300. data/app/views/kaminari/_last_page.html.haml +9 -0
  301. data/app/views/kaminari/_next_page.html.haml +9 -0
  302. data/app/views/kaminari/_page.html.haml +10 -0
  303. data/app/views/kaminari/_paginator.html.haml +18 -0
  304. data/app/views/kaminari/_prev_page.html.haml +9 -0
  305. data/app/views/layouts/_footer.html.haml +5 -0
  306. data/app/views/layouts/_header.html.haml +39 -0
  307. data/app/views/layouts/gemgento.html.haml +22 -0
  308. data/app/workers/gemgento/apply_price_rule.rb +24 -0
  309. data/app/workers/gemgento/cart/add_item_worker.rb +23 -0
  310. data/app/workers/gemgento/cart/update_item_worker.rb +18 -0
  311. data/bin/rails +12 -0
  312. data/config/gemgento_config.yml +12 -0
  313. data/config/initializers/active_admin.rb +14 -0
  314. data/config/initializers/active_touch.rb +3 -0
  315. data/config/initializers/assets.rb +1 -0
  316. data/config/initializers/boolean_typecasting.rb +37 -0
  317. data/config/initializers/devise.rb +49 -0
  318. data/config/initializers/gemgento_config.rb +67 -0
  319. data/config/initializers/mime_types.rb +4 -0
  320. data/config/initializers/state_machine.rb +7 -0
  321. data/config/locales/en.yml +24 -0
  322. data/config/routes.rb +138 -0
  323. data/db/migrate/20151201231300_add_validation_indexes_to_gemgento_products.rb +22 -0
  324. data/db/migrate/20160122174450_gemgento2.rb +969 -0
  325. data/db/migrate/20160217142910_create_gemgento_imports.rb +15 -0
  326. data/db/migrate/20160219235858_add_timestamps_to_gemgento_asset_files.rb +6 -0
  327. data/gemgento.gemspec +44 -0
  328. data/lib/devise/models/validatable.rb +76 -0
  329. data/lib/gemgento.rb +24 -0
  330. data/lib/gemgento/admin/admin_user.rb +29 -0
  331. data/lib/gemgento/admin/api_jobs.rb +30 -0
  332. data/lib/gemgento/admin/image_import.rb +87 -0
  333. data/lib/gemgento/admin/inventory_import.rb +81 -0
  334. data/lib/gemgento/admin/product_import.rb +138 -0
  335. data/lib/gemgento/admin/product_positions.rb +49 -0
  336. data/lib/gemgento/admin/stores.rb +33 -0
  337. data/lib/gemgento/admin/subscribers.rb +31 -0
  338. data/lib/gemgento/admin/syncs.rb +26 -0
  339. data/lib/gemgento/api/soap/authnetcim/payment.rb +127 -0
  340. data/lib/gemgento/api/soap/catalog/category.rb +237 -0
  341. data/lib/gemgento/api/soap/catalog/product.rb +481 -0
  342. data/lib/gemgento/api/soap/catalog/product_attribute.rb +147 -0
  343. data/lib/gemgento/api/soap/catalog/product_attribute_media.rb +202 -0
  344. data/lib/gemgento/api/soap/catalog/product_attribute_set.rb +80 -0
  345. data/lib/gemgento/api/soap/catalog/product_tag.rb +146 -0
  346. data/lib/gemgento/api/soap/catalog/product_tier_price.rb +21 -0
  347. data/lib/gemgento/api/soap/catalog/search.rb +27 -0
  348. data/lib/gemgento/api/soap/catalog_inventory/stock_item.rb +87 -0
  349. data/lib/gemgento/api/soap/catalog_rule/rule.rb +54 -0
  350. data/lib/gemgento/api/soap/checkout/cart.rb +85 -0
  351. data/lib/gemgento/api/soap/checkout/coupon.rb +37 -0
  352. data/lib/gemgento/api/soap/checkout/customer.rb +87 -0
  353. data/lib/gemgento/api/soap/checkout/payment.rb +59 -0
  354. data/lib/gemgento/api/soap/checkout/product.rb +138 -0
  355. data/lib/gemgento/api/soap/checkout/shipping.rb +37 -0
  356. data/lib/gemgento/api/soap/customer/address.rb +143 -0
  357. data/lib/gemgento/api/soap/customer/customer.rb +200 -0
  358. data/lib/gemgento/api/soap/directory/country.rb +49 -0
  359. data/lib/gemgento/api/soap/directory/region.rb +50 -0
  360. data/lib/gemgento/api/soap/enterprise_gift_message/gift_message.rb +26 -0
  361. data/lib/gemgento/api/soap/gift_card.rb +35 -0
  362. data/lib/gemgento/api/soap/globalcollect/saved_cc.rb +58 -0
  363. data/lib/gemgento/api/soap/miscellaneous/store.rb +47 -0
  364. data/lib/gemgento/api/soap/sales/order.rb +249 -0
  365. data/lib/gemgento/api/soap/sales/order_shipment.rb +122 -0
  366. data/lib/gemgento/api/soap/sales/recurring_profile.rb +88 -0
  367. data/lib/gemgento/api/soap/stock_notification.rb +27 -0
  368. data/lib/gemgento/engine.rb +61 -0
  369. data/lib/gemgento/version.rb +3 -0
  370. data/lib/generators/gemgento/assets_generator.rb +11 -0
  371. data/lib/generators/gemgento/install_generator.rb +34 -0
  372. data/lib/generators/gemgento/templates/active_admin.css.scss +1 -0
  373. data/lib/generators/gemgento/templates/active_admin.js.coffee +1 -0
  374. data/lib/generators/gemgento/templates/active_admin.rb +3 -0
  375. data/lib/generators/gemgento/views_generator.rb +10 -0
  376. data/spec/dummy/README.rdoc +28 -0
  377. data/spec/dummy/Rakefile +6 -0
  378. data/spec/dummy/app/assets/images/.keep +0 -0
  379. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  380. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  381. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  382. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  383. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  384. data/spec/dummy/app/mailers/.keep +0 -0
  385. data/spec/dummy/app/models/.keep +0 -0
  386. data/spec/dummy/app/models/concerns/.keep +0 -0
  387. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  388. data/spec/dummy/bin/bundle +3 -0
  389. data/spec/dummy/bin/rails +4 -0
  390. data/spec/dummy/bin/rake +4 -0
  391. data/spec/dummy/bin/setup +29 -0
  392. data/spec/dummy/config.ru +4 -0
  393. data/spec/dummy/config/application.rb +32 -0
  394. data/spec/dummy/config/boot.rb +5 -0
  395. data/spec/dummy/config/database.yml +20 -0
  396. data/spec/dummy/config/environment.rb +5 -0
  397. data/spec/dummy/config/environments/development.rb +41 -0
  398. data/spec/dummy/config/environments/production.rb +79 -0
  399. data/spec/dummy/config/environments/test.rb +42 -0
  400. data/spec/dummy/config/gemgento_config.yml +22 -0
  401. data/spec/dummy/config/initializers/assets.rb +11 -0
  402. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  403. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  404. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  405. data/spec/dummy/config/initializers/inflections.rb +16 -0
  406. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  407. data/spec/dummy/config/initializers/session_store.rb +3 -0
  408. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  409. data/spec/dummy/config/locales/en.yml +23 -0
  410. data/spec/dummy/config/routes.rb +4 -0
  411. data/spec/dummy/config/secrets.yml +22 -0
  412. data/spec/dummy/lib/assets/.keep +0 -0
  413. data/spec/dummy/public/404.html +67 -0
  414. data/spec/dummy/public/422.html +67 -0
  415. data/spec/dummy/public/500.html +66 -0
  416. data/spec/dummy/public/favicon.ico +0 -0
  417. data/spec/factories/gemgento/price_tiers.rb +8 -0
  418. data/spec/factories/gemgento/product_attribute_sets.rb +22 -0
  419. data/spec/factories/gemgento/product_attributes.rb +5 -0
  420. data/spec/factories/gemgento/products.rb +14 -0
  421. data/spec/factories/gemgento/stores.rb +12 -0
  422. data/spec/factories/gemgento/user_groups.rb +6 -0
  423. data/spec/factories/gemgento/users.rb +13 -0
  424. data/spec/factories/gemgento_imports.rb +12 -0
  425. data/spec/model/gemgento/price_tier_spec.rb +45 -0
  426. data/spec/rails_helper.rb +55 -0
  427. data/spec/services/gemgento/price_spec.rb +152 -0
  428. data/spec/spec_helper.rb +87 -0
  429. metadata +919 -0
@@ -0,0 +1,7 @@
1
+ module Gemgento
2
+
3
+ # @author Gemgento LLC
4
+ class OrderStatus < ActiveRecord::Base
5
+ belongs_to :order
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ module Gemgento
2
+
3
+ # @author Gemgento LLC
4
+ class Payment < ActiveRecord::Base
5
+ belongs_to :payable, polymorphic: true
6
+
7
+ attr_accessor :cc_number, :cc_cid, :save_card, :payment_id
8
+
9
+ before_save :set_cc_last4, if: Proc.new { |payment| payment.cc_number.present? || payment.payment_id.present? }
10
+
11
+ validates :method, :payable, presence: true
12
+
13
+ REDIRECTING_PAYMENT_METHODS = {
14
+ paypal_standard: %w[confirm_after],
15
+ paypal_express: %w[payment_after confirm_before]
16
+ }
17
+
18
+ def is_redirecting_payment_method?(step = nil)
19
+ if REDIRECTING_PAYMENT_METHODS.has_key?(self.method.to_sym)
20
+ if step.blank?
21
+ return true
22
+ else
23
+ return REDIRECTING_PAYMENT_METHODS[self.method.to_sym].include? step
24
+ end
25
+ else
26
+ return false
27
+ end
28
+ end
29
+
30
+ # Set cc_last4 to the last 4 numbers of cc_number.
31
+ #
32
+ # @return [void]
33
+ def set_cc_last4
34
+ if self.cc_number.present?
35
+ self.cc_last4 = cc_number[-4..-1]
36
+
37
+ elsif saved_cc = Gemgento::SavedCreditCard.find_by(user: self.payable.user, token: self.payment_id)
38
+ self.cc_last4 = saved_cc.cc_number.to_s[-4..-1]
39
+ end
40
+ end
41
+
42
+ def is_new_credit_card_payment?
43
+ payable_type == 'Gemgento::Quote' &&
44
+ payable.converted_at.nil? &&
45
+ !is_redirecting_payment_method? &&
46
+ method != 'free' && payment_id.nil?
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,258 @@
1
+ module Gemgento
2
+
3
+ # @author Gemgento LLC
4
+ class PriceRule < ActiveRecord::Base
5
+ serialize :conditions, Hash
6
+
7
+ has_and_belongs_to_many :stores, join_table: 'gemgento_price_rules_stores', class_name: 'Store'
8
+ has_and_belongs_to_many :user_groups, join_table: 'gemgento_price_rules_user_groups', class_name: 'UserGroup'
9
+
10
+ after_save :apply, if: -> { changed? }
11
+
12
+ default_scope ->{ order(sort_order: :asc) }
13
+ scope :active, -> { where(is_active: true) }
14
+
15
+ # Calculate a product price based on PriceRules.
16
+ #
17
+ # @param product [Gemgento::Product]
18
+ # @param user_group [Gemgento::UserGroup]
19
+ # @param store [Gemgento::Store]
20
+ # @return [Float]
21
+ def self.calculate_price(product, user_group = nil, store = nil)
22
+ store ||= Store.current
23
+ price = product.attribute_value('price', store).to_f
24
+ user_group ||= UserGroup.find_by(magento_id: 0)
25
+
26
+ PriceRule.active.each do |price_rule|
27
+ if price_rule.is_valid?(product, user_group, store)
28
+ price = price_rule.calculate(price)
29
+ return price if price_rule.stop_rules_processing?
30
+ end
31
+ end
32
+
33
+ return price
34
+ end
35
+
36
+ # Determines if the rule is valid for a product.
37
+ #
38
+ # @param product [Gemgento::Product]
39
+ # @param user_group [Gemgento::UserGroup]
40
+ # @param store [Gemgento::Store]
41
+ # @return [Boolean]
42
+ def is_valid?(product, user_group, store)
43
+ if !self.is_active?
44
+ return false
45
+ elsif !self.stores.include?(store)
46
+ return false
47
+ elsif !self.user_groups.include?(user_group)
48
+ return false
49
+ elsif !self.from_date.nil? && Date.today < from_date
50
+ return false
51
+ elsif !self.to_date.nil? && Date.today > to_date
52
+ return false
53
+ else
54
+ return PriceRule.meets_condition?(self.conditions, product, store)
55
+ end
56
+ end
57
+
58
+ # Calculate the discount based on rule actions.
59
+ #
60
+ # @param price [Float]
61
+ # @return Float
62
+ def calculate(price)
63
+ case self.simple_action
64
+ when 'to_fixed'
65
+ return [self.discount_amount.to_f, price].min
66
+ when 'to_percent'
67
+ return price * self.discount_amount.to_f / 100
68
+ when 'by_fixed'
69
+ return [0, price - self.discount_amount.to_f].max
70
+ when 'by_percent'
71
+ return price * (1 - self.discount_amount.to_f / 100)
72
+ else
73
+ return price
74
+ end
75
+ end
76
+
77
+ # Determines if the condition is met.
78
+ #
79
+ # @param condition [Hash]
80
+ # @param product [Gemgento::Product]
81
+ # @param store [Gemgento::Store]
82
+ # @return [Boolean]
83
+ def self.meets_condition?(condition, product, store)
84
+ if condition['type'] == 'catalogrule/rule_condition_combine'
85
+ return meets_condition_combine?(condition, product, store)
86
+ elsif condition['type'] == 'catalogrule/rule_condition_product'
87
+ return meets_condition_product?(condition, product, store)
88
+ else
89
+ return false # non-determinable condition type
90
+ end
91
+ end
92
+
93
+ # Determines if the combined child conditions are met.
94
+ #
95
+ # @param condition [Hash]
96
+ # @param product [Gemgento::Product]
97
+ # @param store [Gemgento::Store]
98
+ # @return [Boolean]
99
+ def self.meets_condition_combine?(condition, product, store)
100
+ return true if condition['conditions'].nil?
101
+
102
+ condition['conditions'].each do |child_condition|
103
+
104
+ if meets_condition?(child_condition, product, store) # the condition has been met
105
+ if condition['aggregator'] == 'all' && condition['value'] == '0' # all conditions not met
106
+ return false
107
+ elsif condition['aggregator'] == 'any' && condition['value'] == '1' # any condition met
108
+ return true
109
+ end
110
+
111
+ else # the condition has not be met
112
+ if condition['aggregator'] == 'all' && condition['value'] == '1' # all conditions met
113
+ return false
114
+ elsif condition['aggregator'] == 'any' && condition['value'] == '0' # any condition not be met
115
+ return true
116
+ end
117
+ end
118
+ end
119
+
120
+ if condition['aggregator'] == 'all' # all conditions evaluated and all met
121
+ return true
122
+ else # all conditions evaluated and none met
123
+ return false
124
+ end
125
+ end
126
+
127
+ # Determines if the product conditions are met.
128
+ #
129
+ # @param condition [Hash]
130
+ # @param product [Gemgento::Product]
131
+ # @param store [Gemgento::Store]
132
+ # @return [Boolean]
133
+ def self.meets_condition_product?(condition, product, store)
134
+ if condition['attribute'] == 'category_ids'
135
+ return meets_category_condition?(condition, product, store)
136
+ elsif condition['attribute'] == 'attribute_set_id'
137
+ return meets_attribute_set_condition?(condition, product)
138
+ else
139
+ return meets_attribute_condition?(condition, product, store)
140
+ end
141
+ end
142
+
143
+ # Determines if a product meets a category based condition.
144
+ #
145
+ # @param condition [Hash]
146
+ # @param product [Gemgento::Product]
147
+ # @param store [Gemgento::Store]
148
+ # @return [Boolean]
149
+ def self.meets_category_condition?(condition, product, store)
150
+ magento_category_ids = ProductCategory.where(product: product, store: store).includes(:category).map{ |pc| pc.category.magento_id unless pc.category.nil? }.uniq
151
+ condition_category_ids = condition['value'].split(',').map(&:to_i)
152
+
153
+ case condition['operator']
154
+ when *%w[== {} ()]
155
+ return (magento_category_ids & condition_category_ids).any?
156
+ when *%w[!= !{} !()]
157
+ return (magento_category_ids & condition_category_ids).empty?
158
+ else
159
+ return false
160
+ end
161
+ end
162
+
163
+ # Determines if a product meets an attribute set based condition.
164
+ #
165
+ # @param condition [Hash]
166
+ # @param product [Gemgento::Product]
167
+ # @return [Boolean]
168
+ def self.meets_attribute_set_condition?(condition, product)
169
+ magento_attribute_set_id = product.product_attribute_set.magento_id
170
+ condition_attribute_set_id = condition['value'].to_i
171
+
172
+ case condition['operator']
173
+ when *%w[== {} ()]
174
+ return magento_attribute_set_id == condition_attribute_set_id
175
+ when *%w[!= !{} !()]
176
+ return magento_attribute_set_id != condition_attribute_set_id
177
+ else
178
+ return false
179
+ end
180
+ end
181
+
182
+ # Determines if a product meets an attribute based condition.
183
+ #
184
+ # @param condition [Hash]
185
+ # @param product [Gemgento::Product]
186
+ # @param store [Gemgento::Store]
187
+ # @return [Boolean]
188
+ def self.meets_attribute_condition?(condition, product, store)
189
+ return false unless attribute = ProductAttribute.find_by(code: condition['attribute'])
190
+
191
+ product_value = product.attribute_value(attribute.code, store).to_s.downcase
192
+
193
+ if attribute.frontend_input == 'select'
194
+ return false unless option = attribute.product_attribute_options.find_by(value: condition['value'], store: store)
195
+ condition_value = option.label.downcase
196
+ else
197
+ condition_value = condition['value'].downcase
198
+ end
199
+
200
+ case condition['operator']
201
+ when '==' # is
202
+ return product_value == condition_value
203
+ when '!=' # is not
204
+ return product_value != condition_value
205
+ when '>=' # equals or greater than
206
+ return product_value >= condition_value
207
+ when '<=' # equals or less than
208
+ return product_value <= condition_value
209
+ when '>' # greater than
210
+ return product_value > condition_value
211
+ when '<' # less than
212
+ return product_value < condition_value
213
+ when '{}' # contains
214
+ return product_value.include?(condition_value)
215
+ when '!{}' # does not contain
216
+ return !product_value.include?(condition_value)
217
+ when '()' # is one of
218
+ return condition_value.split(',').map(&:strip).include?(product_value)
219
+ when '!()' # is not one of
220
+ return !condition_value.split(',').map(&:strip).include?(product_value)
221
+ else
222
+ return false
223
+ end
224
+ end
225
+
226
+ # Get the earliest date and time that a price rule will expire for a given product.
227
+ #
228
+ # @param product [Gemgento::Product]
229
+ # @param user_group [Gemgento::UserGroup]
230
+ # @param store [Gemgento::Store]
231
+ # @return [DateTime, nil]
232
+ def self.first_to_expire(product, user_group, store)
233
+ expires_at = nil
234
+
235
+ PriceRule.active.each do |price_rule|
236
+ next if price_rule.to_date.nil?
237
+
238
+ if price_rule.is_valid?(product, user_group, store)
239
+ if expires_at.nil? || price_rule.to_date.end_of_day < expires_at
240
+ expires_at = price_rule.to_date.end_of_day
241
+ end
242
+ end
243
+ end
244
+
245
+ return expires_at
246
+ end
247
+
248
+ private
249
+
250
+ # Touch products affected by the PriceRule
251
+ #
252
+ # @return [Void]
253
+ def apply
254
+ ApplyPriceRule.perform_async(self.id)
255
+ end
256
+
257
+ end
258
+ end
@@ -0,0 +1,36 @@
1
+ module Gemgento
2
+ class PriceTier < ActiveRecord::Base
3
+ include Gemgento::ProductTouches
4
+
5
+ belongs_to :store, class_name: 'Gemgento::Store'
6
+ belongs_to :product, class_name: 'Gemgento::Product'
7
+ belongs_to :user_group, class_name: 'Gemgento::UserGroup'
8
+
9
+ validates :store, :product, :quantity, :price, presence: true
10
+
11
+ # Check if PriceTier is valid for the given quantity and user.
12
+ #
13
+ # @param quantity [Float]
14
+ # @param user_group [Gemgento::UserGroup]
15
+ def is_valid?(quantity, user_group)
16
+ return quantity >= self.quantity && (self.user_group.nil? || user_group == self.user_group)
17
+ end
18
+
19
+ # @param product [Gemgento::Product]
20
+ # @param quantity [Float]
21
+ # @param user_group [Gemgento::UserGroup]
22
+ def self.calculate_price(product, quantity = 1.0, user_group = nil, store = nil)
23
+ store ||= Gemgento::Store.current
24
+ price = product.attribute_value('price', store).to_f
25
+ user_group ||= UserGroup.find_by(magento_id: 0)
26
+
27
+ product.price_tiers.where(store: store).where('quantity <= ?', quantity).each do |price_tier|
28
+ next unless price_tier.is_valid? quantity, user_group
29
+ price = price_tier.price if price_tier.price < price
30
+ end
31
+
32
+ return price
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,607 @@
1
+ module Gemgento
2
+
3
+ # @author Gemgento LLC
4
+ class Product < ActiveRecord::Base
5
+
6
+ belongs_to :product_attribute_set
7
+
8
+ has_many :assets, dependent: :destroy
9
+ has_many :bundle_items, class_name: 'Gemgento::Bundle::Item', dependent: :destroy
10
+ has_many :bundle_options, class_name: 'Gemgento::Bundle::Option', dependent: :destroy
11
+ has_many :categories, -> { uniq }, through: :product_categories, class_name: 'Gemgento::Category'
12
+ has_many :inventories, class_name: 'Gemgento::Inventory'
13
+ has_many :line_items, class_name: 'Gemgento::LineItem'
14
+ has_many :orders, through: :line_items, source: :itemizable, source_type: 'Gemgento::Order', class_name: 'Gemgento::Order'
15
+ has_many :price_tiers, class_name: 'Gemgento::PriceTier'
16
+ has_many :product_attribute_values, class_name: 'Gemgento::ProductAttributeValue', dependent: :destroy
17
+ has_many :product_attributes, through: :product_attribute_values, class_name: 'Gemgento::ProductAttribute'
18
+ has_many :product_attribute_options, through: :product_attribute_values, class_name: 'Gemgento::ProductAttributeOption'
19
+ has_many :product_categories, class_name: '::Gemgento::ProductCategory', dependent: :destroy
20
+ has_many :quotes, through: :line_items, source: :itemizable, source_type: 'Gemgento::Quote', class_name: 'Gemgento::Quote'
21
+ has_many :relations, as: :relatable, class_name: 'Relation', dependent: :destroy
22
+ has_many :shipment_items
23
+ has_many :wishlist_items
24
+ has_many :users, through: :wishlist_items
25
+
26
+ has_one :shopify_adapter, class_name: 'Adapter::ShopifyAdapter', as: :gemgento_model
27
+
28
+ has_and_belongs_to_many :stores, -> { distinct }, join_table: 'gemgento_stores_products', class_name: 'Store'
29
+ has_and_belongs_to_many :tags, class_name: 'Tag', join_table: 'gemgento_products_tags'
30
+ has_and_belongs_to_many :configurable_attributes, -> { distinct }, join_table: 'gemgento_configurable_attributes', class_name: 'ProductAttribute'
31
+ has_and_belongs_to_many :configurable_products, -> { distinct },
32
+ join_table: 'gemgento_configurable_simple_relations',
33
+ foreign_key: 'simple_product_id',
34
+ association_foreign_key: 'configurable_product_id',
35
+ class_name: 'Product'
36
+ has_and_belongs_to_many :simple_products, -> { distinct },
37
+ join_table: 'gemgento_configurable_simple_relations',
38
+ foreign_key: 'configurable_product_id',
39
+ association_foreign_key: 'simple_product_id',
40
+ class_name: 'Product'
41
+
42
+ scope :eager, -> { includes([{product_attribute_values: :product_attribute}, {assets: [:asset_file, :asset_types]}, :inventories]) }
43
+ scope :configurable, -> { where(magento_type: 'configurable') }
44
+ scope :simple, -> { where(magento_type: 'simple') }
45
+ scope :enabled, -> { where(status: true) }
46
+ scope :disabled, -> { where(status: false) }
47
+ scope :catalog_visible, -> { where(visibility: [2, 4]) }
48
+ scope :search_visible, -> { where(visibility: [3, 4]) }
49
+ scope :visible, -> { where(visibility: [2, 3, 4]) }
50
+ scope :not_deleted, -> { where(deleted_at: nil) }
51
+ scope :active, -> { where(deleted_at: nil, status: true) }
52
+
53
+ touch :categories, after_touch: :after_touch
54
+ touch :configurable_products, after_touch: :after_touch
55
+ touch :bundle_items, after_touch: :after_touch
56
+
57
+ after_find :manage_cache_expires_at
58
+
59
+ before_save :create_magento_product, if: -> { sync_needed? && magento_id.nil? }
60
+ before_save :update_magento_product, if: -> { sync_needed? && !magento_id.nil? }
61
+
62
+ before_destroy :delete_associations
63
+
64
+ after_commit :remove_from_active_quotes, unless: Proc.new { |product| product.status && product.deleted_at.nil? }
65
+
66
+ validates :sku, uniqueness: { scope: :deleted_at }
67
+ validates :magento_id, uniqueness: true
68
+
69
+ attr_accessor :configurable_attribute_ordering, :attribute_values, :sync_needed
70
+
71
+ def sync_needed?
72
+ self.sync_needed.to_bool
73
+ end
74
+
75
+ # Set an attribute value.
76
+ #
77
+ # @param code [String] attribute code
78
+ # @param value [String, Boolean, Integer, Float, BigDecimal]
79
+ # @param store [Gemgento::Store]
80
+ # @return [Boolean]
81
+ def set_attribute_value(code, value, store = nil)
82
+ store = Store.current if store.nil?
83
+
84
+ product_attribute = ProductAttribute.find_by(code: code)
85
+
86
+ if product_attribute.nil?
87
+ return false
88
+ else
89
+ # enforce a single attribute value per attribute per store per product
90
+ product_attribute_values = ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store)
91
+
92
+ if product_attribute_values.size > 1
93
+ ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store).where('id != ?', product_attribute_values.first.id).destroy_all
94
+ end
95
+
96
+ # set the attribute value
97
+ product_attribute_value = ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store).first_or_initialize
98
+ product_attribute_value.product = self
99
+ product_attribute_value.product_attribute = product_attribute
100
+ product_attribute_value.value = value
101
+ product_attribute_value.store = store
102
+ product_attribute_value.save!
103
+
104
+ self.product_attribute_values << product_attribute_value unless self.product_attribute_values.include?(product_attribute_value)
105
+ self.attribute_values = nil # reload cached attributes values
106
+
107
+ return true
108
+ end
109
+ end
110
+
111
+ # Get an attribute value.
112
+ #
113
+ # @param code [String] attribute code
114
+ # @param store [Gemgento::Store]
115
+ # @return [String, Boolean, nil]
116
+ def attribute_value(code, store = nil)
117
+ store = Gemgento::Store.current if store.nil?
118
+ product_attribute_value = self.attribute_values.select { |value| !value.product_attribute.nil? && value.product_attribute.code == code.to_s && value.store_id == store.id }.first
119
+
120
+ ## if the attribute is not currently associated with the product, check if it exists
121
+ if product_attribute_value.nil?
122
+ product_attribute = Gemgento::ProductAttribute.find_by(code: code)
123
+
124
+ if product_attribute.nil? # throw an error if the code is not recognized
125
+ raise "Unknown product attribute code - #{code}"
126
+ end
127
+ else
128
+ product_attribute = product_attribute_value.product_attribute
129
+ end
130
+
131
+ value = product_attribute_value.nil? ? product_attribute.default_value : product_attribute_value.value
132
+ return nil if value.nil?
133
+
134
+ if product_attribute.frontend_input == 'boolean' || product_attribute.code == 'is_recurring'
135
+ if value == 'Yes' || value == '1' || value == '1.0'
136
+ value = true
137
+ else
138
+ value = false
139
+ end
140
+ elsif product_attribute.frontend_input == 'select'
141
+ option = product_attribute.product_attribute_options.find_by(value: value, store: store)
142
+ value = option.nil? ? nil : option.label
143
+ end
144
+
145
+ return value
146
+ end
147
+
148
+ def attribute_values
149
+ @attribute_values ||= self.product_attribute_values.joins(:product_attribute).includes(:product_attribute)
150
+ end
151
+
152
+ # Attempts to return attribute_value before error.
153
+ def method_missing(method, *args)
154
+ begin
155
+ return self.attribute_value(method)
156
+ rescue
157
+ super
158
+ end
159
+ end
160
+
161
+ # Determine if product has a specific inventory level.
162
+ #
163
+ # @param quantity [Integer, BigDecimal, Float]
164
+ # @param store [Gemgento::Store]
165
+ # @return [Boolean]
166
+ def in_stock?(quantity = 1, store = nil)
167
+ store = Store.current if store.nil?
168
+
169
+ if self.magento_type == 'configurable'
170
+ inventories = Inventory.where(product_id: self.simple_products.active.select(:id), store: store)
171
+
172
+ if inventories.empty? # no inventories means inventory is not tracked
173
+ return true
174
+ else
175
+ inventories.each do |inventory|
176
+ return true if inventory.in_stock?(quantity)
177
+ end
178
+
179
+ return false
180
+ end
181
+
182
+ else
183
+ if inventory = self.inventories.find_by(store: store)
184
+ return inventory.in_stock?(quantity)
185
+ else
186
+ return true
187
+ end
188
+ end
189
+ end
190
+
191
+ # Mark a product deleted.
192
+ #
193
+ # @return [Void]
194
+ def mark_deleted
195
+ self.deleted_at = Time.now
196
+ self.shopify_adapter.destroy if self.shopify_adapter
197
+ end
198
+
199
+ # Mark a product deleted and save.
200
+ #
201
+ # @return [Void]
202
+ def mark_deleted!
203
+ mark_deleted
204
+ self.save
205
+ end
206
+
207
+ # Filter products based on attribute values.
208
+ #
209
+ # filter example:
210
+ # {attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: 'large'})
211
+ # or
212
+ # {attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: %w[large small]})
213
+ # or
214
+ # {attribute: [Gemgento::ProductAttribute.find_by(code: 'size'), Gemgento::ProductAttribute.find_by(code: 'dimension')], value: 'large'})
215
+ # or
216
+ # {attribute: [Gemgento::ProductAttribute.find_by(code: 'size'), Gemgento::ProductAttribute.find_by(code: 'dimension')], value: %w[large small]})
217
+ # or
218
+ # [{attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: 'large'}), {attribute: Gemgento::ProductAttribute.find_by(code: 'color'), value: 'red'})]
219
+ #
220
+ # Filters can also take an optional operand, the default operand is '=' or 'IN' for an array
221
+ #
222
+ # @param filters [Hash, Array(Hash)]
223
+ # @param store [Gemgento::Store]
224
+ # @return [ActiveRecord::Result]
225
+ def self.filter(filters, store = nil)
226
+ store = Store.current if store.nil?
227
+
228
+ filters = [filters] unless filters.is_a? Array
229
+ products = self
230
+
231
+ filters.each_with_index do |filter, index|
232
+ filter[:attribute] = [filter[:attribute]] unless filter[:attribute].is_a? Array
233
+ operand = filter[:value].is_a?(Array) ? 'IN' : ( filter.has_key?(:operand) ? filter[:operand] : '=' )
234
+ value_placeholder = filter[:value].is_a?(Array) ? '(?)' : '?'
235
+
236
+ unless filter[:attribute][0].frontend_input == 'select'
237
+ products = products.joins(ActiveRecord::Base.escape_sql(
238
+ "INNER JOIN gemgento_product_attribute_values AS value#{index} ON value#{index}.product_id = gemgento_products.id AND value#{index}.value #{operand} #{value_placeholder} AND value#{index}.store_id = ?
239
+ INNER JOIN gemgento_product_attributes AS attribute#{index} ON attribute#{index}.id = value#{index}.product_attribute_id AND attribute#{index}.id IN (?)",
240
+ filter[:value],
241
+ store.id,
242
+ filter[:attribute].map { |a| a.id }
243
+ )).distinct.readonly(false)
244
+ else
245
+ products = products.joins(ActiveRecord::Base.escape_sql(
246
+ "INNER JOIN gemgento_product_attribute_values AS value#{index} ON value#{index}.product_id = gemgento_products.id
247
+ INNER JOIN gemgento_product_attributes AS attribute#{index} ON attribute#{index}.id = value#{index}.product_attribute_id
248
+ AND attribute#{index}.id IN (?)
249
+ INNER JOIN gemgento_product_attribute_options AS option#{index} ON option#{index}.product_attribute_id = attribute#{index}.id
250
+ AND value#{index}.value = option#{index}.value
251
+ AND option#{index}.label #{operand} #{value_placeholder}",
252
+ filter[:attribute].map { |a| a.id },
253
+ filter[:value]
254
+ )).distinct.readonly(false) # does not compare against values
255
+ end
256
+ end
257
+
258
+ return products
259
+ end
260
+
261
+ # Order ActiveRecord result by attribute values.
262
+ #
263
+ # @param attribute [Gemgento::ProductAttribute]
264
+ # @param direction ['ASC', 'DESC']
265
+ # @param is_numeric [Boolean]
266
+ # @param store [Gemgento::Store]
267
+ # @return [ActiveRecord::Result]
268
+ def self.order_by_attribute(attribute, direction = 'ASC', is_numeric = false, store = nil)
269
+ store = Store.current if store.nil?
270
+ raise 'Direction must be equivalent to ASC or DESC' if direction != 'ASC' and direction != 'DESC'
271
+
272
+ products = self
273
+
274
+ unless attribute.frontend_input == 'select'
275
+ products = products.joins(
276
+ ActiveRecord::Base.escape_sql(
277
+ 'INNER JOIN gemgento_product_attribute_values ON gemgento_product_attribute_values.product_id = gemgento_products.id AND gemgento_product_attribute_values.product_attribute_id = ? AND gemgento_product_attribute_values.store_id = ? ' +
278
+ 'INNER JOIN gemgento_product_attributes ON gemgento_product_attributes.id = gemgento_product_attribute_values.product_attribute_id ',
279
+ attribute.id,
280
+ store.id
281
+ ))
282
+
283
+ if is_numeric
284
+ products = products.reorder("CAST(gemgento_product_attribute_values.value AS SIGNED) #{direction}")
285
+ else
286
+ products = products.reorder("gemgento_product_attribute_values.value #{direction}")
287
+ end
288
+ else
289
+ products = products.joins(
290
+ ActiveRecord::Base.escape_sql(
291
+ 'INNER JOIN gemgento_product_attribute_values ON gemgento_product_attribute_values.product_id = gemgento_products.id AND gemgento_product_attribute_values.product_attribute_id = ? ' +
292
+ 'INNER JOIN gemgento_product_attributes ON gemgento_product_attributes.id = gemgento_product_attribute_values.product_attribute_id ' +
293
+ 'INNER JOIN gemgento_product_attribute_options ON gemgento_product_attribute_options.product_attribute_id = gemgento_product_attributes.id AND gemgento_product_attribute_options.value = gemgento_product_attribute_values.value ' +
294
+ 'AND gemgento_product_attribute_options.store_id = ?',
295
+ attribute.id,
296
+ store.id
297
+ ))
298
+
299
+ if is_numeric
300
+ products = products.reorder("CAST(gemgento_product_attribute_options.order AS SIGNED) #{direction}")
301
+ else
302
+ products = products.reorder("gemgento_product_attribute_options.order #{direction}")
303
+ end
304
+ end
305
+
306
+ products = products.readonly(false)
307
+
308
+ return products
309
+ end
310
+
311
+ # Get the product price.
312
+ #
313
+ # @param user_group [Gemgento::UserGroup]
314
+ # @param store [Store]
315
+ # @param quantity [Float]
316
+ # @return Float
317
+ def price(user_group = nil, store = nil, quantity = 1.0)
318
+ Gemgento::Price.new(self, user_group, store, quantity).calculate
319
+ end
320
+
321
+ # Determine if product is on sale.
322
+ #
323
+ # @param user_group [Gemgento::UserGroup]
324
+ # @param store [Store]
325
+ # @param quantity [Float]
326
+ # @return Boolean
327
+ def on_sale?(user_group = nil, store = nil, quantity = 1.0)
328
+ return self.attribute_value('price', store).to_f != self.price(user_group, store, quantity)
329
+ end
330
+
331
+ # Get the original, non sale, price for a product.
332
+ #
333
+ # @param [Store, nil] store
334
+ # @return [Float]
335
+ def original_price(store = nil)
336
+ return self.attribute_value('price', store).to_f
337
+ end
338
+
339
+ # Return the ordering of configurable attribute values.
340
+ #
341
+ # @param [Store, nil] store
342
+ # @param [Boolean] active_only
343
+ # @param [Hash(Hash(Array(Integer)))]
344
+ def configurable_attribute_order(store = nil, active_only = true)
345
+ self.configurable_attribute_ordering ||= self.get_configurable_attribute_ordering(store, active_only)
346
+ end
347
+
348
+ # Calculate the ordering of configurable attribute values
349
+ #
350
+ # @param [Store, nil] store
351
+ # @param [Boolean] active_only
352
+ # @return [Hash(Hash(Array(Integer)))]
353
+ def get_configurable_attribute_ordering(store, active_only)
354
+ store = Store.current if store.nil?
355
+ order = {}
356
+
357
+ if self.magento_type != 'configurable' && !self.configurable_products.empty?
358
+ configurable_product = self.configurable_products.first
359
+ else
360
+ configurable_product = self
361
+ end
362
+
363
+ if active_only
364
+ simple_products = configurable_product.simple_products.active.eager
365
+ else
366
+ simple_products = self.simple_products.eager
367
+ end
368
+
369
+ configurable_attributes = self.product_attribute_set.product_attributes.
370
+ where(is_configurable: true, frontend_input: 'select', scope: 'global')
371
+
372
+ configurable_attributes.each do |attribute|
373
+ order[attribute.code] = []
374
+
375
+ simple_products = simple_products.sort_by do |simple_product|
376
+ if o = simple_product.product_attribute_options.find_by(product_attribute: attribute, store: store)
377
+ o.order
378
+ else
379
+ 0
380
+ end
381
+ end
382
+
383
+ mapping = {}
384
+ simple_products.each do |simple_product|
385
+ value = simple_product.attribute_value(attribute.code, store)
386
+ mapping[value] = [] if mapping[value].nil?
387
+ mapping[value] << simple_product.id unless mapping[value].include? simple_product.id
388
+ end
389
+
390
+ order[attribute.code] = []
391
+ mapping.each do |k, value|
392
+ order[attribute.code] << { value: k, simple_product_ids: value }
393
+ end
394
+ end
395
+
396
+ return order
397
+ end
398
+
399
+ # Determine if the product is catalog visible.
400
+ #
401
+ # @return [Boolean]
402
+ def is_catalog_visible?
403
+ return [2, 4].include?(self.visibility)
404
+ end
405
+
406
+ # Set the associated simple products, using an array of Magento product IDs.
407
+ #
408
+ # @param magento_ids [Array(Integer)] Magento IDs of the associated simple products
409
+ # @return [void]
410
+ def set_simple_products_by_magento_ids(magento_ids)
411
+ simple_product_ids = []
412
+
413
+ magento_ids.each do |magento_id|
414
+ simple_product = Product.active.find_by(magento_id: magento_id)
415
+ next if simple_product.nil?
416
+
417
+ self.simple_products << simple_product unless self.simple_products.include? simple_product
418
+ simple_product_ids << simple_product.id
419
+ end
420
+
421
+ self.simple_products.delete(self.simple_products.where('simple_product_id NOT IN (?)', simple_product_ids))
422
+ end
423
+
424
+ # Set the associated configurable products, using an array of Magento product IDs.
425
+ #
426
+ # @param magento_ids [Array(Integer)] Magento IDs of the associated configurable products
427
+ # @return [void]
428
+ def set_configurable_products_by_magento_ids(magento_ids)
429
+ configurable_product_ids = []
430
+
431
+ magento_ids.each do |magento_id|
432
+ configurable_product = Product.active.find_by(magento_id: magento_id)
433
+ next if configurable_product.nil?
434
+
435
+ self.configurable_products << configurable_product unless self.configurable_products.include? configurable_product
436
+ configurable_product_ids << configurable_product.id
437
+ end
438
+
439
+ self.configurable_products.delete(self.configurable_products.where('configurable_product_id NOT IN (?)', configurable_product_ids))
440
+ end
441
+
442
+ # If the product has a cache_expires_at date set, make sure it hasn't expired. If it has, set it again.
443
+ #
444
+ # @return [DateTime, nil]
445
+ def manage_cache_expires_at
446
+ self.set_cache_expires_at if self.cache_expires_at && self.cache_expires_at < Time.now
447
+ end
448
+
449
+ # Calculate the datetime that the product cache should expire.
450
+ #
451
+ # @return [Void]
452
+ def set_cache_expires_at
453
+ self.cache_expires_at = nil
454
+
455
+ Store.all.each do |store|
456
+ UserGroup.all.each do |user_group|
457
+ if Price.new(self, user_group, store).has_special?
458
+ date = self.attribute_value('special_to_date', store)
459
+ else
460
+ date = PriceRule.first_to_expire(self, user_group, store)
461
+ end
462
+
463
+ next if date.nil?
464
+ self.cache_expires_at = date if self.cache_expires_at.nil? || date < self.cache_expires_at
465
+ end
466
+ end
467
+
468
+ self.sync_needed = false
469
+ self.save
470
+ end
471
+
472
+ def to_param
473
+ "#{self.id}-#{self.url_key}"
474
+ end
475
+
476
+ # Determine the current category of a product based on the active navigation categories related to the product.
477
+ # A preferred category id can be specified, if this category is not found in the products navigation categories,
478
+ # then the lowest level navigation category is returned.
479
+ #
480
+ # @param category_id [Integer] id of a preferred category to return
481
+ # @param store [Gemgento::Store]
482
+ # @return [Gemgento::Category]
483
+ def current_category(category_id = nil, store = nil)
484
+ @current_category ||= begin
485
+ self.categories(store || Gemgento::Store.current).active.navigation.find(category_id)
486
+ rescue ActiveRecord::RecordNotFound
487
+ self.categories(store || Gemgento::Store.current).active.navigation.bottom_level.first!
488
+ end
489
+ end
490
+
491
+ # Check if the product is configurable.
492
+ #
493
+ # @return [Boolean]
494
+ def configurable?
495
+ magento_type == 'configurable'
496
+ end
497
+
498
+ # Check if the product is simple.
499
+ #
500
+ # @return [Boolean]
501
+ def simple?
502
+ magento_type == 'simple'
503
+ end
504
+
505
+ # Categories related to the product.
506
+ #
507
+ # @param store [Gemgento::Store]
508
+ # @return []
509
+ def categories(store = nil)
510
+ return super if store.nil?
511
+ Gemgento::Category.where(id: self.product_categories.where(store: store).pluck(:category_id))
512
+ end
513
+
514
+ def remove_from_active_quotes
515
+ self.line_items
516
+ .joins('INNER JOIN gemgento_quotes ON gemgento_line_items.itemizable_id = gemgento_quotes.id')
517
+ .joins('LEFT JOIN gemgento_orders ON gemgento_quotes.id = gemgento_orders.quote_id')
518
+ .where(gemgento_line_items: { itemizable_type: 'Gemgento::Quote' })
519
+ .where(gemgento_orders: { id: nil }).where('gemgento_quotes.created_at >= ?', 30.days.ago)
520
+ .destroy_all
521
+ end
522
+
523
+ def after_touch
524
+ # do nothing
525
+ end
526
+
527
+ private
528
+
529
+ # Create an attribute option in Magento.
530
+ #
531
+ # @param product_attribute [ProductAttribute]
532
+ # @param option_label [String]
533
+ # @param store [Store]
534
+ # @return [ProductAttributeOption]
535
+ def create_attribute_option(product_attribute, option_label, store)
536
+ attribute_option = ProductAttributeOption.new
537
+ attribute_option.product_attribute = product_attribute
538
+ attribute_option.label = option_label
539
+ attribute_option.store = store
540
+ attribute_option.sync_needed = false
541
+ attribute_option.save
542
+
543
+ attribute_option.sync_local_to_magento
544
+ attribute_option.destroy
545
+
546
+ return ProductAttributeOption.find_by(product_attribute: product_attribute, label: option_label, store: store)
547
+ end
548
+
549
+ # Create an associated magento Product.
550
+ #
551
+ # @return [Boolean]
552
+ def create_magento_product
553
+ response = API::SOAP::Catalog::Product.create(self, self.stores.first)
554
+
555
+ if response.success?
556
+ self.magento_id = response.body[:result]
557
+ self.sync_needed = false
558
+
559
+ stores.each_with_index do |store, index|
560
+ next if index == 0
561
+ response = API::SOAP::Catalog::Product.update(self, store)
562
+
563
+ # If there was a problem updating on of the stores, then make sure the product will be synced on next save.
564
+ # The product needs to be saved regardless, since a Magento product was created and the id must be set. So,
565
+ # this will not return false.
566
+ self.sync_needed = true unless response.success?
567
+ end
568
+
569
+ return true
570
+ else
571
+ errors.add(:base, response.body[:faultstring])
572
+ return false
573
+ end
574
+ end
575
+
576
+ # Update associated Magento Product.
577
+ #
578
+ # @return [Boolean]
579
+ def update_magento_product
580
+ self.stores.each do |store|
581
+ response = API::SOAP::Catalog::Product.update(self, store)
582
+
583
+ unless response.success?
584
+ errors.add(:base, response.body[:faultstring])
585
+ return false
586
+ end
587
+ end
588
+
589
+ self.sync_needed = false
590
+ return true
591
+ end
592
+
593
+ # Delete associations.
594
+ #
595
+ # @return [Void]
596
+ def delete_associations
597
+ self.configurable_attributes.destroy_all
598
+ end
599
+
600
+ def to_ary
601
+ nil
602
+ end
603
+
604
+ alias :to_a :to_ary
605
+
606
+ end
607
+ end