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
@@ -158,7 +158,7 @@ module Spree
158
158
  next if value.blank? || Spree.actor_classes.include?(value)
159
159
 
160
160
  errors.add(type_column, :unregistered_actor,
161
- message: Spree.t('errors.messages.unregistered_actor'))
161
+ message: I18n.t('spree.errors.messages.unregistered_actor'))
162
162
  end
163
163
  end
164
164
 
@@ -4,12 +4,21 @@ module Spree
4
4
 
5
5
  include Spree::Actor
6
6
  include Spree::PrefixedId
7
+ include Spree::SignedIdAttachments
7
8
  include Spree::UserRoles
8
9
  include Spree::RansackableAttributes
9
10
 
10
11
  included do
11
12
  has_prefix_id :adm
12
13
 
14
+ publishes_events 'admin_user.password_reset', 'seller_user.password_reset'
15
+ # Carry a live reset token and have no external consumer: core sends
16
+ # these emails itself, so no webhook endpoint ever receives them.
17
+ publishes_event 'admin_user.password_reset_requested',
18
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', webhook: false
19
+ publishes_event 'seller_user.password_reset_requested',
20
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', webhook: false
21
+
13
22
  has_person_name
14
23
 
15
24
  normalizes :email, :first_name, :last_name, with: ->(value) { value&.to_s&.squish&.presence }
@@ -58,6 +67,7 @@ module Spree
58
67
 
59
68
  # Attachments
60
69
  has_one_attached :avatar, service: Spree.public_storage_service_name
70
+ signed_id_attachments :avatar
61
71
 
62
72
  validates :avatar, content_type: Rails.application.config.active_storage.web_image_content_types
63
73
 
@@ -128,10 +138,14 @@ module Spree
128
138
  # @param model [Class]
129
139
  # @param name [Symbol] the acted_by association
130
140
  def clear_actor(model, name)
141
+ columns = Spree::ActedBy.columns_for(name, nil)
142
+ # Append-only tables (revisions) have no updated_at to stamp.
143
+ columns = columns.merge(updated_at: Time.current) if model.column_names.include?('updated_at')
144
+
131
145
  model.
132
146
  where(:"#{name}_id" => id).
133
147
  where(:"#{name}_type" => [self.class.polymorphic_name, nil]).
134
- update_all(Spree::ActedBy.columns_for(name, nil).merge(updated_at: Time.current))
148
+ update_all(columns)
135
149
  end
136
150
  end
137
151
  end
