spree_core 6.0.0.beta4 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (575) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/app/helpers/spree/images_helper.rb +0 -57
  4. data/app/jobs/spree/base_job.rb +21 -0
  5. data/app/jobs/spree/carts/reap_expired_job.rb +2 -2
  6. data/app/jobs/spree/collections/regenerate_time_based_job.rb +6 -3
  7. data/app/jobs/spree/orders/finalize_stale_drafts_job.rb +8 -5
  8. data/app/jobs/spree/payments/cancel_unused_sessions_job.rb +61 -0
  9. data/app/jobs/spree/search_provider/index_job.rb +4 -0
  10. data/app/jobs/spree/seller_payouts/sweep_seller_job.rb +2 -0
  11. data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
  12. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  13. data/app/mailers/spree/base_mailer.rb +97 -13
  14. data/app/mailers/spree/email_template_mailer.rb +27 -0
  15. data/app/mailers/spree/export_mailer.rb +3 -4
  16. data/app/mailers/spree/import_mailer.rb +3 -4
  17. data/app/mailers/spree/invitation_mailer.rb +24 -7
  18. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  19. data/app/mailers/spree/webhook_mailer.rb +2 -2
  20. data/app/models/concerns/spree/acted_by.rb +1 -1
  21. data/app/models/concerns/spree/admin_user_methods.rb +15 -1
  22. data/app/models/concerns/spree/api_typed.rb +91 -0
  23. data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
  24. data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
  25. data/app/models/concerns/spree/calculator/tiers.rb +63 -0
  26. data/app/models/concerns/spree/canonical_decimals.rb +61 -0
  27. data/app/models/concerns/spree/customer_methods.rb +23 -18
  28. data/app/models/concerns/spree/derived_number.rb +11 -13
  29. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  30. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  31. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  32. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  33. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  34. data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
  35. data/app/models/concerns/spree/preference_schema.rb +62 -32
  36. data/app/models/concerns/spree/product_scopes.rb +16 -2
  37. data/app/models/concerns/spree/provider_listing.rb +41 -0
  38. data/app/models/concerns/spree/publishable.rb +82 -14
  39. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  40. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  41. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  42. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  43. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  44. data/app/models/concerns/spree/purchase/payment_processing.rb +8 -4
  45. data/app/models/concerns/spree/purchase/purchase_order.rb +9 -4
  46. data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
  47. data/app/models/concerns/spree/purchase/totals.rb +49 -8
  48. data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
  49. data/app/models/concerns/spree/receivable.rb +1 -1
  50. data/app/models/concerns/spree/searchable.rb +0 -4
  51. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  52. data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
  53. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  54. data/app/models/concerns/spree/stores/email_branding.rb +32 -0
  55. data/app/models/concerns/spree/stores/markets.rb +5 -2
  56. data/app/models/concerns/spree/type_labels.rb +38 -0
  57. data/app/models/concerns/spree/typed_adjustment_line.rb +5 -6
  58. data/app/models/concerns/spree/typed_associations.rb +12 -18
  59. data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
  60. data/app/models/spree/address.rb +12 -2
  61. data/app/models/spree/adjusters/largest_remainder.rb +14 -1
  62. data/app/models/spree/adjusters/promotion.rb +4 -19
  63. data/app/models/spree/allowed_origin.rb +3 -1
  64. data/app/models/spree/api_key.rb +5 -6
  65. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  66. data/app/models/spree/base.rb +99 -22
  67. data/app/models/spree/calculator/flat_percent_item_total.rb +2 -4
  68. data/app/models/spree/calculator/flat_rate.rb +3 -5
  69. data/app/models/spree/calculator/flexi_rate.rb +4 -6
  70. data/app/models/spree/calculator/percent_on_line_item.rb +2 -2
  71. data/app/models/spree/calculator/price_sack.rb +5 -7
  72. data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -5
  73. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +7 -7
  74. data/app/models/spree/calculator/shipping/flat_rate.rb +5 -7
  75. data/app/models/spree/calculator/shipping/flexi_rate.rb +4 -6
  76. data/app/models/spree/calculator/shipping/per_item.rb +3 -5
  77. data/app/models/spree/calculator/shipping/price_sack.rb +5 -7
  78. data/app/models/spree/calculator/tiered_flat_rate.rb +7 -24
  79. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  80. data/app/models/spree/cart.rb +78 -18
  81. data/app/models/spree/catalog.rb +1 -0
  82. data/app/models/spree/catalog_price.rb +0 -7
  83. data/app/models/spree/catalog_price_tier.rb +0 -8
  84. data/app/models/spree/category.rb +3 -1
  85. data/app/models/spree/channel/gating.rb +3 -2
  86. data/app/models/spree/channel.rb +6 -4
  87. data/app/models/spree/claim.rb +22 -1
  88. data/app/models/spree/claim_line_item.rb +41 -6
  89. data/app/models/spree/collection.rb +2 -1
  90. data/app/models/spree/collection_rule.rb +5 -21
  91. data/app/models/spree/commission_line.rb +1 -1
  92. data/app/models/spree/commission_rate.rb +7 -4
  93. data/app/models/spree/commission_rate_value.rb +1 -1
  94. data/app/models/spree/commission_rule.rb +4 -11
  95. data/app/models/spree/commission_rules/category_rule.rb +2 -5
  96. data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
  97. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  98. data/app/models/spree/commission_rules/seller_rule.rb +2 -5
  99. data/app/models/spree/company.rb +1 -0
  100. data/app/models/spree/company_invitation.rb +1 -0
  101. data/app/models/spree/consent_record.rb +0 -1
  102. data/app/models/spree/credit_card.rb +1 -1
  103. data/app/models/spree/current.rb +23 -0
  104. data/app/models/spree/custom_field.rb +2 -2
  105. data/app/models/spree/custom_field_definition.rb +20 -35
  106. data/app/models/spree/custom_fields/boolean.rb +1 -1
  107. data/app/models/spree/custom_fields/json.rb +1 -1
  108. data/app/models/spree/customer_group.rb +1 -0
  109. data/app/models/spree/data_feed/google.rb +0 -2
  110. data/app/models/spree/data_request.rb +2 -1
  111. data/app/models/spree/delivery_method.rb +36 -14
  112. data/app/models/spree/delivery_method_rule.rb +4 -11
  113. data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
  114. data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
  115. data/app/models/spree/delivery_profile.rb +1 -0
  116. data/app/models/spree/delivery_rate.rb +7 -8
  117. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  118. data/app/models/spree/delivery_zone.rb +1 -2
  119. data/app/models/spree/delivery_zone_member.rb +4 -4
  120. data/app/models/spree/digital_asset.rb +7 -0
  121. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  122. data/app/models/spree/digital_link.rb +2 -5
  123. data/app/models/spree/discount.rb +1 -1
  124. data/app/models/spree/email_template.rb +25 -0
  125. data/app/models/spree/email_template_draft.rb +13 -0
  126. data/app/models/spree/email_template_revision.rb +17 -0
  127. data/app/models/spree/email_templates/entry.rb +118 -0
  128. data/app/models/spree/email_templates/sample_record.rb +16 -0
  129. data/app/models/spree/emails/branding.rb +80 -0
  130. data/app/models/spree/exchange.rb +16 -1
  131. data/app/models/spree/exchange_line_item.rb +121 -5
  132. data/app/models/spree/export.rb +0 -1
  133. data/app/models/spree/exports/products.rb +10 -1
  134. data/app/models/spree/exports/report.rb +3 -3
  135. data/app/models/spree/fee.rb +4 -2
  136. data/app/models/spree/file_upload.rb +155 -0
  137. data/app/models/spree/fulfillment.rb +66 -30
  138. data/app/models/spree/fulfillment_changer.rb +2 -2
  139. data/app/models/spree/fulfillment_group.rb +6 -1
  140. data/app/models/spree/fulfillment_item.rb +0 -4
  141. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  142. data/app/models/spree/gateway/bogus.rb +6 -0
  143. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  144. data/app/models/spree/gateway.rb +6 -2
  145. data/app/models/spree/gift_card.rb +18 -8
  146. data/app/models/spree/gift_card_batch.rb +3 -5
  147. data/app/models/spree/import.rb +5 -7
  148. data/app/models/spree/import_mapping.rb +1 -2
  149. data/app/models/spree/import_row.rb +2 -2
  150. data/app/models/spree/import_schemas/products.rb +1 -0
  151. data/app/models/spree/integration.rb +9 -19
  152. data/app/models/spree/invitation.rb +5 -5
  153. data/app/models/spree/line_item.rb +15 -4
  154. data/app/models/spree/locale.rb +11 -8
  155. data/app/models/spree/market.rb +8 -2
  156. data/app/models/spree/media.rb +1 -9
  157. data/app/models/spree/newsletter_subscriber.rb +4 -0
  158. data/app/models/spree/option_type.rb +2 -1
  159. data/app/models/spree/option_value.rb +2 -1
  160. data/app/models/spree/order.rb +60 -142
  161. data/app/models/spree/order_group.rb +18 -4
  162. data/app/models/spree/order_inventory.rb +10 -5
  163. data/app/models/spree/order_promotion.rb +1 -1
  164. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  165. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  166. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  167. data/app/models/spree/order_routing_rule.rb +6 -22
  168. data/app/models/spree/package_type.rb +2 -2
  169. data/app/models/spree/payment/gateway_options.rb +15 -53
  170. data/app/models/spree/payment/processing.rb +2 -4
  171. data/app/models/spree/payment.rb +21 -13
  172. data/app/models/spree/payment_method/check.rb +2 -0
  173. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  174. data/app/models/spree/payment_method/store_credit.rb +15 -13
  175. data/app/models/spree/payment_method.rb +78 -6
  176. data/app/models/spree/payment_session.rb +5 -1
  177. data/app/models/spree/payment_setup_session.rb +1 -0
  178. data/app/models/spree/payout_provider/base.rb +3 -1
  179. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  180. data/app/models/spree/policy.rb +1 -0
  181. data/app/models/spree/price.rb +17 -14
  182. data/app/models/spree/price_adjustment_tier.rb +4 -0
  183. data/app/models/spree/price_list.rb +14 -9
  184. data/app/models/spree/price_rules/channel_rule.rb +3 -6
  185. data/app/models/spree/price_rules/customer_group_rule.rb +3 -6
  186. data/app/models/spree/price_rules/market_rule.rb +2 -5
  187. data/app/models/spree/price_rules/user_rule.rb +1 -3
  188. data/app/models/spree/product/channels.rb +9 -2
  189. data/app/models/spree/product.rb +54 -30
  190. data/app/models/spree/product_submission.rb +0 -12
  191. data/app/models/spree/product_type.rb +1 -1
  192. data/app/models/spree/promotion/actions/create_line_items.rb +28 -18
  193. data/app/models/spree/promotion/rules/category.rb +1 -5
  194. data/app/models/spree/promotion/rules/channel.rb +2 -9
  195. data/app/models/spree/promotion/rules/country.rb +3 -8
  196. data/app/models/spree/promotion/rules/currency.rb +1 -5
  197. data/app/models/spree/promotion/rules/customer_group.rb +2 -9
  198. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  199. data/app/models/spree/promotion/rules/item_total.rb +6 -10
  200. data/app/models/spree/promotion/rules/market.rb +2 -9
  201. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  202. data/app/models/spree/promotion/rules/option_value.rb +2 -6
  203. data/app/models/spree/promotion/rules/product.rb +1 -5
  204. data/app/models/spree/promotion/rules/user.rb +0 -4
  205. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  206. data/app/models/spree/promotion.rb +36 -6
  207. data/app/models/spree/promotion_action.rb +7 -14
  208. data/app/models/spree/promotion_action_line_item.rb +1 -1
  209. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  210. data/app/models/spree/promotion_rule.rb +7 -14
  211. data/app/models/spree/purchase_order.rb +3 -0
  212. data/app/models/spree/quantity_rule.rb +1 -1
  213. data/app/models/spree/refund.rb +41 -18
  214. data/app/models/spree/return.rb +31 -2
  215. data/app/models/spree/return_line_item.rb +65 -14
  216. data/app/models/spree/role.rb +7 -7
  217. data/app/models/spree/saved_report.rb +2 -1
  218. data/app/models/spree/search_provider/base.rb +4 -0
  219. data/app/models/spree/seller.rb +10 -2
  220. data/app/models/spree/seller_payout.rb +8 -0
  221. data/app/models/spree/seller_requirement.rb +5 -12
  222. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  223. data/app/models/spree/seller_requirement_submission.rb +3 -2
  224. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  225. data/app/models/spree/seller_transfer.rb +6 -0
  226. data/app/models/spree/shipping_label.rb +2 -0
  227. data/app/models/spree/stock/availability_validator.rb +2 -2
  228. data/app/models/spree/stock/estimator.rb +1 -1
  229. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  230. data/app/models/spree/stock/packer.rb +2 -2
  231. data/app/models/spree/stock/splitter/weight.rb +1 -1
  232. data/app/models/spree/stock_level/custom_events.rb +6 -0
  233. data/app/models/spree/stock_level.rb +3 -1
  234. data/app/models/spree/stock_location.rb +1 -0
  235. data/app/models/spree/stock_movement.rb +1 -1
  236. data/app/models/spree/stock_receipt_item.rb +1 -1
  237. data/app/models/spree/stock_reservation.rb +2 -2
  238. data/app/models/spree/stock_transfer.rb +6 -2
  239. data/app/models/spree/store.rb +70 -19
  240. data/app/models/spree/store_credit.rb +13 -13
  241. data/app/models/spree/store_credit_event.rb +5 -5
  242. data/app/models/spree/supplier.rb +1 -0
  243. data/app/models/spree/tax_category.rb +1 -0
  244. data/app/models/spree/tax_exemption_certificate.rb +3 -0
  245. data/app/models/spree/tax_identifier.rb +3 -1
  246. data/app/models/spree/tax_line.rb +53 -3
  247. data/app/models/spree/tax_provider/base.rb +71 -14
  248. data/app/models/spree/tax_provider/internal.rb +70 -41
  249. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  250. data/app/models/spree/tax_rate.rb +30 -8
  251. data/app/models/spree/taxon.rb +1 -1
  252. data/app/models/spree/taxonomy.rb +0 -1
  253. data/app/models/spree/update_status.rb +25 -0
  254. data/app/models/spree/variant.rb +10 -12
  255. data/app/models/spree/variant_media.rb +1 -1
  256. data/app/models/spree/webhook_delivery.rb +1 -0
  257. data/app/models/spree/webhook_endpoint.rb +46 -13
  258. data/app/models/spree/wishlist.rb +1 -6
  259. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  260. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  261. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  262. data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
  263. data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
  264. data/app/presenters/spree/csv/product_variant_presenter.rb +59 -20
  265. data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
  266. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  267. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  268. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  269. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  270. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  271. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  272. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  273. data/app/services/spree/carts/create.rb +8 -2
  274. data/app/services/spree/carts/destroy.rb +2 -2
  275. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  276. data/app/services/spree/carts/split_by_seller.rb +1 -1
  277. data/app/services/spree/carts/update.rb +22 -8
  278. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  279. data/app/services/spree/categories/add_products.rb +13 -31
  280. data/app/services/spree/categories/remove_products.rb +12 -51
  281. data/app/services/spree/checkout/advance.rb +1 -4
  282. data/app/services/spree/collections/add_products.rb +10 -23
  283. data/app/services/spree/collections/remove_products.rb +7 -43
  284. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  285. data/app/services/spree/data_requests/create.rb +1 -1
  286. data/app/services/spree/deliveries/destroy.rb +2 -2
  287. data/app/services/spree/email_templates/check.rb +58 -0
  288. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  289. data/app/services/spree/email_templates/preview.rb +99 -0
  290. data/app/services/spree/emails/file_system.rb +23 -0
  291. data/app/services/spree/emails/filters.rb +84 -0
  292. data/app/services/spree/emails/liquid_context.rb +48 -0
  293. data/app/services/spree/emails/rendered_email.rb +14 -0
  294. data/app/services/spree/emails/renderer.rb +125 -0
  295. data/app/services/spree/emails/template.rb +51 -0
  296. data/app/services/spree/emails/template_data.rb +23 -0
  297. data/app/services/spree/emails/template_resolver.rb +109 -0
  298. data/app/services/spree/emails/text_converter.rb +72 -0
  299. data/app/services/spree/emails/translation_inliner.rb +134 -0
  300. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  301. data/app/services/spree/imports/row_processors/product_variant.rb +77 -34
  302. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  303. data/app/services/spree/locales.rb +43 -10
  304. data/app/services/spree/newsletter/subscribe.rb +8 -12
  305. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  306. data/app/services/spree/orders/create.rb +5 -4
  307. data/app/services/spree/orders/discounts/create.rb +14 -11
  308. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  309. data/app/services/spree/orders/discounts/update.rb +1 -0
  310. data/app/services/spree/orders/fees/create.rb +1 -0
  311. data/app/services/spree/orders/fees/destroy.rb +1 -0
  312. data/app/services/spree/orders/fees/update.rb +1 -0
  313. data/app/services/spree/orders/remove_line_item.rb +13 -3
  314. data/app/services/spree/orders/update.rb +10 -6
  315. data/app/services/spree/orders/update_item.rb +6 -3
  316. data/app/services/spree/orders/update_statuses.rb +3 -1
  317. data/app/services/spree/prices/bulk_upsert.rb +32 -25
  318. data/app/services/spree/product_memberships.rb +61 -0
  319. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  320. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  321. data/app/services/spree/seeds/commission_rates.rb +1 -1
  322. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  323. data/app/services/spree/seeds/payment_methods.rb +2 -2
  324. data/app/services/spree/seeds/saved_reports.rb +3 -3
  325. data/app/services/spree/seeds/stores.rb +1 -1
  326. data/app/services/spree/sellers/requirements.rb +1 -2
  327. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  328. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  329. data/app/services/spree/stock_levels/correct.rb +3 -3
  330. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  331. data/app/services/spree/store_credits/apply.rb +15 -13
  332. data/app/services/spree/stores/provision_defaults.rb +23 -25
  333. data/app/services/spree/update_check.rb +160 -0
  334. data/app/services/spree/uploads/assign_stores.rb +92 -0
  335. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  336. data/app/validators/spree/attachment_content_type_validator.rb +19 -2
  337. data/app/validators/spree/locale_codes_validator.rb +22 -0
  338. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  339. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  340. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  341. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  342. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  343. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  344. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  345. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  346. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  347. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  348. data/app/views/spree/shared/_summary_row.liquid +8 -0
  349. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  350. data/app/workflows/spree/carts/add_item.rb +4 -4
  351. data/app/workflows/spree/carts/complete.rb +13 -3
  352. data/app/workflows/spree/carts/merge.rb +1 -1
  353. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  354. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  355. data/app/workflows/spree/claims/approve.rb +5 -22
  356. data/app/workflows/spree/claims/cancel.rb +5 -21
  357. data/app/workflows/spree/claims/create.rb +15 -0
  358. data/app/workflows/spree/claims/deny.rb +7 -0
  359. data/app/workflows/spree/claims/resolve.rb +34 -31
  360. data/app/workflows/spree/customers/anonymize.rb +3 -4
  361. data/app/workflows/spree/customers/create.rb +1 -1
  362. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  363. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  364. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  365. data/app/workflows/spree/email_templates/publish.rb +70 -0
  366. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  367. data/app/workflows/spree/email_templates/revert.rb +29 -0
  368. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  369. data/app/workflows/spree/exchanges/approve.rb +5 -22
  370. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  371. data/app/workflows/spree/exchanges/create.rb +6 -0
  372. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  373. data/app/workflows/spree/exchanges/receive.rb +6 -65
  374. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  375. data/app/workflows/spree/fulfillments/create.rb +53 -49
  376. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  377. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  378. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  379. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  380. data/app/workflows/spree/orders/add_item.rb +5 -1
  381. data/app/workflows/spree/orders/cancel.rb +3 -3
  382. data/app/workflows/spree/orders/complete.rb +16 -8
  383. data/app/workflows/spree/payments/capture.rb +20 -5
  384. data/app/workflows/spree/payments/process.rb +5 -3
  385. data/app/workflows/spree/post_sale/approval.rb +35 -0
  386. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  387. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  388. data/app/workflows/spree/price_lists/update.rb +4 -4
  389. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  390. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  391. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  392. data/app/workflows/spree/purchase_orders/create.rb +3 -1
  393. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  394. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  395. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  396. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  397. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  398. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  399. data/app/workflows/spree/refunds/create.rb +5 -2
  400. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  401. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  402. data/app/workflows/spree/returns/approve.rb +5 -22
  403. data/app/workflows/spree/returns/cancel.rb +5 -22
  404. data/app/workflows/spree/returns/create.rb +8 -0
  405. data/app/workflows/spree/returns/receive.rb +5 -76
  406. data/app/workflows/spree/returns/refund.rb +31 -8
  407. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  408. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  409. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  410. data/app/workflows/spree/shipping_labels/record.rb +7 -7
  411. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  412. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  413. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  414. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  415. data/app/workflows/spree/stock_transfers/create.rb +3 -1
  416. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  417. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  418. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  419. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  420. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  421. data/config/currency_exponents.json +180 -0
  422. data/config/i18n-tasks.yml +32 -18
  423. data/config/initializers/alba.rb +7 -0
  424. data/config/language_names.csv +264 -0
  425. data/config/locales/ar.yml +811 -0
  426. data/config/locales/be.yml +855 -0
  427. data/config/locales/bg.yml +723 -0
  428. data/config/locales/ca.yml +689 -0
  429. data/config/locales/content_locales.rb +9 -0
  430. data/config/locales/cs.yml +855 -0
  431. data/config/locales/da.yml +771 -0
  432. data/config/locales/de-CH.yml +248 -0
  433. data/config/locales/de.yml +835 -0
  434. data/config/locales/el.yml +769 -0
  435. data/config/locales/en.yml +47 -505
  436. data/config/locales/es-CL.yml +301 -0
  437. data/config/locales/es-EC.yml +186 -0
  438. data/config/locales/es-MX.yml +334 -0
  439. data/config/locales/es.yml +831 -0
  440. data/config/locales/et.yml +644 -0
  441. data/config/locales/fa.yml +709 -0
  442. data/config/locales/fi.yml +771 -0
  443. data/config/locales/fr.yml +830 -0
  444. data/config/locales/hu.yml +751 -0
  445. data/config/locales/id.yml +756 -0
  446. data/config/locales/it.yml +833 -0
  447. data/config/locales/ja.yml +726 -0
  448. data/config/locales/km.yml +759 -0
  449. data/config/locales/ko.yml +575 -0
  450. data/config/locales/ku.yml +753 -0
  451. data/config/locales/lt.yml +821 -0
  452. data/config/locales/lv.yml +685 -0
  453. data/config/locales/nb.yml +824 -0
  454. data/config/locales/ne.yml +823 -0
  455. data/config/locales/nl.yml +826 -0
  456. data/config/locales/pl.yml +835 -0
  457. data/config/locales/pt-BR.yml +547 -0
  458. data/config/locales/pt.yml +636 -0
  459. data/config/locales/ro.yml +750 -0
  460. data/config/locales/ru.yml +834 -0
  461. data/config/locales/sk.yml +680 -0
  462. data/config/locales/sl-SI.yml +600 -0
  463. data/config/locales/sv.yml +827 -0
  464. data/config/locales/th.yml +745 -0
  465. data/config/locales/tr.yml +777 -0
  466. data/config/locales/uk.yml +836 -0
  467. data/config/locales/vi.yml +768 -0
  468. data/config/locales/zh-CN.yml +786 -0
  469. data/config/locales/zh-TW.yml +834 -0
  470. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  471. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  472. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  473. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  474. data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
  475. data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
  476. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  477. data/lib/generators/spree/dummy/templates/rails/test.rb +26 -0
  478. data/lib/spree/checkout/default_requirements.rb +37 -13
  479. data/lib/spree/core/configuration.rb +19 -1
  480. data/lib/spree/core/controller_helpers/store.rb +1 -1
  481. data/lib/spree/core/dependencies.rb +5 -4
  482. data/lib/spree/core/emails/escaped_output.rb +60 -0
  483. data/lib/spree/core/engine.rb +72 -165
  484. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  485. data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
  486. data/lib/spree/core/preferences/json_conversion.rb +186 -0
  487. data/lib/spree/core/preferences/masking.rb +55 -2
  488. data/lib/spree/core/preferences/preferable.rb +343 -103
  489. data/lib/spree/core/preferences/preferable_class_methods.rb +188 -136
  490. data/lib/spree/core/previews/import_preview.rb +10 -0
  491. data/lib/spree/core/ransack_configuration.rb +13 -2
  492. data/lib/spree/core/tax/provider_error.rb +37 -0
  493. data/lib/spree/core/version.rb +1 -1
  494. data/lib/spree/core.rb +91 -373
  495. data/lib/spree/emails/editable_templates.rb +82 -0
  496. data/lib/spree/events/catalog.rb +189 -0
  497. data/lib/spree/events.rb +21 -1
  498. data/lib/spree/hooks.rb +0 -17
  499. data/lib/spree/i18n.rb +32 -15
  500. data/lib/spree/iso_data.rb +7 -0
  501. data/lib/spree/localized_number.rb +6 -0
  502. data/lib/spree/migrations.rb +4 -1
  503. data/lib/spree/money.rb +196 -15
  504. data/lib/spree/reporting/adapters/base.rb +7 -6
  505. data/lib/spree/reporting/adapters/live.rb +6 -6
  506. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  507. data/lib/spree/reporting/hydration.rb +1 -1
  508. data/lib/spree/reporting/query.rb +8 -0
  509. data/lib/spree/reporting/registry.rb +13 -0
  510. data/lib/spree/reporting/schema.rb +10 -10
  511. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  512. data/lib/spree/testing_support/emails.rb +22 -0
  513. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  514. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  515. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  516. data/lib/spree/testing_support/factories/product_factory.rb +9 -0
  517. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  518. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  519. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  520. data/lib/spree/testing_support/i18n.rb +3 -94
  521. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  522. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  523. data/lib/spree/testing_support/preferences.rb +0 -5
  524. data/lib/spree/testing_support/store.rb +9 -1
  525. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +67 -1
  526. data/lib/spree/uploads.rb +101 -0
  527. data/lib/tasks/categories_and_collections.rake +3 -3
  528. data/lib/tasks/core.rake +6 -4
  529. data/lib/tasks/markets.rake +2 -1
  530. data/lib/tasks/media.rake +3 -3
  531. data/lib/tasks/migrate_users_to_customers.rake +23 -18
  532. data/lib/tasks/money.rake +46 -0
  533. data/lib/tasks/package_types.rake +1 -1
  534. data/lib/tasks/preferences_json.rake +101 -0
  535. data/lib/tasks/seller_sample_data.rake +2 -2
  536. data/lib/tasks/store_settings.rake +13 -6
  537. data/lib/tasks/typed_adjustments_migration.rake +1 -1
  538. data/lib/tasks/uploads.rake +19 -0
  539. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  540. metadata +219 -46
  541. data/app/jobs/spree/products/touch_categories_job.rb +0 -15
  542. data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
  543. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  544. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  545. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  546. data/app/models/spree/invitations/store.rb +0 -6
  547. data/app/models/spree/preference.rb +0 -14
  548. data/app/models/spree/promotion_handler/page.rb +0 -25
  549. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  550. data/app/models/spree/stock/differentiator.rb +0 -45
  551. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  552. data/app/services/spree/carts/empty.rb +0 -42
  553. data/app/services/spree/country_to_timezone.rb +0 -273
  554. data/app/services/spree/payments/create.rb +0 -92
  555. data/app/validators/spree/url_validator.rb +0 -23
  556. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  557. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  558. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  559. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  560. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  561. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  562. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  563. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  564. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  565. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  566. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  567. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  568. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  569. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  570. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  571. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  572. data/lib/spree/analytics.rb +0 -11
  573. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  574. data/lib/spree/core/preferences/store.rb +0 -105
  575. data/lib/spree/translation_migrations.rb +0 -52
