spree_core 6.0.0.beta3 → 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
@@ -59,17 +59,17 @@ module Spree
59
59
 
60
60
  return false if preferred_stock_location_id.present?
61
61
 
62
- # An address is needed when any item's profile ships to one —
63
- # answered by the profile kind, resolved once per profile.
64
- profile_requires_address = Hash.new do |cache, profile|
65
- cache[profile] = profile.present? && profile.requires_shipping_address?
66
- end
62
+ items_require_shipping_address?
63
+ end
67
64
 
68
- # Both profile paths are preloaded: the variant's own override, and the
69
- # product's for the variants that inherit it.
70
- line_items.includes(variant: [:delivery_profile, { product: :delivery_profile }]).any? do |line_item|
71
- profile_requires_address[line_item.variant&.resolved_delivery_profile]
72
- end
65
+ # Whether delivery can be proposed yet: once there is a destination (a
66
+ # shipping address, or a pickup intent for a purchase that never gets
67
+ # one), or straight away when no item ships to an address. Quoted
68
+ # without a destination, only pickup qualifies.
69
+ #
70
+ # @return [Boolean]
71
+ def can_propose_deliveries?
72
+ ship_address.present? || preferred_stock_location_id.present? || !items_require_shipping_address?
73
73
  end
74
74
 
75
75
  # @deprecated The shipping address is canonical — use +use_shipping+ to
@@ -174,6 +174,19 @@ module Spree
174
174
 
175
175
  private
176
176
 
177
+ def items_require_shipping_address?
178
+ # Answered by the profile kind, resolved once per profile.
179
+ profile_requires_address = Hash.new do |cache, profile|
180
+ cache[profile] = profile.present? && profile.requires_shipping_address?
181
+ end
182
+
183
+ # Both profile paths are preloaded: the variant's own override, and the
184
+ # product's for the variants that inherit it.
185
+ line_items.includes(variant: [:delivery_profile, { product: :delivery_profile }]).any? do |line_item|
186
+ profile_requires_address[line_item.variant&.resolved_delivery_profile]
187
+ end
188
+ end
189
+
177
190
  # The address behind an id a client may select, or nil when the id names
178
191
  # one this purchase has no business shipping to.
179
192
  #
@@ -15,7 +15,7 @@ module Spree
15
15
 
16
16
  supported_codes = store.supported_currencies_list.map(&:iso_code)
17
17
  unless supported_codes.include?(currency)
18
- errors.add(:currency, :currency_not_supported_by_store, message: Spree.t(:currency_not_supported_by_store))
18
+ errors.add(:currency, :currency_not_supported_by_store, message: I18n.t('spree.currency_not_supported_by_store'))
19
19
  end
20
20
  end
21
21
 
