spree_core 6.0.0.beta4 → 6.0.0.rc2

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 (575) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/app/helpers/spree/images_helper.rb +0 -57
  4. data/app/jobs/spree/base_job.rb +21 -0
  5. data/app/jobs/spree/carts/reap_expired_job.rb +2 -2
  6. data/app/jobs/spree/collections/regenerate_time_based_job.rb +6 -3
  7. data/app/jobs/spree/orders/finalize_stale_drafts_job.rb +8 -5
  8. data/app/jobs/spree/payments/cancel_unused_sessions_job.rb +61 -0
  9. data/app/jobs/spree/search_provider/index_job.rb +4 -0
  10. data/app/jobs/spree/seller_payouts/sweep_seller_job.rb +2 -0
  11. data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
  12. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  13. data/app/mailers/spree/base_mailer.rb +97 -13
  14. data/app/mailers/spree/email_template_mailer.rb +27 -0
  15. data/app/mailers/spree/export_mailer.rb +3 -4
  16. data/app/mailers/spree/import_mailer.rb +3 -4
  17. data/app/mailers/spree/invitation_mailer.rb +24 -7
  18. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  19. data/app/mailers/spree/webhook_mailer.rb +2 -2
  20. data/app/models/concerns/spree/acted_by.rb +1 -1
  21. data/app/models/concerns/spree/admin_user_methods.rb +15 -1
  22. data/app/models/concerns/spree/api_typed.rb +91 -0
  23. data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
  24. data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
  25. data/app/models/concerns/spree/calculator/tiers.rb +63 -0
  26. data/app/models/concerns/spree/canonical_decimals.rb +61 -0
  27. data/app/models/concerns/spree/customer_methods.rb +23 -18
  28. data/app/models/concerns/spree/derived_number.rb +11 -13
  29. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  30. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  31. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  32. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  33. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  34. data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
  35. data/app/models/concerns/spree/preference_schema.rb +62 -32
  36. data/app/models/concerns/spree/product_scopes.rb +16 -2
  37. data/app/models/concerns/spree/provider_listing.rb +41 -0
  38. data/app/models/concerns/spree/publishable.rb +82 -14
  39. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  40. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  41. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  42. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  43. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  44. data/app/models/concerns/spree/purchase/payment_processing.rb +8 -4
  45. data/app/models/concerns/spree/purchase/purchase_order.rb +9 -4
  46. data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
  47. data/app/models/concerns/spree/purchase/totals.rb +49 -8
  48. data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
  49. data/app/models/concerns/spree/receivable.rb +1 -1
  50. data/app/models/concerns/spree/searchable.rb +0 -4
  51. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  52. data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
  53. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  54. data/app/models/concerns/spree/stores/email_branding.rb +32 -0
  55. data/app/models/concerns/spree/stores/markets.rb +5 -2
  56. data/app/models/concerns/spree/type_labels.rb +38 -0
  57. data/app/models/concerns/spree/typed_adjustment_line.rb +5 -6
  58. data/app/models/concerns/spree/typed_associations.rb +12 -18
  59. data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
  60. data/app/models/spree/address.rb +12 -2
  61. data/app/models/spree/adjusters/largest_remainder.rb +14 -1
  62. data/app/models/spree/adjusters/promotion.rb +4 -19
  63. data/app/models/spree/allowed_origin.rb +3 -1
  64. data/app/models/spree/api_key.rb +5 -6
  65. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  66. data/app/models/spree/base.rb +99 -22
  67. data/app/models/spree/calculator/flat_percent_item_total.rb +2 -4
  68. data/app/models/spree/calculator/flat_rate.rb +3 -5
  69. data/app/models/spree/calculator/flexi_rate.rb +4 -6
  70. data/app/models/spree/calculator/percent_on_line_item.rb +2 -2
  71. data/app/models/spree/calculator/price_sack.rb +5 -7
  72. data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -5
  73. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +7 -7
  74. data/app/models/spree/calculator/shipping/flat_rate.rb +5 -7
  75. data/app/models/spree/calculator/shipping/flexi_rate.rb +4 -6
  76. data/app/models/spree/calculator/shipping/per_item.rb +3 -5
  77. data/app/models/spree/calculator/shipping/price_sack.rb +5 -7
  78. data/app/models/spree/calculator/tiered_flat_rate.rb +7 -24
  79. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  80. data/app/models/spree/cart.rb +78 -18
  81. data/app/models/spree/catalog.rb +1 -0
  82. data/app/models/spree/catalog_price.rb +0 -7
  83. data/app/models/spree/catalog_price_tier.rb +0 -8
  84. data/app/models/spree/category.rb +3 -1
  85. data/app/models/spree/channel/gating.rb +3 -2
  86. data/app/models/spree/channel.rb +6 -4
  87. data/app/models/spree/claim.rb +22 -1
  88. data/app/models/spree/claim_line_item.rb +41 -6
  89. data/app/models/spree/collection.rb +2 -1
  90. data/app/models/spree/collection_rule.rb +5 -21
  91. data/app/models/spree/commission_line.rb +1 -1
  92. data/app/models/spree/commission_rate.rb +7 -4
  93. data/app/models/spree/commission_rate_value.rb +1 -1
  94. data/app/models/spree/commission_rule.rb +4 -11
  95. data/app/models/spree/commission_rules/category_rule.rb +2 -5
  96. data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
  97. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  98. data/app/models/spree/commission_rules/seller_rule.rb +2 -5
  99. data/app/models/spree/company.rb +1 -0
  100. data/app/models/spree/company_invitation.rb +1 -0
  101. data/app/models/spree/consent_record.rb +0 -1
  102. data/app/models/spree/credit_card.rb +1 -1
  103. data/app/models/spree/current.rb +23 -0
  104. data/app/models/spree/custom_field.rb +2 -2
  105. data/app/models/spree/custom_field_definition.rb +20 -35
  106. data/app/models/spree/custom_fields/boolean.rb +1 -1
  107. data/app/models/spree/custom_fields/json.rb +1 -1
  108. data/app/models/spree/customer_group.rb +1 -0
  109. data/app/models/spree/data_feed/google.rb +0 -2
  110. data/app/models/spree/data_request.rb +2 -1
  111. data/app/models/spree/delivery_method.rb +36 -14
  112. data/app/models/spree/delivery_method_rule.rb +4 -11
  113. data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
  114. data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
  115. data/app/models/spree/delivery_profile.rb +1 -0
  116. data/app/models/spree/delivery_rate.rb +7 -8
  117. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  118. data/app/models/spree/delivery_zone.rb +1 -2
  119. data/app/models/spree/delivery_zone_member.rb +4 -4
  120. data/app/models/spree/digital_asset.rb +7 -0
  121. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  122. data/app/models/spree/digital_link.rb +2 -5
  123. data/app/models/spree/discount.rb +1 -1
  124. data/app/models/spree/email_template.rb +25 -0
  125. data/app/models/spree/email_template_draft.rb +13 -0
  126. data/app/models/spree/email_template_revision.rb +17 -0
  127. data/app/models/spree/email_templates/entry.rb +118 -0
  128. data/app/models/spree/email_templates/sample_record.rb +16 -0
  129. data/app/models/spree/emails/branding.rb +80 -0
  130. data/app/models/spree/exchange.rb +16 -1
  131. data/app/models/spree/exchange_line_item.rb +121 -5
  132. data/app/models/spree/export.rb +0 -1
  133. data/app/models/spree/exports/products.rb +10 -1
  134. data/app/models/spree/exports/report.rb +3 -3
  135. data/app/models/spree/fee.rb +4 -2
  136. data/app/models/spree/file_upload.rb +155 -0
  137. data/app/models/spree/fulfillment.rb +66 -30
  138. data/app/models/spree/fulfillment_changer.rb +2 -2
  139. data/app/models/spree/fulfillment_group.rb +6 -1
  140. data/app/models/spree/fulfillment_item.rb +0 -4
  141. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  142. data/app/models/spree/gateway/bogus.rb +6 -0
  143. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  144. data/app/models/spree/gateway.rb +6 -2
  145. data/app/models/spree/gift_card.rb +18 -8
  146. data/app/models/spree/gift_card_batch.rb +3 -5
  147. data/app/models/spree/import.rb +5 -7
  148. data/app/models/spree/import_mapping.rb +1 -2
  149. data/app/models/spree/import_row.rb +2 -2
  150. data/app/models/spree/import_schemas/products.rb +1 -0
  151. data/app/models/spree/integration.rb +9 -19
  152. data/app/models/spree/invitation.rb +5 -5
  153. data/app/models/spree/line_item.rb +15 -4
  154. data/app/models/spree/locale.rb +11 -8
  155. data/app/models/spree/market.rb +8 -2
  156. data/app/models/spree/media.rb +1 -9
  157. data/app/models/spree/newsletter_subscriber.rb +4 -0
  158. data/app/models/spree/option_type.rb +2 -1
  159. data/app/models/spree/option_value.rb +2 -1
  160. data/app/models/spree/order.rb +60 -142
  161. data/app/models/spree/order_group.rb +18 -4
  162. data/app/models/spree/order_inventory.rb +10 -5
  163. data/app/models/spree/order_promotion.rb +1 -1
  164. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  165. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  166. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  167. data/app/models/spree/order_routing_rule.rb +6 -22
  168. data/app/models/spree/package_type.rb +2 -2
  169. data/app/models/spree/payment/gateway_options.rb +15 -53
  170. data/app/models/spree/payment/processing.rb +2 -4
  171. data/app/models/spree/payment.rb +21 -13
  172. data/app/models/spree/payment_method/check.rb +2 -0
  173. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  174. data/app/models/spree/payment_method/store_credit.rb +15 -13
  175. data/app/models/spree/payment_method.rb +78 -6
  176. data/app/models/spree/payment_session.rb +5 -1
  177. data/app/models/spree/payment_setup_session.rb +1 -0
  178. data/app/models/spree/payout_provider/base.rb +3 -1
  179. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  180. data/app/models/spree/policy.rb +1 -0
  181. data/app/models/spree/price.rb +17 -14
  182. data/app/models/spree/price_adjustment_tier.rb +4 -0
  183. data/app/models/spree/price_list.rb +14 -9
  184. data/app/models/spree/price_rules/channel_rule.rb +3 -6
  185. data/app/models/spree/price_rules/customer_group_rule.rb +3 -6
  186. data/app/models/spree/price_rules/market_rule.rb +2 -5
  187. data/app/models/spree/price_rules/user_rule.rb +1 -3
  188. data/app/models/spree/product/channels.rb +9 -2
  189. data/app/models/spree/product.rb +54 -30
  190. data/app/models/spree/product_submission.rb +0 -12
  191. data/app/models/spree/product_type.rb +1 -1
  192. data/app/models/spree/promotion/actions/create_line_items.rb +28 -18
  193. data/app/models/spree/promotion/rules/category.rb +1 -5
  194. data/app/models/spree/promotion/rules/channel.rb +2 -9
  195. data/app/models/spree/promotion/rules/country.rb +3 -8
  196. data/app/models/spree/promotion/rules/currency.rb +1 -5
  197. data/app/models/spree/promotion/rules/customer_group.rb +2 -9
  198. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  199. data/app/models/spree/promotion/rules/item_total.rb +6 -10
  200. data/app/models/spree/promotion/rules/market.rb +2 -9
  201. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  202. data/app/models/spree/promotion/rules/option_value.rb +2 -6
  203. data/app/models/spree/promotion/rules/product.rb +1 -5
  204. data/app/models/spree/promotion/rules/user.rb +0 -4
  205. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  206. data/app/models/spree/promotion.rb +36 -6
  207. data/app/models/spree/promotion_action.rb +7 -14
  208. data/app/models/spree/promotion_action_line_item.rb +1 -1
  209. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  210. data/app/models/spree/promotion_rule.rb +7 -14
  211. data/app/models/spree/purchase_order.rb +3 -0
  212. data/app/models/spree/quantity_rule.rb +1 -1
  213. data/app/models/spree/refund.rb +41 -18
  214. data/app/models/spree/return.rb +31 -2
  215. data/app/models/spree/return_line_item.rb +65 -14
  216. data/app/models/spree/role.rb +7 -7
  217. data/app/models/spree/saved_report.rb +2 -1
  218. data/app/models/spree/search_provider/base.rb +4 -0
  219. data/app/models/spree/seller.rb +10 -2
  220. data/app/models/spree/seller_payout.rb +8 -0
  221. data/app/models/spree/seller_requirement.rb +5 -12
  222. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  223. data/app/models/spree/seller_requirement_submission.rb +3 -2
  224. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  225. data/app/models/spree/seller_transfer.rb +6 -0
  226. data/app/models/spree/shipping_label.rb +2 -0
  227. data/app/models/spree/stock/availability_validator.rb +2 -2
  228. data/app/models/spree/stock/estimator.rb +1 -1
  229. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  230. data/app/models/spree/stock/packer.rb +2 -2
  231. data/app/models/spree/stock/splitter/weight.rb +1 -1
  232. data/app/models/spree/stock_level/custom_events.rb +6 -0
  233. data/app/models/spree/stock_level.rb +3 -1
  234. data/app/models/spree/stock_location.rb +1 -0
  235. data/app/models/spree/stock_movement.rb +1 -1
  236. data/app/models/spree/stock_receipt_item.rb +1 -1
  237. data/app/models/spree/stock_reservation.rb +2 -2
  238. data/app/models/spree/stock_transfer.rb +6 -2
  239. data/app/models/spree/store.rb +70 -19
  240. data/app/models/spree/store_credit.rb +13 -13
  241. data/app/models/spree/store_credit_event.rb +5 -5
  242. data/app/models/spree/supplier.rb +1 -0
  243. data/app/models/spree/tax_category.rb +1 -0
  244. data/app/models/spree/tax_exemption_certificate.rb +3 -0
  245. data/app/models/spree/tax_identifier.rb +3 -1
  246. data/app/models/spree/tax_line.rb +53 -3
  247. data/app/models/spree/tax_provider/base.rb +71 -14
  248. data/app/models/spree/tax_provider/internal.rb +70 -41
  249. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  250. data/app/models/spree/tax_rate.rb +30 -8
  251. data/app/models/spree/taxon.rb +1 -1
  252. data/app/models/spree/taxonomy.rb +0 -1
  253. data/app/models/spree/update_status.rb +25 -0
  254. data/app/models/spree/variant.rb +10 -12
  255. data/app/models/spree/variant_media.rb +1 -1
  256. data/app/models/spree/webhook_delivery.rb +1 -0
  257. data/app/models/spree/webhook_endpoint.rb +46 -13
  258. data/app/models/spree/wishlist.rb +1 -6
  259. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  260. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  261. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  262. data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
  263. data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
  264. data/app/presenters/spree/csv/product_variant_presenter.rb +59 -20
  265. data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
  266. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  267. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  268. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  269. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  270. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  271. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  272. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  273. data/app/services/spree/carts/create.rb +8 -2
  274. data/app/services/spree/carts/destroy.rb +2 -2
  275. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  276. data/app/services/spree/carts/split_by_seller.rb +1 -1
  277. data/app/services/spree/carts/update.rb +22 -8
  278. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  279. data/app/services/spree/categories/add_products.rb +13 -31
  280. data/app/services/spree/categories/remove_products.rb +12 -51
  281. data/app/services/spree/checkout/advance.rb +1 -4
  282. data/app/services/spree/collections/add_products.rb +10 -23
  283. data/app/services/spree/collections/remove_products.rb +7 -43
  284. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  285. data/app/services/spree/data_requests/create.rb +1 -1
  286. data/app/services/spree/deliveries/destroy.rb +2 -2
  287. data/app/services/spree/email_templates/check.rb +58 -0
  288. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  289. data/app/services/spree/email_templates/preview.rb +99 -0
  290. data/app/services/spree/emails/file_system.rb +23 -0
  291. data/app/services/spree/emails/filters.rb +84 -0
  292. data/app/services/spree/emails/liquid_context.rb +48 -0
  293. data/app/services/spree/emails/rendered_email.rb +14 -0
  294. data/app/services/spree/emails/renderer.rb +125 -0
  295. data/app/services/spree/emails/template.rb +51 -0
  296. data/app/services/spree/emails/template_data.rb +23 -0
  297. data/app/services/spree/emails/template_resolver.rb +109 -0
  298. data/app/services/spree/emails/text_converter.rb +72 -0
  299. data/app/services/spree/emails/translation_inliner.rb +134 -0
  300. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  301. data/app/services/spree/imports/row_processors/product_variant.rb +77 -34
  302. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  303. data/app/services/spree/locales.rb +43 -10
  304. data/app/services/spree/newsletter/subscribe.rb +8 -12
  305. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  306. data/app/services/spree/orders/create.rb +5 -4
  307. data/app/services/spree/orders/discounts/create.rb +14 -11
  308. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  309. data/app/services/spree/orders/discounts/update.rb +1 -0
  310. data/app/services/spree/orders/fees/create.rb +1 -0
  311. data/app/services/spree/orders/fees/destroy.rb +1 -0
  312. data/app/services/spree/orders/fees/update.rb +1 -0
  313. data/app/services/spree/orders/remove_line_item.rb +13 -3
  314. data/app/services/spree/orders/update.rb +10 -6
  315. data/app/services/spree/orders/update_item.rb +6 -3
  316. data/app/services/spree/orders/update_statuses.rb +3 -1
  317. data/app/services/spree/prices/bulk_upsert.rb +32 -25
  318. data/app/services/spree/product_memberships.rb +61 -0
  319. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  320. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  321. data/app/services/spree/seeds/commission_rates.rb +1 -1
  322. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  323. data/app/services/spree/seeds/payment_methods.rb +2 -2
  324. data/app/services/spree/seeds/saved_reports.rb +3 -3
  325. data/app/services/spree/seeds/stores.rb +1 -1
  326. data/app/services/spree/sellers/requirements.rb +1 -2
  327. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  328. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  329. data/app/services/spree/stock_levels/correct.rb +3 -3
  330. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  331. data/app/services/spree/store_credits/apply.rb +15 -13
  332. data/app/services/spree/stores/provision_defaults.rb +23 -25
  333. data/app/services/spree/update_check.rb +160 -0
  334. data/app/services/spree/uploads/assign_stores.rb +92 -0
  335. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  336. data/app/validators/spree/attachment_content_type_validator.rb +19 -2
  337. data/app/validators/spree/locale_codes_validator.rb +22 -0
  338. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  339. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  340. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  341. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  342. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  343. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  344. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  345. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  346. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  347. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  348. data/app/views/spree/shared/_summary_row.liquid +8 -0
  349. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  350. data/app/workflows/spree/carts/add_item.rb +4 -4
  351. data/app/workflows/spree/carts/complete.rb +13 -3
  352. data/app/workflows/spree/carts/merge.rb +1 -1
  353. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  354. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  355. data/app/workflows/spree/claims/approve.rb +5 -22
  356. data/app/workflows/spree/claims/cancel.rb +5 -21
  357. data/app/workflows/spree/claims/create.rb +15 -0
  358. data/app/workflows/spree/claims/deny.rb +7 -0
  359. data/app/workflows/spree/claims/resolve.rb +34 -31
  360. data/app/workflows/spree/customers/anonymize.rb +3 -4
  361. data/app/workflows/spree/customers/create.rb +1 -1
  362. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  363. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  364. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  365. data/app/workflows/spree/email_templates/publish.rb +70 -0
  366. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  367. data/app/workflows/spree/email_templates/revert.rb +29 -0
  368. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  369. data/app/workflows/spree/exchanges/approve.rb +5 -22
  370. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  371. data/app/workflows/spree/exchanges/create.rb +6 -0
  372. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  373. data/app/workflows/spree/exchanges/receive.rb +6 -65
  374. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  375. data/app/workflows/spree/fulfillments/create.rb +53 -49
  376. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  377. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  378. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  379. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  380. data/app/workflows/spree/orders/add_item.rb +5 -1
  381. data/app/workflows/spree/orders/cancel.rb +3 -3
  382. data/app/workflows/spree/orders/complete.rb +16 -8
  383. data/app/workflows/spree/payments/capture.rb +20 -5
  384. data/app/workflows/spree/payments/process.rb +5 -3
  385. data/app/workflows/spree/post_sale/approval.rb +35 -0
  386. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  387. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  388. data/app/workflows/spree/price_lists/update.rb +4 -4
  389. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  390. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  391. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  392. data/app/workflows/spree/purchase_orders/create.rb +3 -1
  393. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  394. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  395. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  396. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  397. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  398. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  399. data/app/workflows/spree/refunds/create.rb +5 -2
  400. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  401. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  402. data/app/workflows/spree/returns/approve.rb +5 -22
  403. data/app/workflows/spree/returns/cancel.rb +5 -22
  404. data/app/workflows/spree/returns/create.rb +8 -0
  405. data/app/workflows/spree/returns/receive.rb +5 -76
  406. data/app/workflows/spree/returns/refund.rb +31 -8
  407. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  408. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  409. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  410. data/app/workflows/spree/shipping_labels/record.rb +7 -7
  411. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  412. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  413. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  414. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  415. data/app/workflows/spree/stock_transfers/create.rb +3 -1
  416. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  417. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  418. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  419. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  420. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  421. data/config/currency_exponents.json +180 -0
  422. data/config/i18n-tasks.yml +32 -18
  423. data/config/initializers/alba.rb +7 -0
  424. data/config/language_names.csv +264 -0
  425. data/config/locales/ar.yml +811 -0
  426. data/config/locales/be.yml +855 -0
  427. data/config/locales/bg.yml +723 -0
  428. data/config/locales/ca.yml +689 -0
  429. data/config/locales/content_locales.rb +9 -0
  430. data/config/locales/cs.yml +855 -0
  431. data/config/locales/da.yml +771 -0
  432. data/config/locales/de-CH.yml +248 -0
  433. data/config/locales/de.yml +835 -0
  434. data/config/locales/el.yml +769 -0
  435. data/config/locales/en.yml +47 -505
  436. data/config/locales/es-CL.yml +301 -0
  437. data/config/locales/es-EC.yml +186 -0
  438. data/config/locales/es-MX.yml +334 -0
  439. data/config/locales/es.yml +831 -0
  440. data/config/locales/et.yml +644 -0
  441. data/config/locales/fa.yml +709 -0
  442. data/config/locales/fi.yml +771 -0
  443. data/config/locales/fr.yml +830 -0
  444. data/config/locales/hu.yml +751 -0
  445. data/config/locales/id.yml +756 -0
  446. data/config/locales/it.yml +833 -0
  447. data/config/locales/ja.yml +726 -0
  448. data/config/locales/km.yml +759 -0
  449. data/config/locales/ko.yml +575 -0
  450. data/config/locales/ku.yml +753 -0
  451. data/config/locales/lt.yml +821 -0
  452. data/config/locales/lv.yml +685 -0
  453. data/config/locales/nb.yml +824 -0
  454. data/config/locales/ne.yml +823 -0
  455. data/config/locales/nl.yml +826 -0
  456. data/config/locales/pl.yml +835 -0
  457. data/config/locales/pt-BR.yml +547 -0
  458. data/config/locales/pt.yml +636 -0
  459. data/config/locales/ro.yml +750 -0
  460. data/config/locales/ru.yml +834 -0
  461. data/config/locales/sk.yml +680 -0
  462. data/config/locales/sl-SI.yml +600 -0
  463. data/config/locales/sv.yml +827 -0
  464. data/config/locales/th.yml +745 -0
  465. data/config/locales/tr.yml +777 -0
  466. data/config/locales/uk.yml +836 -0
  467. data/config/locales/vi.yml +768 -0
  468. data/config/locales/zh-CN.yml +786 -0
  469. data/config/locales/zh-TW.yml +834 -0
  470. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  471. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  472. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  473. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  474. data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
  475. data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
  476. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  477. data/lib/generators/spree/dummy/templates/rails/test.rb +26 -0
  478. data/lib/spree/checkout/default_requirements.rb +37 -13
  479. data/lib/spree/core/configuration.rb +19 -1
  480. data/lib/spree/core/controller_helpers/store.rb +1 -1
  481. data/lib/spree/core/dependencies.rb +5 -4
  482. data/lib/spree/core/emails/escaped_output.rb +60 -0
  483. data/lib/spree/core/engine.rb +72 -165
  484. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  485. data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
  486. data/lib/spree/core/preferences/json_conversion.rb +186 -0
  487. data/lib/spree/core/preferences/masking.rb +55 -2
  488. data/lib/spree/core/preferences/preferable.rb +343 -103
  489. data/lib/spree/core/preferences/preferable_class_methods.rb +188 -136
  490. data/lib/spree/core/previews/import_preview.rb +10 -0
  491. data/lib/spree/core/ransack_configuration.rb +13 -2
  492. data/lib/spree/core/tax/provider_error.rb +37 -0
  493. data/lib/spree/core/version.rb +1 -1
  494. data/lib/spree/core.rb +91 -373
  495. data/lib/spree/emails/editable_templates.rb +82 -0
  496. data/lib/spree/events/catalog.rb +189 -0
  497. data/lib/spree/events.rb +21 -1
  498. data/lib/spree/hooks.rb +0 -17
  499. data/lib/spree/i18n.rb +32 -15
  500. data/lib/spree/iso_data.rb +7 -0
  501. data/lib/spree/localized_number.rb +6 -0
  502. data/lib/spree/migrations.rb +4 -1
  503. data/lib/spree/money.rb +196 -15
  504. data/lib/spree/reporting/adapters/base.rb +7 -6
  505. data/lib/spree/reporting/adapters/live.rb +6 -6
  506. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  507. data/lib/spree/reporting/hydration.rb +1 -1
  508. data/lib/spree/reporting/query.rb +8 -0
  509. data/lib/spree/reporting/registry.rb +13 -0
  510. data/lib/spree/reporting/schema.rb +10 -10
  511. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  512. data/lib/spree/testing_support/emails.rb +22 -0
  513. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  514. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  515. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  516. data/lib/spree/testing_support/factories/product_factory.rb +9 -0
  517. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  518. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  519. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  520. data/lib/spree/testing_support/i18n.rb +3 -94
  521. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  522. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  523. data/lib/spree/testing_support/preferences.rb +0 -5
  524. data/lib/spree/testing_support/store.rb +9 -1
  525. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +67 -1
  526. data/lib/spree/uploads.rb +101 -0
  527. data/lib/tasks/categories_and_collections.rake +3 -3
  528. data/lib/tasks/core.rake +6 -4
  529. data/lib/tasks/markets.rake +2 -1
  530. data/lib/tasks/media.rake +3 -3
  531. data/lib/tasks/migrate_users_to_customers.rake +23 -18
  532. data/lib/tasks/money.rake +46 -0
  533. data/lib/tasks/package_types.rake +1 -1
  534. data/lib/tasks/preferences_json.rake +101 -0
  535. data/lib/tasks/seller_sample_data.rake +2 -2
  536. data/lib/tasks/store_settings.rake +13 -6
  537. data/lib/tasks/typed_adjustments_migration.rake +1 -1
  538. data/lib/tasks/uploads.rake +19 -0
  539. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  540. metadata +219 -46
  541. data/app/jobs/spree/products/touch_categories_job.rb +0 -15
  542. data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
  543. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  544. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  545. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  546. data/app/models/spree/invitations/store.rb +0 -6
  547. data/app/models/spree/preference.rb +0 -14
  548. data/app/models/spree/promotion_handler/page.rb +0 -25
  549. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  550. data/app/models/spree/stock/differentiator.rb +0 -45
  551. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  552. data/app/services/spree/carts/empty.rb +0 -42
  553. data/app/services/spree/country_to_timezone.rb +0 -273
  554. data/app/services/spree/payments/create.rb +0 -92
  555. data/app/validators/spree/url_validator.rb +0 -23
  556. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  557. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  558. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  559. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  560. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  561. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  562. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  563. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  564. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  565. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  566. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  567. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  568. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  569. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  570. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  571. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  572. data/lib/spree/analytics.rb +0 -11
  573. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  574. data/lib/spree/core/preferences/store.rb +0 -105
  575. data/lib/spree/translation_migrations.rb +0 -52