@@ -18,7 +18,7 @@ module Spree
18
18
 
19
19
  def process!
20
20
  reference = attributes['reference'].to_s.strip
21
- raise ArgumentError, Spree.t(:purchase_order_import_reference_required) if reference.blank?
21
+ raise ArgumentError, I18n.t('spree.purchase_order_import_reference_required') if reference.blank?
22
22
 
23
23
  line = {
24
24
  variant: find_variant,
@@ -78,31 +78,31 @@ module Spree
78
78
 
79
79
  def find_supplier
80
80
  name = attributes['supplier'].to_s.strip
81
- raise ArgumentError, Spree.t(:purchase_order_import_supplier_required) if name.blank?
81
+ raise ArgumentError, I18n.t('spree.purchase_order_import_supplier_required') if name.blank?
82
82
 
83
83
  cached_lookup(:purchase_order_import_supplier, name.downcase) do
84
84
  store.suppliers.find_by(Spree::Supplier.arel_table[:name].lower.eq(name.downcase))
85
- end || raise(ArgumentError, Spree.t(:purchase_order_import_unknown_supplier, name: name))
85
+ end || raise(ArgumentError, I18n.t('spree.purchase_order_import_unknown_supplier', name: name))
86
86
  end
87
87
 
88
88
  def find_destination
89
89
  name = attributes['destination'].to_s.strip
90
- raise ArgumentError, Spree.t(:purchase_order_import_destination_required) if name.blank?
90
+ raise ArgumentError, I18n.t('spree.purchase_order_import_destination_required') if name.blank?
91
91
 
92
92
  cached_lookup(:purchase_order_import_destination, name.downcase) do
93
93
  store.stock_locations.find_by(Spree::StockLocation.arel_table[:name].lower.eq(name.downcase))
94
- end || raise(ArgumentError, Spree.t(:purchase_order_import_unknown_destination, name: name))
94
+ end || raise(ArgumentError, I18n.t('spree.purchase_order_import_unknown_destination', name: name))
95
95
  end
96
96
 
97
97
  def find_variant
98
98
  sku = attributes['sku'].to_s.strip
99
- raise ArgumentError, Spree.t(:purchase_order_import_sku_required) if sku.blank?
99
+ raise ArgumentError, I18n.t('spree.purchase_order_import_sku_required') if sku.blank?
100
100
 
101
101
  variants = cached_lookup(:purchase_order_import_variant, sku.downcase) do
102
102
  store.variants.where(Spree::Variant.arel_table[:sku].lower.eq(sku.downcase)).limit(2).to_a
103
103
  end
104
- raise ArgumentError, Spree.t(:purchase_order_import_unknown_sku, sku: sku) if variants.empty?
105
- raise ArgumentError, Spree.t(:purchase_order_import_ambiguous_sku, sku: sku) if variants.many?
104
+ raise ArgumentError, I18n.t('spree.purchase_order_import_unknown_sku', sku: sku) if variants.empty?
105
+ raise ArgumentError, I18n.t('spree.purchase_order_import_ambiguous_sku', sku: sku) if variants.many?
106
106
 
107
107
  variants.first
108
108
  end
@@ -110,7 +110,7 @@ module Spree
110
110
  def quantity_for(value)
111
111
  text = value.to_s.strip
112
112
  unless text.match?(QUANTITY_FORMAT) && text.to_i.positive?
113
- raise ArgumentError, Spree.t(:purchase_order_import_invalid_quantity, value: text)
113
+ raise ArgumentError, I18n.t('spree.purchase_order_import_invalid_quantity', value: text)
114
114
  end
115
115
 
116
116
  text.to_i
@@ -118,7 +118,7 @@ module Spree
118
118
 
119
119
  def amount_for(value)
120
120
  text = value.to_s.strip
121
- raise ArgumentError, Spree.t(:purchase_order_import_invalid_cost, value: text) unless text.match?(AMOUNT_FORMAT)
121
+ raise ArgumentError, I18n.t('spree.purchase_order_import_invalid_cost', value: text) unless text.match?(AMOUNT_FORMAT)
122
122
 
123
123
  BigDecimal(text)
124
124
  end
@@ -127,7 +127,7 @@ module Spree
127
127
  currency = value.to_s.strip.upcase.presence || store.default_currency
128
128
  return currency if supported_currencies.include?(currency)
129
129
 
130
- raise ArgumentError, Spree.t(:purchase_order_import_unsupported_currency, currency: currency)
130
+ raise ArgumentError, I18n.t('spree.purchase_order_import_unsupported_currency', currency: currency)
131
131
  end
132
132
 
133
133
  def supported_currencies
@@ -142,14 +142,14 @@ module Spree
142
142
 
143
143
  Date.iso8601(text)
144
144
  rescue Date::Error
145
- raise ArgumentError, Spree.t(:purchase_order_import_invalid_date, column: column, value: text)
145
+ raise ArgumentError, I18n.t('spree.purchase_order_import_invalid_date', column: column, value: text)
146
146
  end
147
147
 
148
148
  def failure_message(result)
149
149
  error = result.error
150
150
  return error.full_messages.to_sentence if error.respond_to?(:full_messages)
151
151
 
152
- error.to_s.presence || Spree.t(:purchase_order_import_failed)
152
+ error.to_s.presence || I18n.t('spree.purchase_order_import_failed')
153
153
  end
154
154
  end
155
155
  end
@@ -4,11 +4,10 @@ module Spree
4
4
  # (Mobility-backed content such as product names, descriptions, taxons,
5
5
  # option values, etc.).
6
6
  #
7
- # This is deliberately **independent of the admin/storefront UI translation
8
- # set** (`Spree.available_locales`, which reflects which `spree_i18n` UI
9
- # bundles happen to be installed). In a headless setup the storefront ships
10
- # its own UI translations, so the languages a merchant can store content in
11
- # must not be constrained by which admin chrome translations exist.
7
+ # This is deliberately **independent of the languages Spree ships
8
+ # translations for** (`Spree.available_locales`). In a headless setup the
9
+ # storefront ships its own UI translations, so the languages a merchant can
10
+ # store content in must not be constrained by Spree's own translations.
12
11
  #
13
12
  # Codes follow BCP-47 casing (lowercase language, uppercase region —
14
13
  # `pt-BR`, `zh-CN`) so the browser's `Intl.DisplayNames` resolves a clean
@@ -17,15 +16,15 @@ module Spree
17
16
  # English, Brazilian vs. European Portuguese, Simplified vs. Traditional
18
17
  # Chinese, etc.).
19
18
  #
20
- # The underlying translation tables accept any locale string, so this list
21
- # governs only what the locale pickers offer — it is not a hard storage
22
- # constraint.
19
+ # Markets and stores only accept codes from this list when their locales
20
+ # change, so an order's or cart's locale (which must be one its store
21
+ # supports) is one too, unless the store kept a value saved before that.
23
22
  ALL = %w[
24
23
  aa ab ae af ak am an ar as av ay az
25
24
  ba be bg bh bi bm bn bo br bs
26
25
  ca ce ch co cr cs cu cv cy
27
26
  da de de-AT de-CH dv dz
28
- ee el en en-AU en-CA en-GB en-IN en-NZ eo es es-419 es-MX et eu
27
+ ee el en en-AU en-CA en-GB en-IN en-NZ en-US eo es es-419 es-CL es-EC es-MX et eu
29
28
  fa ff fi fj fo fr fr-CA fy
30
29
  ga gd gl gn gu gv
31
30
  ha he hi ho hr ht hu hy hz
@@ -39,7 +38,7 @@ module Spree
39
38
  pa pi pl ps pt pt-BR pt-PT
40
39
  qu
41
40
  rm rn ro ru rw
42
- sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw
41
+ sa sc sd se sg si sk sl sl-SI sm sn so sq sr ss st su sv sw
43
42
  ta te tg th ti tk tl tn to tr ts tt tw ty
44
43
  ug uk ur uz
45
44
  ve vi vo
@@ -48,5 +47,39 @@ module Spree
48
47
  yi yo
49
48
  za zh-CN zh-HK zh-TW zu
50
49
  ].freeze
50
+
51
+ # The codes in a comma-separated list that are not listed, for a
52
+ # validation to name.
53
+ #
54
+ # @param codes [String, nil] e.g. "en,pt-BR"
55
+ # @return [Array<String>]
56
+ def self.unlisted(codes)
57
+ codes.to_s.split(',').map(&:strip).reject(&:empty?) - ALL
58
+ end
59
+
60
+ # A listed locale for a code from elsewhere (a host app's I18n config, a
61
+ # country's official language, a legacy column): the listed spelling of
62
+ # the code itself (`en_US` and `pt-br` give `en-US` and `pt-BR`),
63
+ # otherwise the listed one for its language (`en-IE` gives `en`, `zh`
64
+ # gives `zh-CN`), or nil when Spree knows neither.
65
+ #
66
+ # @param code [String, Symbol, nil]
67
+ # @return [String, nil]
68
+ def self.listed(code)
69
+ code = code.to_s.strip.tr('_', '-')
70
+ return if code.empty?
71
+
72
+ ALL.find { |listed| listed.casecmp?(code) } || for_language(code.split('-').first)
73
+ end
74
+
75
+ # The listed locale for an ISO 639-1 language: the language itself, or
76
+ # its first regional variant when only those are listed (`zh` gives
77
+ # `zh-CN`).
78
+ #
79
+ # @param language [String]
80
+ # @return [String, nil]
81
+ def self.for_language(language)
82
+ Spree.locale_for_language(language.to_s.downcase, ALL)
83
+ end
51
84
  end
52
85
  end
@@ -26,7 +26,7 @@ module Spree
26
26
  end
27
27
  end
28
28
 
29
- subscriber.publish_event('newsletter_subscriber.subscription_requested', subscription_requested_payload) unless subscriber.verified?
29
+ publish_subscription_requested unless subscriber.verified?
30
30
  subscriber
31
31
  end
32
32
 
@@ -34,17 +34,13 @@ module Spree
34
34
 
35
35
  attr_reader :email, :current_user, :current_store, :redirect_url
36
36
 
37
- def subscription_requested_payload
38
- payload = {
39
- id: subscriber.prefixed_id,
40
- email: subscriber.email,
41
- verification_token: subscriber.verification_token,
42
- unsubscribe_token: subscriber.generate_token_for(:unsubscribe),
43
- store_id: current_store.prefixed_id,
44
- customer_id: subscriber.customer&.prefixed_id
45
- }
46
- payload[:redirect_url] = redirect_url if redirect_url.present?
47
- payload
37
+ def publish_subscription_requested
38
+ event_name = 'newsletter_subscriber.subscription_requested'
39
+ payload = subscriber.event_payload_for(
40
+ event_name,
41
+ include_verification_token: true, store: current_store, redirect_url: redirect_url.presence
42
+ )
43
+ subscriber.publish_event(event_name, payload)
48
44
  end
49
45
 
50
46
  def upsert_subscriber
@@ -17,13 +17,14 @@ module Spree
17
17
  class BuildFulfillments
18
18
  prepend Spree::ServiceModule::Base
19
19
 
20
- def call(order:)
20
+ # @param keep_selection [Boolean] false when the destination changed
21
+ def call(order:, keep_selection: true)
21
22
  return success(order) if order.completed?
22
23
  return success(order) unless order.ship_address_id.present?
23
24
  return success(order) unless order.line_items.any?
24
25
  return success(order) unless order.delivery_step_required?
25
26
 
26
- order.rebuild_fulfillments!
27
+ order.rebuild_fulfillments!(keep_selection: keep_selection)
27
28
  order.create_shipment_tax_charge!
28
29
  order.set_fulfillments_cost
29
30
  order.apply_free_shipping_promotions
@@ -45,11 +45,11 @@ module Spree
45
45
  rescue ActiveRecord::RecordInvalid => e
46
46
  failure(e.record, e.record.errors.full_messages.to_sentence)
47
47
  rescue ActiveStorage::FileNotFoundError
48
- # An abandoned direct upload leaves the blob row without its bytes.
48
+ # An abandoned presigned upload leaves the blob row without its bytes.
49
49
  # ActiveStorage raises a message-less error for that, which would
50
50
  # otherwise be an unhandled 500 on the Admin API.
51
51
  order ||= @store.orders.new
52
- order.errors.add(:po_document, :po_document_upload_incomplete, message: Spree.t(:po_document_upload_incomplete))
52
+ order.errors.add(:po_document, :po_document_upload_incomplete, message: I18n.t('spree.po_document_upload_incomplete'))
53
53
  failure(order, order.errors.full_messages.to_sentence)
54
54
  end
55
55
 
@@ -71,8 +71,8 @@ module Spree
71
71
  }