@@ -0,0 +1,91 @@
1
+ module Spree
2
+ # Wire shorthand for the plain-Ruby strategy classes a merchant picks from a
3
+ # registry (fulfillment, delivery-rate, pickup-point, payout and digital
4
+ # asset providers, order routing strategies). Records keep storing the class
5
+ # name; the API reads and writes this shorthand, so the contract never
6
+ # carries a Ruby constant.
7
+ #
8
+ # class Spree::FulfillmentProvider::Base
9
+ # extend Spree::ApiTyped
10
+ # end
11
+ #
12
+ # Spree::FulfillmentProvider::Manual.api_type # => "manual"
13
+ # SpreeEasyPost::FulfillmentProvider.api_type # => "easy_post"
14
+ module ApiTyped
15
+ # The demodulized + underscored leaf. A gem names its class after the
16
+ # family (`SpreeEasyPost::FulfillmentProvider`, `SpreeAcmeOms::Strategy`),
17
+ # where the leaf would collapse every gem to one value — those derive it
18
+ # from the gem's module instead, matching {Spree::Integration.api_type}.
19
+ # Any other class keeps its leaf, so two providers nested in one module
20
+ # never share a value. Override to keep the value stable across a rename.
21
+ #
22
+ # @return [String]
23
+ def api_type
24
+ leaf = name.to_s.demodulize
25
+ outer = name.to_s.deconstantize.demodulize.delete_prefix('Spree')
26
+ return leaf.underscore unless leaf == api_typed_family && outer.present?
27
+
28
+ outer.underscore
29
+ end
30
+
31
+ # The family's own name — the module its base class sits in
32
+ # (`FulfillmentProvider` for `Spree::FulfillmentProvider::Base`).
33
+ #
34
+ # @return [String, nil]
35
+ def api_typed_family
36
+ base = ancestors.reverse.find { |ancestor| ancestor.is_a?(Class) && ancestor.singleton_class.include?(Spree::ApiTyped) }
37
+ base&.name.to_s.deconstantize.demodulize.presence
38
+ end
39
+
40
+ # The registered class answering to a wire shorthand, or nil.
41
+ #
42
+ # @param registry [Array<Class, String>]
43
+ # @param api_type [String, nil]
44
+ # @return [Class, nil]
45
+ def self.find(registry, api_type)
46
+ return nil if api_type.blank?
47
+
48
+ Array(registry).map { |entry| entry.is_a?(Class) ? entry : entry.to_s.safe_constantize }.
49
+ compact.find { |klass| klass.api_type == api_type.to_s }
50
+ end
51
+
52
+ # The registered class a record's stored class name points at, or nil
53
+ # when it is no longer registered (a typo, an uninstalled gem) — so a
54
+ # stored string is never constantized outside the registry.
55
+ #
56
+ # @param registry [Array<Class>]
57
+ # @param class_name [String, nil]
58
+ # @return [Class, nil]
59
+ def self.registered_class(registry, class_name)
60
+ return nil if class_name.blank?
61
+
62
+ Array(registry).find { |klass| klass.to_s == class_name.to_s }
63
+ end
64
+
65
+ # The class name a record stores for a value arriving from the API. A
66
+ # registered shorthand becomes its class name; anything else passes through
67
+ # unchanged so the record's own inclusion validation rejects it.
68
+ #
69
+ # @param registry [Array<Class, String>]
70
+ # @param value [String, nil]
71
+ # @return [String, nil]
72
+ def self.class_name_for(registry, value)
73
+ return value if value.blank?
74
+
75
+ find(registry, value)&.to_s || value
76
+ end
77
+
78
+ # The shorthand for a stored class name. A class that is no longer
79
+ # registered (an uninstalled gem) is shortened the same way rather than
80
+ # constantized.
81
+ #
82
+ # @param class_name [String, nil]
83
+ # @return [String, nil]
84
+ def self.api_type_for(class_name)
85
+ return nil if class_name.blank?
86
+
87
+ klass = class_name.to_s.safe_constantize
88
+ klass.respond_to?(:api_type) ? klass.api_type : class_name.to_s.demodulize.underscore
89
+ end
90
+ end
91
+ end
@@ -26,32 +26,60 @@ module Spree
26
26
  def calculator_type=(calculator_type)
27
27
  return if calculator_type.blank?
28
28
 
29
- registry = self.class.respond_to?(:calculators) ? self.class.calculators : []
30
- klass = registry.find { |k| k.api_type == calculator_type.to_s }
29
+ klass = registered_calculator_class(calculator_type)
31
30
  self.calculator = klass.new if klass && !calculator.instance_of?(klass)
32
31
  end
33
32
 
34
- # API v3 writer for the flat `calculator: { type:, preferences: {} }`
35
- # payload. Routes preferences through `set_preference` so values are
36
- # coerced by the typed `preferred_<name>=` setters — direct
37
- # assignment to the serialized hash would skip coercion.
38
- def assign_calculator_attributes(attrs)
33
+ # API v3 writer for the `calculator: { type:, preferences: {} }`
34
+ # payload. A type the registry does not list is refused like a setting
35
+ # that does not match (`/calculator/type`); preferences are checked
36
+ # against the calculator's schema and written through its typed writers.
37
+ #
38
+ # @param attrs [Hash, ActionController::Parameters, nil]
39
+ # @param pointer [String] where the calculator sits in the request
40
+ # @raise [Spree::Preferences::InvalidPreferences] for a type the registry does not list, or
41
+ # preferences that do not match its schema
42
+ # @return [void]
43
+ def assign_calculator_attributes(attrs, pointer: '/calculator')
39
44
  return if attrs.nil?
