spree_core 5.1.8 → 5.2.0.rc1

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 (389) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/payment_icons/ach.svg +1 -0
  3. data/app/assets/images/payment_icons/achdirectdebit.svg +1 -0
  4. data/app/assets/images/payment_icons/ada.svg +1 -0
  5. data/app/assets/images/payment_icons/adyen.svg +1 -0
  6. data/app/assets/images/payment_icons/affinbank.svg +1 -0
  7. data/app/assets/images/payment_icons/affirm.svg +1 -0
  8. data/app/assets/images/payment_icons/afterpay.svg +1 -0
  9. data/app/assets/images/payment_icons/airtel_money.svg +1 -0
  10. data/app/assets/images/payment_icons/airteltigo_mobile_money.svg +1 -0
  11. data/app/assets/images/payment_icons/alandsbanken.svg +1 -0
  12. data/app/assets/images/payment_icons/alipay.svg +1 -0
  13. data/app/assets/images/payment_icons/alipay_hk.svg +1 -0
  14. data/app/assets/images/payment_icons/alliancebank.svg +1 -0
  15. data/app/assets/images/payment_icons/alma.svg +1 -0
  16. data/app/assets/images/payment_icons/amazon.svg +1 -0
  17. data/app/assets/images/payment_icons/ambank.svg +1 -0
  18. data/app/assets/images/payment_icons/american_express.svg +1 -0
  19. data/app/assets/images/payment_icons/apecoin.svg +1 -0
  20. data/app/assets/images/payment_icons/apple_pay.svg +1 -0
  21. data/app/assets/images/payment_icons/arbitrum.svg +1 -0
  22. data/app/assets/images/payment_icons/atome.svg +1 -0
  23. data/app/assets/images/payment_icons/authorizenet.svg +1 -0
  24. data/app/assets/images/payment_icons/avalanche.svg +1 -0
  25. data/app/assets/images/payment_icons/bacs.svg +1 -0
  26. data/app/assets/images/payment_icons/bancontact.svg +1 -0
  27. data/app/assets/images/payment_icons/bangkokbank.svg +1 -0
  28. data/app/assets/images/payment_icons/bankislam.svg +1 -0
  29. data/app/assets/images/payment_icons/bankmuamalat.svg +1 -0
  30. data/app/assets/images/payment_icons/bankofamerica.svg +1 -0
  31. data/app/assets/images/payment_icons/bankrakyat.svg +1 -0
  32. data/app/assets/images/payment_icons/base.svg +1 -0
  33. data/app/assets/images/payment_icons/becs.svg +1 -0
  34. data/app/assets/images/payment_icons/benefit.svg +1 -0
  35. data/app/assets/images/payment_icons/billie.svg +1 -0
  36. data/app/assets/images/payment_icons/bitcoin.svg +1 -0
  37. data/app/assets/images/payment_icons/bitcoin_cash.svg +1 -0
  38. data/app/assets/images/payment_icons/bizum.svg +1 -0
  39. data/app/assets/images/payment_icons/blik.svg +1 -0
  40. data/app/assets/images/payment_icons/bnbchain.svg +1 -0
  41. data/app/assets/images/payment_icons/bogus.svg +1 -0
  42. data/app/assets/images/payment_icons/bogus_app_coin.svg +1 -0
  43. data/app/assets/images/payment_icons/boleto.svg +1 -0
  44. data/app/assets/images/payment_icons/boost.svg +1 -0
  45. data/app/assets/images/payment_icons/braintree.svg +1 -0
  46. data/app/assets/images/payment_icons/buckaroopaybybank.svg +1 -0
  47. data/app/assets/images/payment_icons/busd.svg +1 -0
  48. data/app/assets/images/payment_icons/cartes_bancaires.svg +1 -0
  49. data/app/assets/images/payment_icons/cash.svg +1 -0
  50. data/app/assets/images/payment_icons/cashappafterpay.svg +1 -0
  51. data/app/assets/images/payment_icons/cashapppay.svg +1 -0
  52. data/app/assets/images/payment_icons/checkout_finance.svg +1 -0
  53. data/app/assets/images/payment_icons/chinabank.svg +1 -0
  54. data/app/assets/images/payment_icons/clearpay.svg +1 -0
  55. data/app/assets/images/payment_icons/collector_bank.svg +1 -0
  56. data/app/assets/images/payment_icons/dai.svg +1 -0
  57. data/app/assets/images/payment_icons/dana.svg +1 -0
  58. data/app/assets/images/payment_icons/dankort.svg +1 -0
  59. data/app/assets/images/payment_icons/danske_bank.svg +1 -0
  60. data/app/assets/images/payment_icons/deutschebank.svg +1 -0
  61. data/app/assets/images/payment_icons/diners_club.svg +1 -0
  62. data/app/assets/images/payment_icons/directbanktransferlatinamerica.svg +1 -0
  63. data/app/assets/images/payment_icons/discover.svg +1 -0
  64. data/app/assets/images/payment_icons/dogecoin.svg +1 -0
  65. data/app/assets/images/payment_icons/duitnow.svg +1 -0
  66. data/app/assets/images/payment_icons/eft_secure.svg +1 -0
  67. data/app/assets/images/payment_icons/eftpos_au.svg +1 -0
  68. data/app/assets/images/payment_icons/eftposau.svg +1 -0
  69. data/app/assets/images/payment_icons/elo.svg +1 -0
  70. data/app/assets/images/payment_icons/eos.svg +1 -0
  71. data/app/assets/images/payment_icons/eps.svg +1 -0
  72. data/app/assets/images/payment_icons/ethereum.svg +1 -0
  73. data/app/assets/images/payment_icons/eurobonuscheckout.svg +1 -0
  74. data/app/assets/images/payment_icons/ezcash.svg +1 -0
  75. data/app/assets/images/payment_icons/fantom.svg +1 -0
  76. data/app/assets/images/payment_icons/fpx.svg +1 -0
  77. data/app/assets/images/payment_icons/gcash.svg +1 -0
  78. data/app/assets/images/payment_icons/generic.svg +1 -0
  79. data/app/assets/images/payment_icons/genericbank.svg +1 -0
  80. data/app/assets/images/payment_icons/gift-card.svg +1 -0
  81. data/app/assets/images/payment_icons/giropay.svg +1 -0
  82. data/app/assets/images/payment_icons/glbepaypal.svg +1 -0
  83. data/app/assets/images/payment_icons/gmobanktransfer.svg +1 -0
  84. data/app/assets/images/payment_icons/gnosis.svg +1 -0
  85. data/app/assets/images/payment_icons/google_pay.svg +1 -0
  86. data/app/assets/images/payment_icons/google_wallet.svg +1 -0
  87. data/app/assets/images/payment_icons/gopay.svg +1 -0
  88. data/app/assets/images/payment_icons/grabpay.svg +1 -0
  89. data/app/assets/images/payment_icons/gusd.svg +1 -0
  90. data/app/assets/images/payment_icons/handelsbanken.svg +1 -0
  91. data/app/assets/images/payment_icons/hongleongbank.svg +1 -0
  92. data/app/assets/images/payment_icons/hypercard.svg +1 -0
  93. data/app/assets/images/payment_icons/hypercash.svg +1 -0
  94. data/app/assets/images/payment_icons/ideal.svg +1 -0
  95. data/app/assets/images/payment_icons/in3viaideal.svg +1 -0
  96. data/app/assets/images/payment_icons/inbank.svg +1 -0
  97. data/app/assets/images/payment_icons/jcb.svg +1 -0
  98. data/app/assets/images/payment_icons/kakao_pay.svg +1 -0
  99. data/app/assets/images/payment_icons/kasikornbank.svg +1 -0
  100. data/app/assets/images/payment_icons/klarna-pay-later.svg +1 -0
  101. data/app/assets/images/payment_icons/klarna-pay-now.svg +1 -0
  102. data/app/assets/images/payment_icons/klarna-slice-it.svg +1 -0
  103. data/app/assets/images/payment_icons/klarna.svg +1 -0
  104. data/app/assets/images/payment_icons/knet.svg +1 -0
  105. data/app/assets/images/payment_icons/krungthaibank.svg +1 -0
  106. data/app/assets/images/payment_icons/landbank.svg +1 -0
  107. data/app/assets/images/payment_icons/laybuybyklarna.svg +1 -0
  108. data/app/assets/images/payment_icons/litecoin.svg +1 -0
  109. data/app/assets/images/payment_icons/lunchcheck.svg +1 -0
  110. data/app/assets/images/payment_icons/mach.svg +1 -0
  111. data/app/assets/images/payment_icons/mada.svg +1 -0
  112. data/app/assets/images/payment_icons/maestro.svg +1 -0
  113. data/app/assets/images/payment_icons/master.svg +1 -0
  114. data/app/assets/images/payment_icons/mayabank.svg +1 -0
  115. data/app/assets/images/payment_icons/maybank.svg +1 -0
  116. data/app/assets/images/payment_icons/maybankm2u.svg +1 -0
  117. data/app/assets/images/payment_icons/maybankqrpay.svg +1 -0
  118. data/app/assets/images/payment_icons/mbway.svg +1 -0
  119. data/app/assets/images/payment_icons/mcash.svg +1 -0
  120. data/app/assets/images/payment_icons/medicinosbankas.svg +1 -0
  121. data/app/assets/images/payment_icons/mercadopago.svg +1 -0
  122. data/app/assets/images/payment_icons/metrobank.svg +1 -0
  123. data/app/assets/images/payment_icons/mobilepay.svg +1 -0
  124. data/app/assets/images/payment_icons/momopay.svg +1 -0
  125. data/app/assets/images/payment_icons/monero.svg +1 -0
  126. data/app/assets/images/payment_icons/mtn_mobile_money.svg +1 -0
  127. data/app/assets/images/payment_icons/mybank.svg +1 -0
  128. data/app/assets/images/payment_icons/naver_pay.svg +1 -0
  129. data/app/assets/images/payment_icons/nelo.svg +1 -0
  130. data/app/assets/images/payment_icons/netbanking.svg +1 -0
  131. data/app/assets/images/payment_icons/novalnetdirectdebitach.svg +1 -0
  132. data/app/assets/images/payment_icons/nubank.svg +1 -0
  133. data/app/assets/images/payment_icons/ocbcbank.svg +1 -0
  134. data/app/assets/images/payment_icons/offlinebanktransferlatinamerica.svg +1 -0
  135. data/app/assets/images/payment_icons/ola_money.svg +1 -0
  136. data/app/assets/images/payment_icons/oney.svg +1 -0
  137. data/app/assets/images/payment_icons/onlinebanking.svg +1 -0
  138. data/app/assets/images/payment_icons/onlinebanktransfer.svg +1 -0
  139. data/app/assets/images/payment_icons/orangemobilemoney.svg +1 -0
  140. data/app/assets/images/payment_icons/otpbank.svg +1 -0
  141. data/app/assets/images/payment_icons/oxxo.svg +1 -0
  142. data/app/assets/images/payment_icons/paybybank.svg +1 -0
  143. data/app/assets/images/payment_icons/paybybankmollie.svg +1 -0
  144. data/app/assets/images/payment_icons/paybybankus.svg +1 -0
  145. data/app/assets/images/payment_icons/paycash.svg +1 -0
  146. data/app/assets/images/payment_icons/payco.svg +1 -0
  147. data/app/assets/images/payment_icons/payeverpaybybank.svg +1 -0
  148. data/app/assets/images/payment_icons/payme.svg +1 -0
  149. data/app/assets/images/payment_icons/paynow.svg +1 -0
  150. data/app/assets/images/payment_icons/paynowmbank.svg +1 -0
  151. data/app/assets/images/payment_icons/paypal.svg +1 -0
  152. data/app/assets/images/payment_icons/paypo.svg +1 -0
  153. data/app/assets/images/payment_icons/payrexxbanktransfer.svg +1 -0
  154. data/app/assets/images/payment_icons/payrexxpaybybank.svg +1 -0
  155. data/app/assets/images/payment_icons/paysafecash.svg +1 -0
  156. data/app/assets/images/payment_icons/pix.svg +1 -0
  157. data/app/assets/images/payment_icons/polygon.svg +1 -0
  158. data/app/assets/images/payment_icons/przelewy24.svg +1 -0
  159. data/app/assets/images/payment_icons/publicbank.svg +1 -0
  160. data/app/assets/images/payment_icons/publicbank_pbe.svg +1 -0
  161. data/app/assets/images/payment_icons/qr_promptpay.svg +1 -0
  162. data/app/assets/images/payment_icons/rabobank.svg +1 -0
  163. data/app/assets/images/payment_icons/revolut.svg +1 -0
  164. data/app/assets/images/payment_icons/rhbbank.svg +1 -0
  165. data/app/assets/images/payment_icons/riverty.svg +1 -0
  166. data/app/assets/images/payment_icons/safetypaybanktransfer.svg +1 -0
  167. data/app/assets/images/payment_icons/samsung_pay.svg +1 -0
  168. data/app/assets/images/payment_icons/satispay.svg +1 -0
  169. data/app/assets/images/payment_icons/scalapay.svg +1 -0
  170. data/app/assets/images/payment_icons/scotiabank.svg +1 -0
  171. data/app/assets/images/payment_icons/seabankid.svg +1 -0
  172. data/app/assets/images/payment_icons/seabankph.svg +1 -0
  173. data/app/assets/images/payment_icons/sepa_bank_transfer.svg +1 -0
  174. data/app/assets/images/payment_icons/sepadirectdebit.svg +1 -0
  175. data/app/assets/images/payment_icons/seveneleven.svg +1 -0
  176. data/app/assets/images/payment_icons/shib.svg +1 -0
  177. data/app/assets/images/payment_icons/shopcash.svg +1 -0
  178. data/app/assets/images/payment_icons/siauliubankas.svg +1 -0
  179. data/app/assets/images/payment_icons/skeps.svg +1 -0
  180. data/app/assets/images/payment_icons/snap_checkout.svg +1 -0
  181. data/app/assets/images/payment_icons/sofort.svg +1 -0
  182. data/app/assets/images/payment_icons/softbank.svg +1 -0
  183. data/app/assets/images/payment_icons/spei.svg +1 -0
  184. data/app/assets/images/payment_icons/storecredit.svg +1 -0
  185. data/app/assets/images/payment_icons/stripe.svg +1 -0
  186. data/app/assets/images/payment_icons/sveacheckout.svg +1 -0
  187. data/app/assets/images/payment_icons/swedbank.svg +1 -0
  188. data/app/assets/images/payment_icons/swish.svg +1 -0
  189. data/app/assets/images/payment_icons/tbibank.svg +1 -0
  190. data/app/assets/images/payment_icons/thanachartbank.svg +1 -0
  191. data/app/assets/images/payment_icons/tnmmoney.svg +1 -0
  192. data/app/assets/images/payment_icons/touchngo.svg +1 -0
  193. data/app/assets/images/payment_icons/trustly.svg +1 -0
  194. data/app/assets/images/payment_icons/twint.svg +1 -0
  195. data/app/assets/images/payment_icons/uaevisa.svg +1 -0
  196. data/app/assets/images/payment_icons/unionpay.svg +1 -0
  197. data/app/assets/images/payment_icons/unzerbanktransfer.svg +1 -0
  198. data/app/assets/images/payment_icons/upi.svg +1 -0
  199. data/app/assets/images/payment_icons/usbank.svg +1 -0
  200. data/app/assets/images/payment_icons/usdc.svg +1 -0
  201. data/app/assets/images/payment_icons/usdt.svg +1 -0
  202. data/app/assets/images/payment_icons/venmo.svg +1 -0
  203. data/app/assets/images/payment_icons/vipps.svg +1 -0
  204. data/app/assets/images/payment_icons/visa.svg +1 -0
  205. data/app/assets/images/payment_icons/volksbank.svg +1 -0
  206. data/app/assets/images/payment_icons/waavepaybybank.svg +1 -0
  207. data/app/assets/images/payment_icons/walley.svg +1 -0
  208. data/app/assets/images/payment_icons/wbtc.svg +1 -0
  209. data/app/assets/images/payment_icons/wechatpay.svg +1 -0
  210. data/app/assets/images/payment_icons/whishcheckout.svg +1 -0
  211. data/app/assets/images/payment_icons/wingbank.svg +1 -0
  212. data/app/assets/images/payment_icons/xrp.svg +1 -0
  213. data/app/assets/images/payment_icons/zip.svg +1 -0
  214. data/app/finders/spree/posts/find.rb +137 -0
  215. data/app/helpers/spree/addresses_helper.rb +9 -3
  216. data/app/helpers/spree/base_helper.rb +8 -4
  217. data/app/jobs/spree/images/save_from_url_job.rb +70 -0
  218. data/app/jobs/spree/imports/create_rows_job.rb +72 -0
  219. data/app/jobs/spree/imports/process_rows_job.rb +19 -0
  220. data/app/models/concerns/spree/adjustment_source.rb +3 -3
  221. data/app/models/concerns/spree/admin_user_methods.rb +52 -0
  222. data/app/models/concerns/spree/display_on.rb +7 -1
  223. data/app/models/concerns/spree/has_image_alt_text.rb +18 -0
  224. data/app/models/concerns/spree/has_one_link.rb +1 -1
  225. data/app/models/concerns/spree/has_page_links.rb +1 -1
  226. data/app/models/concerns/spree/linkable.rb +9 -0
  227. data/app/models/concerns/spree/metadata.rb +2 -0
  228. data/app/models/concerns/spree/metafields.rb +94 -0
  229. data/app/models/concerns/spree/named_type.rb +1 -1
  230. data/app/models/concerns/spree/user_methods.rb +20 -7
  231. data/app/models/concerns/spree/user_payment_source.rb +1 -1
  232. data/app/models/concerns/spree/user_roles.rb +3 -2
  233. data/app/models/spree/ability.rb +4 -0
  234. data/app/models/spree/address.rb +3 -1
  235. data/app/models/spree/adjustable/adjuster/tax.rb +2 -2
  236. data/app/models/spree/adjustable/adjustments_updater.rb +26 -3
  237. data/app/models/spree/adjustment.rb +4 -1
  238. data/app/models/spree/asset.rb +16 -0
  239. data/app/models/spree/base.rb +1 -1
  240. data/app/models/spree/credit_card.rb +1 -0
  241. data/app/models/spree/current.rb +5 -1
  242. data/app/models/spree/custom_domain.rb +5 -0
  243. data/app/models/spree/customer_return.rb +1 -0
  244. data/app/models/spree/export.rb +39 -2
  245. data/app/models/spree/exports/customers.rb +17 -0
  246. data/app/models/spree/exports/gift_cards.rb +13 -0
  247. data/app/models/spree/exports/newsletter_subscribers.rb +13 -0
  248. data/app/models/spree/exports/orders.rb +6 -4
  249. data/app/models/spree/exports/products.rb +9 -3
  250. data/app/models/spree/gift_card.rb +7 -1
  251. data/app/models/spree/gift_card_batch.rb +0 -1
  252. data/app/models/spree/import.rb +279 -0
  253. data/app/models/spree/import_mapping.rb +46 -0
  254. data/app/models/spree/import_row.rb +108 -0
  255. data/app/models/spree/import_schema.rb +25 -0
  256. data/app/models/spree/import_schemas/products.rb +45 -0
  257. data/app/models/spree/imports/products.rb +9 -0
  258. data/app/models/spree/invitation.rb +1 -1
  259. data/app/models/spree/legacy_user.rb +0 -1
  260. data/app/models/spree/line_item.rb +2 -7
  261. data/app/models/spree/metafield.rb +53 -0
  262. data/app/models/spree/metafield_definition.rb +91 -0
  263. data/app/models/spree/metafields/boolean.rb +15 -0
  264. data/app/models/spree/metafields/json.rb +27 -0
  265. data/app/models/spree/metafields/long_text.rb +7 -0
  266. data/app/models/spree/metafields/number.rb +15 -0
  267. data/app/models/spree/metafields/rich_text.rb +18 -0
  268. data/app/models/spree/metafields/short_text.rb +7 -0
  269. data/app/models/spree/newsletter_subscriber/emails.rb +12 -0
  270. data/app/models/spree/newsletter_subscriber.rb +64 -0
  271. data/app/models/spree/option_type.rb +2 -0
  272. data/app/models/spree/option_value.rb +1 -0
  273. data/app/models/spree/order/checkout.rb +7 -0
  274. data/app/models/spree/order.rb +8 -3
  275. data/app/models/spree/order_merger.rb +2 -4
  276. data/app/models/spree/order_promotion.rb +3 -0
  277. data/app/models/spree/order_updater.rb +2 -2
  278. data/app/models/spree/page.rb +5 -1
  279. data/app/models/spree/page_block.rb +4 -0
  280. data/app/models/spree/page_blocks/image.rb +3 -0
  281. data/app/models/spree/page_blocks/metafields.rb +18 -0
  282. data/app/models/spree/page_blocks/products/brand.rb +15 -0
  283. data/app/models/spree/page_blocks/products/buy_buttons.rb +5 -1
  284. data/app/models/spree/page_blocks/products/description.rb +18 -0
  285. data/app/models/spree/page_blocks/products/price.rb +1 -1
  286. data/app/models/spree/page_blocks/products/quantity_selector.rb +6 -2
  287. data/app/models/spree/page_blocks/products/title.rb +5 -1
  288. data/app/models/spree/page_blocks/products/variant_picker.rb +1 -1
  289. data/app/models/spree/page_section.rb +2 -2
  290. data/app/models/spree/page_sections/breadcrumbs.rb +12 -0
  291. data/app/models/spree/page_sections/footer.rb +20 -13
  292. data/app/models/spree/page_sections/image_banner.rb +6 -1
  293. data/app/models/spree/page_sections/image_with_text.rb +5 -0
  294. data/app/models/spree/page_sections/newsletter.rb +7 -0
  295. data/app/models/spree/page_sections/product_details.rb +38 -1
  296. data/app/models/spree/pages/account.rb +4 -0
  297. data/app/models/spree/pages/cart.rb +10 -0
  298. data/app/models/spree/pages/checkout.rb +6 -0
  299. data/app/models/spree/pages/custom.rb +4 -0
  300. data/app/models/spree/pages/homepage.rb +4 -0
  301. data/app/models/spree/pages/login.rb +4 -0
  302. data/app/models/spree/pages/password.rb +2 -0
  303. data/app/models/spree/pages/post_list.rb +4 -0
  304. data/app/models/spree/pages/product_details.rb +1 -0
  305. data/app/models/spree/pages/shop_all.rb +4 -0
  306. data/app/models/spree/pages/wishlist.rb +4 -0
  307. data/app/models/spree/payment.rb +1 -0
  308. data/app/models/spree/payment_method.rb +4 -5
  309. data/app/models/spree/payment_source.rb +1 -0
  310. data/app/models/spree/policy.rb +70 -0
  311. data/app/models/spree/post.rb +4 -2
  312. data/app/models/spree/post_category.rb +1 -0
  313. data/app/models/spree/product.rb +16 -9
  314. data/app/models/spree/promotion/actions/free_shipping.rb +3 -3
  315. data/app/models/spree/promotion.rb +24 -8
  316. data/app/models/spree/promotion_action.rb +1 -1
  317. data/app/models/spree/promotion_handler/cart.rb +1 -1
  318. data/app/models/spree/promotion_handler/coupon.rb +1 -1
  319. data/app/models/spree/promotion_rule.rb +1 -1
  320. data/app/models/spree/property.rb +18 -0
  321. data/app/models/spree/refund.rb +2 -1
  322. data/app/models/spree/role_user.rb +1 -1
  323. data/app/models/spree/shipment.rb +1 -0
  324. data/app/models/spree/shipping_method.rb +1 -0
  325. data/app/models/spree/stock_item.rb +1 -0
  326. data/app/models/spree/stock_transfer.rb +1 -0
  327. data/app/models/spree/store.rb +45 -3
  328. data/app/models/spree/store_credit.rb +2 -1
  329. data/app/models/spree/tax_category.rb +1 -1
  330. data/app/models/spree/tax_rate.rb +18 -3
  331. data/app/models/spree/taxon.rb +12 -2
  332. data/app/models/spree/taxonomy.rb +11 -0
  333. data/app/models/spree/theme.rb +1 -1
  334. data/app/models/spree/variant.rb +17 -0
  335. data/app/presenters/spree/csv/customer_presenter.rb +59 -0
  336. data/app/presenters/spree/csv/gift_card_presenter.rb +49 -0
  337. data/app/presenters/spree/csv/metafields_helper.rb +17 -0
  338. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +39 -0
  339. data/app/presenters/spree/csv/order_line_item_presenter.rb +12 -5
  340. data/app/presenters/spree/csv/product_variant_presenter.rb +9 -5
  341. data/app/services/spree/cart/recalculate.rb +6 -4
  342. data/app/services/spree/cart/remove_line_item.rb +1 -2
  343. data/app/services/spree/imports/row_processors/base.rb +19 -0
  344. data/app/services/spree/imports/row_processors/product_variant.rb +228 -0
  345. data/app/services/spree/line_items/destroy.rb +1 -1
  346. data/app/services/spree/newsletter/subscribe.rb +43 -0
  347. data/app/services/spree/newsletter/verify.rb +30 -0
  348. data/app/sorters/spree/posts/sort.rb +40 -0
  349. data/app/views/spree/shared/_payment.html.erb +3 -3
  350. data/config/locales/en.yml +56 -2
  351. data/db/migrate/20250605131334_add_missing_fields_to_users.rb +23 -13
  352. data/db/migrate/20250728095629_create_spree_metafields.rb +27 -0
  353. data/db/migrate/20250730102644_add_missing_unique_indexes_to_spree_option_types_and_values.rb +40 -0
  354. data/db/migrate/20250730154601_add_unique_index_on_spree_properties_name.rb +24 -0
  355. data/db/migrate/20250811112056_create_spree_policies.rb +22 -0
  356. data/db/migrate/20250821211705_fix_unique_index_on_spree_order_promotions.rb +20 -0
  357. data/db/migrate/20250825175217_add_missing_page_builder_indexes.rb +7 -0
  358. data/db/migrate/20250826093602_create_spree_newsletter_subscribers.rb +12 -0
  359. data/db/migrate/20250902143122_fix_policies_store_association.rb +18 -0
  360. data/db/migrate/20250911205659_add_missing_index_on_spree_products_promotionable.rb +5 -0
  361. data/db/migrate/20250913130044_add_page_links_counter_cache_to_spree_stores.rb +10 -0
  362. data/db/migrate/20250914101955_add_missing_indexes_on_spree_adjustments.rb +6 -0
  363. data/db/migrate/20250915093930_add_metadata_to_spree_newsletter_subscribers.rb +20 -0
  364. data/db/migrate/20250923141845_create_spree_imports.rb +47 -0
  365. data/lib/generators/spree/cursor_rules/cursor_rules_generator.rb +19 -0
  366. data/lib/generators/spree/cursor_rules/templates/spree_rules.mdc +387 -0
  367. data/lib/generators/spree/dummy/dummy_generator.rb +0 -1
  368. data/lib/generators/spree/install/templates/config/initializers/spree.rb +5 -0
  369. data/lib/spree/core/configuration.rb +1 -0
  370. data/lib/spree/core/dependencies.rb +3 -0
  371. data/lib/spree/core/engine.rb +57 -2
  372. data/lib/spree/core/version.rb +1 -1
  373. data/lib/spree/core.rb +3 -1
  374. data/lib/spree/permitted_attributes.rb +18 -3
  375. data/lib/spree/testing_support/capybara_config.rb +9 -3
  376. data/lib/spree/testing_support/factories/export_factory.rb +5 -17
  377. data/lib/spree/testing_support/factories/import_factory.rb +20 -0
  378. data/lib/spree/testing_support/factories/import_mapping_factory.rb +7 -0
  379. data/lib/spree/testing_support/factories/import_row_factory.rb +7 -0
  380. data/lib/spree/testing_support/factories/metafield_definition_factory.rb +72 -0
  381. data/lib/spree/testing_support/factories/metafield_factory.rb +68 -0
  382. data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +18 -0
  383. data/lib/spree/testing_support/factories/policy_factory.rb +8 -0
  384. data/lib/spree/testing_support/factories/post_factory.rb +8 -0
  385. data/lib/spree/testing_support/store.rb +11 -0
  386. data/lib/tasks/core.rake +128 -0
  387. metadata +279 -6
  388. data/app/models/action_text/rich_text_decorator.rb +0 -11
  389. data/app/presenters/spree/csv/product_presenter.rb +0 -63