72
72
 
73
73
  # The buyer's purchase order, when it arrived with the paperwork. A
74
- # signed blob id, so it assigns like any other attribute.
75
- attrs[:po_document] = @params[:po_document] if @params[:po_document].present?
74
+ # signed id, so it assigns like any other attribute.
75
+ attrs[:po_document_signed_id] = @params[:po_document_signed_id] if @params[:po_document_signed_id].present?
76
76
 
77
77
  attrs[:market] = resolve_market if @params[:market_id].present?
78
78
  attrs[:channel] = resolve_channel if @params[:channel_id].present?
@@ -170,6 +170,7 @@ module Spree
170
170
  coupon_code: @params[:coupon_code]
171
171
  }
172
172
  order.coupon_code = nil
173
+ handler.release_stale_coupon_codes
173
174
  end
174
175
  end
175
176
  end
@@ -17,17 +17,20 @@ module Spree
17
17
  # @param line_item [Spree::LineItem, nil] nil distributes order-level
18
18
  # @return [Spree::ServiceModule::Result] value is the created rows
19
19
  def call(order:, label:, value:, value_type: 'flat', line_item: nil)
20
- value = BigDecimal(value.to_s)
21
- return failure(nil, Spree.t('errors.messages.discount_value_must_be_positive')) unless value.positive?
22
- return failure(nil, Spree.t('errors.messages.discount_value_type_invalid')) unless %w[flat percent].include?(value_type)
20
+ value = Spree::Money::Rounding.parse_decimal(value)
21
+ return failure(nil, I18n.t('spree.errors.messages.discount_value_must_be_positive')) unless value&.positive?
22
+ return failure(nil, I18n.t('spree.errors.messages.discount_value_type_invalid')) unless %w[flat percent].include?(value_type)
23
23
 
