spree_core 6.0.0.beta4 → 6.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/seller_payouts/sweep_seller_job.rb +2 -0
  10. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  11. data/app/mailers/spree/base_mailer.rb +96 -13
  12. data/app/mailers/spree/email_template_mailer.rb +27 -0
  13. data/app/mailers/spree/export_mailer.rb +3 -4
  14. data/app/mailers/spree/import_mailer.rb +3 -4
  15. data/app/mailers/spree/invitation_mailer.rb +24 -7
  16. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  17. data/app/mailers/spree/webhook_mailer.rb +2 -2
  18. data/app/models/concerns/spree/acted_by.rb +1 -1
  19. data/app/models/concerns/spree/admin_user_methods.rb +13 -1
  20. data/app/models/concerns/spree/api_typed.rb +91 -0
  21. data/app/models/concerns/spree/calculator/tiers.rb +76 -0
  22. data/app/models/concerns/spree/customer_methods.rb +15 -14
  23. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  24. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  25. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  26. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  27. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  28. data/app/models/concerns/spree/preference_schema.rb +4 -12
  29. data/app/models/concerns/spree/product_scopes.rb +11 -1
  30. data/app/models/concerns/spree/provider_listing.rb +41 -0
  31. data/app/models/concerns/spree/publishable.rb +82 -14
  32. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  33. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  34. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  35. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  36. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  37. data/app/models/concerns/spree/purchase/payment_processing.rb +7 -3
  38. data/app/models/concerns/spree/purchase/purchase_order.rb +2 -2
  39. data/app/models/concerns/spree/purchase/totals.rb +49 -1
  40. data/app/models/concerns/spree/receivable.rb +1 -1
  41. data/app/models/concerns/spree/searchable.rb +0 -4
  42. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  43. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  44. data/app/models/concerns/spree/stores/email_branding.rb +31 -0
  45. data/app/models/concerns/spree/type_labels.rb +38 -0
  46. data/app/models/concerns/spree/typed_adjustment_line.rb +1 -1
  47. data/app/models/concerns/spree/typed_associations.rb +6 -0
  48. data/app/models/spree/address.rb +11 -1
  49. data/app/models/spree/adjusters/promotion.rb +1 -15
  50. data/app/models/spree/allowed_origin.rb +0 -1
  51. data/app/models/spree/api_key.rb +5 -6
  52. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  53. data/app/models/spree/base.rb +70 -22
  54. data/app/models/spree/calculator/flat_percent_item_total.rb +1 -3
  55. data/app/models/spree/calculator/flat_rate.rb +1 -3
  56. data/app/models/spree/calculator/flexi_rate.rb +1 -3
  57. data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
  58. data/app/models/spree/calculator/price_sack.rb +1 -3
  59. data/app/models/spree/calculator/shipping/digital_delivery.rb +1 -3
  60. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +1 -3
  61. data/app/models/spree/calculator/shipping/flat_rate.rb +1 -3
  62. data/app/models/spree/calculator/shipping/flexi_rate.rb +1 -3
  63. data/app/models/spree/calculator/shipping/per_item.rb +1 -3
  64. data/app/models/spree/calculator/shipping/price_sack.rb +1 -3
  65. data/app/models/spree/calculator/tiered_flat_rate.rb +6 -23
  66. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  67. data/app/models/spree/cart.rb +78 -18
  68. data/app/models/spree/channel/gating.rb +1 -1
  69. data/app/models/spree/channel.rb +4 -4
  70. data/app/models/spree/claim.rb +21 -1
  71. data/app/models/spree/claim_line_item.rb +41 -6
  72. data/app/models/spree/collection.rb +0 -1
  73. data/app/models/spree/collection_rule.rb +5 -21
  74. data/app/models/spree/commission_line.rb +1 -1
  75. data/app/models/spree/commission_rate.rb +2 -3
  76. data/app/models/spree/commission_rate_value.rb +1 -1
  77. data/app/models/spree/commission_rule.rb +4 -11
  78. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  79. data/app/models/spree/company_invitation.rb +1 -0
  80. data/app/models/spree/consent_record.rb +0 -1
  81. data/app/models/spree/credit_card.rb +1 -1
  82. data/app/models/spree/current.rb +23 -0
  83. data/app/models/spree/custom_field.rb +2 -2
  84. data/app/models/spree/custom_field_definition.rb +9 -23
  85. data/app/models/spree/custom_fields/boolean.rb +1 -1
  86. data/app/models/spree/custom_fields/json.rb +1 -1
  87. data/app/models/spree/data_feed/google.rb +0 -2
  88. data/app/models/spree/data_request.rb +1 -1
  89. data/app/models/spree/delivery_method.rb +30 -13
  90. data/app/models/spree/delivery_method_rule.rb +4 -11
  91. data/app/models/spree/delivery_rate.rb +4 -5
  92. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  93. data/app/models/spree/delivery_zone.rb +0 -1
  94. data/app/models/spree/delivery_zone_member.rb +4 -4
  95. data/app/models/spree/digital_asset.rb +7 -0
  96. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  97. data/app/models/spree/digital_link.rb +2 -5
  98. data/app/models/spree/discount.rb +1 -1
  99. data/app/models/spree/email_template.rb +25 -0
  100. data/app/models/spree/email_template_draft.rb +13 -0
  101. data/app/models/spree/email_template_revision.rb +17 -0
  102. data/app/models/spree/email_templates/entry.rb +118 -0
  103. data/app/models/spree/email_templates/sample_record.rb +16 -0
  104. data/app/models/spree/emails/branding.rb +80 -0
  105. data/app/models/spree/exchange.rb +15 -1
  106. data/app/models/spree/exchange_line_item.rb +121 -5
  107. data/app/models/spree/export.rb +0 -1
  108. data/app/models/spree/exports/products.rb +10 -1
  109. data/app/models/spree/exports/report.rb +1 -1
  110. data/app/models/spree/fee.rb +4 -2
  111. data/app/models/spree/fulfillment.rb +53 -27
  112. data/app/models/spree/fulfillment_changer.rb +2 -2
  113. data/app/models/spree/fulfillment_item.rb +0 -4
  114. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  115. data/app/models/spree/gateway/bogus.rb +4 -0
  116. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  117. data/app/models/spree/gateway.rb +5 -1
  118. data/app/models/spree/gift_card.rb +12 -1
  119. data/app/models/spree/gift_card_batch.rb +0 -1
  120. data/app/models/spree/import.rb +2 -7
  121. data/app/models/spree/import_mapping.rb +1 -2
  122. data/app/models/spree/import_row.rb +2 -2
  123. data/app/models/spree/import_schemas/products.rb +1 -0
  124. data/app/models/spree/integration.rb +8 -18
  125. data/app/models/spree/invitation.rb +5 -5
  126. data/app/models/spree/line_item.rb +13 -2
  127. data/app/models/spree/locale.rb +11 -8
  128. data/app/models/spree/market.rb +1 -0
  129. data/app/models/spree/newsletter_subscriber.rb +3 -0
  130. data/app/models/spree/order.rb +56 -138
  131. data/app/models/spree/order_group.rb +11 -2
  132. data/app/models/spree/order_inventory.rb +10 -5
  133. data/app/models/spree/order_promotion.rb +1 -1
  134. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  135. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  136. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  137. data/app/models/spree/order_routing_rule.rb +6 -22
  138. data/app/models/spree/package_type.rb +1 -2
  139. data/app/models/spree/payment/gateway_options.rb +14 -52
  140. data/app/models/spree/payment/processing.rb +2 -4
  141. data/app/models/spree/payment.rb +20 -5
  142. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  143. data/app/models/spree/payment_method/store_credit.rb +4 -4
  144. data/app/models/spree/payment_method.rb +36 -4
  145. data/app/models/spree/payment_session.rb +5 -1
  146. data/app/models/spree/payment_setup_session.rb +1 -0
  147. data/app/models/spree/payout_provider/base.rb +3 -1
  148. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  149. data/app/models/spree/price_list.rb +1 -2
  150. data/app/models/spree/price_rules/channel_rule.rb +1 -1
  151. data/app/models/spree/price_rules/customer_group_rule.rb +1 -1
  152. data/app/models/spree/product.rb +31 -7
  153. data/app/models/spree/product_submission.rb +0 -12
  154. data/app/models/spree/product_type.rb +0 -1
  155. data/app/models/spree/promotion/actions/create_line_items.rb +27 -17
  156. data/app/models/spree/promotion/rules/category.rb +0 -4
  157. data/app/models/spree/promotion/rules/channel.rb +0 -4
  158. data/app/models/spree/promotion/rules/country.rb +0 -4
  159. data/app/models/spree/promotion/rules/currency.rb +0 -4
  160. data/app/models/spree/promotion/rules/customer_group.rb +0 -4
  161. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  162. data/app/models/spree/promotion/rules/item_total.rb +4 -8
  163. data/app/models/spree/promotion/rules/market.rb +0 -4
  164. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  165. data/app/models/spree/promotion/rules/option_value.rb +0 -3
  166. data/app/models/spree/promotion/rules/product.rb +0 -4
  167. data/app/models/spree/promotion/rules/user.rb +0 -4
  168. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  169. data/app/models/spree/promotion.rb +35 -6
  170. data/app/models/spree/promotion_action.rb +7 -14
  171. data/app/models/spree/promotion_action_line_item.rb +1 -1
  172. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  173. data/app/models/spree/promotion_rule.rb +7 -14
  174. data/app/models/spree/purchase_order.rb +1 -0
  175. data/app/models/spree/quantity_rule.rb +1 -1
  176. data/app/models/spree/refund.rb +28 -3
  177. data/app/models/spree/return.rb +30 -2
  178. data/app/models/spree/return_line_item.rb +65 -14
  179. data/app/models/spree/role.rb +7 -7
  180. data/app/models/spree/saved_report.rb +1 -1
  181. data/app/models/spree/seller.rb +2 -1
  182. data/app/models/spree/seller_payout.rb +7 -0
  183. data/app/models/spree/seller_requirement.rb +4 -12
  184. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  185. data/app/models/spree/seller_requirement_submission.rb +2 -2
  186. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  187. data/app/models/spree/seller_transfer.rb +5 -0
  188. data/app/models/spree/shipping_label.rb +1 -0
  189. data/app/models/spree/stock/availability_validator.rb +2 -2
  190. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  191. data/app/models/spree/stock/packer.rb +2 -2
  192. data/app/models/spree/stock/splitter/weight.rb +1 -1
  193. data/app/models/spree/stock_level/custom_events.rb +6 -0
  194. data/app/models/spree/stock_level.rb +1 -1
  195. data/app/models/spree/stock_movement.rb +1 -1
  196. data/app/models/spree/stock_receipt_item.rb +1 -1
  197. data/app/models/spree/stock_reservation.rb +2 -2
  198. data/app/models/spree/stock_transfer.rb +4 -2
  199. data/app/models/spree/store.rb +33 -7
  200. data/app/models/spree/store_credit.rb +8 -9
  201. data/app/models/spree/store_credit_event.rb +5 -5
  202. data/app/models/spree/tax_exemption_certificate.rb +2 -0
  203. data/app/models/spree/tax_identifier.rb +3 -1
  204. data/app/models/spree/tax_line.rb +53 -3
  205. data/app/models/spree/tax_provider/base.rb +71 -14
  206. data/app/models/spree/tax_provider/internal.rb +59 -33
  207. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  208. data/app/models/spree/tax_rate.rb +1 -2
  209. data/app/models/spree/taxonomy.rb +0 -1
  210. data/app/models/spree/variant.rb +4 -3
  211. data/app/models/spree/variant_media.rb +1 -1
  212. data/app/models/spree/webhook_endpoint.rb +10 -13
  213. data/app/models/spree/wishlist.rb +1 -6
  214. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  215. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  216. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  217. data/app/presenters/spree/csv/order_line_item_presenter.rb +1 -1
  218. data/app/presenters/spree/csv/product_variant_presenter.rb +42 -12
  219. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  220. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  221. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  222. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  223. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  224. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  225. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  226. data/app/services/spree/carts/create.rb +8 -2
  227. data/app/services/spree/carts/destroy.rb +2 -2
  228. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  229. data/app/services/spree/carts/split_by_seller.rb +1 -1
  230. data/app/services/spree/carts/update.rb +19 -5
  231. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  232. data/app/services/spree/categories/add_products.rb +13 -31
  233. data/app/services/spree/categories/remove_products.rb +12 -51
  234. data/app/services/spree/checkout/advance.rb +1 -4
  235. data/app/services/spree/collections/add_products.rb +10 -23
  236. data/app/services/spree/collections/remove_products.rb +7 -43
  237. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  238. data/app/services/spree/data_requests/create.rb +1 -1
  239. data/app/services/spree/deliveries/destroy.rb +2 -2
  240. data/app/services/spree/email_templates/check.rb +58 -0
  241. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  242. data/app/services/spree/email_templates/preview.rb +99 -0
  243. data/app/services/spree/emails/file_system.rb +23 -0
  244. data/app/services/spree/emails/filters.rb +84 -0
  245. data/app/services/spree/emails/liquid_context.rb +48 -0
  246. data/app/services/spree/emails/rendered_email.rb +14 -0
  247. data/app/services/spree/emails/renderer.rb +125 -0
  248. data/app/services/spree/emails/template.rb +51 -0
  249. data/app/services/spree/emails/template_data.rb +23 -0
  250. data/app/services/spree/emails/template_resolver.rb +109 -0
  251. data/app/services/spree/emails/text_converter.rb +72 -0
  252. data/app/services/spree/emails/translation_inliner.rb +134 -0
  253. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  254. data/app/services/spree/imports/row_processors/product_variant.rb +65 -34
  255. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  256. data/app/services/spree/locales.rb +4 -5
  257. data/app/services/spree/newsletter/subscribe.rb +8 -12
  258. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  259. data/app/services/spree/orders/create.rb +2 -1
  260. data/app/services/spree/orders/discounts/create.rb +7 -4
  261. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  262. data/app/services/spree/orders/discounts/update.rb +1 -0
  263. data/app/services/spree/orders/fees/create.rb +1 -0
  264. data/app/services/spree/orders/fees/destroy.rb +1 -0
  265. data/app/services/spree/orders/fees/update.rb +1 -0
  266. data/app/services/spree/orders/remove_line_item.rb +13 -3
  267. data/app/services/spree/orders/update.rb +10 -6
  268. data/app/services/spree/orders/update_item.rb +6 -3
  269. data/app/services/spree/orders/update_statuses.rb +3 -1
  270. data/app/services/spree/product_memberships.rb +61 -0
  271. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  272. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  273. data/app/services/spree/seeds/commission_rates.rb +1 -1
  274. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  275. data/app/services/spree/seeds/payment_methods.rb +2 -2
  276. data/app/services/spree/seeds/saved_reports.rb +3 -3
  277. data/app/services/spree/sellers/requirements.rb +1 -2
  278. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  279. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  280. data/app/services/spree/stock_levels/correct.rb +3 -3
  281. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  282. data/app/services/spree/store_credits/apply.rb +15 -13
  283. data/app/services/spree/stores/provision_defaults.rb +15 -14
  284. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  285. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  286. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  287. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  288. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  289. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  290. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  291. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  292. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  293. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  294. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  295. data/app/views/spree/shared/_summary_row.liquid +8 -0
  296. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  297. data/app/workflows/spree/carts/add_item.rb +4 -4
  298. data/app/workflows/spree/carts/complete.rb +13 -3
  299. data/app/workflows/spree/carts/merge.rb +1 -1
  300. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  301. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  302. data/app/workflows/spree/claims/approve.rb +5 -22
  303. data/app/workflows/spree/claims/cancel.rb +5 -21
  304. data/app/workflows/spree/claims/create.rb +15 -0
  305. data/app/workflows/spree/claims/deny.rb +7 -0
  306. data/app/workflows/spree/claims/resolve.rb +34 -31
  307. data/app/workflows/spree/customers/anonymize.rb +3 -4
  308. data/app/workflows/spree/customers/create.rb +1 -1
  309. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  310. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  311. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  312. data/app/workflows/spree/email_templates/publish.rb +70 -0
  313. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  314. data/app/workflows/spree/email_templates/revert.rb +29 -0
  315. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  316. data/app/workflows/spree/exchanges/approve.rb +5 -22
  317. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  318. data/app/workflows/spree/exchanges/create.rb +6 -0
  319. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  320. data/app/workflows/spree/exchanges/receive.rb +6 -65
  321. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  322. data/app/workflows/spree/fulfillments/create.rb +51 -47
  323. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  324. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  325. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  326. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  327. data/app/workflows/spree/orders/add_item.rb +5 -1
  328. data/app/workflows/spree/orders/cancel.rb +3 -3
  329. data/app/workflows/spree/orders/complete.rb +16 -8
  330. data/app/workflows/spree/payments/process.rb +2 -2
  331. data/app/workflows/spree/post_sale/approval.rb +35 -0
  332. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  333. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  334. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  335. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  336. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  337. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  338. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  339. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  340. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  341. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  342. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  343. data/app/workflows/spree/refunds/create.rb +5 -2
  344. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  345. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  346. data/app/workflows/spree/returns/approve.rb +5 -22
  347. data/app/workflows/spree/returns/cancel.rb +5 -22
  348. data/app/workflows/spree/returns/create.rb +8 -0
  349. data/app/workflows/spree/returns/receive.rb +5 -76
  350. data/app/workflows/spree/returns/refund.rb +31 -8
  351. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  352. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  353. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  354. data/app/workflows/spree/shipping_labels/record.rb +5 -5
  355. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  356. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  357. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  358. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  359. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  360. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  361. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  362. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  363. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  364. data/config/i18n-tasks.yml +32 -18
  365. data/config/initializers/alba.rb +7 -0
  366. data/config/language_names.csv +264 -0
  367. data/config/locales/ar.yml +811 -0
  368. data/config/locales/be.yml +855 -0
  369. data/config/locales/bg.yml +723 -0
  370. data/config/locales/ca.yml +689 -0
  371. data/config/locales/cs.yml +855 -0
  372. data/config/locales/da.yml +771 -0
  373. data/config/locales/de-CH.yml +248 -0
  374. data/config/locales/de.yml +835 -0
  375. data/config/locales/el.yml +769 -0
  376. data/config/locales/en.yml +37 -503
  377. data/config/locales/es-CL.yml +301 -0
  378. data/config/locales/es-EC.yml +186 -0
  379. data/config/locales/es-MX.yml +334 -0
  380. data/config/locales/es.yml +831 -0
  381. data/config/locales/et.yml +644 -0
  382. data/config/locales/fa.yml +709 -0
  383. data/config/locales/fi.yml +771 -0
  384. data/config/locales/fr.yml +830 -0
  385. data/config/locales/hu.yml +751 -0
  386. data/config/locales/id.yml +756 -0
  387. data/config/locales/it.yml +833 -0
  388. data/config/locales/ja.yml +726 -0
  389. data/config/locales/km.yml +759 -0
  390. data/config/locales/ko.yml +575 -0
  391. data/config/locales/ku.yml +753 -0
  392. data/config/locales/lt.yml +821 -0
  393. data/config/locales/lv.yml +685 -0
  394. data/config/locales/nb.yml +824 -0
  395. data/config/locales/ne.yml +823 -0
  396. data/config/locales/nl.yml +826 -0
  397. data/config/locales/pl.yml +835 -0
  398. data/config/locales/pt-BR.yml +547 -0
  399. data/config/locales/pt.yml +636 -0
  400. data/config/locales/ro.yml +750 -0
  401. data/config/locales/ru.yml +834 -0
  402. data/config/locales/sk.yml +680 -0
  403. data/config/locales/sl-SI.yml +600 -0
  404. data/config/locales/sv.yml +827 -0
  405. data/config/locales/th.yml +745 -0
  406. data/config/locales/tr.yml +777 -0
  407. data/config/locales/uk.yml +836 -0
  408. data/config/locales/vi.yml +768 -0
  409. data/config/locales/zh-CN.yml +786 -0
  410. data/config/locales/zh-TW.yml +834 -0
  411. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  412. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  413. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  414. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  415. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  416. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  417. data/lib/spree/checkout/default_requirements.rb +37 -13
  418. data/lib/spree/core/configuration.rb +1 -1
  419. data/lib/spree/core/controller_helpers/store.rb +1 -1
  420. data/lib/spree/core/dependencies.rb +5 -4
  421. data/lib/spree/core/emails/escaped_output.rb +60 -0
  422. data/lib/spree/core/engine.rb +48 -167
  423. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  424. data/lib/spree/core/preferences/json_conversion.rb +182 -0
  425. data/lib/spree/core/preferences/masking.rb +4 -1
  426. data/lib/spree/core/preferences/preferable.rb +137 -83
  427. data/lib/spree/core/preferences/preferable_class_methods.rb +81 -146
  428. data/lib/spree/core/previews/import_preview.rb +10 -0
  429. data/lib/spree/core/tax/provider_error.rb +37 -0
  430. data/lib/spree/core/version.rb +1 -1
  431. data/lib/spree/core.rb +85 -370
  432. data/lib/spree/emails/editable_templates.rb +82 -0
  433. data/lib/spree/events/catalog.rb +189 -0
  434. data/lib/spree/events.rb +21 -1
  435. data/lib/spree/hooks.rb +0 -17
  436. data/lib/spree/i18n.rb +32 -15
  437. data/lib/spree/reporting/adapters/live.rb +6 -6
  438. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  439. data/lib/spree/reporting/hydration.rb +1 -1
  440. data/lib/spree/reporting/query.rb +8 -0
  441. data/lib/spree/reporting/schema.rb +10 -10
  442. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  443. data/lib/spree/testing_support/emails.rb +22 -0
  444. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  445. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  446. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  447. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  448. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  449. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  450. data/lib/spree/testing_support/i18n.rb +3 -94
  451. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  452. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  453. data/lib/spree/testing_support/preferences.rb +0 -5
  454. data/lib/spree/testing_support/store.rb +9 -1
  455. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +45 -0
  456. data/lib/tasks/categories_and_collections.rake +3 -3
  457. data/lib/tasks/core.rake +6 -4
  458. data/lib/tasks/media.rake +3 -3
  459. data/lib/tasks/package_types.rake +1 -1
  460. data/lib/tasks/preferences_json.rake +101 -0
  461. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  462. metadata +188 -44
  463. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  464. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  465. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  466. data/app/models/spree/invitations/store.rb +0 -6
  467. data/app/models/spree/preference.rb +0 -14
  468. data/app/models/spree/promotion_handler/page.rb +0 -25
  469. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  470. data/app/models/spree/stock/differentiator.rb +0 -45
  471. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  472. data/app/services/spree/carts/empty.rb +0 -42
  473. data/app/services/spree/country_to_timezone.rb +0 -273
  474. data/app/services/spree/payments/create.rb +0 -92
  475. data/app/validators/spree/url_validator.rb +0 -23
  476. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  477. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  478. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  479. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  480. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  481. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  482. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  483. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  484. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  485. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  486. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  487. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  488. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  489. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  490. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  491. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  492. data/lib/spree/analytics.rb +0 -11
  493. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  494. data/lib/spree/core/preferences/store.rb +0 -105
  495. data/lib/spree/translation_migrations.rb +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed64967de26da96d2727829b206b0b6b46e36dd52a6a9b54f51f61d571de5415
