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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed64967de26da96d2727829b206b0b6b46e36dd52a6a9b54f51f61d571de5415
4
- data.tar.gz: 91298d5cadb8ea19904b9b4b3704075b06106fa68f42ac34436e6da0df00b85b
3
+ metadata.gz: 2b2f1ca5c6b7f00a17f11bd7c7f34eac185b07acfbc58ee5cb177b0ddc804f05
4
+ data.tar.gz: 521bffafb8eb7ea97c31c9e71206bb65ee5f46c453516a8365e9875b965780f7
5
5
  SHA512:
6
- metadata.gz: 43e098f9562a1ad458455bb83c0bafa9f0eab01836084bb6053bd56459ef2d67c903eff8f5b3faccd052dff9a220e2c6b6edb1b90e489539e40ee93a8fa8d1e7
7
- data.tar.gz: f544e4360b2bcecec6b2e8694a596c4136f9842908cc86a58d4914d8150561ba413ce2e2d4701205e8c799d11236333c5ec6611e35d187e26da039b79de86298
6
+ metadata.gz: 49c46ec423a04b65729afe6ed2f560f49927947f615551fdf35ff5b4edeab841e689a778321c1311310125237cde58007bd0ecd56b6554b0fec2756833da8fde
7
+ data.tar.gz: a8cc2d507005043e8e92e01398321e70d7af4cf687c4d9dd5bb46064c52743a42026eb5131aca5ed72735ae6389cd31423199041e13754a3a0d0a5edc3550c85
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
@@ -40,8 +40,8 @@ Services follow a consistent interface pattern and are located in `app/services/
40
40
 
41
41
  ```ruby
42
42
  # Add item to cart
43
- Spree.cart_add_item_service.call(
44
- order: order,
43
+ Spree.cart_add_item_workflow.call(
44
+ cart: cart,
45
45
  variant: variant,
46
46
  quantity: 1
47
47
  )
@@ -76,7 +76,7 @@ Swap out default implementations with custom services:
76
76
 
77
77
  ```ruby
78
78
  # config/initializers/spree.rb
79
- Spree::Dependencies.cart_add_item_service = 'MyCustom::CartAddItem'
79
+ Spree::Dependencies.cart_add_item_workflow = 'MyCustom::CartAddItem'
80
80
  ```
81
81
 
82
82
  ## Configuration
@@ -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
@@ -10,6 +10,10 @@ module Spree
10
10
  # (ActiveJob handler lookup is reverse-declaration-order).
11
11
  discard_on ActiveJob::DeserializationError
12
12
 
13
+ # Bulk writers enqueue this inside their transaction; run before it
14
+ # commits, the job would index the product's previous prices.
15
+ self.enqueue_after_transaction_commit = true
16
+
13
17
  # @param resource_class [String] e.g. 'Spree::Product'
14
18
  # @param resource_id [String] always pass as string for UUID support
15
19
  # @param store_id [String] always pass as string for UUID support
@@ -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|
@@ -0,0 +1,49 @@
1
+ module Spree
2
+ module Uploads
3
+ # Deletes uploaded files that were never attached to anything, once they
4
+ # are older than `Spree::Config.unattached_upload_retention_days`.
5
+ #
6
+ # Rails deletes nothing on its own, so an upload abandoned halfway (a
7
+ # closed tab, a failed form) would otherwise stay in storage forever. The
8
+ # retention outlasts an upload reference, which expires after a day, so no
9
+ # file a client can still attach is deleted.
10
+ #
11
+ # Scheduled daily by the host app (`spree/spree-starter`'s recurring.yml
12
+ # ships the entry). Continuable: the cursor is the last file handled, so a
13
+ # deploy mid-run resumes rather than starting over.
14
+ class PurgeUnattachedJob < Spree::BaseJob
15
+ include ActiveJob::Continuable
16
+
17
+ def perform
18
+ step :purge
19
+ end
20
+
21
+ private
22
+
23
+ def purge(step)
24
+ # Only files that belong to a store: a host app may keep blobs of its
25
+ # own outside attachments, and those are not Spree's to delete.
26
+ blobs = ActiveStorage::Blob.unattached.where.not(store_id: nil).
27
+ where(created_at: ...Spree::Config.unattached_upload_retention_days.days.ago)
28
+ blobs = blobs.where(ActiveStorage::Blob.arel_table[:id].gt(step.cursor)) if step.cursor
29
+
30
+ blobs.find_each(order: :asc) do |blob|
31
+ purge_blob(blob)
32
+ step.set!(blob.id)
33
+ end
34
+ end
35
+
36
+ # A file attached between the query and the purge is kept. The check
37
+ # covers databases that do not enforce the attachments foreign key; the
38
+ # rescue covers the moment between the check and the delete on those
39
+ # that do.
40
+ def purge_blob(blob)
41
+ return if blob.attachments.exists?
42
+
43
+ blob.purge
44
+ rescue ActiveRecord::InvalidForeignKey
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -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,73 @@ 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 values, and
76
+ # languages Spree ships no translations for, fall through.
77
+ def staff_locale(user, store)
78
+ shipped = Spree.available_locales.map(&:to_s)
79
+ [user.try(:selected_locale), store&.preferred_admin_locale].find { |locale| shipped.include?(locale.to_s) }
80
+ end
81
+
82
+ # Renders the current action's email from its Liquid template and builds
83
+ # the message. The template's front matter supplies the subject, and the
84
+ # plain-text part is generated from the HTML unless a `.text.liquid` sits
85
+ # next to the template.
86
+ #
87
+ # @param assigns [Hash] the template's variables — serializer output and
88
+ # mailer-built values such as token-carrying URLs, never models
89
+ # @param template [String] the template key, defaults to the action's view path
90
+ # @param headers [Hash] mail headers (`to:`, `store_url:`, ...)
91
+ # @return [Mail::Message]
92
+ def mail_template(assigns = {}, template: "#{mailer_name}/#{action_name}", **headers)
93
+ in_store_locale do
94
+ email = render_email_template(template, assigns)
95
+
96
+ mail(headers.merge(subject: email.subject)) do |format|
97
+ format.text { render plain: email.text, layout: false }
98
+ format.html { render html: email.html.html_safe, layout: false }
99
+ end
63
100
  end
64
101
  end
65
102
 
66
- protected
103
+ # The currency an email's amounts are in: its order's, else the store's.
104
+ #
105
+ # @return [String]
106
+ def email_currency
107
+ (@order || @order_group)&.currency || current_store.default_currency
108
+ end
67
109
 
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}"
110
+ # A record as its template reads it: the serializer's JSON, the same data
111
+ # a renderer outside Ruby would receive. Download links and other bearer
112
+ # tokens stay out; the mailer builds any URL that carries one.
113
+ #
114
+ # @param object [Object, nil] the record
115
+ # @param serializer [Class] an email serializer
116
+ # @param params [Hash] serializer params, over the email's store, currency and locale
117
+ # @return [Hash, nil]
118
+ def email_data(object, serializer, **params)
119
+ Spree::Emails::TemplateData.call(object, serializer, store: current_store, currency: email_currency, **params)
72
120
  end