24
24
  rows = order.with_lock do
25
25
  created = line_item ? [line_item_row(order, line_item, label, value, value_type)].compact : distributed_rows(order, label, value, value_type)
26
- Spree.order_recalculate_totals_workflow.call(order: order) if created.any?
26
+ if created.any?
27
+ Spree.order_recalculate_totals_workflow.call(order: order)
28
+ order.update_statuses!
29
+ end
27
30
  created
28
31
  end
29
32
 
30
- return failure(nil, Spree.t('errors.messages.discount_has_no_effect')) if rows.empty?
33
+ return failure(nil, I18n.t('spree.errors.messages.discount_has_no_effect')) if rows.empty?
31
34
 
32
35
  success(rows)
33
36
  end
@@ -36,7 +39,7 @@ module Spree
36
39
 
37
40
  def line_item_row(order, line_item, label, value, value_type)
38
41
  base = discountable_base(line_item)
39
- amount = -[amount_for(base, value, value_type), base].min
42
+ amount = -[amount_for(base, value, value_type, order.currency), base].min
40
43
  return if amount.zero?
41
44
 
42
45
  create_row(order, line_item, label, amount, value, value_type)
@@ -48,19 +51,19 @@ module Spree
48
51
  bases_sum = bases.sum
49
52
  return [] if bases_sum <= 0