4
- data.tar.gz: 91298d5cadb8ea19904b9b4b3704075b06106fa68f42ac34436e6da0df00b85b
3
+ metadata.gz: 16e068be2852f0ba21dcbb6c34792612f42956a1fb72fbfe6fd8356103df4fea
4
+ data.tar.gz: e0c952c2895b904f00ff5b3a3fd99a5b06a9cd56f751ab7972a31a36eb58d8b7
5
5
  SHA512:
6
- metadata.gz: 43e098f9562a1ad458455bb83c0bafa9f0eab01836084bb6053bd56459ef2d67c903eff8f5b3faccd052dff9a220e2c6b6edb1b90e489539e40ee93a8fa8d1e7
7
- data.tar.gz: f544e4360b2bcecec6b2e8694a596c4136f9842908cc86a58d4914d8150561ba413ce2e2d4701205e8c799d11236333c5ec6611e35d187e26da039b79de86298
6
+ metadata.gz: 16656693e4a5bbfde9ea1c53c86a7fb7605fbb689d92d28c261c62bfb2e8862e12357ff181c2568d0e2d847de0db122a2c62a7d8d936a4cae54082e4a078b49c
7
+ data.tar.gz: 602c2b586f08e4332edb18714dbc0e1d97bc4dbe33705ece95259e3f8d2de7bfc0acab52f1150c0154fcab9517c06b47978e872b07f89eaefca1ed7f99d5c58f
data/README.md CHANGED
@@ -28,8 +28,8 @@ All models are namespaced under `Spree::` and include:
28
28
  - `Spree::Product` / `Spree::Variant` - Product catalog