@@ -0,0 +1,279 @@
1
+ require 'csv'
2
+
3
+ module Spree
4
+ class Import < Spree.base_class
5
+ include Spree::NumberIdentifier
6
+ include Spree::NumberAsParam
7
+
8
+ include Spree::Core::NumberGenerator.new(prefix: 'IM')
9
+
10
+ #
11
+ # Associations
12
+ #
13
+ belongs_to :owner, polymorphic: true # Store, Vendor, etc.
14
+ belongs_to :user, class_name: Spree.admin_user_class.to_s, optional: true
15
+ has_many :mappings, class_name: 'Spree::ImportMapping', dependent: :destroy, inverse_of: :import
16
+ alias import_mappings mappings
17
+ has_many :rows, class_name: 'Spree::ImportRow', dependent: :destroy_async, inverse_of: :import
18
+ alias import_rows rows
19
+
20
+ #
21
+ # Validations
22
+ #
23
+ validates :owner, :user, :type, presence: true
24
+ validates :attachment, presence: true, unless: -> { Rails.env.test? }
25
+ validate :ensure_whitelisted_type
26
+ validate :ensure_attachment_content_type
27
+
28
+ #
29
+ # Ransack configuration
30
+ #
31
+ self.whitelisted_ransackable_attributes = %w[number type]
32
+
33
+ #
34
+ # Attachments
35
+ #
36
+ has_one_attached :attachment, service: Spree.private_storage_service_name
37
+
38
+ #
39
+ # State machine
40
+ #
41
+ state_machine initial: :pending, attribute: :status do
42
+ event :start_mapping do
43
+ transition to: :mapping
44
+ end
45
+ before_transition to: :mapping, do: :create_mappings
46
+
47
+ event :complete_mapping do
48
+ transition from: :mapping, to: :completed_mapping
49
+ end
50
+ after_transition to: :completed_mapping, do: :create_rows_async
51
+
52
+ event :start_processing do
53
+ transition from: :completed_mapping, to: :processing
54
+ end
55
+
56
+ event :complete do
57
+ transition from: :processing, to: :completed
58
+ end
59
+ after_transition to: :completed, do: :send_import_completed_email
60
+ after_transition to: :completed, do: :update_loader_in_import_view
61
+
62
+ event :fail do
63
+ transition to: :failed
64
+ end
65
+ end
66
+
67
+ #
68
+ # Preferences
69
+ #
70
+ preference :delimiter, :string, default: ','
71
+
72
+ # Returns true if the import is in mapping state
73
+ # @return [Boolean]
74
+ def mapping?
75
+ status == 'mapping'
76
+ end
77
+
78
+ # Returns true if the import is processing or completed mapping
79
+ # @return [Boolean]
80
+ def processing?
81
+ ['processing', 'completed_mapping'].include?(status)
82
+ end
83
+
84
+ # Returns true if the import is complete
85
+ # @return [Boolean]
86
+ def complete?
87
+ status == 'completed'
88
+ end
89
+
90
+ # Returns the model class for the import
91
+ # @return [Class]
92
+ def model_class
93
+ if type == 'Spree::Imports::Customers'
94
+ Spree.user_class
95
+ else
96
+ "Spree::#{type.demodulize.singularize}".safe_constantize
97
+ end
98
+ end
99
+
100
+ # Returns the import schema for the import
101
+ # @return [Spree::ImportSchema]
102
+ def import_schema
103
+ "Spree::ImportSchemas::#{type.demodulize}".safe_constantize.new
104
+ end
105
+
106
+ # Returns the row processor class for the import
107
+ # @return [Class]
108
+ def row_processor_class
109
+ "Spree::ImportRowProcessors::#{type.demodulize.singularize}".safe_constantize
110
+ end
111
+
112
+ # Returns the fields for the import schema
113
+ # If model supports metafields, it will include the metafield definitions for this model
114
+ # @return [Array<Hash>]
115
+ def schema_fields
116
+ base_fields = import_schema.fields
117
+
118
+ # Dynamically add metafield definitions if the model supports metafields
119
+ if model_class_supports_metafields?
120
+ metafield_fields = metafield_definitions_for_model.map do |definition|
121
+ {
122
+ name: definition.csv_header_name,
123
+ label: definition.name
124
+ }
125
+ end
126
+ base_fields + metafield_fields
127
+ else
128
+ base_fields
129
+ end
130
+ end
131
+
132
+ # Returns the file columns that are not mapped
133
+ # @return [Array<String>]
134
+ def unmapped_file_columns
135
+ csv_headers.reject { |header| mappings.mapped.exists?(file_column: header) }
136
+ end
137
+
138
+ # Returns the required fields for the import schema
139
+ # @return [Array<String>]
140
+ def required_fields
141
+ import_schema.required_fields
142
+ end
143
+
144
+ # Returns the mapped fields for the import schema
145
+ # @return [Array<String>]
146
+ def mapped_fields
147
+ @mapped_fields ||= mappings.mapped.where(schema_field: required_fields)
148
+ end
149
+
150
+ # Returns true if the mapping is done
151
+ # @return [Boolean]
152
+ def mapping_done?
153
+ mapped_fields.count == required_fields.count
154
+ end
155
+
156
+ # Returns the display name for the import
157
+ # @return [String]
158
+ def display_name
159
+ "#{Spree.t(type.demodulize.pluralize.downcase)} #{number}"
160
+ end
161
+
162
+ def send_import_completed_email
163
+ # Spree::ImportMailer.import_done(self).deliver_later
164
+ end
165
+
166
+ # Returns the headers of the csv file
167
+ # @return [Array<String>]
168
+ def csv_headers
169
+ return [] if attachment_file_content.blank?
170
+
171
+ @csv_headers ||= ::CSV.parse_line(
172
+ attachment_file_content,
173
+ col_sep: preferred_delimiter
174
+ )
175
+ end
176
+
177
+ # Returns the content of the attachment file
178
+ # @return [String]
179
+ def attachment_file_content
180
+ @attachment_file_content ||= attachment.attached? ? attachment.blob.download&.force_encoding('UTF-8') : nil
181
+ end
182
+
183
+ # Creates mappings from the schema fields
184
+ # TODO: get mappings from the previous import if it exists, so user won't have to map the same columns again
185
+ def create_mappings
186
+ schema_fields.each do |schema_field|
187
+ mapping = mappings.find_or_create_by!(schema_field: schema_field[:name])
188
+ mapping.try_to_auto_assign_file_column(csv_headers)
189
+ mapping.save!
190
+ end
191
+ end
192
+
193
+ # Creates rows asynchronously
194
+ # @return [void]
195
+ def create_rows_async
196
+ Spree::Imports::CreateRowsJob.set(wait: 2.seconds).perform_later(id)
197
+ end
198
+
199
+ # Processes rows asynchronously
200
+ # @return [void]
201
+ def process_rows_async
202
+ Spree::Imports::ProcessRowsJob.perform_later(id)
203
+ end
204
+
205
+ # Returns the store for the import
206
+ # @return [Spree::Store]
207
+ def store
208
+ owner.is_a?(Spree::Store) ? owner : owner.respond_to?(:store) ? owner.store : Spree::Store.default
209
+ end
210
+
211
+ def update_loader_in_import_view
212
+ return unless defined?(broadcast_update_to)
213
+
214
+ broadcast_update_to "import_#{id}_loader", target: 'loader', partial: 'spree/admin/imports/loader', locals: { import: self }
215
+ end
216
+
217
+ # Returns the current ability for the import
218
+ # @return [Spree::Ability]
219
+ def current_ability
220
+ @current_ability ||= Spree::Dependencies.ability_class.constantize.new(user, { store: store })
221
+ end
222
+
223
+ class << self
224
+ # Returns the available types for the import
225
+ # @return [Array<Class>]
226
+ def available_types
227
+ Rails.application.config.spree.import_types
228
+ end
229
+
230
+ # Returns the available models for the import
231
+ # @return [Array<Class>]
232
+ def available_models
233
+ available_types.map(&:model_class)
234
+ end
235
+
236
+ # Returns the type for the model
237
+ # @return [Class]
238
+ def type_for_model(model)
239
+ available_types.find { |type| type.model_class.to_s == model.to_s }
240
+ end
241
+
242
+ # eg. Spree::Imports::Orders => Spree::Order
243
+ def model_class
244
+ klass = "Spree::#{to_s.demodulize.singularize}".safe_constantize
245
+
246
+ raise NameError, "Missing model class for #{to_s}" unless klass
247
+
248
+ klass
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ def ensure_whitelisted_type
255
+ return if type.blank?
256
+
257
+ allowed = self.class.available_types.map(&:to_s)
258
+ errors.add(:type, :inclusion) unless allowed.include?(type)
259
+ end
260
+
261
+ def ensure_attachment_content_type
262
+ return if attachment.blank?
263
+
264
+ errors.add(:attachment, :content_type) unless attachment.content_type.in?(%w[text/csv])
265
+ end
266
+
267
+ def model_class_supports_metafields?
268
+ return false unless model_class.present?
269
+
270
+ model_class.included_modules.include?(Spree::Metafields)
271
+ end
272
+
273
+ def metafield_definitions_for_model
274
+ return [] unless model_class.present?
275
+
276
+ Spree::MetafieldDefinition.for_resource_type(model_class.name)
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,46 @@
1
+ module Spree
2
+ class ImportMapping < Spree.base_class
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :import, class_name: 'Spree::Import', inverse_of: :mappings, required: true
7
+
8
+ #
9
+ # Validations
10
+ #
11
+ validates :import, :schema_field, presence: true
12
+ validates :schema_field, uniqueness: { scope: [:import_id] }
13
+ validates :file_column, uniqueness: { scope: [:import_id] }, allow_blank: true
14
+
15
+ #
16
+ # Scopes
17
+ #
18
+ scope :required, -> { where(schema_field: import.required_fields) }
19
+ scope :mapped, -> { where.not(file_column: [nil, '']) }
20
+
21
+ # Returns true if the mapping is required
22
+ # @return [Boolean]
23
+ def required?
24
+ import.required_fields.include?(schema_field)
25
+ end
26
+
27
+ # Returns true if the mapping has a file column
28
+ # @return [Boolean]
29
+ def mapped?
30
+ file_column.present?
31
+ end
32
+
33
+ def try_to_auto_assign_file_column(csv_headers)
34
+ self.file_column = csv_headers.find { |header| header.parameterize.underscore.downcase.strip == schema_field.parameterize.underscore.downcase.strip }
35
+ end
36
+
37
+ # Returns the label for the schema field
38
+ # @return [String]
39
+ def schema_field_label
40
+ @schema_field_label ||= begin
41
+ field = import.schema_fields.find { |field| field[:name] == schema_field }
42
+ field[:label] if field.present?
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,108 @@
1
+ module Spree
2
+ class ImportRow < Spree.base_class
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :import, class_name: 'Spree::Import', counter_cache: :rows_count
7
+ belongs_to :item, polymorphic: true, optional: true # eg. Spree::Variant, Spree::Order, etc.
8
+ delegate :store, to: :import
9
+
10
+ #
11
+ # Validations
12
+ #
13
+ validates :import, :data, presence: true
14
+ validates :row_number, uniqueness: { scope: :import_id }, numericality: { only_integer: true, greater_than: 0 }, presence: true
15
+
16
+ #
17
+ # State machine
18
+ #
19
+ state_machine initial: :pending, attribute: :status do
20
+ event :start_processing do
21
+ transition to: :processing
22
+ end
23
+
24
+ event :fail do
25
+ transition to: :failed
26
+ end
27
+ after_transition to: :failed, do: :add_row_to_import_view
28
+ after_transition to: :failed, do: :update_footer_in_import_view
29
+
30
+ event :complete do
31
+ transition to: :completed
32
+ end
33
+
34
+ after_transition to: :completed, do: :add_row_to_import_view
35
+ after_transition to: :completed, do: :update_footer_in_import_view
36
+ end
37
+
38
+ #
39
+ # Scopes
40
+ #
41
+ scope :pending_and_failed, -> { where(status: %i[pending failed]) }
42
+ scope :completed, -> { where(status: :completed) }
43
+ scope :failed, -> { where(status: :failed) }
44
+ scope :processed, -> { where(status: %i[completed failed]) }
45
+
46
+ def data_json
47
+ @data_json ||= JSON.parse(data)
48
+ rescue JSON::ParserError
49
+ {}
50
+ end
51
+
52
+ def to_schema_hash
53
+ @to_schema_hash ||= begin
54
+ mappings = import.mappings.mapped
55
+ schema_fields = import.schema_fields
56
+
57
+ attributes = {}
58
+ schema_fields.each do |field|
59
+ attributes[field[:name]] = attribute_by_schema_field(field[:name], mappings)
60
+ end
61
+ attributes
62
+ end
63
+ end
64
+
65
+ def attribute_by_schema_field(schema_field, mappings = nil)
66
+ mappings ||= import.mappings.mapped
67
+
68
+ mapping = mappings.find { |m| m.schema_field == schema_field }
69
+ return unless mapping&.mapped?
70
+
71
+ data_json[mapping.file_column]
72
+ end
73
+
74
+ def process!
75
+ start_processing!
76
+ self.item = import.row_processor_class.new(self).process!
77
+ complete!
78
+ rescue StandardError => e
79
+ self.validation_errors = e.message
80
+ fail!
81
+ end
82
+
83
+ def add_row_to_import_view
84
+ return unless defined?(broadcast_append_to)
85
+
86
+ # we need to render this partial with store context to properly generate image URLs
87
+ with_store_context do
88
+ broadcast_append_to "import_#{import.id}_rows", target: 'rows', partial: 'spree/admin/imports/row', locals: { row: self, import: import }
89
+ end
90
+ end
91
+
92
+ def update_footer_in_import_view
93
+ return unless defined?(broadcast_replace_to)
94
+
95
+ broadcast_replace_to "import_#{import_id}_footer", target: 'footer', partial: 'spree/admin/imports/footer', locals: { import: import }
96
+ end
97
+
98
+ private
99
+
100
+ def with_store_context
101
+ Spree::Current.store = store
102
+ Rails.application.routes.default_url_options[:host] = store.url_or_custom_domain
103
+ yield
104
+ ensure
105
+ Spree::Current.reset if defined?(Spree::Current)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ class ImportSchema
3
+ FIELDS = []
4
+
5
+ def fields
6
+ self.class::FIELDS
7
+ end
8
+
9
+ def label_for_field(field_name)
10
+ fields.find { |f| f[:name] == field_name }[:label]
11
+ end
12
+
13
+ def required_fields
14
+ self.class::FIELDS.select { |f| f[:required] }.map { |f| f[:name] }
15
+ end
16
+
17
+ def optional_fields
18
+ self.class::FIELDS.reject { |f| f[:required] }.map { |f| f[:name] }
19
+ end
20
+
21
+ def headers
22
+ self.class::FIELDS.map { |f| f[:name] }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module Spree
2
+ module ImportSchemas
3
+ class Products < Spree::ImportSchema
4
+ FIELDS = [
5
+ { name: 'slug', label: 'Slug', required: true },
6
+ { name: 'sku', label: 'SKU', required: true },
7
+ { name: 'name', label: 'Name', required: true },
8
+ { name: 'price', label: 'Price', required: true },
9
+ { name: 'status', label: 'Status' },
10
+ { name: 'description', label: 'Description' },
11
+ { name: 'meta_title', label: 'Meta Title' },
12
+ { name: 'meta_description', label: 'Meta Description' },
13
+ { name: 'meta_keywords', label: 'Meta Keywords' },
14
+ { name: 'tags', label: 'Tags' },
15
+ { name: 'compare_at_price', label: 'Compare at Price' },
16
+ { name: 'currency', label: 'Currency' },
17
+ { name: 'width', label: 'Width' },
18
+ { name: 'height', label: 'Height' },
19
+ { name: 'depth', label: 'Depth' },
20
+ { name: 'dimensions_unit', label: 'Dimensions Unit' },
21
+ { name: 'weight', label: 'Weight' },
22
+ { name: 'weight_unit', label: 'Weight Unit' },
23
+ { name: 'available_on', label: 'Available On' },
24
+ { name: 'discontinue_on', label: 'Discontinue On' },
25
+ { name: 'track_inventory', label: 'Track Inventory' },
26
+ { name: 'inventory_count', label: 'Inventory Count' },
27
+ { name: 'inventory_backorderable', label: 'Inventory Backorderable' },
28
+ { name: 'tax_category', label: 'Tax Category' },
29
+ { name: 'digital', label: 'Digital' },
30
+ { name: 'image1_src', label: 'Image 1 URL' },
31
+ { name: 'image2_src', label: 'Image 2 URL' },
32
+ { name: 'image3_src', label: 'Image 3 URL' },
33
+ { name: 'option1_name', label: 'Option 1 Name' },
34
+ { name: 'option1_value', label: 'Option 1 Value' },
35
+ { name: 'option2_name', label: 'Option 2 Name' },
36
+ { name: 'option2_value', label: 'Option 2 Value' },
37
+ { name: 'option3_name', label: 'Option 3 Name' },
38
+ { name: 'option3_value', label: 'Option 3 Value' },
39
+ { name: 'category1', label: 'Category 1' },
40
+ { name: 'category2', label: 'Category 2' },
41
+ { name: 'category3', label: 'Category 3' }
42
+ ].freeze
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ module Imports
3
+ class Products < Spree::Import
4
+ def row_processor_class
5
+ Spree::Imports::RowProcessors::ProductVariant
6
+ end
7
+ end
8
+ end
9
+ end
@@ -15,7 +15,7 @@ module Spree
15
15
  belongs_to :inviter, polymorphic: true # User or AdminUser