50
53
 
51
- total = [amount_for(bases_sum, value, value_type), bases_sum].min
52
- shares = Spree::Adjusters::LargestRemainder.largest_remainder_shares((total * 100).round, bases)
54
+ total = [amount_for(bases_sum, value, value_type, order.currency), bases_sum].min
55
+ shares = Spree::Adjusters::LargestRemainder.apportion(total, bases, order.currency)
53
56
 
54
57
  line_items.each_with_index.filter_map do |line_item, index|
55
- amount = -BigDecimal(shares[index]) / 100
58
+ amount = -shares[index]
56
59
  next if amount.zero?
57
60
 
58
61
  create_row(order, line_item, label, amount, value, value_type)
59
62
  end
60
63
  end
61
64
 
62
- def amount_for(base, value, value_type)
63
- value_type == 'percent' ? (base * value / 100).round(2) : value
65
+ def amount_for(base, value, value_type, currency)
66
+ Spree::Money::Rounding.to_currency(value_type == 'percent' ? base * value / 100 : value, currency)
64
67
  end
65
68
 
66
69
  # Remaining discountable base: amount net of already-applied discounts.
@@ -15,6 +15,7 @@ module Spree
15
15
  order.with_lock do
16
16
  discount.destroy!
17
17
  Spree.order_recalculate_totals_workflow.call(order: order)
