spina-shop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (263) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/config/spina_shop_manifest.js +0 -0
  6. data/app/assets/fonts/montserrat-bold.ttf +0 -0
  7. data/app/assets/fonts/montserrat-italic.ttf +0 -0
  8. data/app/assets/fonts/montserrat-medium.ttf +0 -0
  9. data/app/assets/fonts/montserrat-regular.ttf +0 -0
  10. data/app/assets/fonts/montserrat-semibold.ttf +0 -0
  11. data/app/assets/images/spina/shop/delete-big-confirm.png +0 -0
  12. data/app/assets/images/spina/shop/delete-big.png +0 -0
  13. data/app/assets/javascripts/spina/shop/admin/app.infinitescroll.js.coffee +23 -0
  14. data/app/assets/javascripts/spina/shop/admin/customers.coffee +27 -0
  15. data/app/assets/javascripts/spina/shop/admin/orders.coffee +18 -0
  16. data/app/assets/javascripts/spina/shop/admin/product_bundles.coffee +15 -0
  17. data/app/assets/javascripts/spina/shop/admin/product_categories.coffee +15 -0
  18. data/app/assets/javascripts/spina/shop/admin/products.coffee +88 -0
  19. data/app/assets/javascripts/spina/shop/admin/shop.js +10 -0
  20. data/app/assets/stylesheets/spina/shop/admin/_customers.sass +10 -0
  21. data/app/assets/stylesheets/spina/shop/admin/_forms.sass +191 -0
  22. data/app/assets/stylesheets/spina/shop/admin/_product_bundles.sass +12 -0
  23. data/app/assets/stylesheets/spina/shop/admin/_status_dots.sass +16 -0
  24. data/app/assets/stylesheets/spina/shop/admin/_tables.sass +2 -0
  25. data/app/assets/stylesheets/spina/shop/admin/_tooltips.sass +31 -0
  26. data/app/assets/stylesheets/spina/shop/admin/shop.css.sass +11 -0
  27. data/app/controllers/spina/shop/admin/admin_controller.rb +8 -0
  28. data/app/controllers/spina/shop/admin/customer_accounts_controller.rb +38 -0
  29. data/app/controllers/spina/shop/admin/customers_controller.rb +67 -0
  30. data/app/controllers/spina/shop/admin/discounts_controller.rb +65 -0
  31. data/app/controllers/spina/shop/admin/gift_cards_controller.rb +65 -0
  32. data/app/controllers/spina/shop/admin/invoices_controller.rb +12 -0
  33. data/app/controllers/spina/shop/admin/order_items_controller.rb +36 -0
  34. data/app/controllers/spina/shop/admin/orders/packing_slips_controller.rb +27 -0
  35. data/app/controllers/spina/shop/admin/orders/shipping_labels_controller.rb +19 -0
  36. data/app/controllers/spina/shop/admin/orders_controller.rb +123 -0
  37. data/app/controllers/spina/shop/admin/product_reviews_controller.rb +17 -0
  38. data/app/controllers/spina/shop/admin/products/product_bundles_controller.rb +62 -0
  39. data/app/controllers/spina/shop/admin/products/stock_forecasts_controller.rb +15 -0
  40. data/app/controllers/spina/shop/admin/products/stock_level_adjustments_controller.rb +33 -0
  41. data/app/controllers/spina/shop/admin/products_controller.rb +135 -0
  42. data/app/controllers/spina/shop/admin/reports_controller.rb +33 -0
  43. data/app/controllers/spina/shop/admin/settings/product_categories_controller.rb +45 -0
  44. data/app/controllers/spina/shop/admin/settings/product_category_properties_controller.rb +49 -0
  45. data/app/controllers/spina/shop/admin/settings/sales_categories_controller.rb +14 -0
  46. data/app/controllers/spina/shop/admin/settings/shop_controller.rb +9 -0
  47. data/app/controllers/spina/shop/admin/settings/tax_groups_controller.rb +47 -0
  48. data/app/controllers/spina/shop/admin/variants_controller.rb +8 -0
  49. data/app/jobs/spina/shop/in_stock_reminder_job.rb +17 -0
  50. data/app/jobs/spina/shop/invoice_report_job.rb +23 -0
  51. data/app/jobs/spina/shop/payments_report_job.rb +23 -0
  52. data/app/mailers/spina/shop/customer_account_mailer.rb +32 -0
  53. data/app/mailers/spina/shop/export_mailer.rb +22 -0
  54. data/app/mailers/spina/shop/in_stock_reminder_mailer.rb +22 -0
  55. data/app/mailers/spina/shop/order_mailer.rb +32 -0
  56. data/app/models/concerns/spina/shop/preferable.rb +27 -0
  57. data/app/models/concerns/spina/shop/tax_rateable.rb +11 -0
  58. data/app/models/spina/shop/address.rb +19 -0
  59. data/app/models/spina/shop/application_record.rb +5 -0
  60. data/app/models/spina/shop/bundled_product.rb +6 -0
  61. data/app/models/spina/shop/campaign.rb +4 -0
  62. data/app/models/spina/shop/collectable.rb +8 -0
  63. data/app/models/spina/shop/country.rb +5 -0
  64. data/app/models/spina/shop/customer.rb +60 -0
  65. data/app/models/spina/shop/customer_account.rb +15 -0
  66. data/app/models/spina/shop/customer_group.rb +9 -0
  67. data/app/models/spina/shop/delivery_option.rb +27 -0
  68. data/app/models/spina/shop/discount.rb +39 -0
  69. data/app/models/spina/shop/discount_action.rb +16 -0
  70. data/app/models/spina/shop/discount_rule.rb +17 -0
  71. data/app/models/spina/shop/discounts/actions/percent_off.rb +17 -0
  72. data/app/models/spina/shop/discounts/rules/all_orders.rb +13 -0
  73. data/app/models/spina/shop/discounts/rules/collection.rb +14 -0
  74. data/app/models/spina/shop/discounts/rules/order_total.rb +17 -0
  75. data/app/models/spina/shop/discounts_order.rb +6 -0
  76. data/app/models/spina/shop/favorite.rb +8 -0
  77. data/app/models/spina/shop/gift_card.rb +53 -0
  78. data/app/models/spina/shop/gift_cards_order.rb +6 -0
  79. data/app/models/spina/shop/in_stock_reminder.rb +7 -0
  80. data/app/models/spina/shop/invoice.rb +49 -0
  81. data/app/models/spina/shop/invoice_line.rb +13 -0
  82. data/app/models/spina/shop/number_sequence.rb +19 -0
  83. data/app/models/spina/shop/order.rb +222 -0
  84. data/app/models/spina/shop/order/billing.rb +85 -0
  85. data/app/models/spina/shop/order/state_machine_transitions.rb +106 -0
  86. data/app/models/spina/shop/order_attachment.rb +9 -0
  87. data/app/models/spina/shop/order_item.rb +101 -0
  88. data/app/models/spina/shop/order_transition.rb +5 -0
  89. data/app/models/spina/shop/product.rb +227 -0
  90. data/app/models/spina/shop/product_bundle.rb +51 -0
  91. data/app/models/spina/shop/product_bundle_part.rb +13 -0
  92. data/app/models/spina/shop/product_category.rb +18 -0
  93. data/app/models/spina/shop/product_category_property.rb +14 -0
  94. data/app/models/spina/shop/product_collection.rb +8 -0
  95. data/app/models/spina/shop/product_image.rb +18 -0
  96. data/app/models/spina/shop/product_item.rb +93 -0
  97. data/app/models/spina/shop/product_part.rb +13 -0
  98. data/app/models/spina/shop/product_relation.rb +6 -0
  99. data/app/models/spina/shop/product_review.rb +22 -0
  100. data/app/models/spina/shop/property_option.rb +16 -0
  101. data/app/models/spina/shop/sales_category.rb +43 -0
  102. data/app/models/spina/shop/sales_category_code.rb +20 -0
  103. data/app/models/spina/shop/shop_review.rb +19 -0
  104. data/app/models/spina/shop/stock_level_adjustment.rb +17 -0
  105. data/app/models/spina/shop/tax_group.rb +62 -0
  106. data/app/models/spina/shop/tax_rate.rb +20 -0
  107. data/app/models/spina/shop/zone.rb +11 -0
  108. data/app/pdfs/spina/shop/invoice_pdf.rb +114 -0
  109. data/app/pdfs/spina/shop/packing_slip_pdf.rb +74 -0
  110. data/app/presenters/spina/shop/invoice_presenter.rb +30 -0
  111. data/app/services/spina/shop/allocate_stock.rb +42 -0
  112. data/app/services/spina/shop/cache_order.rb +47 -0
  113. data/app/services/spina/shop/change_stock_level.rb +38 -0
  114. data/app/services/spina/shop/country_importer.rb +14 -0
  115. data/app/services/spina/shop/customer_generator.rb +32 -0
  116. data/app/services/spina/shop/deallocate_stock.rb +17 -0
  117. data/app/services/spina/shop/invoice_generator.rb +72 -0
  118. data/app/services/spina/shop/invoice_number_generator.rb +10 -0
  119. data/app/services/spina/shop/invoices_pdfs_exporter.rb +22 -0
  120. data/app/services/spina/shop/order_number_generator.rb +10 -0
  121. data/app/services/spina/shop/payments_excel_exporter.rb +41 -0
  122. data/app/services/spina/shop/store_address.rb +23 -0
  123. data/app/state_machines/spina/shop/order_state_machine.rb +116 -0
  124. data/app/uploaders/spina/shop/exports_uploader.rb +9 -0
  125. data/app/views/layouts/spina/shop/admin/settings/shop.html.haml +16 -0
  126. data/app/views/layouts/spina/shop/mail.html.erb +1 -0
  127. data/app/views/spina/admin/hooks/shop/_head.html.haml +2 -0
  128. data/app/views/spina/admin/hooks/shop/_primary_navigation.html.haml +31 -0
  129. data/app/views/spina/admin/hooks/shop/_settings_secondary_navigation.html.haml +2 -0
  130. data/app/views/spina/admin/page_partables/shop/product_bundle_parts/_form.html.haml +6 -0
  131. data/app/views/spina/admin/page_partables/shop/product_parts/_form.html.haml +6 -0
  132. data/app/views/spina/admin/structure_partables/shop/product_parts/_form.html.haml +3 -0
  133. data/app/views/spina/shop/admin/customer_accounts/_form.html.haml +32 -0
  134. data/app/views/spina/shop/admin/customer_accounts/edit.html.haml +1 -0
  135. data/app/views/spina/shop/admin/customers/_address_fields.html.haml +46 -0
  136. data/app/views/spina/shop/admin/customers/_customer.html.haml +11 -0
  137. data/app/views/spina/shop/admin/customers/_form.html.haml +67 -0
  138. data/app/views/spina/shop/admin/customers/edit.html.haml +1 -0
  139. data/app/views/spina/shop/admin/customers/index.html.haml +52 -0
  140. data/app/views/spina/shop/admin/customers/index.js.erb +4 -0
  141. data/app/views/spina/shop/admin/customers/new.html.haml +1 -0
  142. data/app/views/spina/shop/admin/customers/show.html.haml +121 -0
  143. data/app/views/spina/shop/admin/discounts/_form.html.haml +60 -0
  144. data/app/views/spina/shop/admin/discounts/edit.html.haml +1 -0
  145. data/app/views/spina/shop/admin/discounts/fields/_all_orders.html.haml +0 -0
  146. data/app/views/spina/shop/admin/discounts/fields/_discount_action_fields.html.haml +8 -0
  147. data/app/views/spina/shop/admin/discounts/fields/_discount_rule_fields.html.haml +0 -0
  148. data/app/views/spina/shop/admin/discounts/index.html.haml +36 -0
  149. data/app/views/spina/shop/admin/discounts/new.html.haml +1 -0
  150. data/app/views/spina/shop/admin/gift_cards/_form.html.haml +28 -0
  151. data/app/views/spina/shop/admin/gift_cards/_gift_card.html.haml +17 -0
  152. data/app/views/spina/shop/admin/gift_cards/index.html.haml +37 -0
  153. data/app/views/spina/shop/admin/gift_cards/index.js.erb +4 -0
  154. data/app/views/spina/shop/admin/gift_cards/new.html.haml +1 -0
  155. data/app/views/spina/shop/admin/gift_cards/show.html.haml +46 -0
  156. data/app/views/spina/shop/admin/order_items/_modal.html.haml +18 -0
  157. data/app/views/spina/shop/admin/order_items/new.js.erb +2 -0
  158. data/app/views/spina/shop/admin/orders/_form.html.haml +159 -0
  159. data/app/views/spina/shop/admin/orders/_order.html.haml +22 -0
  160. data/app/views/spina/shop/admin/orders/edit.html.haml +1 -0
  161. data/app/views/spina/shop/admin/orders/index.html.haml +78 -0
  162. data/app/views/spina/shop/admin/orders/index.js.erb +4 -0
  163. data/app/views/spina/shop/admin/orders/new.html.haml +1 -0
  164. data/app/views/spina/shop/admin/orders/show.html.haml +338 -0
  165. data/app/views/spina/shop/admin/product_reviews/_product_review.html.haml +22 -0
  166. data/app/views/spina/shop/admin/product_reviews/index.html.haml +29 -0
  167. data/app/views/spina/shop/admin/product_reviews/index.js.erb +4 -0
  168. data/app/views/spina/shop/admin/products/_form.html.haml +259 -0
  169. data/app/views/spina/shop/admin/products/_new_product_modal.html.haml +15 -0
  170. data/app/views/spina/shop/admin/products/_price_exception_fields.html.haml +11 -0
  171. data/app/views/spina/shop/admin/products/_product.html.haml +36 -0
  172. data/app/views/spina/shop/admin/products/edit.html.haml +1 -0
  173. data/app/views/spina/shop/admin/products/index.html.haml +73 -0
  174. data/app/views/spina/shop/admin/products/index.js.erb +4 -0
  175. data/app/views/spina/shop/admin/products/new.html.haml +1 -0
  176. data/app/views/spina/shop/admin/products/new_by_category.js.erb +1 -0
  177. data/app/views/spina/shop/admin/products/product_bundles/_bundled_products_fields.html.haml +11 -0
  178. data/app/views/spina/shop/admin/products/product_bundles/_form.html.haml +64 -0
  179. data/app/views/spina/shop/admin/products/product_bundles/edit.html.haml +1 -0
  180. data/app/views/spina/shop/admin/products/product_bundles/index.html.haml +42 -0
  181. data/app/views/spina/shop/admin/products/product_bundles/new.html.haml +1 -0
  182. data/app/views/spina/shop/admin/products/properties/_property_fields.html.haml +37 -0
  183. data/app/views/spina/shop/admin/products/stock_forecasts/_product.html.haml +49 -0
  184. data/app/views/spina/shop/admin/products/stock_forecasts/show.html.haml +47 -0
  185. data/app/views/spina/shop/admin/products/stock_forecasts/show.js.erb +4 -0
  186. data/app/views/spina/shop/admin/products/stock_level_adjustments/_form.html.haml +37 -0
  187. data/app/views/spina/shop/admin/products/stock_level_adjustments/_history.html.haml +30 -0
  188. data/app/views/spina/shop/admin/products/stock_level_adjustments/index.js.erb +1 -0
  189. data/app/views/spina/shop/admin/products/stock_level_adjustments/new.js.erb +1 -0
  190. data/app/views/spina/shop/admin/reports/index.html.haml +23 -0
  191. data/app/views/spina/shop/admin/settings/product_categories/_form.html.haml +18 -0
  192. data/app/views/spina/shop/admin/settings/product_categories/_product_category.html.haml +3 -0
  193. data/app/views/spina/shop/admin/settings/product_categories/_property_fields.html.haml +77 -0
  194. data/app/views/spina/shop/admin/settings/product_categories/edit.html.haml +1 -0
  195. data/app/views/spina/shop/admin/settings/product_categories/index.html.haml +3 -0
  196. data/app/views/spina/shop/admin/settings/product_categories/show.html.haml +24 -0
  197. data/app/views/spina/shop/admin/settings/product_category_properties/_property_options_fields.html.haml +11 -0
  198. data/app/views/spina/shop/admin/settings/product_category_properties/edit_options.html.haml +16 -0
  199. data/app/views/spina/shop/admin/settings/sales_categories/_sales_category.html.haml +17 -0
  200. data/app/views/spina/shop/admin/settings/sales_categories/index.html.haml +1 -0
  201. data/app/views/spina/shop/admin/settings/tax_groups/_form.html.haml +19 -0
  202. data/app/views/spina/shop/admin/settings/tax_groups/_tax_group.html.haml +21 -0
  203. data/app/views/spina/shop/admin/settings/tax_groups/index.html.haml +1 -0
  204. data/app/views/spina/shop/admin/settings/tax_groups/new.js.erb +1 -0
  205. data/app/views/spina/shop/admin/shared/_locale_switcher.html.haml +9 -0
  206. data/app/views/spina/shop/customer_account_mailer/forgot_password.html.erb +1 -0
  207. data/app/views/spina/shop/customer_account_mailer/forgot_password.txt.erb +1 -0
  208. data/app/views/spina/shop/export_mailer/exported.html.erb +2 -0
  209. data/app/views/spina/shop/in_stock_reminder_mailer/reminder.html.erb +1 -0
  210. data/app/views/spina/shop/in_stock_reminder_mailer/reminder.txt.erb +1 -0
  211. data/app/views/spina/shop/order_mailer/confirmation.html.erb +5 -0
  212. data/app/views/spina/shop/order_mailer/confirmation.txt.erb +5 -0
  213. data/app/views/spina/shop/order_mailer/shipped.html.erb +1 -0
  214. data/app/views/spina/shop/order_mailer/shipped.txt.erb +1 -0
  215. data/config/initializers/kaminari_monkey_patch.rb +15 -0
  216. data/config/locales/en.yml +214 -0
  217. data/config/locales/nl.yml +452 -0
  218. data/config/routes.rb +92 -0
  219. data/db/countries.txt +252 -0
  220. data/db/migrate/10_create_spina_shop_products_without_items.rb +23 -0
  221. data/db/migrate/1_create_spina_shop_tables.rb +518 -0
  222. data/db/migrate/20170701135207_create_spina_shop_bundled_products.rb +11 -0
  223. data/db/migrate/20170717125930_add_stock_enabled_to_spina_shop_products.rb +5 -0
  224. data/db/migrate/20170718084146_add_editable_sku_to_spina_shop_products.rb +5 -0
  225. data/db/migrate/20170718084626_add_deletable_to_spina_shop_products.rb +5 -0
  226. data/db/migrate/20170719082531_rename_column_order_picked_at_in_spina_shop_orders.rb +5 -0
  227. data/db/migrate/20170719112920_create_spina_shop_order_attachments.rb +15 -0
  228. data/db/migrate/20170719184125_add_price_exceptions_to_spina_shop_products.rb +5 -0
  229. data/db/migrate/20170720133304_add_vat_id_to_spina_shop_customers.rb +5 -0
  230. data/db/migrate/20170731082027_add_price_includes_tax_to_spina_shop_products.rb +5 -0
  231. data/db/migrate/20170731132620_add_reverse_charge_vat_to_spina_shop_tax_rates.rb +5 -0
  232. data/db/migrate/20170731150630_add_vat_id_to_spina_shop_invoices.rb +5 -0
  233. data/db/migrate/20170801093935_add_price_includes_tax_to_spina_shop_delivery_options.rb +5 -0
  234. data/db/migrate/20170814092924_add_business_to_spina_shop_orders.rb +6 -0
  235. data/db/migrate/20170815080839_rename_vat_reverse_charge_column_on_tax_rates.rb +5 -0
  236. data/db/migrate/20170815081018_add_business_to_spina_shop_tax_rates.rb +5 -0
  237. data/db/migrate/20170817075706_add_archived_to_spina_shop_products.rb +5 -0
  238. data/db/migrate/20170824175231_add_metadata_to_photos.rb +7 -0
  239. data/db/migrate/20170829085402_add_business_to_spina_shop_sales_category_codes.rb +5 -0
  240. data/db/migrate/20170911121034_add_trend_to_spina_shop_products.rb +5 -0
  241. data/db/migrate/20171106122225_add_editable_fields_to_spina_shop_product_category_properties.rb +6 -0
  242. data/db/migrate/20171106122537_create_property_options.rb +19 -0
  243. data/db/migrate/20171118110256_add_promotional_price_to_spina_shop_products.rb +6 -0
  244. data/db/migrate/2_create_spina_shop_tax_rates.rb +15 -0
  245. data/db/migrate/3_create_spina_shop_zones.rb +9 -0
  246. data/db/migrate/4_add_tax_rateable_to_spina_shop_tax_rates.rb +9 -0
  247. data/db/migrate/5_add_type_to_spina_shop_zones.rb +5 -0
  248. data/db/migrate/6_add_code_to_spina_shop_zones.rb +5 -0
  249. data/db/migrate/7_create_spina_shop_product_collections.rb +16 -0
  250. data/db/migrate/8_create_spina_shop_sales_category_codes.rb +13 -0
  251. data/db/migrate/9_create_spina_shop_customer_groups.rb +11 -0
  252. data/lib/generators/spina/shop/install_generator.rb +44 -0
  253. data/lib/spina/shop.rb +24 -0
  254. data/lib/spina/shop/admin_helpers.rb +49 -0
  255. data/lib/spina/shop/engine.rb +27 -0
  256. data/lib/spina/shop/errors/empty_order.rb +6 -0
  257. data/lib/spina/shop/errors/order_already_received.rb +6 -0
  258. data/lib/spina/shop/railtie.rb +31 -0
  259. data/lib/spina/shop/simple_linear_regression.rb +36 -0
  260. data/lib/spina/shop/version.rb +5 -0
  261. data/lib/spina/shop/view_helpers.rb +11 -0
  262. data/lib/tasks/spina/shop.rake +32 -0
  263. metadata +627 -0