40
45
 
41
46
  attrs = attrs.to_h.with_indifferent_access
42
- self.calculator_type = attrs[:type] if attrs[:type].present?
43
-
44
- return if calculator.nil? || attrs[:preferences].blank?
47
+ type = attrs[:type].to_s
48
+ target = calculator
49
+ if type.present? && calculator&.class&.api_type != type
50
+ klass = registered_calculator_class(type)
51
+ unless klass
52
+ raise Spree::Preferences::InvalidPreferences.new(
53
+ [{ pointer: '/type', message: I18n.t('spree.errors.messages.unknown_calculator_type', type: type) }], prefix: pointer
54
+ )
55
+ end
45
56
 
46
- attrs[:preferences].to_h.each do |key, value|
47
- next unless calculator.has_preference?(key.to_sym)
57
+ target = klass.new
58
+ end
48
59
 
49
- calculator.set_preference(key.to_sym, value)
60
+ if attrs[:preferences].present?
61
+ # Preferences sent without a type land on the default calculator
62
+ # rather than on nothing, for a model that has one.
63
+ target ||= default_calculator if respond_to?(:default_calculator, true)
64
+ target&.assign_preferences(attrs[:preferences], pointer: "#{pointer}/preferences")
50
65
  end
66
+
67
+ # Attached only once its settings are accepted: on a saved owner,
68
+ # assigning a has_one replaces the stored calculator immediately.
69
+ self.calculator = target unless target.equal?(calculator)
51
70
  end
52
71
 
53
72
  private
54
73
 
74
+ # The registered calculator for an API shorthand, resolved from the
75
+ # registry entry's own name: nothing user-supplied reaches
76
+ # `constantize`, and in development the registry can hold a class from
77
+ # an earlier reload whose instances fail the association's type check.
78
+ def registered_calculator_class(type)
79
+ registry = self.class.respond_to?(:calculators) ? self.class.calculators : []
80
+ registry.find { |klass| klass.api_type == type.to_s }&.to_s&.constantize
81
+ end
82
+
55
83
  def self.model_name_without_spree_namespace
56
84
  to_s.tableize.tr('/', '_').sub('spree_', '')
57
85
  end
@@ -13,7 +13,7 @@ module Spree
13
13
  extend ActiveSupport::Concern
14
14
 
15
15
  included do
16
- preference :amounts, :hash, default: {}
16
+ preference :amounts, :hash, keys: :currency, values: :money, default: {}
17
17
  end
18
18
 
19
19
  # The amount this calculator quotes for a currency: the per-currency