73
121
 
74
122
  # URI-based merge preserves existing query params and fragments so the token
@@ -86,6 +134,42 @@ module Spree
86
134
 
87
135
  private
88
136
 
137
+ # A store's published template passed its checks with sample data, but a
138
+ # real record can still trip it. The customer then gets the email from the
139
+ # files rather than none, and the error is reported.
140
+ def render_email_template(template, assigns)
141
+ resolver = email_resolver(template)
142
+ email_renderer(resolver).render(find_email_template(resolver, template), assigns)
143
+ rescue Liquid::Error, MRML::Error => e
144
+ raise unless resolver&.stored_templates_used?
145
+
146
+ Rails.error.report(e, context: { email_template: template, store_id: current_store&.id })
147
+ fallback = email_resolver
148
+ email_renderer(fallback).render(find_email_template(fallback, template), assigns)
149
+ end
150
+
151
+ def find_email_template(resolver, template)
152
+ resolver.find(template) || raise(ArgumentError, "Missing email template #{template}.liquid")
153
+ end
154
+
155
+ # The store's published templates apply only to the emails merchants may
156
+ # edit; every other email, and the layout and partials it uses, comes
157
+ # from files.
158
+ def email_resolver(template = nil)
159
+ view_paths = self.class.view_paths.paths.map(&:path)
160
+ return Spree::Emails::TemplateResolver.new(view_paths) unless Spree.editable_email_templates.include?(template)
161
+
162
+ Spree::Emails::TemplateResolver.new(view_paths, store: current_store, locale: I18n.locale)
163
+ end
164
+
165
+ def email_renderer(resolver = email_resolver)
166
+ Spree::Emails::Renderer.new(resolver: resolver, store: current_store, currency: email_currency)
167
+ end
168
+
169
+ def in_store_locale(&block)
170
+ @_store_locale_active ? yield : with_store_locale(current_store, &block)
171
+ end
172
+
89
173
  # this ensures that ActionMailer::Base.default_url_options[:host] is always set
90
174
  # this is only a fail-safe solution if developer didn't set this in environment files
91
175
  # 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