@@ -7,10 +7,12 @@ module Spree
7
7
 
8
8
  include Spree::SingleStoreResource
9
9
  include Spree::HasListPosition
10
+ include Spree::CanonicalDecimals
11
+
12
+ canonical_decimals :price_adjustment_percentage, places: 3
10
13
 
11
14
  MATCH_POLICIES = %w[all any].freeze
12
15
 
13
- belongs_to :store, class_name: 'Spree::Store'
14
16
  # The owning agreement, or nil for a standalone list. An owned list is
15
17
  # reached only through its catalog — generic rule matching skips it in
16
18
  # SQL (see .for_context) — so a deactivated catalog's list goes dormant
@@ -95,6 +97,7 @@ module Spree
95
97
  # `catalog_id` is queryable so a picker can offer the lists that are
96
98
  # actually available — unowned, plus the one the catalog already holds.
97
99
  self.whitelisted_ransackable_attributes = %w[status match_policy starts_at ends_at catalog_id]
100
+ search_by :name
98
101
 
99
102
  scope :by_position, -> { ordered }
100
103
  scope :for_store, ->(store) { where(store: store) }
@@ -155,7 +158,7 @@ module Spree
155
158
  end
156
159
 
157
160
  def self.match_policies
158
- MATCH_POLICIES.map { |key| [Spree.t(key), key] }
161
+ MATCH_POLICIES.map { |key| [I18n.t("spree.#{key}"), key] }
159
162
  end