@@ -0,0 +1,87 @@
1
+ module Spree
2
+ module Purchase
3
+ # One-release bridges for the names renamed in 6.0, shared by
4
+ # {Spree::Cart} and {Spree::Order}. Before 6.0 a cart was an incomplete
5
+ # order, so extensions written against 5.x (payment gateways, checkout
6
+ # plugins) call the legacy names on carts too. Every legacy name warns and
7
+ # forwards to its replacement. Removed in 6.1.
8
+ #
9
+ # Renamed columns are also registered with +alias_attribute+, so the legacy
10
+ # name keeps working in +where+ / +find_by+ (those paths do not warn).
11
+ module DeprecatedAliases
12
+ extend ActiveSupport::Concern
13
+
14
+ RENAMED_ATTRIBUTES = {
15
+ promo_total: :discount_total,
16
+ item_count: :total_quantity,
17
+ shipment_total: :delivery_total,
18
+ ship_total: :delivery_total,
19
+ special_instructions: :customer_note
20
+ }.freeze
21
+
22
+ RENAMED_METHODS = {
23
+ display_promo_total: :display_discount_total,
24
+ display_shipment_total: :display_delivery_total,
25
+ display_ship_total: :display_delivery_total,
26
+ shipping_discount: :fulfillment_discount,
27
+ delivery_required?: :delivery_step_required?,
28
+ requires_ship_address?: :shipping_address_required?,
29
+ set_shipments_cost: :set_fulfillments_cost,
30
+ ensure_updated_shipments: :ensure_updated_fulfillments,
31
+ create_proposed_fulfillments: :rebuild_fulfillments!,
32
+ create_proposed_shipments: :rebuild_fulfillments!,
33
+ associate_user!: :associate_customer!,
34
+ update_with_updater!: :recalculate_totals!,
35
+ all_line_items: :line_items
36
+ }.freeze
37
+
38
+ # 5.x took a store argument; payment methods now always come from the
39
+ # purchase's own store.
40
+ RENAMED_PAYMENT_METHOD_READERS = %i[
41
+ available_payment_methods
42
+ collect_payment_methods
43
+ collect_frontend_payment_methods
44
+ ].freeze
45
+
46
+ included do
47
+ include Spree::DeprecatedCustomerAlias
48
+
49
+ RENAMED_ATTRIBUTES.each do |legacy_name, current_name|
50
+ alias_attribute legacy_name, current_name
51
+ end
52
+ end
53
+
54
+ RENAMED_ATTRIBUTES.each do |legacy_name, current_name|
55
+ define_method(legacy_name) do
56
+ warn_deprecated_name(legacy_name, current_name)
57
+ public_send(current_name)
58
+ end
59
+
60
+ define_method(:"#{legacy_name}=") do |value|
61
+ warn_deprecated_name(:"#{legacy_name}=", :"#{current_name}=")
62
+ public_send(:"#{current_name}=", value)
63
+ end
64
+ end
65
+
66
+ RENAMED_METHODS.each do |legacy_name, current_name|
67
+ define_method(legacy_name) do |*args, **options, &block|
68
+ warn_deprecated_name(legacy_name, current_name)
69
+ public_send(current_name, *args, **options, &block)
70
+ end
71
+ end
72
+
73
+ RENAMED_PAYMENT_METHOD_READERS.each do |legacy_name|
74
+ define_method(legacy_name) do |*|
75
+ warn_deprecated_name(legacy_name, :payment_methods)
76
+ payment_methods
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def warn_deprecated_name(legacy_name, current_name)
83
+ Spree::Deprecation.warn("#{self.class.name}##{legacy_name} is deprecated and will be removed in Spree 6.1. Use ##{current_name} instead.")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -51,16 +51,22 @@ module Spree
51
51
 
52
52
  payment = payments.checkout.store_credits.where(source: gift_card.store_credits).first
53
53
  return unless payment
54
+ # Nothing is left for the card to pay, and a store credit cannot hold
55
+ # a zero amount, so the card is released rather than resized.
56
+ return remove_gift_card unless total.positive?
54
57
 
55
58
  gift_card.with_lock do
56
- new_amount = [gift_card.amount_remaining + payment.amount, total].min
59
+ # A locking read: a plain reload can return the transaction's older
60
+ # snapshot, and a resize committed since would be applied again.
61
+ payment.lock!
62
+ new_amount = [gift_card.spendable_amount + payment.amount, total].min
57
63
  next if payment.amount == new_amount
58
64
 
59
65
  difference = new_amount - payment.amount
60
- # update_column bypasses Payment#max_amount validation which can
61
- # fail on stale in-memory state; bounds enforced via min() above.
62
- payment.update_column(:amount, new_amount)
63
66
  payment.source.update_column(:amount, new_amount)
67
+ # Skips Payment#max_amount, which can fail on stale in-memory state;
68
+ # min() above bounds the amount.
69
+ payment.update_store_credit_amount!(new_amount)
64
70
  gift_card.amount_used += difference
65
71
  gift_card.save!
66
72
  end
@@ -22,7 +22,7 @@ module Spree
22
22
  return if locale.blank? || store.blank?
23
23
 
24
24
  unless store.supported_locales_list.include?(locale)
25
- errors.add(:locale, :locale_not_supported_by_store, message: Spree.t(:locale_not_supported_by_store))
25
+ errors.add(:locale, :locale_not_supported_by_store, message: I18n.t('spree.locale_not_supported_by_store'))
26
26
  end
27
27
  end
28
28
  end
@@ -31,11 +31,15 @@ module Spree
31
31
  payments.pending