29
29
  - `Spree::Order` / `Spree::LineItem` - Order management
30
30
  - `Spree::Payment` / `Spree::PaymentMethod` - Payment processing
31
- - `Spree::Shipment` / `Spree::ShippingMethod` - Shipping and fulfillment
32
- - `Spree::Taxon` / `Spree::Taxonomy` - Product categorization
31
+ - `Spree::Fulfillment` / `Spree::DeliveryMethod` - Shipping and fulfillment
32
+ - `Spree::Category` / `Spree::Collection` - Product categorization
33
33
  - `Spree::Store` - Multi-store support
34
34
  - `Spree::Promotion` - Promotions and discounts
35
35
  - `Spree::GiftCard` - Gift card functionality
@@ -1,30 +1,5 @@
1
1
  module Spree
2
2
  module ImagesHelper
3
- # render an image tag with a spree image variant
4
- # it also automatically scales the width and height by 2x to look great on retina screens
5
- #
6
- # @param image [ActiveStorage::Attachment] the image to render
7
- # @param options [Hash] options for the image tag
8
- # @option options [Integer] :width the width of the image
9
- # @option options [Integer] :height the height of the image
10
- # @option options [Symbol] :variant use a preprocessed named variant (e.g., :mini, :small, :medium, :large, :xlarge)
11
- def spree_image_tag(image, options = {})
12
- return unless image
13
- return unless image.variable?
14
- return if image.respond_to?(:attached?) && !image.attached?
15
-
16
- url_options = if options[:variant].present?
17
- { variant: options[:variant] }
18
- else
19
- { width: options[:width], height: options[:height], format: options[:format] }
20
- end
21
-
22
- image_tag(
23
- spree_image_url(image, url_options),
24
- options.except(:variant, :format)
25
- )
26
- end
27
-
28
3
  def spree_image_url(image, options = {})
