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
@@ -0,0 +1,91 @@
1
+ module Spree
2
+ # Wire shorthand for the plain-Ruby strategy classes a merchant picks from a
3
+ # registry (fulfillment, delivery-rate, pickup-point, payout and digital
4
+ # asset providers, order routing strategies). Records keep storing the class
5
+ # name; the API reads and writes this shorthand, so the contract never
6
+ # carries a Ruby constant.
7
+ #
8
+ # class Spree::FulfillmentProvider::Base
9
+ # extend Spree::ApiTyped
10
+ # end
11
+ #
12
+ # Spree::FulfillmentProvider::Manual.api_type # => "manual"
13
+ # SpreeEasyPost::FulfillmentProvider.api_type # => "easy_post"
14
+ module ApiTyped
15
+ # The demodulized + underscored leaf. A gem names its class after the
16
+ # family (`SpreeEasyPost::FulfillmentProvider`, `SpreeAcmeOms::Strategy`),
17
+ # where the leaf would collapse every gem to one value — those derive it
18
+ # from the gem's module instead, matching {Spree::Integration.api_type}.
19
+ # Any other class keeps its leaf, so two providers nested in one module
20
+ # never share a value. Override to keep the value stable across a rename.
21
+ #
22
+ # @return [String]
23
+ def api_type
24
+ leaf = name.to_s.demodulize
25
+ outer = name.to_s.deconstantize.demodulize.delete_prefix('Spree')
26
+ return leaf.underscore unless leaf == api_typed_family && outer.present?
27
+
28
+ outer.underscore
29
+ end
30
+
31
+ # The family's own name — the module its base class sits in
32
+ # (`FulfillmentProvider` for `Spree::FulfillmentProvider::Base`).
33
+ #
34
+ # @return [String, nil]
35
+ def api_typed_family
36
+ base = ancestors.reverse.find { |ancestor| ancestor.is_a?(Class) && ancestor.singleton_class.include?(Spree::ApiTyped) }
37
+ base&.name.to_s.deconstantize.demodulize.presence
38
+ end
39
+
40
+ # The registered class answering to a wire shorthand, or nil.
41
+ #
42
+ # @param registry [Array<Class, String>]
43
+ # @param api_type [String, nil]
44
+ # @return [Class, nil]
45
+ def self.find(registry, api_type)
46
+ return nil if api_type.blank?
47
+
48
+ Array(registry).map { |entry| entry.is_a?(Class) ? entry : entry.to_s.safe_constantize }.
49
+ compact.find { |klass| klass.api_type == api_type.to_s }
50
+ end
51
+
52
+ # The registered class a record's stored class name points at, or nil
53
+ # when it is no longer registered (a typo, an uninstalled gem) — so a
54
+ # stored string is never constantized outside the registry.
55
+ #
56
+ # @param registry [Array<Class>]
57
+ # @param class_name [String, nil]
58
+ # @return [Class, nil]
59
+ def self.registered_class(registry, class_name)
60
+ return nil if class_name.blank?
61
+
62
+ Array(registry).find { |klass| klass.to_s == class_name.to_s }
63
+ end
64
+
65
+ # The class name a record stores for a value arriving from the API. A
66
+ # registered shorthand becomes its class name; anything else passes through
67
+ # unchanged so the record's own inclusion validation rejects it.
68
+ #
69
+ # @param registry [Array<Class, String>]
70
+ # @param value [String, nil]
71
+ # @return [String, nil]
72
+ def self.class_name_for(registry, value)
73
+ return value if value.blank?
74
+
75
+ find(registry, value)&.to_s || value
76
+ end
77
+
78
+ # The shorthand for a stored class name. A class that is no longer
79
+ # registered (an uninstalled gem) is shortened the same way rather than
80
+ # constantized.
81
+ #
82
+ # @param class_name [String, nil]
83
+ # @return [String, nil]
84
+ def self.api_type_for(class_name)
85
+ return nil if class_name.blank?
86
+
87
+ klass = class_name.to_s.safe_constantize
88
+ klass.respond_to?(:api_type) ? klass.api_type : class_name.to_s.demodulize.underscore
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,76 @@
1
+ module Spree
2
+ class Calculator < Spree.base_class
3
+ # A tier ladder for the tiered calculators: a list of
4
+ # `{ "threshold" => "100.0", "value" => "10.0" }` objects, where the value
5
+ # is a percentage or an amount depending on the calculator. A list rather
6
+ # than a hash keyed by threshold, because JSON object keys are always
7
+ # strings and a number key would not survive storage.
8
+ module Tiers
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ preference :tiers, :array, default: [], parse_on_set: ->(value) { Tiers.normalize(value) }
13
+
14
+ validate :preferred_tiers_content
15
+ end
16
+
17
+ # @param value [Object] tiers as an operator or API client sent them
18
+ # @return [Object] an array of string-keyed hashes with numbers as exact
19
+ # decimal strings — the form JSON stores, so a fresh value compares
20
+ # equal to one read back — or the value unchanged when it is not a list
21
+ # of hashes, so validation can name the problem
22
+ def self.normalize(value)
23
+ return value unless value.is_a?(Array) && value.all? { |tier| tier.respond_to?(:to_h) && !tier.is_a?(Array) }
24
+
25
+ value.map do |tier|
26
+ tier.to_h.stringify_keys.to_h { |key, number| [key, decimal(number)&.as_json || number] }
27
+ end
28
+ end
29
+
30
+ # @return [BigDecimal, nil] nil when the value is not a number
31
+ def self.decimal(value)
32
+ BigDecimal(value.to_s, exception: false)
33
+ end
34
+
35
+ # The value of the highest tier whose threshold the amount reaches.
36
+ #
37
+ # @param amount [Numeric]
38
+ # @return [BigDecimal, nil] nil when the amount reaches no tier
39
+ def tier_value_for(amount)
40
+ tier_pairs.reverse_each.detect { |threshold, _| amount >= threshold }&.last
41
+ end
42
+
43
+ private
44
+
45
+ # Tiers that are not numbers are skipped: validation refuses them on
46
+ # save, but an unsaved calculator can still be asked to compute. Tiers
47
+ # still in the pre-6.0 hash keyed by threshold, left by an upgrade that
48
+ # could not convert them, keep discounting until they are converted.
49
+ #
50
+ # @return [Array<Array(BigDecimal, BigDecimal)>] threshold and value, lowest threshold first
51
+ def tier_pairs
52
+ tiers = preferred_tiers
53
+ pairs = case tiers
54
+ when Hash then tiers.to_a
55
+ when Array then tiers.grep(Hash).map { |tier| tier.values_at('threshold', 'value') }
56
+ else []
57
+ end
58
+ pairs.map { |threshold, value| [Tiers.decimal(threshold), Tiers.decimal(value)] }.select(&:all?).sort_by(&:first)
59
+ end
60
+
61
+ def preferred_tiers_content
62
+ tiers = preferred_tiers
63
+ unless tiers.is_a?(Array) && tiers.all? { |tier| tier.is_a?(Hash) }
64
+ return errors.add(:preferred_tiers, :should_be_list)
65
+ end
66
+
67
+ thresholds = tiers.map { |tier| Tiers.decimal(tier['threshold']) }
68
+ values = tiers.map { |tier| Tiers.decimal(tier['value']) }
69
+
70
+ errors.add(:preferred_tiers, :thresholds_should_be_positive_number) unless thresholds.all? { |threshold| threshold&.positive? }
71
+ errors.add(:preferred_tiers, :thresholds_should_be_unique) unless thresholds.uniq.size == thresholds.size
72
+ validate_tier_values(values)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -20,8 +20,10 @@ module Spree
20
20
  # 2026-03-16 "Consolidate metadata") — no public/private split.