160
163
 
161
164
  # Returns true if the price list is applicable to the context
@@ -334,9 +337,9 @@ module Spree
334
337
 
335
338
  return if prices_to_insert.empty?
336
339
 
337
- # Use upsert_all with on_duplicate: :skip to handle race conditions
340
+ # Use upsert_all with on_duplicate: :skip to handle race conditions.
341
+ # Placeholders charge nothing, so the variants are not touched.
338
342
  Spree::Price.upsert_all(prices_to_insert, on_duplicate: :skip)
339
- touch_variants(variant_ids)
340
343
  touch
341
344
  end
342
345
 
@@ -355,10 +358,13 @@ module Spree
355
358
  variant_ids = Spree::Variant.where(product_id: product_ids).distinct.pluck(:id)
356
359
  return if variant_ids.empty?
357
360
 
361
+ removed_prices = prices.where(variant_id: variant_ids)
362
+ priced_variant_ids = removed_prices.where.not(amount: nil).distinct.pluck(:variant_id)
363
+
358
364
  # Use delete_all for hard delete - this bypasses acts_as_paranoid
359
365
  # which is required for the unique index to work when re-adding products
360
- prices.where(variant_id: variant_ids).delete_all
361
- touch_variants(variant_ids)
366
+ removed_prices.delete_all
367
+ touch_variants(priced_variant_ids)
362
368
  touch