16
16
  belongs_to :invitee, polymorphic: true, optional: true # User or AdminUser
17
17
  belongs_to :role, class_name: 'Spree::Role'
18
- has_one :role_user, dependent: :destroy, class_name: 'Spree::RoleUser'
18
+ has_one :role_user, dependent: :nullify, class_name: 'Spree::RoleUser', inverse_of: :invitation
19
19
 
20
20
  #
21
21
  # Validations
@@ -4,7 +4,6 @@ module Spree
4
4
  include Spree::UserAddress
5
5
  include Spree::UserPaymentSource
6
6
  include Spree::UserMethods
7
- include Spree::Metadata
8
7
 
9
8
  self.table_name = 'spree_users'
10
9
 
@@ -1,5 +1,6 @@
1
1
  module Spree
2
2
  class LineItem < Spree.base_class
3
+ include Spree::Metafields
3
4
  include Spree::Metadata
4
5
  if defined?(Spree::Webhooks::HasWebhooks)
5
6
  include Spree::Webhooks::HasWebhooks
@@ -18,7 +19,7 @@ module Spree
18
19
  has_one :product, -> { with_deleted }, class_name: 'Spree::Product', through: :variant
19
20
 
20
21
  has_many :adjustments, as: :adjustable, dependent: :destroy