21
21
  attribute :metadata, default: -> { {} }
22
22
 
23
- # Enable lifecycle events for user models
24
23
  publishes_lifecycle_events
24
+ publishes_events 'customer.password_reset', 'customer.anonymized'
25
+ publishes_event 'customer.password_reset_requested',
26
+ serializer: 'Spree::Api::V3::PasswordResetRequestedEventSerializer', credential: 'write_customers'
25
27
 
26
28
  # Password reset token (Rails 7.1+ signed token, no DB column needed)
27
29
  # Token auto-invalidates when password changes (salt changes)
@@ -184,13 +186,10 @@ module Spree
184
186
  where(conditions.reduce(:or))
185
187
  end
186
188
 
187
- # Backward compatibility alias — remove in Spree 6.0
188
- def self.multi_search(query) = search(query)
189
-
190
189
  self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles orders customer_groups]
191
190
  self.whitelisted_ransackable_attributes = %w[id email first_name last_name phone accepts_email_marketing
192
191
  created_at updated_at last_sign_in_at]
193
- self.whitelisted_ransackable_scopes = %w[search multi_search with_min_total_spent with_standing_for_company
192
+ self.whitelisted_ransackable_scopes = %w[search with_min_total_spent with_standing_for_company
194
193
  anonymized]
195
194
 
196
195
  # Two-state scope: see Spree::Base.ransack_flag? for why the cast is
@@ -278,6 +277,16 @@ module Spree
278
277
  end
279
278
  end
280
279
 
280
+ class_methods do
281
+ # Customer lifecycle events are `user.*`; an installation whose staff
282
+ # class shares these methods publishes theirs as `admin.*`.
283
+ #
284
+ # @return [String]
285
+ def event_prefix
286
+ self == Spree.admin_user_class && Spree.admin_user_class != Spree.customer_class ? 'admin' : 'user'
287
+ end
288
+ end
289
+
281
290
  # Returns the last incomplete spree order for the current store
282
291
  # @deprecated Carts are {Spree::Cart} since 6.0, so an incomplete order is
283
292
  # a backoffice draft rather than the customer's cart. Use
@@ -337,7 +346,7 @@ module Spree
337
346
  # @return [Spree::Wishlist]
338
347
  def default_wishlist_for_store(current_store)
339
348
  wishlists.find_by(is_default: true, store_id: current_store.id) || ActiveRecord::Base.connected_to(role: :writing) do
340
- wishlists.create!(store: current_store, is_default: true, name: Spree.t(:default_wishlist_name))
349
+ wishlists.create!(store: current_store, is_default: true, name: I18n.t('spree.default_wishlist_name'))
341
350
  end
342
351
  end
343
352
 
@@ -368,14 +377,6 @@ module Spree
368
377
  'Spree::Api::V3::CustomerSerializer'.safe_constantize
369
378
  end
370
379
 
371
- def event_prefix
372
- if self.class == Spree.admin_user_class && Spree.admin_user_class != Spree.customer_class
373
- 'admin'
374
- else
375
- 'user'
376
- end
377
- end
378
-
379
380
  # @param store [Spree::Store] store to scope the lookup to; defaults to the current store
380
381
  # @return [Spree::NewsletterSubscriber, nil] this user's newsletter subscriber for the given store
381
382
  def newsletter_subscriber(store = Spree::Current.store)
@@ -0,0 +1,52 @@
1
+ module Spree
2
+ # What a store's published email template and its draft share: which
3
+ # editable template they are, in which language, and the default they
4
+ # started from.
5
+ module EmailTemplateIdentity
6
+ extend ActiveSupport::Concern
7
+
8
+ # The version every language uses. Never NULL, so the unique index on
9
+ # store, key and locale holds for it too.
10
+ ANY_LOCALE = 'any'.freeze
11
+ LOCALE_FORMAT = /\A(any|[a-z]{2,3}(-[A-Za-z0-9]{2,8})*)\z/
12
+
13
+ included do
14
+ include Spree::SingleStoreResource
15
+
16
+ normalizes :locale, with: ->(locale) { locale.to_s.strip.presence || ANY_LOCALE }, apply_to_nil: true
17
+
18
+ validates :key, presence: true, uniqueness: { scope: spree_base_uniqueness_scope + [:store_id, :locale] }
19
+ validates :locale, format: { with: LOCALE_FORMAT }
20
+ validates :body, :base_body, presence: true
21
+ validate :key_editable
22
+ validate :locale_supported
23
+
24
+ scope :for_key, ->(key, locale = ANY_LOCALE) { where(key: key, locale: locale) }
25
+ end
26
+
27
+ # @return [Spree::Emails::EditableTemplates::Definition, nil]
28
+ def definition
29
+ Spree.editable_email_templates[key]
30
+ end
31
+
32
+ # @return [Boolean] whether Spree's default changed since this version started from it
33
+ # @param default [Spree::Emails::Template, nil] the current default
34
+ def default_changed?(default)
35
+ return false unless default
36
+
37
+ default.body != base_body || default.subject.to_s != base_subject.to_s
38
+ end
39
+
40
+ private
41
+
42
+ def key_editable
43
+ errors.add(:key, :invalid) if key.present? && definition.nil?
44
+ end
45
+
46
+ def locale_supported
47
+ return if locale == ANY_LOCALE || store.nil?
48
+
49
+ errors.add(:locale, :inclusion) unless store.supported_locales_list.include?(locale)
50
+ end
51
+ end
52
+ end
@@ -20,6 +20,8 @@ module Spree
20
20
  end
21
21
  end
22
22
 
23
+ scope :with_name, ->(value) { where(name: value.to_s.parameterize) }
24
+
23
25
  def set_name_from_label
24
26
  self.name = label
25
27
  end
@@ -24,10 +24,16 @@ module Spree
24
24
  pending?
25
25
  end
26
26
 
27
- %w[complete fail cancel expire].each do |transition|
27
+ %w[complete fail expire].each do |transition|
28
28
  define_method(:"can_#{transition}?") { pending? || processing? }
29
29
  end
30
30
 
31
+ # A failed confirm can leave the provider intent payable. Cancel is how
32
+ # placement retires it; the other transitions stay closed.
33
+ def can_cancel?
34
+ pending? || processing? || failed?
35
+ end
36
+
31
37
  def process
32
38
  return false unless transition_to('processing', from: %w[pending])
33
39
 
@@ -50,7 +56,7 @@ module Spree
50
56
  end
51
57
 
52
58
  def cancel
53
- return false unless transition_to('canceled', from: %w[pending processing])
59
+ return false unless transition_to('canceled', from: %w[pending processing failed])
54
60
 
55
61
  publish_canceled_event
56
62
  true
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ module PostSale
3
+ # Asks the order's tax provider what tax a return, claim or exchange gives
4
+ # back. Including models name their lines through +taxed_lines+.
5
+ #
6
+ # The provider is asked twice: when the record opens, for what was
7
+ # announced, and again before money moves, for what actually goes back.
8
+ # Only the first is copied onto the lines' columns — they keep what was
9
+ # announced, the way a return line's +pre_tax_amount+ does — while the rows
10
+ # always say what the customer is, or was, refunded.
11
+ module Taxation
12
+ extend ActiveSupport::Concern
13
+
14
+ # @return [void]
15
+ # @raise [Spree::Tax::ProviderError]
16
+ def calculate_tax!
17
+ lines = taxed_lines.to_a
18
+ estimate_tax(lines)
19
+ lines.each(&:fold_tax_lines!)
20
+ end
21
+
22
+ # @param amounts [Hash{Object => BigDecimal}, nil] the money refunded per
23
+ # line when it is less than the line is worth
24
+ # @return [void]
25
+ # @raise [Spree::Tax::ProviderError]
26
+ def settle_tax!(amounts: nil)
27
+ lines = taxed_lines.select(&:settles_tax?)
28
+ estimate_tax(lines, amounts: amounts) if lines.any?
29
+ end
30
+
31
+ # Drops every row on the record's lines, for a record withdrawn before
32
+ # anything went back. Nothing is worked out, so the provider is not asked
33
+ # — a cancellation must not wait on, or fail with, a tax service.
34
+ #
35
+ # @return [void]
36
+ def clear_tax!
37
+ Spree::TaxLine.post_sale.where(Spree::TaxLine.adjustable_key_for(taxed_lines.klass) => taxed_lines.select(:id)).delete_all
38
+ end
39
+
40
+ # The tax the record's lines give back, as their credit rows stand.
41
+ #
42
+ # @return [BigDecimal]
43
+ def credited_tax_total
44
+ taxed_lines.sum(0.to_d, &:credited_tax_total)
45
+ end
46
+
47
+ private
48
+
49
+ def estimate_tax(lines, amounts: nil)
50
+ order.tax_provider.estimate_refund(order, lines, amounts: amounts, tax_date: order.completed_at)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ module PostSale
3
+ # The tax side of a line on a return, claim or exchange: the rows the tax
4
+ # provider writes on it (see Spree::TaxProvider::Base#estimate_refund).
5
+ #
6
+ # Including models answer +credited_quantity+ and +credited_worth+, which
7
+ # the provider reads, and +fold_tax_lines!+, which copies the rows written
8
+ # at opening onto the line's own columns.
9
+ module TaxedLine
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ has_many :tax_lines, class_name: 'Spree::TaxLine', dependent: :destroy
14
+ end
15
+
16
+ # Whether this line's tax goes through the provider when it is refunded.
17
+ # A line opened before refunds carried tax recorded none against a taxed
18
+ # sale line, and is refunded exactly as it was opened.
19
+ #
20
+ # Read from the columns, which are written once at opening, rather than
21
+ # the rows: settling rewrites or deletes the rows, and a settle whose
22
+ # money step then failed would otherwise leave the line looking like one
23
+ # that never carried tax.
24
+ #
25
+ # @return [Boolean]
26
+ def settles_tax?
27
+ recorded_tax_total.positive? || line_item.nil? || !line_item.tax_lines.exists?
28
+ end
29
+
30
+ # The tax this line gives back, as its credit rows stand now.
31
+ #
32
+ # @return [BigDecimal]
33
+ def credited_tax_total
34
+ tax_lines.credits.sum(:amount)
35
+ end
36
+
37
+ private
38
+
39
+ # The tax copied onto the line's columns when it opened.
40
+ def recorded_tax_total
41
+ tax_total
42
+ end
43
+
44
+ # @return [Array(BigDecimal, BigDecimal)] included and additional tax
45
+ def tax_totals_from(rows)
46
+ included, additional = rows.partition(&:included?)
47
+ [included.sum(0.to_d, &:amount), additional.sum(0.to_d, &:amount)]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -93,17 +93,6 @@ module Spree
93
93
  .freeze
94
94
  end
95
95
 
96
- # Declared order first, so a form reads the way its author grouped the
97
- # fields; anything the macro did not record (an association writer that
98
- # looks like a preference) keeps its existing place at the end.
99
- def ordered_preferences(instance)
100
- defined = instance.defined_preferences
101
- return defined unless respond_to?(:declared_preference_order)
102
-
103
- declared = declared_preference_order & defined
104
- declared + (defined - declared)
105
- end
106
-
107
96
  def compute_preference_schema
108
97
  # Only instantiation is guarded. `new` touches the database to read
109
98
  # the column list, so it fails whenever the schema is asked for before
@@ -121,7 +110,7 @@ module Spree
121
110
  return nil
122
111
  end
123
112
 
124
- ordered_preferences(instance).filter_map do |pref|
113
+ instance.defined_preferences.filter_map do |pref|
125
114
  next if instance.preference_deprecated(pref)
126
115
  # Written by Spree, not supplied by the operator — a value a
127
116
  # provider hands back after we register something with it. Offering
@@ -216,6 +205,9 @@ module Spree
216
205
  # their wire shape byte-identical. Pickers stop offering a
217
206
  # superseded kind; existing rows still render and match.
218
207
  entry[:superseded] = true if klass.respond_to?(:superseded?) && klass.superseded?
208
+ # Provider families (payment methods) add their gallery logo and
209
+ # setup guide; other families keep their wire shape unchanged.
210
+ entry.merge!(klass.provider_listing) if klass.respond_to?(:provider_listing)
219
211
  entry
220
212
  end.sort_by { |entry| entry[:label] }
221
213
  end
@@ -156,7 +156,8 @@ module Spree
156
156
  where(Spree::ProductCategory.table_name => { category_id: taxon_ids }).
157
157
  select("#{Product.table_name}.*", "#{min_position_sql} AS min_taxon_position").
158
158
  group("#{Product.table_name}.id").
159
- order(Arel.sql("#{min_position_sql} ASC"))
159
+ # Ties fall back to id, so the order holds across queries and pages.
160
+ order(Arel.sql("#{min_position_sql} ASC"), arel_table[:id].asc)
160
161
  }