363
369
  end
364
370
 
@@ -387,9 +393,8 @@ module Spree
387
393
 
388
394
  current = current_values[price_id]
389
395
 
390
- # Parse amounts using LocalizedNumber for proper decimal handling
391
- amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
392
- compare_at_amount = attrs[:compare_at_amount].present? ? Spree::LocalizedNumber.parse(attrs[:compare_at_amount]) : nil
396
+ amount = Spree::Money::Rounding.parse_decimal(attrs[:amount])
397
+ compare_at_amount = Spree::Money::Rounding.parse_decimal(attrs[:compare_at_amount])
393
398
 
394
399
  # Clear compare_at_amount if it equals amount
395
400
  compare_at_amount = nil if compare_at_amount == amount
@@ -9,11 +9,8 @@ module Spree
9
9
  # against raw `channel_id` rows directly. Scope confines the
10
10
  # existence check to the price-list's store so cross-store channel
11
11
  # IDs can't sneak in.
12
- preference :channel_ids, :array, default: [],
13
- parse_on_set: normalize_id_preference(
14
- klass: Spree::Channel,
15
- scope: ->(rule) { rule.store.channels }
16
- )
12
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
13
+ scope: ->(rule) { rule.store.channels }
17
14
 
18
15
  def channels
19
16
  return [] if preferred_channel_ids.blank?