21
- has_many :inventory_units, inverse_of: :line_item
22
+ has_many :inventory_units, class_name: 'Spree::InventoryUnit', inverse_of: :line_item, dependent: :destroy
22
23
  has_many :shipments, through: :inventory_units, source: :shipment
23
24
  has_many :digital_links, dependent: :destroy
24
25
 
@@ -41,8 +42,6 @@ module Spree
41
42
 
42
43
  before_destroy :verify_order_inventory_before_destroy, if: -> { order.has_checkout_step?('delivery') }
43
44
 
44
- before_destroy :destroy_inventory_units
45
-
46
45
  after_save :update_inventory
47
46
  after_save :update_adjustments
48
47
 
@@ -226,10 +225,6 @@ module Spree
226
225
  Spree::OrderInventory.new(order, self).verify(target_shipment)
227
226
  end
228
227
 
229
- def destroy_inventory_units
230
- throw(:abort) unless inventory_units.destroy_all
231
- end
232
-
233
228
  def update_adjustments
234
229
  if saved_change_to_quantity?
235
230
  recalculate_adjustments
@@ -0,0 +1,53 @@
1
+ module Spree
2
+ class Metafield < Spree.base_class
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :resource, polymorphic: true, touch: true
7
+ belongs_to :metafield_definition, class_name: 'Spree::MetafieldDefinition'
8
+
9
+ #
10
+ # Delegations
11
+ #
12
+ delegate :key, :full_key, :name, :display_on, to: :metafield_definition, allow_nil: true
13
+
14
+ #
15
+ # Callbacks
16
+ #
17
+ before_validation :set_type_from_metafield_definition, on: :create
18
+
19
+ #
20
+ # Validations
21
+ #
22
+ validates :metafield_definition, :type, :resource, :value, presence: true
23
+ validates :metafield_definition_id, uniqueness: { scope: [:resource_type, :resource_id] }
24
+ validate :type_must_match_metafield_definition
25
+
26
+ #
27
+ # Scopes
28
+ #
29
+ scope :available_on_front_end, -> { joins(:metafield_definition).merge(Spree::MetafieldDefinition.available_on_front_end) }
30
+ scope :available_on_back_end, -> { joins(:metafield_definition).merge(Spree::MetafieldDefinition.available_on_back_end) }
31
+ scope :with_key, ->(namespace, key) { joins(:metafield_definition).where(spree_metafield_definitions: { namespace: namespace, key: key }) }
32
+
33
+ def serialize_value
34
+ value
35
+ end
36
+
37
+ def csv_value
38
+ value.to_s
39
+ end
40
+
41
+ private
42
+
43
+ def set_type_from_metafield_definition
44
+ self.type ||= metafield_definition.metafield_type
45
+ end
46
+
47
+ def type_must_match_metafield_definition
48
+ return if metafield_definition.blank?
49
+
50
+ errors.add(:type, 'must match metafield definition') unless type == metafield_definition.metafield_type
51
+ end
52
+ end
53
+ end