161
162
 
162
163
  scope :with_option_value, ->(option, value) {
@@ -218,6 +219,15 @@ module Spree
218
219
  channel = Spree::Current.channel
219
220
  return where(discontinue_on: [nil, Time.current.beginning_of_minute..]) unless channel
220
221
 
222
+ not_discontinued_on(channel)
223
+ end
224
+
225
+ # Published on the channel and not yet unpublished from it — a channel
226
+ # publication's end date is the product's discontinuation there.
227
+ #
228
+ # @param channel [Spree::Channel]
229
+ # @return [ActiveRecord::Relation<Spree::Product>]
230
+ def self.not_discontinued_on(channel)
221
231
  for_channel(channel).where(Spree::ProductPublication.table_name => { unpublished_at: [nil, Time.current.beginning_of_minute..] })
222
232
  end
223
233
 
@@ -0,0 +1,41 @@
1
+ module Spree
2
+ # What a provider shows on its gallery card in the dashboard: a logo and a
3
+ # link to its setup guide. Shared by integrations and payment methods so a
4
+ # provider gem declares both the same way whichever family it extends.
5
+ #
6
+ # class SpreeStripe::Gateway < Spree::Gateway
7
+ # def self.logo_url = 'https://example.com/stripe.png'
8
+ # def self.docs_url = 'https://spreecommerce.org/docs/integrations/payments/stripe'
9
+ # end
10
+ module ProviderListing
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ # Logo shown on the gallery card: an absolute URL to publicly hosted
15
+ # brand assets, or a `data:` URI for gems that want to be self-contained
16
+ # (works air-gapped, no CSP domain to allowlist). Anything an `<img src>`
17
+ # accepts. Deliberately not an asset-pipeline path — provider gems must
18
+ # not force an asset pipeline onto headless API hosts. Hosted logos are a
19
+ # courtesy, not a guarantee: the dashboard falls back to a letter avatar
20
+ # when unset or unreachable.
21
+ #
22
+ # @return [String, nil]
23
+ def logo_url
24
+ nil
25
+ end
26
+
27
+ # Absolute URL of the provider's setup guide, linked from its gallery
28
+ # card. A third-party gem points at its own documentation.
29
+ #
30
+ # @return [String, nil]
31
+ def docs_url
32
+ nil
33
+ end
34
+
35
+ # @return [Hash] the listing attributes merged into a type-discovery entry
36
+ def provider_listing
37
+ { logo_url: logo_url, docs_url: docs_url }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -36,9 +36,12 @@ module Spree
36
36
  module Publishable
37
37
  extend ActiveSupport::Concern
38
38
 
39
+ LIFECYCLE_ACTIONS = { create: :created, update: :updated, delete: :deleted }.freeze
40
+
39
41
  included do
40
42
  class_attribute :publish_events, default: true
41
43
  class_attribute :lifecycle_events_enabled, default: false
44
+ class_attribute :lifecycle_event_actions, default: []
42
45
 
43
46
  after_update :mark_real_update_for_events
44
47
  end
@@ -76,6 +79,9 @@ module Spree
76
79
  events &= Array(options[:only]) if options[:only]
77
80
  events -= Array(options[:except]) if options[:except]
78
81
 
82
+ self.lifecycle_event_actions = events.map { |event| LIFECYCLE_ACTIONS.fetch(event) }
83
+ lifecycle_event_actions.each { |action| publishes_event(action) }
84
+
79
85
  if events.include?(:create)
80
86
  after_commit :publish_create_event, on: :create, if: :should_publish_events?
81
87
  end
@@ -90,6 +96,51 @@ module Spree
90
96
  end
91
97
  end
92
98
 
99
+ # A subclass publishes its lifecycle events under its own prefix, which may
100
+ # differ from its parent's, so they are declared again for it.
101
+ def inherited(subclass)
102
+ super
103
+ subclass.lifecycle_event_actions.each { |action| subclass.publishes_event(action) }
104
+ end
105
+
106
+ # Declare events this model publishes, each carrying this model's Store
107
+ # serializer as its payload.
108
+ #
109
+ # @param names [Array<Symbol, String>] actions (`:placed`, prefixed with
110
+ # {event_prefix}) or full dotted names (`'customer.password_reset'`)
111
+ # @param options [Hash] see {publishes_event}
112
+ # @return [void]
113
+ #
114
+ # @example
115
+ # publishes_events :placed, :paid, :shipped
116
+ def publishes_events(*names, **options)
117
+ names.each { |name| publishes_event(name, **options) }
118
+ end
119
+
120
+ # Declare one event this model publishes. Publishing a name no model
121
+ # declared raises in development and test.
122
+ #
123
+ # @param name [Symbol, String] an action or a full dotted name
124
+ # @param serializer [String, nil] payload serializer class name, when the
125
+ # payload is not this model's Store serializer
126
+ # @param credential [String, nil] for a payload carrying a live credential,
127
+ # the permission key needed to point a webhook endpoint at it; such an
128
+ # event reaches only endpoints that name it outright
129
+ # @param deprecated_alias_of [String, nil] the event this one duplicates
130
+ # @param webhook [Boolean] false for an event no webhook endpoint may
131
+ # receive, even one subscribed to `*`
132
+ # @return [void]
133
+ #
134
+ # @example
135
+ # publishes_event :canceled, serializer: 'Spree::Api::V3::OrderCanceledEventSerializer'
136
+ # publishes_event 'customer.password_reset_requested', serializer: '...', credential: 'write_customers'
137
+ def publishes_event(name, serializer: nil, credential: nil, deprecated_alias_of: nil, webhook: true)
138
+ Spree::Events.catalog.declare(
139
+ self, name,
140
+ serializer: serializer, credential: credential, deprecated_alias_of: deprecated_alias_of, webhook: webhook
141
+ )
142
+ end
143
+
93
144
  # Disable lifecycle events for this model
94
145
  #
95
146
  # @example
@@ -145,10 +196,13 @@ module Spree
145
196
  # order.publish_event('order.placed', metadata: { user_id: 1 })
146
197
  #
147
198
  def publish_event(event_name, payload = nil, metadata = {})
199
+ # Checked even while events are disabled, so every spec that reaches a
200
+ # publish also proves the event is declared.
201
+ Spree::Events.catalog.verify_declared!(event_name)
148
202
  return unless Spree::Events.enabled?
149
203
 
150
204
  @_current_event_name = event_name
151
- payload ||= event_payload
205
+ payload ||= event_payload_for(event_name)
152
206
  Spree::Events.publish(event_name, payload, metadata)
153
207
  ensure
154
208
  @_current_event_name = nil
@@ -161,20 +215,19 @@ module Spree
161
215
  #
162
216
  # @return [Hash]
163
217
  def event_payload
164
- serializer = event_serializer_class
165
-
166
- unless serializer
167
- return {
168
- id: respond_to?(:prefixed_id) ? prefixed_id : id,
169
- created_at: created_at&.iso8601,
170
- updated_at: updated_at&.iso8601
171
- }
172
- end
218
+ build_event_payload(event_serializer_class)
219
+ end
173
220
 
174
- # Use as_json to ensure all values are JSON-safe primitives.
175
- # Alba's to_h can return raw Ruby objects (e.g., Spree::Money) which
176
- # ActiveJob cannot serialize for async event subscribers.
177
- serializer.new(self, params: event_serializer_params).to_h.as_json
221
+ # The payload for one event, built with the serializer the catalog declares
222
+ # for it, falling back to this model's Store serializer.
223
+ #
224
+ # @param event_name [String]
225
+ # @param params [Hash] extra serializer params the event serializer reads,
226
+ # e.g. the reset token of a password reset request
227
+ # @return [Hash]
228
+ def event_payload_for(event_name, **params)
229
+ serializer = Spree::Events.catalog.find(event_name)&.serializer_name&.safe_constantize
230
+ build_event_payload(serializer || event_serializer_class, params)
178
231
  end
179
232
 
180
233
  # Find the event serializer class for this model
@@ -223,6 +276,21 @@ module Spree
223
276
 
224
277
  private
225
278
 
279
+ def build_event_payload(serializer, params = {})
280
+ unless serializer
281
+ return {
282
+ id: respond_to?(:prefixed_id) ? prefixed_id : id,
283
+ created_at: created_at&.iso8601,
284
+ updated_at: updated_at&.iso8601
285
+ }
286
+ end
287
+
288
+ # Use as_json to ensure all values are JSON-safe primitives.
289
+ # Alba's to_h can return raw Ruby objects (e.g., Spree::Money) which
290
+ # ActiveJob cannot serialize for async event subscribers.
291
+ serializer.new(self, params: event_serializer_params.merge(params)).to_h.as_json
292
+ end
293
+
226
294
  # Build params for V3 serializers
227
295
  #
228
296
  # @return [Hash]