29
4
  return unless image
30
5
  return unless image.variable?
@@ -56,38 +31,6 @@ module Spree
56
31
  end
57
32
  end
58
33
 
59
- def spree_asset_aspect_ratio(attachment)
60
- return unless attachment.present?
61
- return unless attachment.analyzed?
62
-
63
- metadata = attachment.metadata
64
- aspect_ratio = metadata['aspect_ratio'].presence
65
-
66
- return aspect_ratio if aspect_ratio
67
-
68
- width = metadata['width']&.to_f
69
- return unless width
70
-
71
- height = metadata['height']&.to_f
72
- return unless height
73
- return if height.zero?
74
-
75
- w = width.to_f
76
- h = height.to_f
77
-
78
- # Always return width / height, flipping if needed
79
- ratio = if h > w
80
- h / w
81
- elsif h < w
82
- w / h
83
- else
84
- # h == w, square image
85
- 1.0
86
- end
87
-
88
- ratio.round(3)
89
- end
90
-
91
34
  # convert images to webp with some sane optimization defaults
92
35
  # it also automatically scales the width and height by 2x to look great on retina screens
93
36
  #
@@ -19,5 +19,26 @@ module Spree
19
19
  retry_on ActiveRecord::RecordNotFound, wait: 2.seconds, attempts: 3