18
+ order.update_statuses!
18
19
  end
19
20
 
20
21
  success(discount)
@@ -18,6 +18,7 @@ module Spree
18
18
  next false unless discount.update(attributes)
19
19
 
20
20
  Spree.order_recalculate_totals_workflow.call(order: order)
21
+ order.update_statuses!
21
22
  true
22
23
  end
23
24
 
@@ -17,6 +17,7 @@ module Spree
17
17
  next false unless fee.save
18
18
 
19
19
  Spree.order_recalculate_totals_workflow.call(order: order)
20
+ order.update_statuses!
20
21
  true
21
22
  end
22
23
 
@@ -12,6 +12,7 @@ module Spree
12
12
  order.with_lock do
13
13
  fee.destroy!
14
14
  Spree.order_recalculate_totals_workflow.call(order: order)
15
+ order.update_statuses!
15
16
  end
16
17
 
17
18
  success(fee)
@@ -14,6 +14,7 @@ module Spree
14
14
  next false unless fee.update(attributes)
15
15
 
16
16
  Spree.order_recalculate_totals_workflow.call(order: order)
17
+ order.update_statuses!
17
18
  true
18
19
  end
19
20
 
@@ -1,12 +1,22 @@
1
1
  module Spree
2
2
  module Orders
3
- # Draft-order twin of the cart service — order: is the canonical keyword
4
- # on this side, delegating onto the shared implementation.
3
+ # Draft-order twin of the cart service. Removing an item is an upsert with
4
+ # quantity zero, so the removal passes the same 'orders.upsert_items.validate'
5
+ # hook every other draft-order item edit does.
5
6
  class RemoveLineItem