@@ -0,0 +1,63 @@
1
+ module Spree
2
+ class Calculator < Spree.base_class
3
+ # A tier ladder for the tiered calculators: a list of
4
+ # `{ "threshold" => "100.0", "value" => "10.0" }` objects, where the value
5
+ # is a percentage or an amount depending on the calculator. A list rather
6
+ # than a hash keyed by threshold, because JSON object keys are always
7
+ # strings and a number key would not survive storage.
8
+ module Tiers
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ preference :tiers, :array, of: :object, properties: { threshold: :money, value: :decimal }, default: []
13
+
14
+ validate :preferred_tiers_content
15
+ end
16
+
17
+ # @return [BigDecimal, nil] nil when the value is not a number
18
+ def self.decimal(value)
19
+ BigDecimal(value.to_s, exception: false)
20
+ end
21
+
22
+ # The value of the highest tier whose threshold the amount reaches.
23
+ #
24
+ # @param amount [Numeric]
25
+ # @return [BigDecimal, nil] nil when the amount reaches no tier
26
+ def tier_value_for(amount)
27
+ tier_pairs.reverse_each.detect { |threshold, _| amount >= threshold }&.last
28
+ end
29
+
30
+ private
31
+
32
+ # Tiers that are not numbers are skipped: validation refuses them on
33
+ # save, but an unsaved calculator can still be asked to compute. Tiers
34
+ # still in the pre-6.0 hash keyed by threshold, left by an upgrade that
35
+ # could not convert them, keep discounting until they are converted.
36
+ #
37
+ # @return [Array<Array(BigDecimal, BigDecimal)>] threshold and value, lowest threshold first
38
+ def tier_pairs
39
+ tiers = preferred_tiers
40
+ pairs = case tiers
41
+ when Hash then tiers.to_a
42
+ when Array then tiers.grep(Hash).map { |tier| tier.values_at('threshold', 'value') }
43
+ else []
44
+ end
45
+ pairs.map { |threshold, value| [Tiers.decimal(threshold), Tiers.decimal(value)] }.select(&:all?).sort_by(&:first)
46
+ end
47
+
48
+ def preferred_tiers_content
49
+ tiers = preferred_tiers
50
+ unless tiers.is_a?(Array) && tiers.all? { |tier| tier.is_a?(Hash) }
51
+ return errors.add(:preferred_tiers, :should_be_list)
52
+ end
53
+
54
+ thresholds = tiers.map { |tier| Tiers.decimal(tier['threshold']) }
55
+ values = tiers.map { |tier| Tiers.decimal(tier['value']) }
56
+
57
+ errors.add(:preferred_tiers, :thresholds_should_be_positive_number) unless thresholds.all? { |threshold| threshold&.positive? }
58
+ errors.add(:preferred_tiers, :thresholds_should_be_unique) unless thresholds.uniq.size == thresholds.size
59
+ validate_tier_values(values)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,61 @@
1
+ module Spree
2
+ # Decimal attributes written as a number or canonical decimal text
3
+ # ("1234.56"). Text in any other shape raises Spree::Money::InvalidFormat,
4
+ # which the API answers as `invalid_money_format`, instead of being read as
5
+ # a wrong number the way Rails' cast reads "1,599.99" as 1 and "$10" as 0.
6
+ #
7
+ # The columns hold four decimals, so a value with more decimals than it may
8
+ # carry is refused on validation rather than stored and left to drift: an
9
+ # amount that changes hands keeps its currency's decimals, a unit price four,
10
+ # a rate its column's scale.
11
+ module CanonicalDecimals
12
+ extend ActiveSupport::Concern
13
+
14
+ private
15
+
16
+ # nil when the record has no currency yet (a payment before its order),
17
+ # which leaves the check to the record's own presence validations.
18
+ def canonical_decimals_currency_places
19
+ currency = try(:currency)
20
+ Spree::Money::Rounding.precision(currency) if currency.present?
21
+ rescue ActiveSupport::DelegationError
22
+ nil
23
+ end
24
+
25
+ class_methods do
26
+ # @param names [Array<Symbol>]
27
+ # @param places [:currency, Integer, nil] decimals allowed: the record's
28
+ # currency's, a fixed count, or nil for no check
29
+ # @param ignore_blank [Boolean] leave the attribute as it was when given a blank
30
+ def canonical_decimals(*names, places: :currency, ignore_blank: false)
31
+ names.each do |name|
32
+ define_method("#{name}=") do |value|
33
+ next if ignore_blank && value.blank?
34
+
35
+ super(Spree::Money::Rounding.parse_decimal(value))
36
+ rescue Spree::Money::InvalidFormat => error
37
+ raise Spree::Money::InvalidFormat.new(error.message, field: name)
38
+ end
39
+ end
40
+
41
+ return if places.nil?
42
+
43
+ validate do
44
+ allowed = places == :currency ? canonical_decimals_currency_places : places
45
+ next if allowed.nil?
46
+
47
+ names.each do |name|
48
+ next unless will_save_change_to_attribute?(name)
49
+
50
+ # Read before the column's scale rounds it away.
51
+ raw = read_attribute_before_type_cast(name)
52
+ value = raw.is_a?(Numeric) ? BigDecimal(raw.to_s) : self[name]
53
+ next if value.nil? || value.round(allowed) == value
54
+
55
+ errors.add(name, :too_many_decimals, count: allowed)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -3,6 +3,7 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  include Spree::PrefixedId
6
+ include Spree::SignedIdAttachments
6
7
  include Spree::HasCustomFields