20
20
 
21
21
  discard_on ActiveJob::DeserializationError
22
+
23
+ private
24
+
25
+ # Runs the block once per store owning a record in +records+, inside that
26
+ # store and with +records+ narrowed to it.
27
+ #
28
+ # For installation-wide sweeps. A job has no store of its own, so the work
29
+ # done for a record — its emails, events, store settings — would otherwise
30
+ # run in the default store whichever store the record belongs to.
31
+ # Narrowing the relation keeps each query store-scoped once the store
32
+ # context arms Spree::StoreScopeGuard.
33
+ #
34
+ # @param records [ActiveRecord::Relation] records of a store-owned model
35
+ # @yieldparam store_records [ActiveRecord::Relation] +records+ of one store
36
+ # @return [void]
37
+ def each_store_of(records)
38
+ # Deleted stores too — their records were swept before this was per store.
39
+ Spree::Store.with_deleted.where(id: records.select(:store_id)).find_each do |store|
40
+ Spree::Current.with_store(store) { yield records.where(store_id: store.id) }
41
+ end
42
+ end
22
43
  end
23
44
  end
@@ -20,7 +20,7 @@ module Spree
20
20
  def base_scope
21
21
  Spree::Cart.incomplete.
22
22
  where.not(id: Spree::PaymentSession.where(status: %w[pending processing]).select(:cart_id)).
23
- where.not(id: Spree::Payment.where.not(state: Spree::Payment::INVALID_STATES).where.not(cart_id: nil).select(:cart_id))
23
+ where.not(id: Spree::Payment.valid.where.not(cart_id: nil).select(:cart_id))
24
24
  end
25
25
 
26
26
  def empty_scope
@@ -36,7 +36,7 @@ module Spree
36
36
  end
37
37
 
38
38
  def reap(scope)
39
- scope.in_batches(of: 200, &:destroy_all)
39
+ each_store_of(scope) { |carts| carts.in_batches(of: 200, &:destroy_all) }
40
40
  end
41
41
  end
42
42
  end
@@ -9,9 +9,12 @@ module Spree
9
9
  def perform
10
10
  types = Rails.application.config.spree.time_based_collection_rules.map(&:to_s)
11
11
 
12
- Spree::Collection.automatic.
13
- where(id: Spree::CollectionRule.where(type: types).select(:collection_id)).
14
- find_each { |collection| Spree::Collections::RegenerateProducts.call(collection: collection) }
12
+ time_based = Spree::Collection.automatic.
13
+ where(id: Spree::CollectionRule.where(type: types).select(:collection_id))
14
+
15
+ each_store_of(time_based) do |collections|
16
+ collections.find_each { |collection| Spree::Collections::RegenerateProducts.call(collection: collection) }
17
+ end
15
18
  end
16
19
  end
17
20
  end
@@ -10,11 +10,13 @@ module Spree
10
10
  STALE_AFTER = 10.minutes
11
11
 
12
12
  def perform
13
- Spree::Order.drafts.
14
- where.not(cart_id: nil).
15
- where(completed_at: nil).
16
- where(created_at: ...STALE_AFTER.ago).
17
- find_each do |order|
13
+ stale_drafts = Spree::Order.drafts.
14
+ where.not(cart_id: nil).
15
+ where(completed_at: nil).
16
+ where(created_at: ...STALE_AFTER.ago)
17
+
18
+ each_store_of(stale_drafts) do |orders|
19
+ orders.find_each do |order|
18
20
  cart = order.cart