@@ -0,0 +1,16 @@
1
+ module Spina::Shop
2
+ class DiscountAction < ApplicationRecord
3
+ include Preferable
4
+
5
+ belongs_to :discount
6
+
7
+ def compute(order_item)
8
+ true
9
+ end
10
+ end
11
+ end
12
+
13
+ # Necessary for STI and the descendants method
14
+ Dir[Spina::Shop::Engine.root.join *%w(app models spina discounts actions *) ].each do |file|
15
+ require_dependency file
16
+ end
@@ -0,0 +1,17 @@
1
+ module Spina::Shop
2
+ class DiscountRule < ApplicationRecord
3
+ include Preferable
4
+
5
+ belongs_to :discount
6
+
7
+ def eligible?(order_item)
8
+ true
9
+ end
10
+
11
+ end
12
+ end
13
+
14
+ # Necessary for STI and the descendants method
15
+ Dir[Spina::Shop::Engine.root.join *%w(app models spina discounts rules *) ].each do |file|
16
+ require_dependency file
17
+ end
@@ -0,0 +1,17 @@
1
+ module Spina::Shop
2
+ module Discounts
3
+ module Actions
4
+ class PercentOff < DiscountAction
5
+
6
+ preferences :percent_off
7
+
8
+ validates :percent_off, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100}
9
+
10
+ def compute(order_item)
11
+ (order_item.total_without_discount * percent_off.to_i / 100).round(2)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Spina::Shop
2
+ module Discounts
3
+ module Rules
4
+ class AllOrders < DiscountRule
5
+
6
+ def eligible?(order_item)
7
+ true
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Spina::Shop
2
+ module Discounts
3
+ module Rules
4
+ class Collection < DiscountRule
5
+ preferences :collection_id
6
+
7
+ def eligible?(order_item)
8
+ true
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module Spina::Shop
2
+ module Discounts
3
+ module Rules
4
+ class OrderTotal < DiscountRule
5
+ preferences :total
6
+
7
+ validates :total, presence: true, numericality: true
8
+
9
+ def eligible?(order_item)
10
+ # order_item.order.order_total >= total
11
+ true
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ module Spina::Shop
2
+ class DiscountsOrder < ApplicationRecord
3
+ belongs_to :discount
4
+ belongs_to :order
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Spina::Shop
2
+ class Favorite < ApplicationRecord
3
+ belongs_to :customer
4
+ belongs_to :product
5
+
6
+ validates :customer_id, uniqueness: {scope: :product_id}
7
+ end
8
+ end
@@ -0,0 +1,53 @@
1
+ module Spina::Shop
2
+ class GiftCard < ApplicationRecord
3
+ before_create :generate_unique_code
4
+ before_create :set_remaining_balance
5
+
6
+ scope :available, -> { where('remaining_balance > ?', BigDecimal(0)).where('expires_at > ?', Date.today) }
7
+
8
+ has_many :gift_cards_orders
9
+ has_many :orders, through: :gift_cards_orders, dependent: :restrict_with_exception
10
+
11
+ validates :expires_at, :value, :remaining_balance, presence: true
12
+ validates :value, numericality: {greater_than: 0}
13
+
14
+ def amount_for_order(order)
15
+ [remaining_balance, order.total].min
16
+ end
17
+
18
+ def readable_code
19
+ code.insert(4, ' - ')
20
+ end
21
+
22
+ def expired?
23
+ expires_at < Date.today
24
+ end
25
+
26
+ def status
27
+ if remaining_balance == BigDecimal(0)
28
+ 'used'
29
+ elsif expired?
30
+ 'expired'
31
+ elsif remaining_balance == value
32
+ 'unused'
33
+ else
34
+ 'partially_used'
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Generate a pretty and readable code with distinct characters
41
+ def generate_unique_code(size: 8)
42
+ begin
43
+ charset = %w{2 3 4 6 7 9 A C D E F G H J K M N P Q R T V W X Y Z}
44
+ self['code'] = (0...size).map{charset.to_a[SecureRandom.random_number(charset.size)]}.join
45
+ end while GiftCard.exists?(code: self['code'])
46
+ end
47
+
48
+ def set_remaining_balance
49
+ self.remaining_balance = value
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,6 @@
1
+ module Spina::Shop
2
+ class GiftCardsOrder < ApplicationRecord
3
+ belongs_to :gift_card
4
+ belongs_to :order
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Spina::Shop
2
+ class InStockReminder < ApplicationRecord
3
+ belongs_to :orderable, polymorphic: true
4
+
5
+ validates :email, email: true, presence: true, uniqueness: {scope: [:orderable_id, :orderable_type]}
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ module Spina::Shop
2
+ class Invoice < ApplicationRecord
3
+ belongs_to :order
4
+ belongs_to :customer
5
+ belongs_to :country
6
+
7
+ has_many :invoice_lines, dependent: :destroy
8
+
9
+ def filename
10
+ "inv_#{invoice_number}.pdf"
11
+ end
12
+
13
+ def sub_total
14
+ if prices_include_tax
15
+ invoice_lines.inject(BigDecimal(0)) { |t, i| t + (i.total / i.tax_modifier).round(2) }
16
+ else
17
+ invoice_lines.inject(BigDecimal(0)) { |t, i| t + i.total }
18
+ end
19
+ end
20
+
21
+ def tax_amount
22
+ tax_amount_by_rates.inject(BigDecimal(0)) { |t, r| t + r[1][:tax_amount] }
23
+ end
24
+
25
+ def total
26
+ sub_total + tax_amount
27
+ end
28
+
29
+ def tax_amount_by_rates
30
+ if prices_include_tax
31
+ rates = invoice_lines.inject({}) do |h, line|
32
+ rate = h[line.tax_rate] ||= { tax_amount: BigDecimal(0), total: BigDecimal(0) }
33
+ rate[:total] += (line.total / line.tax_modifier).round(2)
34
+ rate[:tax_amount] += line.total - (line.total / line.tax_modifier).round(2)
35
+ h
36
+ end
37
+ else
38
+ rates = invoice_lines.inject({}) do |h, line|
39
+ rate = h[line.tax_rate] ||= { tax_amount: BigDecimal(0), total: BigDecimal(0) }
40
+ rate[:total] += line.total
41
+ rate[:tax_amount] += (line.total * (BigDecimal(line.tax_rate) / BigDecimal(100))).round(2)
42
+ h
43
+ end
44
+ end
45
+
46
+ rates.sort{|x, y| y[0] <=> x[0]}.to_h
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ module Spina::Shop
2
+ class InvoiceLine < ApplicationRecord
3
+ belongs_to :invoice
4
+
5
+ def total
6
+ quantity * unit_price
7
+ end
8
+
9
+ def tax_modifier
10
+ (tax_rate + BigDecimal(100)) / BigDecimal(100)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Spina::Shop
2
+ class NumberSequence < ApplicationRecord
3
+ validates :name, :next_number, presence: true
4
+ validates :next_number, uniqueness: {scope: :name}
5
+
6
+ def self.by_name(name)
7
+ NumberSequence.where(name: name).first_or_create!
8
+ end
9
+
10
+ def increment!
11
+ with_lock do
12
+ number = next_number
13
+ self.next_number = number + 1
14
+ save!
15
+ number
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,222 @@
1
+ module Spina::Shop
2
+ class Error < StandardError; end
3
+ end
4
+
5
+ module Spina::Shop
6
+ class Order < ApplicationRecord
7
+ require_dependency 'spina/shop/order/state_machine_transitions'
8
+ require_dependency 'spina/shop/order/billing'
9
+
10
+ has_secure_token
11
+
12
+ attr_accessor :validate_details, :validate_stock, :validate_delivery, :validate_payment, :password
13
+
14
+ belongs_to :customer, optional: true
15
+ belongs_to :billing_country, class_name: "Spina::Shop::Country"
16
+ belongs_to :delivery_country, class_name: "Spina::Shop::Country", optional: true
17
+ belongs_to :delivery_option, optional: true
18
+ belongs_to :duplicate, class_name: "Spina::Shop::Order", optional: true
19
+
20
+ has_many :order_transitions, autosave: false, dependent: :destroy
21
+ has_many :order_items, dependent: :destroy # Destroy order items if the order is destroyed as well
22
+ has_many :invoices, dependent: :restrict_with_exception
23
+ has_many :order_attachments, dependent: :destroy
24
+ has_one :shop_review, dependent: :destroy
25
+
26
+ has_one :discounts_order, class_name: "Spina::Shop::DiscountsOrder"
27
+ has_one :discount, through: :discounts_order, class_name: "Spina::Shop::Discount"
28
+ has_one :gift_cards_order, class_name: "Spina::Shop::GiftCardsOrder"
29
+ has_one :gift_card, through: :gift_cards_order, class_name: "Spina::Shop::GiftCard"
30
+
31
+ scope :sorted, -> { order(order_number: :desc, id: :desc) }
32
+ scope :received, -> { where.not(received_at: nil) }
33
+ scope :prepared, -> { where.not(order_prepared_at: nil) }
34
+ scope :confirmed, -> { where.not(confirming_at: nil) }
35
+ scope :paid, -> { where.not(paid_at: nil) }
36
+ scope :building, -> { in_state(:building) }
37
+
38
+ # Always validate
39
+ validates :password, confirmation: true
40
+ validates :password, length: {minimum: 6, maximum: 40}, allow_blank: true
41
+
42
+ # Validate when after checkout phase
43
+ validates :order_number, presence: true, uniqueness: true, unless: -> { in_state?(:building) }
44
+
45
+ # Validate details
46
+ validates :first_name, :last_name, :email, :billing_street1, :billing_city, :billing_postal_code, :billing_house_number, :billing_country_id, presence: true, if: -> { validate_details }
47
+ validates :delivery_name, :delivery_street1, :delivery_city, :delivery_postal_code, :delivery_house_number, presence: true, if: -> { validate_details && separate_delivery_address? }
48
+ validates :email, email: true, if: -> { validate_details }
49
+ validate :must_be_of_age_to_buy_products, if: -> { validate_details }
50
+
51
+ # Validate delivery
52
+ validates :delivery_option, presence: true, if: -> { validate_delivery }
53
+
54
+ # Validate payment
55
+ validates :payment_method, presence: true, if: -> { validate_payment }
56
+
57
+ # Validate Stock
58
+ validate :items_must_be_in_stock, if: -> { validate_stock }
59
+ validate :must_have_at_least_one_item, if: -> { validate_stock }
60
+
61
+ accepts_nested_attributes_for :order_items
62
+
63
+ # Override addresses if necessary
64
+ [:delivery_name, :delivery_street1, :delivery_postal_code, :delivery_city, :delivery_house_number, :delivery_house_number_addition, :delivery_country].each do |f|
65
+ define_method(f) do
66
+ separate_delivery_address? ? super() : send(f.to_s.gsub('delivery_', 'billing_'))
67
+ end
68
+ end
69
+
70
+ def billing_address
71
+ "#{billing_street1} #{billing_house_number}#{billing_house_number_addition}".strip
72
+ end
73
+
74
+ def delivery_address
75
+ "#{delivery_street1} #{delivery_house_number}#{delivery_house_number_addition}".strip
76
+ end
77
+
78
+ def assign_address(address, address_type:)
79
+ [:street1, :postal_code, :city, :house_number, :house_number_addition, :country].each do |f|
80
+ send("#{address_type}_#{f}=", address.send(f))
81
+ end
82
+ end
83
+
84
+ def delivery_options
85
+ DeliveryOption.all
86
+ end
87
+
88
+ def soonest_delivery_date
89
+ delivery_option.try(:soonest_delivery_date_for_order, self)
90
+ end
91
+
92
+ def empty?
93
+ order_items.none?
94
+ end
95
+
96
+ def requires_shipping?
97
+ delivery_option.try(:requires_shipping)
98
+ end
99
+
100
+ def products
101
+ order_items.map do |order_item|
102
+ if order_item.is_product_bundle?
103
+ order_item.orderable.products
104
+ else
105
+ order_item.orderable
106
+ end
107
+ end.flatten.uniq
108
+ end
109
+
110
+ # By default this method returns false, but you can override it and add your own logic
111
+ def vat_reverse_charge?
112
+ false
113
+ end
114
+
115
+ # If VAT is reverse charged, all prices must be excluding VAT
116
+ def prices_include_tax
117
+ vat_reverse_charge? ? false : read_attribute(:prices_include_tax)
118
+ end
119
+
120
+ # Order number with zeroes
121
+ def number
122
+ order_number ? order_number.to_s.rjust(8, '0') : nil
123
+ end
124
+
125
+ def billing_name
126
+ full_name = "#{first_name} #{last_name}".strip
127
+ company.present? ? "#{company} (#{full_name})" : full_name
128
+ end
129
+
130
+ def total_items
131
+ order_items.sum(:quantity)
132
+ end
133
+
134
+ def total_weight
135
+ order_items.inject(BigDecimal(0)) { |t, i| t + i.total_weight }
136
+ end
137
+
138
+ def total_weight_in_ounces
139
+ Measurement.parse("#{total_weight} gr").convert_to(:oz).quantity
140
+ end
141
+
142
+ def merge!(order)
143
+ raise Error if confirmed? || order.confirmed?
144
+
145
+ transaction do
146
+ order.order_items.where(orderable: order_items.map(&:orderable)).each do |duplicate|
147
+ current_order_item = order_items.find_by(orderable: duplicate.orderable)
148
+ current_order_item.increment(:quantity, duplicate.quantity)
149
+ current_order_item.save
150
+ end
151
+
152
+ order.order_items.where(orderable: order_items.map(&:orderable)).destroy_all
153
+ order.order_items.update_all(order_id: id)
154
+ end
155
+ end
156
+
157
+ def age
158
+ now = Time.zone.now
159
+ if date_of_birth
160
+ now.year - date_of_birth.year - ((now.month > date_of_birth.month || (now.month == date_of_birth.month && now.day >= date_of_birth.day)) ? 0 : 1)
161
+ end
162
+ end
163
+
164
+ def of_age?
165
+ age >= 18 if age
166
+ end
167
+
168
+ def apply_gift_card!
169
+ transaction do
170
+ update_attributes!(gift_card_amount: gift_card_amount)
171
+ gift_card.update_attributes!(remaining_balance: gift_card.remaining_balance - gift_card_amount)
172
+ end
173
+ end
174
+
175
+ def remove_gift_card!
176
+ transaction do
177
+ gift_card.update_attributes!(remaining_balance: gift_card.remaining_balance + gift_card_amount)
178
+ update_attributes!(gift_card_amount: nil)
179
+ end
180
+ end
181
+
182
+ def duplicate!
183
+ # Duplicate order
184
+ transaction do
185
+ shopping_cart = Order.create!(attributes.reject{|key, value| key.in? %w(id delivery_price delivery_tax_rate status received_at shipped_at paid_at delivered_at order_prepared_at payment_id payment_url failed_at cancelled_at delivery_tracking_ids picked_up_at order_number confirming_at created_at updated_at)})
186
+ shopping_cart.discount = discount
187
+ shopping_cart.gift_card = gift_card
188
+ order_items.each do |order_item|
189
+ OrderItem.create!(order_item.attributes.reject{|key, value| key.in? %w(id created_at updated_at unit_price unit_cost_price discount_amount weight tax_rate order_id)}.merge(order_id: shopping_cart.id))
190
+ end
191
+ update_attributes!(duplicate: shopping_cart)
192
+ end
193
+ end
194
+
195
+ def everything_valid?
196
+ self.validate_details = true
197
+ self.validate_stock = true
198
+ self.validate_payment = true
199
+ self.validate_delivery = true
200
+ valid?
201
+ end
202
+
203
+ private
204
+
205
+ def items_must_be_in_stock
206
+ errors.add(:stock_level, "not sufficient") unless order_items.all?(&:in_stock?)
207
+ end
208
+
209
+ def must_have_at_least_one_item
210
+ if order_items.none?
211
+ errors.add(:shopping_cart, "empty")
212
+ end
213
+ end
214
+
215
+ def must_be_of_age_to_buy_products
216
+ if order_items.any?{|item| item.orderable.must_be_of_age_to_buy?}
217
+ errors.add(:date_of_birth, :not_of_age) unless of_age?
218
+ end
219
+ end
220
+
221
+ end
222
+ end