7
8
  include Spree::UserAddress
8
9
  include Spree::UserPaymentSource
@@ -20,8 +21,10 @@ module Spree
20
21
  # 2026-03-16 "Consolidate metadata") — no public/private split.
21
22
  attribute :metadata, default: -> { {} }
22
23
 
23
- # Enable lifecycle events for user models
24
24
  publishes_lifecycle_events
25
+ publishes_events 'customer.password_reset', 'customer.anonymized'
26
+ publishes_event 'customer.password_reset_requested',
27
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', credential: 'write_customers'
25
28
 
26
29
  # Password reset token (Rails 7.1+ signed token, no DB column needed)
27
30
  # Token auto-invalidates when password changes (salt changes)
@@ -117,6 +120,7 @@ module Spree
117
120
  # Attachments
118
121
  #
119
122
  has_one_attached :avatar, service: Spree.public_storage_service_name
123
+ signed_id_attachments :avatar
120
124
 
121
125
  has_spree_rich_text :internal_note
122
126
 
@@ -184,14 +188,12 @@ module Spree
184
188
  where(conditions.reduce(:or))
185
189
  end
186
190
 
187
- # Backward compatibility alias — remove in Spree 6.0
188
- def self.multi_search(query) = search(query)
189
-
190
191
  self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles orders customer_groups]
191
192
  self.whitelisted_ransackable_attributes = %w[id email first_name last_name phone accepts_email_marketing
192
- created_at updated_at last_sign_in_at]
193
- self.whitelisted_ransackable_scopes = %w[search multi_search with_min_total_spent with_standing_for_company
193
+ created_at updated_at]
194
+ self.whitelisted_ransackable_scopes = %w[search with_min_total_spent with_standing_for_company
194
195
  anonymized]
196
+ self.ransackable_scope_types = { 'with_min_total_spent' => 'decimal', 'with_standing_for_company' => { list: 'id' }, 'anonymized' => 'boolean' }
195
197
 
196
198
  # Two-state scope: see Spree::Base.ransack_flag? for why the cast is
197
199
  # opted out of here and done inside the scope instead.
@@ -223,10 +225,11 @@ module Spree
223
225
  }
224
226
 
225
227
  # Customers with standing for a company: members of the node or of any