19
21
  next if cart.nil? || cart.completed?
20
22
 
@@ -23,6 +25,7 @@ module Spree
23
25
  rescue StandardError => e
24
26
  Rails.logger.error("[Spree] Completion recovery raised for order #{order.number}: #{e.message}")
25
27
  end
28
+ end
26
29
  end
27
30
  end
28
31
  end
@@ -0,0 +1,61 @@
1
+ module Spree
2
+ module Payments
3
+ # Cancels the payment sessions an order was placed without, so none of
4
+ # them can still be paid. The cursor is the last session handled. A
5
+ # refusal is handled: asking again cannot change it. A session the
6
+ # provider could not be reached about is not, so a later run asks
7
+ # again. The buyer already holds that session's client secret.
8
+ class CancelUnusedSessionsJob < Spree::BaseJob
9
+ include ActiveJob::Continuable
10
+
11
+ # Continuable resumes only after a cursor is saved. The first session
12
+ # has none, so an outage there would drop the job and leave the intent
13
+ # payable. A refusal never reaches this retry: #cancel reports it.
14
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
15
+ # Must come after `retry_on StandardError` so DeserializationError lands in discard.
16
+ discard_on ActiveJob::DeserializationError
17
+
18
+ # Placement can run inside a webhook's transaction; enqueued before it
19
+ # commits, the job could read the order before its sessions moved onto it.
20
+ self.enqueue_after_transaction_commit = true
21
+
22
+ def perform(order_id)
23
+ # Outside the step on purpose: runs on every execution, resumes included.
24
+ @order = Spree::Order.find_by(id: order_id)
25
+ return if @order.nil?
26
+
27
+ Spree::Current.store = @order.store
28
+
29
+ step :cancel_sessions
30
+ end
31
+
32
+ private
33
+
34
+ def cancel_sessions(step)
35
+ payment_sessions = @order.payment_sessions.unused.order(:id)
36
+ payment_sessions = payment_sessions.where(Spree::PaymentSession.arel_table[:id].gt(step.cursor)) if step.cursor
37
+
38
+ payment_sessions.each do |payment_session|
39
+ cancel(payment_session)
40
+ step.set!(payment_session.id)
41
+ end
42
+ end
43
+
44
+ def cancel(payment_session)
45
+ payment_session.payment_method.cancel_payment_session(payment_session: payment_session)
46
+ rescue Spree::Core::GatewayError => error
47
+ # An unknown outcome is not a refusal. Cancel can be asked again;
48
+ # counting it handled leaves the intent payable.
49
+ raise if error.is_a?(Spree::Core::AmbiguousGatewayError)
50
+
51
+ # A refusal means the session was paid after all; retrying
52
+ # cannot change that, and the order is placed either way.
53
+ Rails.error.report(
54
+ error,
55
+ context: { payment_session_id: payment_session.id, order_id: @order.id },
56
+ source: 'spree.payments.cancel_unused_sessions'
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
@@ -17,6 +17,8 @@ module Spree
17
17
  def perform(seller_id)
18
18
  seller = Spree::Seller.find_by(id: seller_id)
19
19
  return if seller.nil?
20
+
21
+ Spree::Current.store = seller.store
20
22
  return if seller.resolved_payouts_schedule_interval == 'manual'
21
23
 
22
24
  currencies_owed(seller).each do |currency|
@@ -8,31 +8,16 @@ module Spree
8
8
  @current_store = store
9
9
  @reset_url = password_reset_url(token, store, redirect_url)
10
10
 
11
- with_store_locale(store, preferred_locale(admin_user, store)) do
12
- mail(
13
- to: admin_user.email,
14
- subject: "#{store.name} #{Spree.t('admin_user_mailer.password_reset_email.subject')}",
15
- store_url: store.formatted_url
11
+ with_store_locale(store, staff_locale(admin_user, store)) do
12
+ mail_template(
13
+ { user: email_data(admin_user, Spree::Emails::UserSerializer), reset_url: @reset_url },
14
+ to: admin_user.email, store_url: store.formatted_url
16
15
  )
17
16
  end
18
17
  end
19
18
 
20
19
  private
21
20
 
22
- # Locale chain for admin auth emails:
23
- # the admin's own dashboard language (persisted by the profile/language
24
- # switcher via `PATCH /api/v3/admin/me`) → the store's configured admin
25
- # locale → nil, which lets with_store_locale fall back to the store's
26
- # default (storefront) locale. Blank or unavailable values fall through.
27
- def preferred_locale(admin_user, store)
28
- [admin_user.try(:selected_locale), store&.preferred_admin_locale]
29
- .find { |locale| available_locale?(locale) }
30
- end
31
-
32
- def available_locale?(locale)
33
- locale.present? && I18n.available_locales.map(&:to_s).include?(locale.to_s)
34
- end
35
-
36
21
  # The dashboard SPA passes a validated redirect URL; the token is appended as
37
22
  # a query param, falling back to the dashboard's own reset page.
38
23
  def password_reset_url(token, store, redirect_url)
@@ -41,7 +41,7 @@ module Spree
41
41
  end
42
42
 
43
43
  def reply_to_address
44
- current_store.customer_support_email.presence || current_store.mail_from_address
44
+ current_store.support_email_address
45
45
  end
46
46
 
47
47
  def money(amount, currency = nil)
@@ -50,25 +50,72 @@ module Spree
50
50
  end
51
51
  helper_method :money
52
52
 
53
+ # Wraps HTML a mailer rendered from its own ERB views in the Liquid email
54
+ # layout, so every email a Spree mailer sends carries the store's logo,
55
+ # header and footer. Called by `layouts/spree/base_mailer.html.erb`.
56
+ #
57
+ # @param html [String] the rendered view
58
+ # @return [ActiveSupport::SafeBuffer]
59
+ def render_in_email_layout(html)
60
+ email_renderer.wrap(html, subject: message.subject).html_safe
61
+ end
62
+ helper_method :render_in_email_layout
63
+
53
64
  def mail(headers = {}, &block)
54
65
  ensure_default_action_mailer_url_host(headers[:store_url])
66
+ # Mailers that don't wrap their action in `with_store_locale` still get
67
+ # the store's default locale, as `mail` applied before Spree 5.6.
68
+ in_store_locale { super }
69
+ end
70
+
71
+ protected
55
72
 
56
- if @_store_locale_active
57
- super
58
- else
59
- # Subclasses that call `mail` without wrapping their action in
60
- # `with_store_locale` (e.g. extension mailers) still get the
61
- # store default locale, as `mail` applied before Spree 5.6.
62
- with_store_locale(current_store) { super }
73
+ # Locale for an email to a staff member or seller: their own dashboard
74
+ # language, then the store's admin locale, then nil — which lets
75
+ # with_store_locale fall back to the store's default. Blank or unavailable
76
+ # values fall through.
77
+ def staff_locale(user, store)
78
+ [user.try(:selected_locale), store&.preferred_admin_locale].find { |locale| I18n.locale_available?(locale) }
79
+ end
80
+
81
+ # Renders the current action's email from its Liquid template and builds
82
+ # the message. The template's front matter supplies the subject, and the
83
+ # plain-text part is generated from the HTML unless a `.text.liquid` sits
84
+ # next to the template.
85
+ #
86
+ # @param assigns [Hash] the template's variables — serializer output and
87
+ # mailer-built values such as token-carrying URLs, never models
88
+ # @param template [String] the template key, defaults to the action's view path
89
+ # @param headers [Hash] mail headers (`to:`, `store_url:`, ...)
90
+ # @return [Mail::Message]
91
+ def mail_template(assigns = {}, template: "#{mailer_name}/#{action_name}", **headers)
92
+ in_store_locale do
93
+ email = render_email_template(template, assigns)
94
+
95
+ mail(headers.merge(subject: email.subject)) do |format|
96
+ format.text { render plain: email.text, layout: false }
97
+ format.html { render html: email.html.html_safe, layout: false }
98
+ end
63
99
  end
64
100
  end
65
101
 
66
- protected
102
+ # The currency an email's amounts are in: its order's, else the store's.
103
+ #
104
+ # @return [String]
105
+ def email_currency
106
+ (@order || @order_group)&.currency || current_store.default_currency
107
+ end
67
108
 
68
- # The "<store> <subject> #<number>" subject line shared by customer-facing
69
- # order emails, with the optional [RESEND] prefix.
70
- def order_email_subject(store, subject, number, resend: false)
71
- "#{resend ? "[#{Spree.t(:resend).upcase}] " : ''}#{store.name} #{subject} ##{number}"
109
+ # A record as its template reads it: the serializer's JSON, the same data
110
+ # a renderer outside Ruby would receive. Download links and other bearer
111
+ # tokens stay out; the mailer builds any URL that carries one.
112
+ #
113
+ # @param object [Object, nil] the record
114
+ # @param serializer [Class] an email serializer
115
+ # @param params [Hash] serializer params, over the email's store, currency and locale
116
+ # @return [Hash, nil]
117
+ def email_data(object, serializer, **params)
118
+ Spree::Emails::TemplateData.call(object, serializer, store: current_store, currency: email_currency, **params)
72
119
  end
73
120
 
74
121
  # URI-based merge preserves existing query params and fragments so the token
@@ -86,6 +133,42 @@ module Spree
86
133
 
87
134
  private
88
135
 
136
+ # A store's published template passed its checks with sample data, but a
137
+ # real record can still trip it. The customer then gets the email from the
138
+ # files rather than none, and the error is reported.
139
+ def render_email_template(template, assigns)
140
+ resolver = email_resolver(template)
141
+ email_renderer(resolver).render(find_email_template(resolver, template), assigns)
142
+ rescue Liquid::Error, MRML::Error => e
143
+ raise unless resolver&.stored_templates_used?
144
+
145
+ Rails.error.report(e, context: { email_template: template, store_id: current_store&.id })
146
+ fallback = email_resolver
147
+ email_renderer(fallback).render(find_email_template(fallback, template), assigns)
148
+ end
149
+
150
+ def find_email_template(resolver, template)
151
+ resolver.find(template) || raise(ArgumentError, "Missing email template #{template}.liquid")
152
+ end
153
+
154
+ # The store's published templates apply only to the emails merchants may
155
+ # edit; every other email, and the layout and partials it uses, comes
156
+ # from files.
157
+ def email_resolver(template = nil)
158
+ view_paths = self.class.view_paths.paths.map(&:path)
159
+ return Spree::Emails::TemplateResolver.new(view_paths) unless Spree.editable_email_templates.include?(template)
160
+
161
+ Spree::Emails::TemplateResolver.new(view_paths, store: current_store, locale: I18n.locale)
162
+ end
163
+
164
+ def email_renderer(resolver = email_resolver)
165
+ Spree::Emails::Renderer.new(resolver: resolver, store: current_store, currency: email_currency)
166
+ end
167
+
168
+ def in_store_locale(&block)
169
+ @_store_locale_active ? yield : with_store_locale(current_store, &block)
170
+ end
171
+
89
172
  # this ensures that ActionMailer::Base.default_url_options[:host] is always set
90
173
  # this is only a fail-safe solution if developer didn't set this in environment files
91
174
  # http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ # Sends an editable email template, rendered with sample data as the editor
3
+ # previews it, so a merchant can see it in a real inbox before publishing.
4
+ class EmailTemplateMailer < BaseMailer
5
+ # @param store [Spree::Store]
6
+ # @param recipient [String] the email address of the admin who asked for it
7
+ # @param key [String] the editable template
8
+ # @param locale [String] a language code, or "any"
9
+ # @param subject [String, nil] an unsaved subject
10
+ # @param body [String, nil] an unsaved body
11
+ # @param record_id [String, nil] the record to build sample data from
12
+ # @param email_key [String, nil] for the layout or a partial, the email to show it in
13
+ # @param branding [Hash] unsaved branding values
14
+ def test_email(store, recipient, key, locale: nil, subject: nil, body: nil, record_id: nil, email_key: nil, branding: {})
15
+ @current_store = store
16
+ email = Spree::EmailTemplates::Preview.new(
17
+ store: store, key: key, locale: locale, subject: subject, body: body, record_id: record_id, email_key: email_key,
18
+ branding: branding
19
+ ).call
20
+
21
+ mail(to: recipient, subject: I18n.t('spree.email_templates.test_email_subject', subject: email.subject)) do |format|
22
+ format.text { render plain: email.text, layout: false }
23
+ format.html { render html: email.html.html_safe, layout: false }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -4,10 +4,9 @@ module Spree
4
4
  @export = export
5
5
 
6
6
  with_store_locale(@export.store) do
7
- mail(
8
- to: @export.user.email,
9
- subject: Spree.t('export_mailer.export_done.subject', export_number: @export.number).to_s,
10
- store_url: current_store.url
7
+ mail_template(
8
+ { export: email_data(@export, Spree::Emails::ExportSerializer) },
9
+ to: @export.user.email, store_url: current_store.url
11
10
  )
12
11
  end
13
12
  end
@@ -4,10 +4,9 @@ module Spree
4
4
  @import = import
5
5
 
6
6
  with_store_locale(@import.store) do
7
- mail(
8
- to: @import.user.email,
9
- subject: Spree.t('import_mailer.import_done.subject', import_number: @import.number).to_s,
10
- store_url: current_store.url
7
+ mail_template(
8
+ { import: email_data(@import, Spree::Emails::ImportSerializer) },
9
+ to: @import.user.email, store_url: current_store.url
11
10
  )
12
11
  end
13
12
  end
@@ -8,9 +8,10 @@ module Spree
8
8
  # default store's branding on multi-store installs.
9
9
  @current_store = invitation.store
10
10
  with_store_locale(invitation.store) do
11
- mail(to: invitation.email,
12
- subject: Spree.t('invitation_mailer.invitation_email.subject',
13
- resource_name: invitation.resource&.name))
11
+ mail_template(
12
+ { invitation: email_data(invitation, Spree::Emails::InvitationSerializer), accept_url: accept_url(invitation) },
13
+ to: invitation.email
14
+ )
14
15
  end
15
16
  end
16
17
 
@@ -19,10 +20,26 @@ module Spree
19
20
  @invitation = invitation
20
21
  @current_store = invitation.store
21
22
  with_store_locale(invitation.store) do
22
- mail(to: invitation.inviter.email,
23
- subject: Spree.t('invitation_mailer.invitation_accepted.subject',
24
- invitee_name: invitation.invitee&.name,
25
- resource_name: invitation.resource&.name))
23
+ mail_template(
24
+ { invitation: email_data(invitation, Spree::Emails::InvitationSerializer) },
25
+ to: invitation.inviter.email
26
+ )
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # The legacy `spree_admin` gem's own acceptance page when it is installed,
33
+ # otherwise the dashboard's.
34
+ def accept_url(invitation)
35
+ spree_routes = Spree::Core::Engine.routes.url_helpers
36
+
37
+ if spree_routes.respond_to?(:admin_invitation_url)
38
+ spree_routes.admin_invitation_url(invitation, token: invitation.token, host: invitation.store.formatted_url)
39
+ else
40
+ # Built with the mailer's own URL options, as the view built it before.
41
+ ensure_default_action_mailer_url_host
42
+ main_app.admin_invitation_acceptance_url(invitation)
26
43
  end
27
44
  end
28
45
  end
@@ -12,28 +12,16 @@ module Spree
12
12
  @current_store = store
13
13
  @reset_url = password_reset_url(token, store, redirect_url)
14
14
 
15
- with_store_locale(store, preferred_locale(seller_user, store)) do
16
- mail(
17
- to: seller_user.email,
18
- subject: "#{store.name} #{Spree.t('seller_user_mailer.password_reset_email.subject')}",
19
- store_url: store.formatted_url
15
+ with_store_locale(store, staff_locale(seller_user, store)) do
16
+ mail_template(
17
+ { user: email_data(seller_user, Spree::Emails::UserSerializer), reset_url: @reset_url },
18
+ to: seller_user.email, store_url: store.formatted_url
20
19
  )
21
20
  end
22
21
  end
23
22
 
24
23
  private
25
24
 
26
- # The seller's own panel language, then the store's admin locale, then nil —
27
- # which lets with_store_locale fall back to the store's default.
28
- def preferred_locale(seller_user, store)
29
- [seller_user.try(:selected_locale), store&.preferred_admin_locale]
30
- .find { |locale| available_locale?(locale) }
31
- end
32
-
33
- def available_locale?(locale)
34
- locale.present? && I18n.available_locales.map(&:to_s).include?(locale.to_s)
35
- end
36
-
37
25
  # The panel passes a validated redirect URL when it has one. Without it the
38
26
  # panel origin is resolved server-side rather than falling back to the store
39
27
  # URL: a storefront link cannot reset a seller password, and the marketplace
@@ -7,9 +7,9 @@ module Spree
7
7
  @current_store = webhook_endpoint.store
8
8
 
9
9
  with_store_locale(webhook_endpoint.store) do
10
- mail(
10
+ mail_template(
11
+ { endpoint: email_data(@endpoint, Spree::Emails::WebhookEndpointSerializer) },
11
12
  to: @current_store.new_order_notifications_email.presence || @current_store.mail_from_address,
12
- subject: Spree.t('webhook_mailer.endpoint_disabled.subject', endpoint_name: @endpoint.name || @endpoint.url),
13
13
  store_url: @current_store.formatted_url
14
14
  )
15
15
  end
@@ -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
 
@@ -10,6 +10,14 @@ module Spree
10
10
  included do
11
11
  has_prefix_id :adm
12
12
 
13
+ publishes_events 'admin_user.password_reset', 'seller_user.password_reset'
14
+ # Carry a live reset token and have no external consumer: core sends
15
+ # these emails itself, so no webhook endpoint ever receives them.
16
+ publishes_event 'admin_user.password_reset_requested',
17
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', webhook: false
18
+ publishes_event 'seller_user.password_reset_requested',
19
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', webhook: false
20
+
13
21
  has_person_name
14
22
 
15
23
  normalizes :email, :first_name, :last_name, with: ->(value) { value&.to_s&.squish&.presence }
@@ -128,10 +136,14 @@ module Spree
128
136
  # @param model [Class]
129
137
  # @param name [Symbol] the acted_by association
130
138
  def clear_actor(model, name)
139
+ columns = Spree::ActedBy.columns_for(name, nil)
140
+ # Append-only tables (revisions) have no updated_at to stamp.
141
+ columns = columns.merge(updated_at: Time.current) if model.column_names.include?('updated_at')
142
+
131
143
  model.
132
144
  where(:"#{name}_id" => id).
133
145
  where(:"#{name}_type" => [self.class.polymorphic_name, nil]).
134
- update_all(Spree::ActedBy.columns_for(name, nil).merge(updated_at: Time.current))
146
+ update_all(columns)
135
147
  end
136
148
  end
137
149
  end