@@ -32,7 +29,7 @@ module Spree
32
29
  end
33
30
 
34
31
  def self.description
35
- Spree.t('price_rules.channel_rule.description')
32
+ I18n.t('spree.price_rules.channel_rule.description')
36
33
  end
37
34
  end
38
35
  end
@@ -10,11 +10,8 @@ module Spree
10
10
  # callers and decodes them on write so eligibility checks compare
11
11
  # against raw `customer_group_id` rows directly. Scope confines
12
12
  # the existence check to the price-list's store.
13
- preference :customer_group_ids, :array, default: [],
14
- parse_on_set: normalize_id_preference(
15
- klass: Spree::CustomerGroup,
16
- scope: ->(rule) { rule.store.customer_groups }
17
- )
13
+ preference :customer_group_ids, :array, of: :id, model: 'Spree::CustomerGroup', default: [],
14
+ scope: ->(rule) { rule.store.customer_groups }
18
15
 
19
16
  def customer_groups
20
17
  return [] if preferred_customer_group_ids.blank?
@@ -33,7 +30,7 @@ module Spree
33
30
  end
34
31
 
35
32
  def self.description
36
- Spree.t('price_rules.customer_group_rule.description')
33
+ I18n.t('spree.price_rules.customer_group_rule.description')
37
34
  end
38
35
 
39
36
  def self.superseded?
@@ -6,11 +6,8 @@ module Spree
6
6
  # against raw `market_id` rows directly. Scope confines the
7
7
  # existence check to the price-list's store so cross-store market
8
8
  # IDs can't sneak in.
9
- preference :market_ids, :array, default: [],
10
- parse_on_set: normalize_id_preference(
11
- klass: Spree::Market,
12
- scope: ->(rule) { rule.store.markets }
13
- )
9
+ preference :market_ids, :array, of: :id, model: 'Spree::Market', default: [],
10
+ scope: ->(rule) { rule.store.markets }
14
11
 
15
12
  def markets
16
13
  return [] if preferred_market_ids.blank?
@@ -10,9 +10,7 @@ module Spree
10
10
  # e.g. `usr_…`) from API callers and decodes them on write. Resolves
11
11
  # `Spree.customer_class` lazily — the user class is configured at boot,
12
12
  # and class-body evaluation runs before that on cold loads.
13
- preference :user_ids, :array, default: [], parse_on_set: ->(values) {
14
- normalize_id_preference(klass: Spree.customer_class).call(values)
15
- }
13
+ preference :user_ids, :array, of: :id, model: -> { Spree.customer_class }, default: []
16
14
 
17
15
  def users
18
16
  return [] if preferred_user_ids.blank?
@@ -43,8 +43,7 @@ module Spree
43
43
  "Spree::Product##{legacy_attr}= is deprecated; set #{publication_attr} on " \
44
44
  "ProductPublication instead (writes to every channel's publication). "
45
45
  )
46
- super(value)
47
- product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
46
+ write_publication_date(legacy_attr, value)
48
47
  end
49
48
 
50
49
  # Reading +available_on+/+discontinue_on+ prefers the current-channel
@@ -92,6 +91,14 @@ module Spree
92
91
 
93
92
  private
94
93
 