6
7
  prepend Spree::ServiceModule::Base
7
8
 
9
+ # @param order [Spree::Order]
10
+ # @param line_item [Spree::LineItem]
11
+ # @param options [Hash] accepted for signature compatibility and ignored
12
+ # @return [Spree::ServiceModule::Result] value is the removed line item
8
13
  def call(order:, line_item:, options: {})
9
- Spree::Carts::RemoveLineItem.call(cart: order, line_item: line_item, options: options)
14
+ result = Spree.order_upsert_items_workflow.call(
15
+ order: order,
16
+ items: [{ variant_id: line_item.variant_id, quantity: 0 }]
17
+ )
18
+
19
+ result.success? ? success(line_item) : failure(line_item, result.error)
10
20
  end
11
21
  end
12
22
  end
@@ -15,7 +15,10 @@ module Spree
15
15
  items_param = @params.delete(:items)
16
16
  address_params = extract_address_params
17
17
 
18
- ApplicationRecord.transaction do
18
+ # requires_new: the rescue below sits outside the block, so joining a
19
+ # caller's open transaction (the API's order lock) would let it commit
20
+ # the half-applied edit.
21
+ ApplicationRecord.transaction(requires_new: true) do
19
22
  ship_address_id_before = @order.ship_address_id
20
23
  assign_addresses(address_params)
21
24
 
@@ -25,11 +28,12 @@ module Spree
25
28
 
26
29
  process_items(items_param) if items_param
27
30
 
28
- if items_param || @order.ship_address_id != ship_address_id_before
29
- build_fulfillments
30
- end
31
+ # An address row edited in place keeps its id, so an edit to the fields a quote reads counts too.
32
+ destination_changed = @order.ship_address_id != ship_address_id_before || @order.ship_address&.saved_change_to_destination?
33
+ build_fulfillments(keep_selection: !destination_changed) if items_param || destination_changed
31
34
 
32
35
  @order.recalculate_totals!
36
+ @order.update_statuses!
33
37
  end
34
38
 
35
39
  success(@order.reload)
@@ -70,8 +74,8 @@ module Spree
70
74
  propagate_step_failure!(result, fallback: 'Failed to update items on order') if result.failure?
71
75
  end
72
76
 
73
- def build_fulfillments
74
- result = Spree::Orders::BuildFulfillments.call(order: @order)
77
+ def build_fulfillments(keep_selection:)
78
+ result = Spree::Orders::BuildFulfillments.call(order: @order, keep_selection: keep_selection)
75
79
  propagate_step_failure!(result, fallback: 'Failed to build shipments') if result.failure?
76
80
  end
77
81
 
@@ -21,7 +21,7 @@ module Spree
21
21
  price_provided = !price.equal?(PRICE_NOT_PROVIDED)
22
22
 
23
23
  if price_provided && order.completed?
24
- return reject_price(line_item, :price_override_not_allowed, Spree.t('cart_line_item.price_override_not_allowed'))
24
+ return reject_price(line_item, :price_override_not_allowed, I18n.t('spree.cart_line_item.price_override_not_allowed'))
25
25
  end
26
26
 
27
27
  attributes = {}
@@ -30,7 +30,7 @@ module Spree
30
30
 
31
31
  if price_provided && !price.nil?
32
32
  manual_price = parse_manual_price(price)
33
- return reject_price(line_item, :invalid_price, Spree.t('cart_line_item.invalid_price')) if manual_price.nil?
33
+ return reject_price(line_item, :invalid_price, I18n.t('spree.cart_line_item.invalid_price')) if manual_price.nil?
34
34
 
35
35
  attributes[:price] = manual_price
36
36
  attributes[:price_source] = Spree::LineItem::MANUAL_PRICE_SOURCE
@@ -47,7 +47,10 @@ module Spree
47
47
  ActiveRecord::Base.transaction do
48
48
  return failure(line_item) unless line_item.update(attributes)
49
49
 
50
- Spree::Orders::Recalculate.call(order: order, line_item: line_item) if quantity_changing || price_provided
50
+ if quantity_changing || price_provided
51
+ Spree::Orders::Recalculate.call(order: order, line_item: line_item)
52
+ order.update_statuses!
53
+ end
51
54
  end
52
55
 
53
56
  success(line_item)
@@ -4,7 +4,9 @@ module Spree
4
4
  # Derive-then-persist: statuses are recomputed from payment/refund/
5
5
  # fulfillment records and stored in indexed columns so admin filtering
6
6
  # keeps working. Triggered from payment/refund/fulfillment/return event
7
- # subscribers — never inline from controllers.
7
+ # subscribers, and by each edit service that moves an order's total
8
+ # (fees, discounts, items) once it has re-summed — no event announces
9
+ # that change. Never inline from controllers.
8
10
  class UpdateStatuses
9
11
  prepend Spree::ServiceModule::Base
10
12
 
@@ -44,9 +44,12 @@ module Spree
44
44
  # (docs/plans/6.0-volume-pricing.md).
45
45
  def call(rows:)
46
46
  rows = Array(rows).map { |r| r.with_indifferent_access }
47
- # Amounts are parsed once, here; everything below reads numbers.
48
47
  keyed = rows.select { |r| r[:variant_id].present? && r[:currency].present? }
49
- .map { |r| r.merge(amount: parse_amount(r[:amount]), compare_at_amount: parse_amount(r[:compare_at_amount])) }
48
+ unreadable = rows_with_malformed_amount(keyed)
49
+ return failure(nil, malformed_amounts: unreadable) if unreadable.any?
50
+
51
+ # Amounts are parsed once, here; everything below reads numbers.
52
+ keyed = keyed.map { |r| r.merge(amount: Spree::Money::Rounding.parse_decimal(r[:amount]), compare_at_amount: Spree::Money::Rounding.parse_decimal(r[:compare_at_amount])) }
50
53
  # Checked before anything is deduped: `row_key` coerces the quantity,