226
- # ancestor (see #standing_for?). Accepts a record, an id, or an array.
227
- scope :with_standing_for_company, ->(companies) {
228
+ # ancestor (see #standing_for?). Accepts records, ids, or an array; a
229
+ # splat because Ransack passes each value of a list as its own argument.
230
+ scope :with_standing_for_company, ->(*companies) {
228
231
  scoped = Spree::Current.store&.companies || Spree::Company.none
229
- nodes = Array.wrap(companies).filter_map do |company|
232
+ nodes = companies.flatten.filter_map do |company|
230
233
  company.is_a?(Spree::Company) ? company : scoped.find_by_param(company)
231
234
  end
232
235
  next none if nodes.empty?
@@ -278,6 +281,16 @@ module Spree
278
281
  end
279
282
  end
280
283
 
284
+ class_methods do
285
+ # Customer lifecycle events are `user.*`; an installation whose staff
286
+ # class shares these methods publishes theirs as `admin.*`.
287
+ #
288
+ # @return [String]
289
+ def event_prefix
290
+ self == Spree.admin_user_class && Spree.admin_user_class != Spree.customer_class ? 'admin' : 'user'
291
+ end
292
+ end
293
+
281
294
  # Returns the last incomplete spree order for the current store
282
295
  # @deprecated Carts are {Spree::Cart} since 6.0, so an incomplete order is
283
296
  # a backoffice draft rather than the customer's cart. Use
@@ -337,7 +350,7 @@ module Spree
337
350
  # @return [Spree::Wishlist]
338
351
  def default_wishlist_for_store(current_store)
339
352
  wishlists.find_by(is_default: true, store_id: current_store.id) || ActiveRecord::Base.connected_to(role: :writing) do
340
- wishlists.create!(store: current_store, is_default: true, name: Spree.t(:default_wishlist_name))
353
+ wishlists.create!(store: current_store, is_default: true, name: I18n.t('spree.default_wishlist_name'))
341
354
  end
342
355
  end
343
356
 
@@ -368,14 +381,6 @@ module Spree
368
381
  'Spree::Api::V3::CustomerSerializer'.safe_constantize
369
382
  end
370
383
 
371
- def event_prefix
372
- if self.class == Spree.admin_user_class && Spree.admin_user_class != Spree.customer_class
373
- 'admin'
374
- else
375
- 'user'
376
- end
377
- end
378
-
379
384
  # @param store [Spree::Store] store to scope the lookup to; defaults to the current store
380
385
  # @return [Spree::NewsletterSubscriber, nil] this user's newsletter subscriber for the given store
381
386
  def newsletter_subscriber(store = Spree::Current.store)
@@ -43,24 +43,22 @@ module Spree
43
43
  # unsaved record sorts last, which is what a merchant watching a new row
44
44
  # appear expects.
45
45
  def position_among_siblings
46
- scope = sibling_scope
47
- return 1 if scope.nil?
46
+ parent = owner
47
+ association = sibling_association_name
48
+ return 1 if parent.nil? || !parent.respond_to?(association)
48
49
 
49
- earlier = id.nil? ? scope.count : scope.where(scope.arel_table[:id].lt(id)).count
50
+ earlier = if id.present? && parent.read_association_in_memory?(association)
51
+ parent.public_send(association).to_a.count { |sibling| sibling.id.present? && sibling.id < id }
52
+ else
53
+ scope = parent.public_send(association).unscope(:order)
54
+ id.nil? ? scope.count : scope.where(scope.arel_table[:id].lt(id)).count
55
+ end
50
56
 
51
57
  earlier + 1
52
58
  end
53
59
 
54
- # The siblings sharing this record's parent. Nil when there is no parent
55
- # to group by.
56
- def sibling_scope
57
- parent = owner
58
- return if parent.nil?
59
-
60
- association = self.class.name.demodulize.underscore.pluralize
61
- return unless parent.respond_to?(association)
62
-
63
- parent.public_send(association).unscope(:order)
60
+ def sibling_association_name
61
+ self.class.name.demodulize.underscore.pluralize
64
62
  end
65
63
  end
66
64
  end
@@ -0,0 +1,52 @@
1
+ module Spree
2
+ # What a store's published email template and its draft share: which
3
+ # editable template they are, in which language, and the default they
4
+ # started from.
5
+ module EmailTemplateIdentity
6
+ extend ActiveSupport::Concern
7
+
8
+ # The version every language uses. Never NULL, so the unique index on
9
+ # store, key and locale holds for it too.
10
+ ANY_LOCALE = 'any'.freeze
11
+ LOCALE_FORMAT = /\A(any|[a-z]{2,3}(-[A-Za-z0-9]{2,8})*)\z/
12
+
13
+ included do
14
+ include Spree::SingleStoreResource
15
+
16
+ normalizes :locale, with: ->(locale) { locale.to_s.strip.presence || ANY_LOCALE }, apply_to_nil: true
17
+
18
+ validates :key, presence: true, uniqueness: { scope: spree_base_uniqueness_scope + [:store_id, :locale] }
19
+ validates :locale, format: { with: LOCALE_FORMAT }
20
+ validates :body, :base_body, presence: true
21
+ validate :key_editable
22
+ validate :locale_supported
23
+
24
+ scope :for_key, ->(key, locale = ANY_LOCALE) { where(key: key, locale: locale) }
25
+ end
26
+
27
+ # @return [Spree::Emails::EditableTemplates::Definition, nil]
28
+ def definition
29
+ Spree.editable_email_templates[key]
30
+ end
31
+
32
+ # @return [Boolean] whether Spree's default changed since this version started from it
33
+ # @param default [Spree::Emails::Template, nil] the current default
34
+ def default_changed?(default)
35
+ return false unless default
36
+
37
+ default.body != base_body || default.subject.to_s != base_subject.to_s
38
+ end
39
+
40
+ private
41
+
42
+ def key_editable
43
+ errors.add(:key, :invalid) if key.present? && definition.nil?
44
+ end
45
+
46
+ def locale_supported
47
+ return if locale == ANY_LOCALE || store.nil?
48
+
49
+ errors.add(:locale, :inclusion) unless store.supported_locales_list.include?(locale)
50
+ end
51
+ end
52
+ end
@@ -20,6 +20,8 @@ module Spree
20
20
  end
21
21
  end
22
22
 
23
+ scope :with_name, ->(value) { where(name: value.to_s.parameterize) }
24
+
23
25
  def set_name_from_label
24
26
  self.name = label
25
27
  end
@@ -24,10 +24,16 @@ module Spree
24
24
  pending?
25
25
  end
26
26
 
27
- %w[complete fail cancel expire].each do |transition|
27
+ %w[complete fail expire].each do |transition|
28
28
  define_method(:"can_#{transition}?") { pending? || processing? }
29
29
  end
30
30
 
31
+ # A failed confirm can leave the provider intent payable. Cancel is how
32
+ # placement retires it; the other transitions stay closed.
33
+ def can_cancel?
34
+ pending? || processing? || failed?
35
+ end
36
+
31
37
  def process
32
38
  return false unless transition_to('processing', from: %w[pending])
33
39
 
@@ -50,7 +56,7 @@ module Spree
50
56
  end
51
57
 
52
58
  def cancel
53
- return false unless transition_to('canceled', from: %w[pending processing])
59
+ return false unless transition_to('canceled', from: %w[pending processing failed])
54
60
 
55
61
  publish_canceled_event
56
62
  true
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ module PostSale
3
+ # Asks the order's tax provider what tax a return, claim or exchange gives
4
+ # back. Including models name their lines through +taxed_lines+.
5
+ #
6
+ # The provider is asked twice: when the record opens, for what was
7
+ # announced, and again before money moves, for what actually goes back.
8
+ # Only the first is copied onto the lines' columns — they keep what was
9
+ # announced, the way a return line's +pre_tax_amount+ does — while the rows
10
+ # always say what the customer is, or was, refunded.
11
+ module Taxation
12
+ extend ActiveSupport::Concern
13
+
14
+ # @return [void]
15
+ # @raise [Spree::Tax::ProviderError]
16
+ def calculate_tax!
17
+ lines = taxed_lines.to_a
18
+ estimate_tax(lines)
19
+ lines.each(&:fold_tax_lines!)
20
+ end
21
+
22
+ # @param amounts [Hash{Object => BigDecimal}, nil] the money refunded per
23
+ # line when it is less than the line is worth
24
+ # @return [void]
25
+ # @raise [Spree::Tax::ProviderError]
26
+ def settle_tax!(amounts: nil)
27
+ lines = taxed_lines.select(&:settles_tax?)
28
+ estimate_tax(lines, amounts: amounts) if lines.any?
29
+ end
30
+
31
+ # Drops every row on the record's lines, for a record withdrawn before
32
+ # anything went back. Nothing is worked out, so the provider is not asked
33
+ # — a cancellation must not wait on, or fail with, a tax service.
34
+ #
35
+ # @return [void]
36
+ def clear_tax!
37
+ Spree::TaxLine.post_sale.where(Spree::TaxLine.adjustable_key_for(taxed_lines.klass) => taxed_lines.select(:id)).delete_all
38
+ end
39
+
40
+ # The tax the record's lines give back, as their credit rows stand.
41
+ #
42
+ # @return [BigDecimal]
43
+ def credited_tax_total
44
+ taxed_lines.sum(0.to_d, &:credited_tax_total)
45
+ end
46
+
47
+ private
48
+
49
+ def estimate_tax(lines, amounts: nil)
50
+ order.tax_provider.estimate_refund(order, lines, amounts: amounts, tax_date: order.completed_at)
51
+ end
52
+ end
53
+ end
54
+ end