94
+ # Writes a legacy date column together with the matching date on every
95
+ # channel's publication, so the two never disagree.
96
+ def write_publication_date(legacy_attr, value)
97
+ write_attribute(legacy_attr, value)
98
+ publication_attr = DEPRECATED_DATE_TO_PUBLICATION_FIELD.fetch(legacy_attr.to_sym)
99
+ product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
100
+ end
101
+
95
102
  def assign_default_store
96
103
  self.store ||= Spree::Current.store || Spree::Store.default
97
104
  end
@@ -38,9 +38,9 @@ module Spree
38
38
  include Spree::StorePreferences
39
39
 
40
40
  publishes_lifecycle_events
41
+ publishes_events :activated, :approved, :archived, :back_in_stock, :drafted, :out_of_stock, :proposed, :rejected
41
42
 
42
- MEMOIZED_METHODS = %w[total_on_hand category_and_ancestors
43
- default_variant_id tax_category default_variant variant_for_images
43
+ MEMOIZED_METHODS = %w[total_on_hand default_variant_id tax_category default_variant variant_for_images
44
44
  primary_category buy_box_variants resolved_delivery_profile
45
45
  purchasable? in_stock? backorderable? digital?]
46
46
 
@@ -242,8 +242,6 @@ module Spree
242
242
  after_create :set_default_variant
243
243
 
244
244
  after_save :auto_promote_default_variant
245
- after_save :run_touch_callbacks, if: :saved_changes?
246
- after_touch :touch_categories
247
245
 
248
246
  after_commit :auto_match_collections, if: :eligible_for_collection_matching?
249
247
 
@@ -302,9 +300,6 @@ module Spree
302
300
  where(id: (product_ids + custom_field_ids).uniq.compact)
303
301
  }
304
302
 
305
- # Backward compatibility alias — remove in Spree 6.0
306
- scope :multi_search, ->(*args) { search(*args) }
307
-
308
303
  scope :archivable, -> { where(status: %w[active draft]) }
309
304
  scope :by_source, ->(source) { send(source) }
310
305
  scope :published, -> { where(status: 'active') }
@@ -366,7 +361,21 @@ module Spree
366
361
  super(manual_ids | collections.automatic.ids)
367
362
  end
368
363
 
369
- self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at seller_id]
364
+ self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at seller_id price]
365
+
366
+ # The default variant's base price in the current currency — what the
367
+ # product lists show — so a list can filter on it like a column.
368
+ ransacker :price, type: :decimal do |parent|
369
+ prices = Spree::Price.arel_table
370
+ Arel::Nodes::Grouping.new(
371
+ prices.project(prices[:amount]).
372
+ where(prices[:variant_id].eq(parent.table[:default_variant_id])).
373
+ where(prices[:currency].eq(Spree::Current.currency)).
374
+ where(prices[:price_list_id].eq(nil)).
375
+ where(prices[:deleted_at].eq(nil)).
376
+ take(1)
377
+ )
378
+ end
370
379
  self.whitelisted_ransackable_associations = %w[categories collections store channels variants default_variant tags labels
371
380
  product_type product_categories option_types seller]
372
381
  # The storefront filters by tag, category and collection; every other hop
@@ -375,9 +384,15 @@ module Spree
375
384
  self.storefront_ransackable_associations = %w[tags categories collections]
376
385
  self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon in_category in_categories in_collection price_between
377
386
  price_lte price_gte
378
- search multi_search in_stock out_of_stock with_option_value_ids
387
+ search in_stock out_of_stock with_option_value_ids
379
388
 
380
389
  ascend_by_price descend_by_price]
390
+ self.ransackable_scope_types = {
391
+ 'in_taxon' => 'id', 'in_category' => 'id', 'in_collection' => 'id',
392
+ 'in_categories' => { list: 'id' }, 'with_option_value_ids' => { list: 'id' },
393
+ 'price_between' => %w[decimal decimal], 'price_lte' => 'decimal', 'price_gte' => 'decimal',
394
+ 'ascend_by_price' => 'boolean', 'descend_by_price' => 'boolean'
395
+ }
381
396
 
382
397
  # All product-level convenience attributes delegate to the default variant —
383
398
  # one target, no branching. Reads resolve the existing default variant
@@ -693,12 +708,12 @@ module Spree
693
708
  # variant), reading preloaded associations to avoid an N+1.
694
709
  # @return [ActiveSupport::TimeWithZone, nil]
695
710
  def preorder_ships_at
696
- candidates = has_variants? ? variants : [master]
711
+ candidates = has_variants? ? variants : [default_variant]
697
712
  candidates.select(&:preorder?).filter_map(&:preorder_ships_at).max
698
713
  end
699
714
 
700
715
  def discontinue!
701
- self.discontinue_on = Time.current
716
+ write_publication_date(:discontinue_on, Time.current)
702
717
  self.status = 'archived'
703
718
  save(validate: false)
704
719
  end
@@ -817,7 +832,13 @@ module Spree
817
832
  Spree::Products::AutoMatchCollectionsJob.set(wait: 30.seconds).perform_later(id)
818
833
  end
819
834
 
820
- def to_csv(store = nil)
835
+ # @param seller [Spree::Seller, nil] the seller exporting their own
836
+ # products, whose rows cover only their own locations; nil for the
837
+ # operator, whose rows cover the marketplace's and the product's seller's
838
+ # @param default_stock_location [Spree::StockLocation, nil] the location
839
+ # the full rows carry; further locations get stock-only rows. The
840
+ # store's default when nil
841
+ def to_csv(store = nil, seller: nil, default_stock_location: nil)
821
842
  store ||= self.store
822
843
  properties_for_csv = if respond_to?(:product_properties) && Spree::Config.respond_to?(:product_properties_enabled) && Spree::Config[:product_properties_enabled]
823
844
  Spree::Property.order(:position).flat_map do |property|
@@ -840,11 +861,12 @@ module Spree
840
861
  all_variants = variants.to_a
841
862
  default_currency = store.default_currency
842
863
  additional_currencies = store.supported_currencies_list.map(&:iso_code) - [default_currency]
864
+ default_stock_location ||= store.default_stock_location
843
865
 
844
866
  # Primary rows in the store's default currency
845
867
  all_variants.each_with_index do |variant, index|
846
868
  csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, index, properties_for_csv, categories_for_csv, store,
847
- custom_field_values).call
869
+ custom_field_values, default_stock_location: default_stock_location).call
848
870
  end
849
871
 
850
872
  # Price-only rows for each additional currency
@@ -857,6 +879,25 @@ module Spree
857
879
  end
858
880
  end
859
881
 
882
+ # Stock-only rows for every other location the importer can name back. A
883
+ # row the import cannot route to its variant is left out: with no SKU it
884
+ # would land on the default variant.
885
+ location_owner_ids = seller ? [seller.id] : [nil, seller_id].uniq
886
+ all_variants.each do |variant|
887
+ next unless variant.should_track_inventory?
888
+ next if variant.sku.blank? && variant != default_variant
889
+
890
+ variant.stock_levels.each do |stock_level|
891
+ location = stock_level.stock_location
892
+ next if location.nil? || location == default_stock_location
893
+ next unless location.store_id == store.id && location_owner_ids.include?(location.seller_id)
894
+ next if stock_level.count_on_hand.zero?
895
+
896
+ csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, 0, [], [], store,
897
+ stock_level: stock_level).call
898
+ end
899
+ end
900
+
860
901
  csv_lines