32
32
  end
33
33
 
34
- # Active front-end payment methods available for this record.
34
+ # Active front-end payment methods available for this record. Store
35
+ # credit is never one of them: it is applied through its own endpoint
36
+ # before the buyer chooses how to pay the rest.
35
37
  #
36
38
  # @return [Array<Spree::PaymentMethod>]
37
39
  def payment_methods
38
- store.payment_methods.active.storefront_visible.select { |payment_method| payment_method.available_for_order?(self) }
40
+ store.payment_methods.active.storefront_visible.select do |payment_method|
41
+ !payment_method.store_credit? && payment_method.available_for_order?(self)
42
+ end
39
43
  end
40
44
 
41
45
  # Free checkouts have no money to collect.
@@ -71,7 +75,7 @@ module Spree
71
75
  # Don't run if there are authorized payments
72
76
  return if pending_payments.any? && unprocessed_payments.empty?
73
77
  # Never complete without a successfully processed payment.
74
- raise Spree::Core::GatewayError, Spree.t(:no_payment_found) if unprocessed_payments.empty?
78
+ raise Spree::Core::GatewayError, I18n.t('spree.no_payment_found') if unprocessed_payments.empty?
75
79
 
76
80
  unprocessed_payments.each do |payment|
77
81
  break if settled_payment_total >= total
@@ -83,8 +83,8 @@ module Spree
83
83
  return if stored_size <= MAX_PO_DOCUMENT_SIZE
84
84
 