51
54
  # so two malformed rows would collapse into one and the batch would be
52
55
  # judged on a shape the caller never sent.
@@ -98,12 +101,13 @@ module Spree
98
101
  upsert_batch(base_rows, BASE_UNIQUE_BY)
99
102
  end
100
103
  upsert_batch(override_rows, OVERRIDE_UNIQUE_BY)
101
- sweep(affected_keys, clear_rows)
104
+ cleared_variant_ids = sweep(affected_keys, clear_rows)
102
105
  # `upsert_all` and `delete_all` both skip AR callbacks, so the
103
106
  # `Price -> Variant -> Product` `touch:` chain never fires —
104
107
  # downstream caches (`cache_key_with_version`) would stay stale.
105
- # Re-trigger the chain with one `.touch` per affected variant.
106
- touch_variants(affected_keys.map { |k| k[0] }.uniq)
108
+ # A blank row clearing a rung that had no amount changes nothing,
109
+ # so only priced rows and cleared amounts touch their variant.
110
+ touch_variants((upsert_rows.map { |r| r[:variant_id].to_s } + cleared_variant_ids).uniq)
107
111
  end
108
112
 
109
113
  success(price_count: payload.length)
@@ -167,6 +171,18 @@ module Spree
167
171
  Integer(raw.to_s.strip, 10, exception: false)
168
172
  end
169
173
 
174
+ # Rows whose amount is not a number or plain decimal text ("1,599.99",
175
+ # `false`), refused before a malformed value can read as "clear".
176
+ def rows_with_malformed_amount(rows)
177
+ rows.each_with_index.filter_map do |row, index|
178
+ Spree::Money::Rounding.parse_decimal(row[:amount])
179
+ Spree::Money::Rounding.parse_decimal(row[:compare_at_amount])
180
+ nil
181
+ rescue Spree::Money::InvalidFormat
182
+ { index: index }
183
+ end
184
+ end
185
+
170
186
  # Rows pricing below zero. This path runs no model validations, so the
171
187
  # sign is checked here for every caller rather than in each of them.
172
188
  def rows_with_negative_amount(rows)
@@ -370,15 +386,6 @@ module Spree
370
386
  [row[:variant_id].to_s, row[:currency], row[:price_list_id].to_s]
371
387
  end
372
388
 
373
- # Parses locale-aware decimal input ("1.234,56" in DE, "1,234.56"
374
- # in en-US). Numeric values pass through; blank values become nil.
375
- def parse_amount(value)
376
- return nil if value.blank?
377
- return value if value.is_a?(Numeric)
378
-
379
- Spree::LocalizedNumber.parse(value)
380
- end
381
-
382
389
  def sweep(affected_keys, clear_rows)
383
390
  cleared_keys = clear_rows.map { |r| row_key(r) }.to_set
384
391
  affected_set = affected_keys.to_set
@@ -392,16 +399,14 @@ module Spree
392
399
  )
393
400
  .pluck(:id, :variant_id, :currency, :price_list_id, :min_quantity, :amount)
394
401
 
395
- doomed_ids = candidates.filter_map do |id, variant_id, currency, price_list_id, min_quantity, amount|
402
+ doomed = candidates.select do |_, variant_id, currency, price_list_id, min_quantity, amount|
396
403
  key = [variant_id.to_s, currency, price_list_id&.to_s, min_quantity]
397
- next unless affected_set.include?(key)
398
- next id if amount.nil?
399
- next id if cleared_keys.include?(key)
400
-
401
- nil
404
+ affected_set.include?(key) && (amount.nil? || cleared_keys.include?(key))
402
405
  end
403
406
 
404
- Spree::Price.where(id: doomed_ids).delete_all if doomed_ids.any?
407
+ Spree::Price.where(id: doomed.map(&:first)).delete_all if doomed.any?
408
+
409
+ doomed.filter_map { |_, variant_id, *, amount| variant_id.to_s if amount }
405
410
  end
406
411
 
407
412
  # Bumps `updated_at` on the affected variants and their parent
@@ -410,15 +415,17 @@ module Spree
410
415
  # `Variant -> Product` `touch:` chains otherwise.
411
416
  #
412
417
  # We use `touch_all` rather than per-record `Variant#touch`: it bumps
413
- # every affected row in a single UPDATE and skips AR callbacks, giving
414
- # the cache bust without extra side effects.
418
+ # every affected row in a single UPDATE. It also skips the product's
419
+ # commit callbacks, so the search index, which stores prices, is
420
+ # refreshed here.
415
421
  def touch_variants(variant_ids)
416
422
  return if variant_ids.empty?
417
423
 
418
424
  variants = Spree::Variant.where(id: variant_ids)
419
- product_ids = variants.pluck(:product_id).uniq
425
+ products = Spree::Product.where(id: variants.select(:product_id))
420
426
  variants.touch_all
421
- Spree::Product.where(id: product_ids).touch_all if product_ids.any?
427
+ products.touch_all
428
+ products.find_each(&:enqueue_search_index)
422
429
  end
423
430
 
424
431
  # MySQL infers conflict targets from its own unique indexes and
@@ -0,0 +1,61 @@
1
+ module Spree
2
+ # Bulk adding and removing products on categories or collections. The
3
+ # including service names the join model (`membership_class`), the grouping
4
+ # it belongs to (`group_class`, `group_key`) and refreshes the grouping's
5
+ # own counters in `refresh_group_counters`.
6
+ module ProductMemberships
7
+ private
8
+
9
+ def add_memberships(groups, products)
10
+ rows = groups.pluck(:id).flat_map do |group_id|
11
+ position = membership_class.where(group_key => group_id).count
12
+ products.pluck(:id).map do |product_id|
13
+ {
14
+ group_key => group_id, product_id: product_id, position: (position += 1),
15
+ created_at: Time.current, updated_at: Time.current
16
+ }
17
+ end
18
+ end
19
+ membership_class.insert_all(rows)
20
+
21
+ refresh_after_membership_change(groups.pluck(:id), products)
22
+ end
23
+
24
+ # Removes the products, then re-packs the remaining positions from 1.
25
+ def remove_memberships(groups, products)
26
+ group_ids = groups.pluck(:id)
27
+
28
+ ApplicationRecord.transaction do
29
+ membership_class.where(group_key => group_ids, product_id: products.pluck(:id)).delete_all
30
+
31
+ rows = group_ids.flat_map do |group_id|
32
+ membership_class.where(group_key => group_id).order(:position).pluck(:product_id).map.with_index(1) do |product_id, position|
33
+ {
34
+ group_key => group_id, product_id: product_id, position: position,
35
+ created_at: Time.current, updated_at: Time.current
36
+ }
37
+ end
38
+ end
39
+ membership_class.upsert_all(rows, unique_by: ([group_key, :product_id] unless Spree.mysql?)) if rows.any?
40
+ end
41
+
42
+ refresh_after_membership_change(group_ids, products)
43
+ end
44
+
45
+ # Bulk writes skip the join model's callbacks, so counters, caches and the
46
+ # search index are refreshed here.
47
+ def refresh_after_membership_change(group_ids, products)
48
+ product_ids = products.pluck(:id)
49
+ product_ids.each { |id| Spree::Product.reset_counters(id, membership_class.model_name.element.pluralize.to_sym) }
50
+ refresh_group_counters(group_ids)
51
+
52
+ Spree::Product.where(id: product_ids).touch_all
53
+ products.each(&:enqueue_search_index)
54
+ group_class.where(id: group_ids).touch_all
55
+ end
56
+
57
+ def group_key
58
+ group_class.model_name.element.foreign_key.to_sym
59
+ end
60
+ end
61
+ end