861
902
  end
862
903
 
@@ -972,23 +1013,6 @@ module Spree
972
1013
  errors.add(:seller, :invalid)
973
1014
  end
974
1015
 
975
- def run_touch_callbacks
976
- run_callbacks(:touch)
977
- end
978
-
979
- def category_and_ancestors
980
- @category_and_ancestors ||= categories.map(&:self_and_ancestors).flatten.uniq
981
- end
982
-
983
- # Iterate through this product's categories and touch their timestamps in a batch
984
- def touch_categories
985
- if categories.any?
986
- Spree::Products::TouchCategoriesJob.
987
- set(wait: 5.seconds).
988
- perform_later(category_and_ancestors.map(&:id))
989
- end
990
- end
991
-
992
1016
  def ensure_not_in_complete_orders
993
1017
  if orders.complete.any?
994
1018
  errors.add(:base, :cannot_destroy_if_attached_to_line_items)
@@ -56,17 +56,5 @@ module Spree
56
56
  def store
57
57
  product&.store
58
58
  end
59
-
60
- # Named explicitly rather than left to the `Spree::Api::V3::` convention:
61
- # a serializer at that level is picked up by the Store API's generated
62
- # types and OpenAPI document, and a review decision is not something the
63
- # storefront has any business describing. The seller's own view is the
64
- # right shape for an event — it already withholds who decided, which a
65
- # webhook payload leaving the marketplace should not carry.
66
- #
67
- # @return [Class]
68
- def event_serializer_class
69
- Spree::Api::V3::Seller::ProductSubmissionSerializer
70
- end
71
59
  end
72
60
  end
@@ -10,7 +10,6 @@ module Spree
10
10
  TRANSLATABLE_FIELDS = %i[name].freeze
11
11
  translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback)
12
12
 
13
- belongs_to :store, class_name: 'Spree::Store'
14
13
  # Creation-time template only: stamped onto the product when it is
15
14
  # created with this type, never managed through it afterwards — the same
16
15
  # doctrine as every other type-driven attribute. Nil means the store's
@@ -38,6 +37,7 @@ module Spree
38
37
  validates :store, presence: true
39
38
 
40
39
  self.whitelisted_ransackable_attributes = %w[name]
40
+ search_by :name
41
41
  self.whitelisted_ransackable_associations = %w[option_types]
42
42
 
43
43
  # Replace-set writer for the flat API payload. Each entry is
@@ -42,7 +42,7 @@ module Spree
42
42
  # @param line_item [Spree::LineItem]
43
43
  # @return [BigDecimal] never positive
44
44
  def compute_amount(line_item)
45
- line_item.price * gifted_quantity_of(line_item) * -1
45
+ -Spree::Money::Rounding.to_currency(line_item.price * gifted_quantity_of(line_item), line_item.currency)
46
46
  end
47
47
 
48
48
  # Tops the order up to the promised quantity and discounts it.
@@ -104,6 +104,31 @@ module Spree
104
104
  quantifier.can_supply? quantity
105
105
  end
106
106
 
107
+ # Whether the promotion gives its gift on this order, judged with other
108
+ # promotions' gifts still counting toward its own rules — a single
109
+ # level, so two gift promotions measuring each other cannot loop.
110
+ #
111
+ # @param order [Spree::Order, Spree::Cart]
112
+ # @return [Boolean]
113
+ def gives_away?(order)
114
+ promotion.eligible?(order, own_gift_only: true) && qualifies_beyond_the_gift?(order, own_gift_only: true)
115
+ end
116
+
117
+ # How many units of this line the promotion pays for — never more than
118
+ # the line holds, so a shopper buying three of a variant gifted once
119
+ # still pays for two. `quantity` is nullable and written by `upsert_all`,
120
+ # which skips validation, so a missing or negative one gifts nothing
121
+ # rather than raising or turning the discount into a surcharge.
122
+ #
123
+ # @param line_item [Spree::LineItem]
124
+ # @return [Integer]
125
+ def gifted_quantity_of(line_item)
126
+ gifted = promotion_action_line_items.detect { |item| item.variant_id == line_item.variant_id }
127
+ return 0 if gifted.nil?
128
+
129
+ [[line_item.quantity, gifted.quantity.to_i].min, 0].max
130
+ end
131
+
107
132
  private
108
133
 
109
134
  # Whether this line holds a variant the promotion gives away.
@@ -117,30 +142,15 @@ module Spree
117
142
  # to hold a line the rules count with units this action is not already
118
143
  # covering. A promotion with no rules qualifies on anything, so there is
119
144
  # nothing for the gift to stand in for.
120
- def qualifies_beyond_the_gift?(order)
145
+ def qualifies_beyond_the_gift?(order, options = {})
121
146
  return true if promotion.promotion_rules.empty?
122
147
 
123
148
  order.line_items.any? do |line_item|
124
- promotion.line_item_actionable?(order, line_item) &&
149
+ promotion.line_item_actionable?(order, line_item, options) &&
125
150
  gifted_quantity_of(line_item) < line_item.quantity
126
151
  end
127
152
  end
128
153
 
129
- # How many units of this line the promotion pays for — never more than
130
- # the line holds, so a shopper buying three of a variant gifted once
131
- # still pays for two. `quantity` is nullable and written by `upsert_all`,
132
- # which skips validation, so a missing or negative one gifts nothing
133
- # rather than raising or turning the discount into a surcharge.
134
- #
135
- # @param line_item [Spree::LineItem]
136
- # @return [Integer]
137
- def gifted_quantity_of(line_item)
138
- gifted = promotion_action_line_items.detect { |item| item.variant_id == line_item.variant_id }
139
- return 0 if gifted.nil?
140
-
141
- [[line_item.quantity, gifted.quantity.to_i].min, 0].max
142
- end
143
-
144
154
  def add_missing_line_items(order)
145
155
  added_results = promotion_action_line_items.map do |item|
146
156
  missing = item.quantity.to_i - order.quantity_of(item.variant)
@@ -35,7 +35,7 @@ module Spree
35
35
  # Preferences
36
36
  #
37
37
  MATCH_POLICIES = %w(any all)
38
- preference :match_policy, :string, default: MATCH_POLICIES.first
38
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
39
39
 
40
40
  #
41
41
  # Attributes
@@ -47,10 +47,6 @@ module Spree
47
47
  #
48
48
  after_save :add_categories
49
49
 
50
- def applicable?(promotable)
51
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
52
- end
53
-
54
50
  def eligible_category_ids
55
51
  @eligible_category_ids ||= promotion_rule_categories.pluck(:category_id)
56
52
  end
@@ -8,15 +8,8 @@ module Spree
8
8
  # against the order's raw `channel_id` directly. Scope confines the
9
9
  # existence check to the promotion's store so cross-store channel
10
10
  # IDs can't sneak in.
11
- preference :channel_ids, :array, default: [],
12
- parse_on_set: normalize_id_preference(
13
- klass: Spree::Channel,
14
- scope: ->(rule) { rule.promotion.store.channels }
15
- )
16
-
17
- def applicable?(promotable)
18
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
19
- end
11
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
12
+ scope: ->(rule) { rule.promotion.store.channels }
20
13
 
21
14
  def channels