85
85
  errors.add(
86
- :po_document, :po_document_too_large, message: Spree.t(
87
- :po_document_too_large,
86
+ :po_document, :po_document_too_large, message: I18n.t(
87
+ 'spree.po_document_too_large',
88
88
  size: ActiveSupport::NumberHelper.number_to_human_size(MAX_PO_DOCUMENT_SIZE)
89
89
  )
90
90
  )
@@ -10,9 +10,37 @@ module Spree
10
10
  line_items.sum(:quantity)
11
11
  end
12
12
 
13
+ # What promotion calculators take a share of: the line amounts less the
14
+ # free gifts, so a gift never enlarges a percentage off the order.
15
+ #
13
16
  # @return [BigDecimal]
14
17
  def amount
15
- line_items.sum(BigDecimal('0'), &:amount)
18
+ line_items.sum(BigDecimal('0'), &:amount) - gift_amount
19
+ end
20
+
21
+ # The list price of the units gift promotions give away. The gift keeps
22
+ # its price on its line and is paid for by a discount, so anything
23
+ # measuring what the shopper spends has to take it out.
24
+ #
25
+ # @param promotion [Spree::Promotion, nil] the promotion doing the
26
+ # measuring; its own gifts are always taken out, since a gift cannot
27
+ # be what qualifies the promotion giving it away
28
+ # @param own_gift_only [Boolean] true while deciding whether another
29
+ # promotion gives its gift, so two gift promotions with spend
30
+ # thresholds do not each wait on the other
31
+ # @return [BigDecimal]
32
+ def gift_amount(promotion: nil, own_gift_only: false)
33
+ return BigDecimal('0') if line_items.empty?
34
+
35
+ excluded_gift_promo_actions = gift_promo_actions(promotion, own_gift_only).select do |action|
36
+ action.promotion_id == promotion&.id || action.gives_away?(self)
37
+ end
38
+ return BigDecimal('0') if excluded_gift_promo_actions.empty?
39
+
40
+ line_items.sum(BigDecimal('0')) do |line_item|
41
+ gifted_quantity = excluded_gift_promo_actions.sum { |action| action.gifted_quantity_of(line_item) }
42
+ line_item.price * [gifted_quantity, line_item.quantity].min
43
+ end
16
44
  end
17
45
 
18
46
  # Re-sums what the customer has actually paid, and nothing else. A
@@ -78,6 +106,26 @@ module Spree
78
106
 
79
107
  private
80
108
 
109
+ # Other promotions' gifts are looked for among the promotions a
110
+ # recalculation writes discounts for, so a gift leaves the measure
111
+ # exactly when its discount can be written.
112
+ def gift_promo_actions(promotion, own_gift_only)
113
+ own = promotion ? promotion.actions.grep(Spree::Promotion::Actions::CreateLineItems) : []
114
+ return own if own_gift_only
115
+
116
+ discounting_ids = promotion_ids + Spree::Promotion.held_by_saved_coupon_code(self).map(&:id)
117
+ return own if discounting_ids.empty?
118
+
119
+ gifting_cart_variants = Spree::PromotionActionLineItem.
120
+ where(variant_id: line_items.map(&:variant_id)).
121
+ select(:promotion_action_id)
122
+ others = Spree::Promotion::Actions::CreateLineItems.
123
+ where(promotion_id: discounting_ids, id: gifting_cart_variants).
124
+ includes(:promotion, :promotion_action_line_items)
125
+
126
+ (own + others.to_a).uniq(&:id)
127
+ end
128
+
81
129
  # Completed payments less their refunds, as a scalar subquery so the
82
130
  # sum and the write are one statement.
83
131
  #
@@ -97,7 +97,7 @@ module Spree
97
97
  variant_ids = items.reject(&:marked_for_destruction?).filter_map(&:variant_id)
98
98
  return if variant_ids.uniq.size == variant_ids.size
99
99
 
100
- errors.add(:items, :duplicate_variant, message: Spree.t('errors.messages.duplicate_variant'))
100
+ errors.add(:items, :duplicate_variant, message: I18n.t('spree.errors.messages.duplicate_variant'))
101
101
  end
102
102
  end
103
103
  end
@@ -25,10 +25,6 @@ module Spree
25
25
  model_class.arel_table[attribute.to_sym].lower.matches("%#{escaped}%", '\\')
26
26
  end
27
27
  end
28
-
29
- # Backward compatibility aliases — remove in Spree 6.0
30
- def self.sanitize_query_for_multi_search(query) = sanitize_query_for_search(query)
31
- def self.multi_search_condition(model_class, attribute, query) = search_condition(model_class, attribute, query)
32
28
  end
33
29
  end
34
30
  end
@@ -0,0 +1,59 @@
1
+ module Spree
2
+ # Stores a model's `:password` preferences in a separate `secret_preferences`
3
+ # text column, encrypted with Active Record encryption whenever keys are
4
+ # configured, so the `preferences` column never holds a secret. Declarations
5
+ # are unchanged: `preference :secret_key, :password` and
6
+ # `preferred_secret_key` read and write the encrypted column.
7
+ #
8
+ # Values written before encryption was enabled stay readable and are
9
+ # encrypted on the next save, or at once by
10
+ # `spree:upgrade:encrypt_secret_preferences`.
11
+ module SecretPreferences
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ serialize :secret_preferences, coder: TextHashSerializer
16
+
17
+ if ActiveRecord::Encryption.config.has_primary_key?
18
+ encrypts :secret_preferences, support_unencrypted_data: true
19
+ end
20
+
21
+ before_save :move_secret_preferences
22
+ end
23
+
24
+ # The column is `text`, since ciphertext cannot live in a JSON column. An
25
+ # empty column stays nil rather than `{}`: encrypted attributes compare the
26
+ # stored value with the loaded one, so `{}` would mark every record without
27
+ # secrets as changed.
28
+ class TextHashSerializer
29
+ def self.dump(hash)
30
+ ActiveSupport::JSON.encode(hash) unless hash.nil?
31
+ end
32
+
33
+ def self.load(json)
34
+ Spree::Metadata::HashSerializer.load(ActiveSupport::JSON.decode(json)) unless json.nil?
35
+ end
36
+ end
37
+
38
+ # Both columns as they should be stored, when `preferences` holds a secret
39
+ # — one assigned as part of a whole hash, or one the upgrade has not moved
40
+ # yet. A secret found there is the newer value and wins.
41
+ #
42
+ # @return [Hash{Symbol => Hash}, nil] `preferences:` and `secret_preferences:`, or nil when there is nothing to move
43
+ def separated_secret_preferences
44
+ secrets = (preferences || {}).slice(*self.class.secret_preference_names)
45
+ return if secrets.empty?
46
+
47
+ { preferences: preferences.except(*secrets.keys), secret_preferences: (secret_preferences || {}).merge(secrets) }
48
+ end
49
+
50
+ private
51
+
52
+ # Keeps a secret out of the plain `preferences` column when the record is
53
+ # saved.
54
+ def move_secret_preferences
55
+ separated = separated_secret_preferences
56
+ assign_attributes(separated) if separated
57
+ end
58
+ end
59
+ end
@@ -19,7 +19,7 @@ module Spree
19
19
 
20
20
  def ensure_store_association_is_not_changed
21
21
  if store_id_changed? && persisted?
22
- errors.add(:store, :store_association_can_not_be_changed, message: Spree.t('errors.messages.store_association_can_not_be_changed'))
22
+ errors.add(:store, :store_association_can_not_be_changed, message: I18n.t('spree.errors.messages.store_association_can_not_be_changed'))
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Stores
3
+ # The colors and font of a store's customer emails (see Spree::Emails::Branding).
4
+ # All optional: a blank one keeps the shipped design.
5
+ module EmailBranding
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ Spree::Emails::Branding.attribute_names.each { |name| preference :"email_#{name}", :string }
10
+
11
+ validate :email_branding_valid
12
+ end
13
+
14
+ # @param overrides [Hash] unsaved values, e.g. to preview them
15
+ # @return [Spree::Emails::Branding]
16
+ def email_branding(overrides = {})
17
+ saved = Spree::Emails::Branding.attribute_names.to_h { |name| [name, public_send(:"preferred_email_#{name}")] }
18
+ Spree::Emails::Branding.new(saved.merge(overrides.to_h.stringify_keys.slice(*saved.keys)))
19
+ end
20
+
21
+ private
22
+
23
+ def email_branding_valid
24
+ branding = email_branding
25
+ return if branding.valid?
26
+
27
+ branding.errors.each { |error| errors.add(:"preferred_email_#{error.attribute}", error.type) }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ module Spree
2
+ # Names and describes each kind in a family of typed subclasses (promotion
3
+ # rules, collection rules, …) from one translation scope keyed by
4
+ # +api_type+, so the `/types` endpoints and dashboard pickers label
5
+ # extension kinds the same way as built-in ones.
6
+ #
7
+ # class Spree::PromotionRule < Spree.base_class
8
+ # include Spree::TypeLabels
9
+ # self.type_labels_scope = 'spree.promotion_rule_types'
10
+ # end
11
+ #
12
+ # Spree::Promotion::Rules::Currency.human_name # => spree.promotion_rule_types.currency.name
13
+ module TypeLabels
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ class_attribute :type_labels_scope, instance_writer: false
18
+ end
19
+
20
+ class_methods do
21
+ # @return [String] the kind's name, falling back to its humanized +api_type+
22
+ def human_name
23
+ I18n.t("#{type_labels_scope}.#{api_type}.name", default: api_type.titleize)
24
+ end
25
+
26
+ # @return [String] what the kind does, empty when it has no translation
27
+ def human_description
28
+ I18n.t("#{type_labels_scope}.#{api_type}.description", default: '')
29
+ end
30
+ end
31
+
32
+ # @return [String]
33
+ def human_name = self.class.human_name
34
+
35
+ # @return [String]
36
+ def human_description = self.class.human_description
37
+ end
38
+ end
@@ -49,7 +49,7 @@ module Spree
49
49
  private
50
50
 
51
51
  def exactly_one_owner
52
- errors.add(:base, :exactly_one_of_cart_or_order, message: Spree.t('errors.messages.exactly_one_of_cart_or_order')) unless [order, cart].compact.one?
52
+ errors.add(:base, :exactly_one_of_cart_or_order, message: I18n.t('spree.errors.messages.exactly_one_of_cart_or_order')) unless [order, cart].compact.one?
53
53
  end
54
54
  end
55
55
  end
@@ -93,6 +93,12 @@ module Spree
93
93
  record.save! if record.changed?
94
94
 
95
95
  record.id
96
+ rescue ActiveRecord::RecordInvalid => e
97
+ # A row saved from the owner's `after_save` fails inside the owner's own
98
+ # save, and a plain `save` swallows the exception — so its errors are
99
+ # copied onto the owner, or the caller learns only that something failed.
100
+ e.record.errors.full_messages.each { |message| errors.add(:base, message) }
101
+ raise
96
102
  end
97
103
 
98
104
  # Decode `*_ids` array preferences (`customer_group_ids`, `user_ids`,
@@ -7,7 +7,7 @@ module Spree
7
7
  include Spree::HasCustomFields
8
8
  include Spree::Metadata
9
9
 
10
- serialize :preferences, type: Hash, coder: YAML, default: {}
10
+ serialize :preferences, coder: Spree::Metadata::HashSerializer, default: {}
11
11
 
12
12
  NO_ZIPCODE_ISO_CODES ||= [
13
13
  'AO', 'AG', 'AW', 'BS', 'BZ', 'BJ', 'BM', 'BO', 'BW', 'BF', 'BI', 'CM', 'CF', 'KM', 'CG',
@@ -27,6 +27,10 @@ module Spree
27
27
  # those attributes depending of the logic of their applications
28
28
  ADDRESS_FIELDS = %w(first_name last_name company address1 address2 city state postal_code country phone)
29
29
 
30
+ # The columns a delivery quote reads. A change to anything else (a phone
31
+ # number, the recipient's name) leaves the parcel going where it was.
32
+ DESTINATION_FIELDS = %w(address1 address2 city state_code postal_code country_code).freeze
33
+
30
34
  # Beyond identity and ownership, three groups are excluded because they
31
35
  # cannot tell two addresses apart. Latitude and longitude are derived from
32
36
  # the address, so an entry whose geocoding has already run would otherwise
@@ -379,6 +383,12 @@ module Spree
379
383
  attributes.except(*EXCLUDED_KEYS_FOR_COMPARISON)
380
384
  end
381
385
 
386
+ # @return [Boolean] whether the last save moved this address somewhere
387
+ # else, as opposed to correcting who receives the parcel
388
+ def saved_change_to_destination?
389
+ saved_changes.keys.intersect?(DESTINATION_FIELDS)
390
+ end
391
+
382
392
  # A country alone doesn't make an address — a new one is pre-filled with the
383
393
  # store's default, so both the code and the association are ignored here.
384
394
  def empty?
@@ -65,22 +65,8 @@ module Spree
65
65
  end
66
66
  end
67
67
 
68
- # The owner's PERSISTED coupon code keeps its promotion in candidacy
69
- # even before it ever applied — the discount activates on the exact
70
- # recalculation where the cart first qualifies, and deactivates the
71
- # same way (Shopify-parity for cart-level discount codes). In-memory
72
- # assignments deliberately don't participate: unsaved codes belong to
73
- # the explicit PromotionHandler::Coupon path.
74
68
  def coupon_promotions
75
- return [] unless order.class.respond_to?(:column_names) && order.class.column_names.include?('coupon_code')
76
-
77
- code = order.read_attribute(:coupon_code)
78
- return [] if code.blank?
79
-
80
- promotion = order.store.promotions.active.with_coupon_code(code)
81
- return [] if promotion.nil? || promotion.usage_limit_exceeded?(order)
82
-
83
- [promotion]
69
+ Spree::Promotion.held_by_saved_coupon_code(order)
84
70
  end
85
71
 
86
72
  # A coupon promotion that produced rows becomes an applied promotion —
@@ -10,7 +10,6 @@ module Spree
10
10
 
11
11
  include Spree::SingleStoreResource
12
12
 
13
- belongs_to :store, class_name: 'Spree::Store'
14
13
 
15
14
  validates :origin, presence: true
16
15
  validates :origin, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
@@ -44,7 +44,6 @@ module Spree
44
44
  publishable? ? token : @plaintext_token
45
45
  end
46
46
 
47
- belongs_to :store, class_name: 'Spree::Store'
48
47
  # Optional single-channel binding for publishable keys. A bound key
49
48
  # server-assigns the request's channel (see Api::V3::ChannelResolution),
50
49
  # making channel identity unforgeable instead of client-asserted via the
@@ -162,23 +161,23 @@ module Spree
162
161
  return if invalid.empty?
163
162
 
164
163
  errors.add(:scopes, :unknown_scopes, scopes: invalid.join(', '),
165
- message: Spree.t('errors.messages.unknown_scopes', scopes: invalid.join(', ')))
164
+ message: I18n.t('spree.errors.messages.unknown_scopes', scopes: invalid.join(', ')))
166
165
  end
167
166
 
168
167
  def scopes_immutable
169
- errors.add(:scopes, :api_key_scopes_immutable, message: Spree.t(:api_key_scopes_immutable))
168
+ errors.add(:scopes, :api_key_scopes_immutable, message: I18n.t('spree.api_key_scopes_immutable'))
170
169
  end
171
170
 
172
171
  def channel_binding_valid
173
172
  if secret?
174
- errors.add(:channel, :api_key_channel_publishable_only, message: Spree.t(:api_key_channel_publishable_only))
173
+ errors.add(:channel, :api_key_channel_publishable_only, message: I18n.t('spree.api_key_channel_publishable_only'))
175
174
  elsif channel && channel.store_id != store_id
176
- errors.add(:channel, :api_key_channel_must_belong_to_store, message: Spree.t(:api_key_channel_must_belong_to_store))
175
+ errors.add(:channel, :api_key_channel_must_belong_to_store, message: I18n.t('spree.api_key_channel_must_belong_to_store'))
177
176
  end
178
177
  end
179
178
 
180
179
  def channel_immutable
181
- errors.add(:channel, :api_key_channel_immutable, message: Spree.t(:api_key_channel_immutable))
180
+ errors.add(:channel, :api_key_channel_immutable, message: I18n.t('spree.api_key_channel_immutable'))
182
181
  end
183
182
 
184
183
  # Generates the token on creation. For publishable keys, stores the raw token
@@ -129,7 +129,7 @@ module Spree
129
129
  # Password login is not available through an OIDC provider — the browser
130
130
  # redirect is the only entry point.
131
131
  def authenticate
132
- failure(Spree.t('errors.messages.oidc_requires_redirect'))
132
+ failure(I18n.t('spree.errors.messages.oidc_requires_redirect'))
133
133
  end
134
134
 
135
135
  # Completes the login started by +#authorization_url+: exchanges the
@@ -138,13 +138,13 @@ module Spree
138
138
  # @return [Spree::ServiceModule::Result]
139
139
  def callback
140
140
  code = params[:code]
141
- return authentication_failure(Spree.t('errors.messages.oidc_missing_code')) if code.blank?
141
+ return authentication_failure(I18n.t('spree.errors.messages.oidc_missing_code')) if code.blank?
142
142
 
143
143
  claims = verify_id_token(exchange_code_for_tokens(code).fetch('id_token'))
144
144
  resolve_user(claims)
145
145
  rescue StandardError => e
146
146
  Rails.logger.error("[Spree] OIDC callback failed for #{config[:label] || config[:issuer]}: #{e.message}")
147
- authentication_failure(Spree.t('errors.messages.oidc_authentication_failed'))
147
+ authentication_failure(I18n.t('spree.errors.messages.oidc_authentication_failed'))
148
148
  end
149
149
 
150
150
  # True when the last +#callback+ failed because no account here is
@@ -168,7 +168,7 @@ module Spree
168
168
  # every member of a corporate tenant would become store staff.
169
169
  def resolve_user(claims)
170
170
  subject = claims['sub']
171
- return authentication_failure(Spree.t('errors.messages.oidc_authentication_failed')) if subject.blank?
171
+ return authentication_failure(I18n.t('spree.errors.messages.oidc_authentication_failed')) if subject.blank?
172
172
 
173
173
  identity = Spree::UserIdentity.find_by(
174
174
  provider: registry_key,
@@ -191,7 +191,7 @@ module Spree
191
191
 
192
192
  def not_provisioned_failure
193
193
  @account_not_provisioned = true
194
- failure(Spree.t('errors.messages.oidc_account_not_provisioned'))
194
+ failure(I18n.t('spree.errors.messages.oidc_account_not_provisioned'))
195
195
  end
196
196
 
197
197
  # Only an email the provider asserts as verified may claim an existing