22
15
  return Spree::Channel.none if preferred_channel_ids.blank?
@@ -9,15 +9,10 @@ module Spree
9
9
  class Promotion
10
10
  module Rules
11
11
  class Country < Spree::PromotionRule
12
- preference :country_codes, :array, default: [], parse_on_set: lambda { |values|
13
- normalize_id_preference.call(values).map(&:upcase)
14
- }
12
+ preference :country_codes, :array, of: :string, format: :iso_country, default: [],
13
+ parse_on_set: ->(values) { Array(values).flat_map { |value| value.to_s.split(',') }.map { |code| code.strip.upcase }.compact_blank }
15
14
  preference :country_id, :integer # legacy single-country shortcut
16
- preference :country_code, :string # legacy ISO-based shortcut
17
-
18
- def applicable?(promotable)
19
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
20
- end
15
+ preference :country_code, :string, format: :iso_country # legacy ISO-based shortcut
21
16
 
22
17
  def countries
23
18
  isos = configured_country_codes.presence || [configured_country_code].compact_blank
@@ -3,11 +3,7 @@ module Spree
3
3
  class Promotion
4
4
  module Rules
5
5
  class Currency < Spree::PromotionRule
6
- preference :currency, :string
7
-
8
- def applicable?(promotable)
9
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
10
- end
6
+ preference :currency, :string, format: :currency
11
7
 
12
8
  def eligible?(order, options = {})
13
9
  return true if order.currency == preferred_currency
@@ -9,15 +9,8 @@ module Spree
9
9
  # Scoped to the promotion's own store: groups are store-owned, so an
10
10
  # id from elsewhere must raise rather than link a promotion to an
11
11
  # audience its store cannot see.
12
- preference :customer_group_ids, :array, default: [],
13
- parse_on_set: normalize_id_preference(
14
- klass: Spree::CustomerGroup,
15
- scope: ->(rule) { rule.promotion.store.customer_groups }
16
- )
17
-
18
- def applicable?(promotable)
19
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
20
- end
12
+ preference :customer_group_ids, :array, of: :id, model: 'Spree::CustomerGroup', default: [],
13
+ scope: ->(rule) { rule.promotion.store.customer_groups }
21
14
 
22
15
  def customer_groups
23
16
  return Spree::CustomerGroup.none if preferred_customer_group_ids.blank?
@@ -4,10 +4,6 @@ module Spree
4
4
  class FirstOrder < Spree::PromotionRule
5
5
  attr_reader :user, :email, :store
6
6
 
7
- def applicable?(promotable)
8
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
9
- end
10
-
11
7
  def eligible?(order, options = {})
12
8
  @user = order.try(:customer) || options[:user]
13
9
  @email = if options[:email].present?
@@ -1,23 +1,19 @@
1
1
  # A rule to apply to an order greater than (or greater than or equal to)
2
- # a specific amount
2
+ # a specific amount. Free gifts do not count toward it.
3
3
  module Spree
4
4
  class Promotion
5
5
  module Rules
6
6
  class ItemTotal < Spree::PromotionRule
7
- preference :amount_min, :decimal, default: 100.00
7
+ preference :amount_min, :money, default: 100.00
8
8
  preference :operator_min, :string, default: '>'
9
- preference :amount_max, :decimal, default: nil, nullable: true
9
+ preference :amount_max, :money, default: nil, nullable: true
10
10
  preference :operator_max, :string, default: '<'
11
11
 
12
12
  OPERATORS_MIN = ['gt', 'gte']
13
13
  OPERATORS_MAX = ['lt', 'lte']
14
14
 
15
- def applicable?(promotable)
16
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
17
- end
18
-
19
- def eligible?(order, _options = {})
20
- item_total = order.item_total
15
+ def eligible?(order, options = {})
16
+ item_total = order.item_total - order.gift_amount(promotion: promotion, own_gift_only: options.fetch(:own_gift_only, false))
21
17
 
22
18
  lower_limit_condition = item_total.send(preferred_operator_min == 'gte' ? :>= : :>, BigDecimal(preferred_amount_min.to_s))
23
19
 
@@ -43,7 +39,7 @@ module Spree
43
39
  if preferred_amount_max.present?
44
40
  Spree::Money.new(preferred_amount_max, currency: order.currency).to_s
45
41
  else
46
- Spree.t('no_maximum')
42
+ I18n.t('spree.no_maximum')
47
43
  end
48
44
  end
49
45
 
@@ -8,15 +8,8 @@ module Spree
8
8
  # against the order's raw `market_id` directly. Scope confines the
9
9
  # existence check to the promotion's store so cross-store market
10
10
  # IDs can't sneak in.
11
- preference :market_ids, :array, default: [],
12
- parse_on_set: normalize_id_preference(
13
- klass: Spree::Market,
14
- scope: ->(rule) { rule.promotion.store.markets }
15
- )
16
-
17
- def applicable?(promotable)
18
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
19
- end
11
+ preference :market_ids, :array, of: :id, model: 'Spree::Market', default: [],
12
+ scope: ->(rule) { rule.promotion.store.markets }
20
13
 
21
14
  def markets
22
15
  return Spree::Market.none if preferred_market_ids.blank?
@@ -2,10 +2,6 @@ module Spree
2
2
  class Promotion
3
3
  module Rules
4
4
  class OneUsePerUser < Spree::PromotionRule
5
- def applicable?(promotable)
6
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
7
- end
8
-
9
5
  def eligible?(order, _options = {})
10
6
  if order.customer.present?
11
7
  if promotion.used_by?(order.customer, [order])
@@ -3,19 +3,15 @@ module Spree
3
3
  module Rules
4
4
  class OptionValue < Spree::PromotionRule
5
5
  MATCH_POLICIES = %w(any)
6
- preference :match_policy, :string, default: MATCH_POLICIES.first
6
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
7
7
  # Stored as raw Spree::OptionValue ids. Accepts prefixed IDs (`optval_…`)
8
8
  # from API callers and decodes them on write so eligibility checks compare
9
9
  # against stable option-value rows, not join-table ids that shift when
10
10
  # variants are reconfigured.
11
- preference :eligible_values, :array, default: [],
12
- parse_on_set: normalize_id_preference(klass: Spree::OptionValue)
11
+ preference :eligible_values, :array, of: :id, model: 'Spree::OptionValue', default: []
13
12
 
14
13
  # @param promotable [Object]
15
14
  # @return [Boolean]
16
- def applicable?(promotable)
17
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
18
- end
19
15
 
20
16
  # @return [ActiveRecord::Relation<Spree::OptionValue>]
21
17
  def option_values
@@ -28,7 +28,7 @@ module Spree
28
28
  # Preferences
29
29
  #
30
30
  MATCH_POLICIES = %w(any all none)
31
- preference :match_policy, :string, default: MATCH_POLICIES.first
31
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
32
32
 
33
33
  #
34
34
  # Attributes
@@ -49,10 +49,6 @@ module Spree
49
49
  @eligible_product_ids ||= product_promotion_rules.pluck(:product_id)
50
50
  end
51
51
 
52
- def applicable?(promotable)
53
- promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
54
- end
55
-
56
52
  def eligible?(order, _options = {})
57
53
  return true if eligible_product_ids.empty?
58
54