spree_core 6.0.0.beta4 → 6.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/helpers/spree/images_helper.rb +0 -57
  4. data/app/jobs/spree/base_job.rb +21 -0
  5. data/app/jobs/spree/carts/reap_expired_job.rb +2 -2
  6. data/app/jobs/spree/collections/regenerate_time_based_job.rb +6 -3
  7. data/app/jobs/spree/orders/finalize_stale_drafts_job.rb +8 -5
  8. data/app/jobs/spree/payments/cancel_unused_sessions_job.rb +61 -0
  9. data/app/jobs/spree/seller_payouts/sweep_seller_job.rb +2 -0
  10. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  11. data/app/mailers/spree/base_mailer.rb +96 -13
  12. data/app/mailers/spree/email_template_mailer.rb +27 -0
  13. data/app/mailers/spree/export_mailer.rb +3 -4
  14. data/app/mailers/spree/import_mailer.rb +3 -4
  15. data/app/mailers/spree/invitation_mailer.rb +24 -7
  16. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  17. data/app/mailers/spree/webhook_mailer.rb +2 -2
  18. data/app/models/concerns/spree/acted_by.rb +1 -1
  19. data/app/models/concerns/spree/admin_user_methods.rb +13 -1
  20. data/app/models/concerns/spree/api_typed.rb +91 -0
  21. data/app/models/concerns/spree/calculator/tiers.rb +76 -0
  22. data/app/models/concerns/spree/customer_methods.rb +15 -14
  23. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  24. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  25. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  26. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  27. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  28. data/app/models/concerns/spree/preference_schema.rb +4 -12
  29. data/app/models/concerns/spree/product_scopes.rb +11 -1
  30. data/app/models/concerns/spree/provider_listing.rb +41 -0
  31. data/app/models/concerns/spree/publishable.rb +82 -14
  32. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  33. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  34. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  35. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  36. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  37. data/app/models/concerns/spree/purchase/payment_processing.rb +7 -3
  38. data/app/models/concerns/spree/purchase/purchase_order.rb +2 -2
  39. data/app/models/concerns/spree/purchase/totals.rb +49 -1
  40. data/app/models/concerns/spree/receivable.rb +1 -1
  41. data/app/models/concerns/spree/searchable.rb +0 -4
  42. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  43. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  44. data/app/models/concerns/spree/stores/email_branding.rb +31 -0
  45. data/app/models/concerns/spree/type_labels.rb +38 -0
  46. data/app/models/concerns/spree/typed_adjustment_line.rb +1 -1
  47. data/app/models/concerns/spree/typed_associations.rb +6 -0
  48. data/app/models/spree/address.rb +11 -1
  49. data/app/models/spree/adjusters/promotion.rb +1 -15
  50. data/app/models/spree/allowed_origin.rb +0 -1
  51. data/app/models/spree/api_key.rb +5 -6
  52. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  53. data/app/models/spree/base.rb +70 -22
  54. data/app/models/spree/calculator/flat_percent_item_total.rb +1 -3
  55. data/app/models/spree/calculator/flat_rate.rb +1 -3
  56. data/app/models/spree/calculator/flexi_rate.rb +1 -3
  57. data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
  58. data/app/models/spree/calculator/price_sack.rb +1 -3
  59. data/app/models/spree/calculator/shipping/digital_delivery.rb +1 -3
  60. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +1 -3
  61. data/app/models/spree/calculator/shipping/flat_rate.rb +1 -3
  62. data/app/models/spree/calculator/shipping/flexi_rate.rb +1 -3
  63. data/app/models/spree/calculator/shipping/per_item.rb +1 -3
  64. data/app/models/spree/calculator/shipping/price_sack.rb +1 -3
  65. data/app/models/spree/calculator/tiered_flat_rate.rb +6 -23
  66. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  67. data/app/models/spree/cart.rb +78 -18
  68. data/app/models/spree/channel/gating.rb +1 -1
  69. data/app/models/spree/channel.rb +4 -4
  70. data/app/models/spree/claim.rb +21 -1
  71. data/app/models/spree/claim_line_item.rb +41 -6
  72. data/app/models/spree/collection.rb +0 -1
  73. data/app/models/spree/collection_rule.rb +5 -21
  74. data/app/models/spree/commission_line.rb +1 -1
  75. data/app/models/spree/commission_rate.rb +2 -3
  76. data/app/models/spree/commission_rate_value.rb +1 -1
  77. data/app/models/spree/commission_rule.rb +4 -11
  78. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  79. data/app/models/spree/company_invitation.rb +1 -0
  80. data/app/models/spree/consent_record.rb +0 -1
  81. data/app/models/spree/credit_card.rb +1 -1
  82. data/app/models/spree/current.rb +23 -0
  83. data/app/models/spree/custom_field.rb +2 -2
  84. data/app/models/spree/custom_field_definition.rb +9 -23
  85. data/app/models/spree/custom_fields/boolean.rb +1 -1
  86. data/app/models/spree/custom_fields/json.rb +1 -1
  87. data/app/models/spree/data_feed/google.rb +0 -2
  88. data/app/models/spree/data_request.rb +1 -1
  89. data/app/models/spree/delivery_method.rb +30 -13
  90. data/app/models/spree/delivery_method_rule.rb +4 -11
  91. data/app/models/spree/delivery_rate.rb +4 -5
  92. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  93. data/app/models/spree/delivery_zone.rb +0 -1
  94. data/app/models/spree/delivery_zone_member.rb +4 -4
  95. data/app/models/spree/digital_asset.rb +7 -0
  96. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  97. data/app/models/spree/digital_link.rb +2 -5
  98. data/app/models/spree/discount.rb +1 -1
  99. data/app/models/spree/email_template.rb +25 -0
  100. data/app/models/spree/email_template_draft.rb +13 -0
  101. data/app/models/spree/email_template_revision.rb +17 -0
  102. data/app/models/spree/email_templates/entry.rb +118 -0
  103. data/app/models/spree/email_templates/sample_record.rb +16 -0
  104. data/app/models/spree/emails/branding.rb +80 -0
  105. data/app/models/spree/exchange.rb +15 -1
  106. data/app/models/spree/exchange_line_item.rb +121 -5
  107. data/app/models/spree/export.rb +0 -1
  108. data/app/models/spree/exports/products.rb +10 -1
  109. data/app/models/spree/exports/report.rb +1 -1
  110. data/app/models/spree/fee.rb +4 -2
  111. data/app/models/spree/fulfillment.rb +53 -27
  112. data/app/models/spree/fulfillment_changer.rb +2 -2
  113. data/app/models/spree/fulfillment_item.rb +0 -4
  114. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  115. data/app/models/spree/gateway/bogus.rb +4 -0
  116. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  117. data/app/models/spree/gateway.rb +5 -1
  118. data/app/models/spree/gift_card.rb +12 -1
  119. data/app/models/spree/gift_card_batch.rb +0 -1
  120. data/app/models/spree/import.rb +2 -7
  121. data/app/models/spree/import_mapping.rb +1 -2
  122. data/app/models/spree/import_row.rb +2 -2
  123. data/app/models/spree/import_schemas/products.rb +1 -0
  124. data/app/models/spree/integration.rb +8 -18
  125. data/app/models/spree/invitation.rb +5 -5
  126. data/app/models/spree/line_item.rb +13 -2
  127. data/app/models/spree/locale.rb +11 -8
  128. data/app/models/spree/market.rb +1 -0
  129. data/app/models/spree/newsletter_subscriber.rb +3 -0
  130. data/app/models/spree/order.rb +56 -138
  131. data/app/models/spree/order_group.rb +11 -2
  132. data/app/models/spree/order_inventory.rb +10 -5
  133. data/app/models/spree/order_promotion.rb +1 -1
  134. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  135. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  136. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  137. data/app/models/spree/order_routing_rule.rb +6 -22
  138. data/app/models/spree/package_type.rb +1 -2
  139. data/app/models/spree/payment/gateway_options.rb +14 -52
  140. data/app/models/spree/payment/processing.rb +2 -4
  141. data/app/models/spree/payment.rb +20 -5
  142. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  143. data/app/models/spree/payment_method/store_credit.rb +4 -4
  144. data/app/models/spree/payment_method.rb +36 -4
  145. data/app/models/spree/payment_session.rb +5 -1
  146. data/app/models/spree/payment_setup_session.rb +1 -0
  147. data/app/models/spree/payout_provider/base.rb +3 -1
  148. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  149. data/app/models/spree/price_list.rb +1 -2
  150. data/app/models/spree/price_rules/channel_rule.rb +1 -1
  151. data/app/models/spree/price_rules/customer_group_rule.rb +1 -1
  152. data/app/models/spree/product.rb +31 -7
  153. data/app/models/spree/product_submission.rb +0 -12
  154. data/app/models/spree/product_type.rb +0 -1
  155. data/app/models/spree/promotion/actions/create_line_items.rb +27 -17
  156. data/app/models/spree/promotion/rules/category.rb +0 -4
  157. data/app/models/spree/promotion/rules/channel.rb +0 -4
  158. data/app/models/spree/promotion/rules/country.rb +0 -4
  159. data/app/models/spree/promotion/rules/currency.rb +0 -4
  160. data/app/models/spree/promotion/rules/customer_group.rb +0 -4
  161. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  162. data/app/models/spree/promotion/rules/item_total.rb +4 -8
  163. data/app/models/spree/promotion/rules/market.rb +0 -4
  164. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  165. data/app/models/spree/promotion/rules/option_value.rb +0 -3
  166. data/app/models/spree/promotion/rules/product.rb +0 -4
  167. data/app/models/spree/promotion/rules/user.rb +0 -4
  168. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  169. data/app/models/spree/promotion.rb +35 -6
  170. data/app/models/spree/promotion_action.rb +7 -14
  171. data/app/models/spree/promotion_action_line_item.rb +1 -1
  172. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  173. data/app/models/spree/promotion_rule.rb +7 -14
  174. data/app/models/spree/purchase_order.rb +1 -0
  175. data/app/models/spree/quantity_rule.rb +1 -1
  176. data/app/models/spree/refund.rb +28 -3
  177. data/app/models/spree/return.rb +30 -2
  178. data/app/models/spree/return_line_item.rb +65 -14
  179. data/app/models/spree/role.rb +7 -7
  180. data/app/models/spree/saved_report.rb +1 -1
  181. data/app/models/spree/seller.rb +2 -1
  182. data/app/models/spree/seller_payout.rb +7 -0
  183. data/app/models/spree/seller_requirement.rb +4 -12
  184. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  185. data/app/models/spree/seller_requirement_submission.rb +2 -2
  186. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  187. data/app/models/spree/seller_transfer.rb +5 -0
  188. data/app/models/spree/shipping_label.rb +1 -0
  189. data/app/models/spree/stock/availability_validator.rb +2 -2
  190. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  191. data/app/models/spree/stock/packer.rb +2 -2
  192. data/app/models/spree/stock/splitter/weight.rb +1 -1
  193. data/app/models/spree/stock_level/custom_events.rb +6 -0
  194. data/app/models/spree/stock_level.rb +1 -1
  195. data/app/models/spree/stock_movement.rb +1 -1
  196. data/app/models/spree/stock_receipt_item.rb +1 -1
  197. data/app/models/spree/stock_reservation.rb +2 -2
  198. data/app/models/spree/stock_transfer.rb +4 -2
  199. data/app/models/spree/store.rb +33 -7
  200. data/app/models/spree/store_credit.rb +8 -9
  201. data/app/models/spree/store_credit_event.rb +5 -5
  202. data/app/models/spree/tax_exemption_certificate.rb +2 -0
  203. data/app/models/spree/tax_identifier.rb +3 -1
  204. data/app/models/spree/tax_line.rb +53 -3
  205. data/app/models/spree/tax_provider/base.rb +71 -14
  206. data/app/models/spree/tax_provider/internal.rb +59 -33
  207. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  208. data/app/models/spree/tax_rate.rb +1 -2
  209. data/app/models/spree/taxonomy.rb +0 -1
  210. data/app/models/spree/variant.rb +4 -3
  211. data/app/models/spree/variant_media.rb +1 -1
  212. data/app/models/spree/webhook_endpoint.rb +10 -13
  213. data/app/models/spree/wishlist.rb +1 -6
  214. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  215. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  216. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  217. data/app/presenters/spree/csv/order_line_item_presenter.rb +1 -1
  218. data/app/presenters/spree/csv/product_variant_presenter.rb +42 -12
  219. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  220. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  221. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  222. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  223. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  224. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  225. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  226. data/app/services/spree/carts/create.rb +8 -2
  227. data/app/services/spree/carts/destroy.rb +2 -2
  228. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  229. data/app/services/spree/carts/split_by_seller.rb +1 -1
  230. data/app/services/spree/carts/update.rb +19 -5
  231. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  232. data/app/services/spree/categories/add_products.rb +13 -31
  233. data/app/services/spree/categories/remove_products.rb +12 -51
  234. data/app/services/spree/checkout/advance.rb +1 -4
  235. data/app/services/spree/collections/add_products.rb +10 -23
  236. data/app/services/spree/collections/remove_products.rb +7 -43
  237. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  238. data/app/services/spree/data_requests/create.rb +1 -1
  239. data/app/services/spree/deliveries/destroy.rb +2 -2
  240. data/app/services/spree/email_templates/check.rb +58 -0
  241. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  242. data/app/services/spree/email_templates/preview.rb +99 -0
  243. data/app/services/spree/emails/file_system.rb +23 -0
  244. data/app/services/spree/emails/filters.rb +84 -0
  245. data/app/services/spree/emails/liquid_context.rb +48 -0
  246. data/app/services/spree/emails/rendered_email.rb +14 -0
  247. data/app/services/spree/emails/renderer.rb +125 -0
  248. data/app/services/spree/emails/template.rb +51 -0
  249. data/app/services/spree/emails/template_data.rb +23 -0
  250. data/app/services/spree/emails/template_resolver.rb +109 -0
  251. data/app/services/spree/emails/text_converter.rb +72 -0
  252. data/app/services/spree/emails/translation_inliner.rb +134 -0
  253. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  254. data/app/services/spree/imports/row_processors/product_variant.rb +65 -34
  255. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  256. data/app/services/spree/locales.rb +4 -5
  257. data/app/services/spree/newsletter/subscribe.rb +8 -12
  258. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  259. data/app/services/spree/orders/create.rb +2 -1
  260. data/app/services/spree/orders/discounts/create.rb +7 -4
  261. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  262. data/app/services/spree/orders/discounts/update.rb +1 -0
  263. data/app/services/spree/orders/fees/create.rb +1 -0
  264. data/app/services/spree/orders/fees/destroy.rb +1 -0
  265. data/app/services/spree/orders/fees/update.rb +1 -0
  266. data/app/services/spree/orders/remove_line_item.rb +13 -3
  267. data/app/services/spree/orders/update.rb +10 -6
  268. data/app/services/spree/orders/update_item.rb +6 -3
  269. data/app/services/spree/orders/update_statuses.rb +3 -1
  270. data/app/services/spree/product_memberships.rb +61 -0
  271. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  272. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  273. data/app/services/spree/seeds/commission_rates.rb +1 -1
  274. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  275. data/app/services/spree/seeds/payment_methods.rb +2 -2
  276. data/app/services/spree/seeds/saved_reports.rb +3 -3
  277. data/app/services/spree/sellers/requirements.rb +1 -2
  278. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  279. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  280. data/app/services/spree/stock_levels/correct.rb +3 -3
  281. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  282. data/app/services/spree/store_credits/apply.rb +15 -13
  283. data/app/services/spree/stores/provision_defaults.rb +15 -14
  284. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  285. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  286. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  287. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  288. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  289. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  290. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  291. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  292. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  293. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  294. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  295. data/app/views/spree/shared/_summary_row.liquid +8 -0
  296. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  297. data/app/workflows/spree/carts/add_item.rb +4 -4
  298. data/app/workflows/spree/carts/complete.rb +13 -3
  299. data/app/workflows/spree/carts/merge.rb +1 -1
  300. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  301. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  302. data/app/workflows/spree/claims/approve.rb +5 -22
  303. data/app/workflows/spree/claims/cancel.rb +5 -21
  304. data/app/workflows/spree/claims/create.rb +15 -0
  305. data/app/workflows/spree/claims/deny.rb +7 -0
  306. data/app/workflows/spree/claims/resolve.rb +34 -31
  307. data/app/workflows/spree/customers/anonymize.rb +3 -4
  308. data/app/workflows/spree/customers/create.rb +1 -1
  309. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  310. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  311. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  312. data/app/workflows/spree/email_templates/publish.rb +70 -0
  313. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  314. data/app/workflows/spree/email_templates/revert.rb +29 -0
  315. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  316. data/app/workflows/spree/exchanges/approve.rb +5 -22
  317. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  318. data/app/workflows/spree/exchanges/create.rb +6 -0
  319. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  320. data/app/workflows/spree/exchanges/receive.rb +6 -65
  321. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  322. data/app/workflows/spree/fulfillments/create.rb +51 -47
  323. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  324. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  325. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  326. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  327. data/app/workflows/spree/orders/add_item.rb +5 -1
  328. data/app/workflows/spree/orders/cancel.rb +3 -3
  329. data/app/workflows/spree/orders/complete.rb +16 -8
  330. data/app/workflows/spree/payments/process.rb +2 -2
  331. data/app/workflows/spree/post_sale/approval.rb +35 -0
  332. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  333. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  334. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  335. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  336. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  337. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  338. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  339. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  340. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  341. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  342. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  343. data/app/workflows/spree/refunds/create.rb +5 -2
  344. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  345. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  346. data/app/workflows/spree/returns/approve.rb +5 -22
  347. data/app/workflows/spree/returns/cancel.rb +5 -22
  348. data/app/workflows/spree/returns/create.rb +8 -0
  349. data/app/workflows/spree/returns/receive.rb +5 -76
  350. data/app/workflows/spree/returns/refund.rb +31 -8
  351. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  352. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  353. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  354. data/app/workflows/spree/shipping_labels/record.rb +5 -5
  355. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  356. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  357. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  358. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  359. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  360. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  361. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  362. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  363. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  364. data/config/i18n-tasks.yml +32 -18
  365. data/config/initializers/alba.rb +7 -0
  366. data/config/language_names.csv +264 -0
  367. data/config/locales/ar.yml +811 -0
  368. data/config/locales/be.yml +855 -0
  369. data/config/locales/bg.yml +723 -0
  370. data/config/locales/ca.yml +689 -0
  371. data/config/locales/cs.yml +855 -0
  372. data/config/locales/da.yml +771 -0
  373. data/config/locales/de-CH.yml +248 -0
  374. data/config/locales/de.yml +835 -0
  375. data/config/locales/el.yml +769 -0
  376. data/config/locales/en.yml +37 -503
  377. data/config/locales/es-CL.yml +301 -0
  378. data/config/locales/es-EC.yml +186 -0
  379. data/config/locales/es-MX.yml +334 -0
  380. data/config/locales/es.yml +831 -0
  381. data/config/locales/et.yml +644 -0
  382. data/config/locales/fa.yml +709 -0
  383. data/config/locales/fi.yml +771 -0
  384. data/config/locales/fr.yml +830 -0
  385. data/config/locales/hu.yml +751 -0
  386. data/config/locales/id.yml +756 -0
  387. data/config/locales/it.yml +833 -0
  388. data/config/locales/ja.yml +726 -0
  389. data/config/locales/km.yml +759 -0
  390. data/config/locales/ko.yml +575 -0
  391. data/config/locales/ku.yml +753 -0
  392. data/config/locales/lt.yml +821 -0
  393. data/config/locales/lv.yml +685 -0
  394. data/config/locales/nb.yml +824 -0
  395. data/config/locales/ne.yml +823 -0
  396. data/config/locales/nl.yml +826 -0
  397. data/config/locales/pl.yml +835 -0
  398. data/config/locales/pt-BR.yml +547 -0
  399. data/config/locales/pt.yml +636 -0
  400. data/config/locales/ro.yml +750 -0
  401. data/config/locales/ru.yml +834 -0
  402. data/config/locales/sk.yml +680 -0
  403. data/config/locales/sl-SI.yml +600 -0
  404. data/config/locales/sv.yml +827 -0
  405. data/config/locales/th.yml +745 -0
  406. data/config/locales/tr.yml +777 -0
  407. data/config/locales/uk.yml +836 -0
  408. data/config/locales/vi.yml +768 -0
  409. data/config/locales/zh-CN.yml +786 -0
  410. data/config/locales/zh-TW.yml +834 -0
  411. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  412. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  413. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  414. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  415. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  416. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  417. data/lib/spree/checkout/default_requirements.rb +37 -13
  418. data/lib/spree/core/configuration.rb +1 -1
  419. data/lib/spree/core/controller_helpers/store.rb +1 -1
  420. data/lib/spree/core/dependencies.rb +5 -4
  421. data/lib/spree/core/emails/escaped_output.rb +60 -0
  422. data/lib/spree/core/engine.rb +48 -167
  423. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  424. data/lib/spree/core/preferences/json_conversion.rb +182 -0
  425. data/lib/spree/core/preferences/masking.rb +4 -1
  426. data/lib/spree/core/preferences/preferable.rb +137 -83
  427. data/lib/spree/core/preferences/preferable_class_methods.rb +81 -146
  428. data/lib/spree/core/previews/import_preview.rb +10 -0
  429. data/lib/spree/core/tax/provider_error.rb +37 -0
  430. data/lib/spree/core/version.rb +1 -1
  431. data/lib/spree/core.rb +85 -370
  432. data/lib/spree/emails/editable_templates.rb +82 -0
  433. data/lib/spree/events/catalog.rb +189 -0
  434. data/lib/spree/events.rb +21 -1
  435. data/lib/spree/hooks.rb +0 -17
  436. data/lib/spree/i18n.rb +32 -15
  437. data/lib/spree/reporting/adapters/live.rb +6 -6
  438. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  439. data/lib/spree/reporting/hydration.rb +1 -1
  440. data/lib/spree/reporting/query.rb +8 -0
  441. data/lib/spree/reporting/schema.rb +10 -10
  442. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  443. data/lib/spree/testing_support/emails.rb +22 -0
  444. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  445. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  446. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  447. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  448. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  449. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  450. data/lib/spree/testing_support/i18n.rb +3 -94
  451. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  452. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  453. data/lib/spree/testing_support/preferences.rb +0 -5
  454. data/lib/spree/testing_support/store.rb +9 -1
  455. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +45 -0
  456. data/lib/tasks/categories_and_collections.rake +3 -3
  457. data/lib/tasks/core.rake +6 -4
  458. data/lib/tasks/media.rake +3 -3
  459. data/lib/tasks/package_types.rake +1 -1
  460. data/lib/tasks/preferences_json.rake +101 -0
  461. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  462. metadata +188 -44
  463. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  464. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  465. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  466. data/app/models/spree/invitations/store.rb +0 -6
  467. data/app/models/spree/preference.rb +0 -14
  468. data/app/models/spree/promotion_handler/page.rb +0 -25
  469. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  470. data/app/models/spree/stock/differentiator.rb +0 -45
  471. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  472. data/app/services/spree/carts/empty.rb +0 -42
  473. data/app/services/spree/country_to_timezone.rb +0 -273
  474. data/app/services/spree/payments/create.rb +0 -92
  475. data/app/validators/spree/url_validator.rb +0 -23
  476. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  477. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  478. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  479. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  480. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  481. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  482. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  483. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  484. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  485. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  486. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  487. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  488. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  489. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  490. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  491. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  492. data/lib/spree/analytics.rb +0 -11
  493. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  494. data/lib/spree/core/preferences/store.rb +0 -105
  495. data/lib/spree/translation_migrations.rb +0 -52
@@ -0,0 +1,82 @@
1
+ module Spree
2
+ module Emails
3
+ # The email templates merchants may edit in the dashboard: the emails
4
+ # customers receive, plus the layout and shared partials those emails are
5
+ # built from. Staff, store-owner and seller emails are never registered, so
6
+ # they always render from files. `spree_emails` registers its own; an
7
+ # extension adding a customer email registers it the same way.
8
+ #
9
+ # @example
10
+ # Spree.editable_email_templates.register(
11
+ # 'spree/loyalty_mailer/points_earned_email',
12
+ # kind: :email, sample: 'MyApp::EmailSamples::PointsEarned'
13
+ # )
14
+ class EditableTemplates
15
+ include Enumerable
16
+
17
+ KINDS = %i[email layout partial].freeze
18
+
19
+ # One editable template.
20
+ class Definition
21
+ attr_reader :key, :kind, :sample
22
+
23
+ # @param key [String] the template's view path, e.g. "spree/order_mailer/confirm_email"
24
+ # @param kind [Symbol] :email, :layout or :partial
25
+ # @param sample [String, nil] class building sample variables for an email (see Spree::Emails::Samples::Base)
26
+ def initialize(key:, kind:, sample: nil)
27
+ raise ArgumentError, "Unknown email template kind: #{kind.inspect}" unless KINDS.include?(kind)
28
+
29
+ @key = key
30
+ @kind = kind
31
+ @sample = sample
32
+ end
33
+
34
+ def email?
35
+ kind == :email
36
+ end
37
+
38
+ # @return [Class, nil]
39
+ def sample_class
40
+ sample&.constantize
41
+ end
42
+ end
43
+
44
+ def initialize
45
+ @definitions = {}
46
+ end
47
+
48
+ # @param key [String]
49
+ # @param kind [Symbol]
50
+ # @param sample [String, nil]
51
+ # @return [Spree::Emails::EditableTemplates::Definition]
52
+ def register(key, kind: :email, sample: nil)
53
+ @definitions[key] = Definition.new(key: key, kind: kind, sample: sample)
54
+ end
55
+
56
+ # @param key [String]
57
+ def delete(key)
58
+ @definitions.delete(key)
59
+ end
60
+
61
+ # @param key [String]
62
+ # @return [Spree::Emails::EditableTemplates::Definition, nil]
63
+ def [](key)
64
+ @definitions[key]
65
+ end
66
+
67
+ # @param key [String]
68
+ def include?(key)
69
+ @definitions.key?(key)
70
+ end
71
+
72
+ def each(&block)
73
+ @definitions.each_value(&block)
74
+ end
75
+
76
+ # @return [Array<Spree::Emails::EditableTemplates::Definition>] the editable emails, without layout and partials
77
+ def emails
78
+ select(&:email?)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Events
5
+ # Raised in development and test when code publishes an event that no model
6
+ # declared with `publishes_event`.
7
+ class UndeclaredEventError < StandardError; end
8
+
9
+ # Every event Spree publishes, declared once on the model whose record is the
10
+ # payload (`publishes_events` / `publishes_event` / `publishes_lifecycle_events`).
11
+ #
12
+ # The catalog is the single source for the webhook event types generated into
13
+ # `@spree/sdk/webhooks`, the Admin API list of subscribable events and the
14
+ # rule that keeps credential-carrying events off wildcard webhook endpoints.
15
+ class Catalog
16
+ # One declared event.
17
+ #
18
+ # @!attribute name [String] the full event name, e.g. `order.placed`
19
+ # @!attribute model_name [String] the declaring model's class name
20
+ # @!attribute serializer_name [String, nil] the serializer named in the declaration, if any
21
+ # @!attribute credential_permission [String, nil] for an event carrying a live
22
+ # credential, the permission needed to point a webhook endpoint at it
23
+ # @!attribute deprecated_alias_of [String, nil] the event this one duplicates, when deprecated
24
+ # @!attribute webhook [Boolean] false for an event never forwarded to webhook endpoints
25
+ Entry = Data.define(:name, :model_name, :serializer_name, :credential_permission, :deprecated_alias_of, :webhook) do
26
+ # The resource segment of the name, used to group events for display.
27
+ #
28
+ # @return [String] e.g. `order` for `order.placed`
29
+ def group
30
+ name.split('.').first
31
+ end
32
+
33
+ # Whether the payload carries a live credential (a password reset token),
34
+ # which a webhook endpoint receives only when it names the event outright.
35
+ #
36
+ # @return [Boolean]
37
+ def credential?
38
+ credential_permission.present?
39
+ end
40
+
41
+ alias_method :webhook?, :webhook
42
+
43
+ # @return [Boolean]
44
+ def deprecated?
45
+ deprecated_alias_of.present?
46
+ end
47
+
48
+ # The serializer that builds the payload: the declared one, or the model's
49
+ # Store serializer resolved the way `Spree::Publishable#event_payload` does.
50
+ # Nil when there is none — the payload is then only the record's `id`,
51
+ # `created_at` and `updated_at` — or when `spree_api` is not installed.
52
+ #
53
+ # @return [Class, nil]
54
+ def payload_serializer
55
+ serializer_name&.safe_constantize || model_name.constantize.allocate.event_serializer_class
56
+ end
57
+ end
58
+
59
+ def initialize
60
+ @declarations = {}
61
+ end
62
+
63
+ # Declare an event for a model. A bare action (`:placed`) is prefixed with
64
+ # the model's event prefix when the catalog is read, so a prefix assigned
65
+ # later in the class body still applies; a dotted name is used as given.
66
+ #
67
+ # Re-declaring the same model and name replaces the earlier declaration,
68
+ # which keeps the catalog correct across code reloads.
69
+ #
70
+ # @param model [Class] the model whose record is the payload
71
+ # @param name [Symbol, String] an action or a full event name
72
+ # @param options [Hash] `serializer:`, `credential:` (the permission key
73
+ # required to receive it), `deprecated_alias_of:`, `webhook:` (false to
74
+ # never forward it to webhook endpoints)
75
+ # @return [void]
76
+ def declare(model, name, **options)
77
+ # A named class is keyed by its name so a reloaded class replaces its
78
+ # stale copy; an anonymous one (a spec's `Class.new`) by the class.
79
+ @declarations[[model.name || model, name.to_s]] = options
80
+ @entries = @sorted = nil
81
+ end
82
+
83
+ # @param name [String]
84
+ # @return [Entry, nil]
85
+ def find(name)
86
+ entries[name.to_s]
87
+ end
88
+
89
+ # Every declared event, sorted by name.
90
+ #
91
+ # @return [Array<Entry>]
92
+ def all
93
+ load!
94
+ @sorted ||= entries.values.sort_by(&:name)
95
+ end
96
+
97
+ # Events a webhook endpoint can subscribe to.
98
+ #
99
+ # @return [Array<Entry>]
100
+ def webhook_events
101
+ all.select(&:webhook?)
102
+ end
103
+
104
+ # Whether an event may be forwarded to webhook endpoints. An undeclared
105
+ # event may, so a production store keeps delivering it while the missing
106
+ # declaration is only logged. A miss loads every model first: where
107
+ # classes load lazily, the model declaring `webhook: false` may not be
108
+ # loaded in the process delivering webhooks yet.
109
+ #
110
+ # @param name [String]
111
+ # @return [Boolean]
112
+ def webhook?(name)
113
+ entry = find(name)
114
+ unless entry
115
+ load!
116
+ entry = find(name)
117
+ end
118
+ entry.nil? || entry.webhook?
119
+ end
120
+
121
+ # @param name [String]
122
+ # @return [Boolean]
123
+ def credential?(name)
124
+ find(name)&.credential? || false
125
+ end
126
+
127
+ # Events whose payload carries a live credential, optionally only those
128
+ # among the given names.
129
+ #
130
+ # @param names [Array<String>, nil]
131
+ # @return [Array<Entry>]
132
+ def credential_events(names = nil)
133
+ events = all.select(&:credential?)
134
+ names ? events.select { |entry| names.include?(entry.name) } : events
135
+ end
136
+
137
+ # Checks that an event about to be published is declared. Raises
138
+ # {UndeclaredEventError} in development and test, logs a warning otherwise.
139
+ #
140
+ # @param name [String]
141
+ # @return [void]
142
+ def verify_declared!(name)
143
+ return if find(name)
144
+
145
+ # A class declared before it had a name is only listed once it has one,
146
+ # so a miss rebuilds once — and only while anonymous declarations exist.
147
+ if !@loaded || @declarations.each_key.any? { |model_key, _| model_key.is_a?(Class) }
148
+ @entries = @sorted = nil
149
+ load!
150
+ return if find(name)
151
+ end
152
+
153
+ message = "Spree event #{name.inspect} is not declared. Declare it with `publishes_event` on the model whose record is the payload."
154
+ raise UndeclaredEventError, message if Spree::Events.raise_on_undeclared_events
155
+
156
+ Rails.logger.warn("[Spree Events] #{message}")
157
+ end
158
+
159
+ # Loads every model so the catalog holds all declarations, which only
160
+ # matters where Rails does not eager load (development and test).
161
+ #
162
+ # @return [void]
163
+ def load!
164
+ return if @loaded
165
+
166
+ @loaded = true
167
+ Rails.application.eager_load! unless Rails.application.config.eager_load
168
+ end
169
+
170
+ private
171
+
172
+ def entries
173
+ @entries ||= @declarations.each_with_object({}) do |((model_key, name), options), result|
174
+ model = model_key.is_a?(Class) ? model_key : model_key.safe_constantize
175
+ next unless model&.name
176
+
177
+ # A parent and its subclass can declare the same name; the parent,
178
+ # declared first, stays the owner.
179
+ full_name = name.include?('.') ? name : "#{model.event_prefix}.#{name}"
180
+ result[full_name] ||= Entry.new(
181
+ name: full_name, model_name: model.name, serializer_name: options[:serializer],
182
+ credential_permission: options[:credential], deprecated_alias_of: options[:deprecated_alias_of],
183
+ webhook: options.fetch(:webhook, true)
184
+ )
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
data/lib/spree/events.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'events/registry'
4
+ require_relative 'events/catalog'
4
5
  require_relative 'events/adapters/base'
5
6
  require_relative 'events/adapters/active_support_notifications'
6
7
 
@@ -34,6 +35,24 @@ module Spree
34
35
  # be logged multiple times.
35
36
  attr_accessor :log_subscription
36
37
 
38
+ # Whether publishing an undeclared event raises (development and test) or
39
+ # only logs a warning (production), see {Catalog#verify_declared!}.
40
+ attr_writer :raise_on_undeclared_events
41
+
42
+ # @return [Boolean]
43
+ def raise_on_undeclared_events
44
+ return @raise_on_undeclared_events unless @raise_on_undeclared_events.nil?
45
+
46
+ Rails.env.development? || Rails.env.test?
47
+ end
48
+
49
+ # Every event Spree publishes, see {Catalog}.
50
+ #
51
+ # @return [Spree::Events::Catalog]
52
+ def catalog
53
+ @catalog ||= Catalog.new
54
+ end
55
+
37
56
  # Publish an event to all matching subscribers
38
57
  #
39
58
  # @param name [String] The event name (e.g., 'order.completed')
@@ -151,11 +170,12 @@ module Spree
151
170
  # In development, Zeitwerk may have reloaded the class, creating a new
152
171
  # class object while the old one is still referenced in Spree.subscribers.
153
172
  # This method resolves the constant fresh to get the current class.
173
+ # A class name given as a String is resolved in every environment.
154
174
  #
155
175
  # @param subscriber [Class, String] The subscriber class or class name
156
176
  # @return [Class, nil] The resolved class or nil if not found
157
177
  def resolve_subscriber(subscriber)
158
- return subscriber unless Rails.env.development? || Rails.env.test?
178
+ return subscriber unless subscriber.is_a?(String) || Rails.env.development? || Rails.env.test?
159
179
 
160
180
  class_name = subscriber.is_a?(String) ? subscriber : subscriber.name
161
181
  return nil unless class_name
data/lib/spree/hooks.rb CHANGED
@@ -98,23 +98,6 @@ module Spree
98
98
  @handlers.clear
99
99
  end
100
100
 
101
- # Introspection facade: Spree.hooks.carts => { 'carts.add_item' => [...] }
102
- def method_missing(name, *args, &block)
103
- prefix = "#{name}."
104
- matching = @workflows.keys.select { |workflow_key| workflow_key.start_with?(prefix) || workflow_key == name.to_s }
105
- return super if matching.empty? && !@workflows.keys.any? { |k| k.start_with?(prefix) }
106
-
107
- matching.index_with do |workflow_key|
108
- workflow = @workflows[workflow_key]&.safe_constantize
109
- workflow ? workflow.declared_hooks : []
110
- end
111
- end
112
-
113
- def respond_to_missing?(name, include_private = false)
114
- prefix = "#{name}."
115
- @workflows.keys.any? { |workflow_key| workflow_key.start_with?(prefix) } || super
116
- end
117
-
118
101
  private
119
102
 
120
103
  # Last writer wins on a collision, which is only defensible if it's
data/lib/spree/i18n.rb CHANGED
@@ -1,30 +1,47 @@
1
1
  require 'i18n'
2
- require 'active_support/core_ext/array/extract_options'
3
- require 'action_view'
4
2
 
5
3
  module Spree
6
- class TranslationHelperWrapper
7
- include ActionView::Helpers::TranslationHelper
8
- end
4
+ SHIPPED_LOCALES = Dir[File.expand_path('../../config/locales/*.yml', __dir__)].map { |file| File.basename(file, '.yml').to_sym }.freeze
9
5
 
10
6
  class << self
11
- # Add spree namespace and delegate to Rails TranslationHelper for some nice
12
- # extra functionality. e.g return reasonable strings for missing translations
7
+ # @deprecated Use +I18n.t+ with the full key, e.g. +I18n.t('spree.free')+.
8
+ # Will be removed in Spree 6.1.
13
9
  def translate(key, options = {})
14
- options[:scope] = [*options[:scope]].unshift(:spree).uniq
10
+ Spree::Deprecation.warn('Spree.t is deprecated and will be removed in Spree 6.1. Use I18n.t with the full key, e.g. I18n.t("spree.free").') if defined?(Spree::Deprecation)
15
11
 
16
- TranslationHelperWrapper.new.translate(key, **options)
12
+ I18n.t(key, **options.symbolize_keys, scope: [:spree, *Array(options[:scope]).map(&:to_sym)].uniq)
17
13
  end
14
+ alias t translate
18
15
 
16
+ # Locales Spree ships translations for that the app allows, plus English
17
+ # and the app's default and current locale.
18
+ #
19
+ # @return [Array<Symbol>]
19
20
  def available_locales
20
- locales = defined?(SpreeI18n) ? SpreeI18n::Locale.all.map(&:to_sym) : []
21
- locales << :en
22
- locales << I18n.locale
23
- locales << Rails.application.config.i18n.default_locale
21
+ (configured_locales & I18n.available_locales.map(&:to_sym)) | [:en, I18n.locale, Rails.application.config.i18n.default_locale].compact
22
+ end
24
23
 
25
- locales.uniq.compact
24
+ # {available_locales} as read from configuration alone, without loading
25
+ # any translations, for use while the app boots.
26
+ #
27
+ # @return [Array<Symbol>]
28
+ def configured_locales
29
+ allowed = Array(Rails.application.config.i18n.available_locales).map(&:to_sym)
30
+ allowed.empty? ? SHIPPED_LOCALES : SHIPPED_LOCALES & allowed
26
31
  end
27
32
 
28
- alias t translate
33
+ # The available locale a store should use for a language: the language
34
+ # itself when Spree ships it, otherwise its first regional variant ("zh"
35
+ # becomes "zh-CN", since Spree ships Chinese only by region).
36
+ #
37
+ # @param language [String] ISO 639-1 code, e.g. "de"
38
+ # @param locales [Array<Symbol>] the locales to choose from
39
+ # @return [String, nil] nil when the language is not available at all
40
+ def locale_for_language(language, locales = available_locales)
41
+ codes = locales.map(&:to_s)
42
+ return language if codes.include?(language)
43
+
44
+ codes.sort.find { |code| code.start_with?("#{language}-") }
45
+ end
29
46
  end
30
47
  end
@@ -642,12 +642,12 @@ module Spree
642
642
  end.to_h
643
643
  end
644
644
 
645
- # Both periods span the same number of days, so their bucket counts
646
- # differ by at most one partial edge. Pairing runs from the start —
647
- # the nth bucket of this period with the nth of the previous — because
648
- # the ranges share a start offset: a range beginning mid-month pairs
649
- # its partial first bucket with the previous period's partial first
650
- # bucket. A bucket past the end of the previous list has no
645
+ # Both periods span the same number of days (or of whole months), so
646
+ # their bucket counts differ by at most one partial edge. Pairing runs
647
+ # from the start — the nth bucket of this period with the nth of the
648
+ # previous — because the ranges share a start offset: a range
649
+ # beginning mid-month pairs its partial first bucket with the previous
650
+ # period's partial first bucket. A bucket past the end of the previous list has no
651
651
  # counterpart and pairs with nothing rather than wrapping around.
652
652
  def bucket_pairs(grain)
653
653
  current = expected_buckets(query.time_range, grain)
@@ -113,16 +113,27 @@ module Spree
113
113
  SUM(COALESCE((SELECT SUM(r.amount) FROM %{refunds} r WHERE r.order_id = %{orders}.id), 0))
114
114
  SQL
115
115
 
116
- # A refund is issued against the order, not a line, so it is apportioned
117
- # by the line's share of the order's pre-tax value. The denominator is
118
- # summed over the order's own lines rather than read from item_total:
119
- # item_total is priced before discounts, so on a discounted order the
120
- # shares would not add up to one and part of the refund would vanish.
121
- # The `* 1.0` forces real division: SQLite divides two integer-valued
122
- # decimals as integers, which silently rounds every share to zero.
116
+ # A refund that gives tax back carries it in its own column, so the goods
117
+ # and the tax it returned are netted from goods and tax respectively.
118
+ REFUNDED_GOODS_SUBQUERY = <<~SQL.squish.freeze
119
+ SUM(COALESCE((SELECT SUM(r.amount - r.tax_amount) FROM %{refunds} r WHERE r.order_id = %{orders}.id), 0))
120
+ SQL
121
+
122
+ REFUNDED_TAX_SUBQUERY = <<~SQL.squish.freeze
123
+ SUM(COALESCE((SELECT SUM(r.tax_amount) FROM %{refunds} r WHERE r.order_id = %{orders}.id), 0))
124
+ SQL
125
+
126
+ # A refund is issued against the order, not a line, so its goods are
127
+ # apportioned by the line's share of the order's pre-tax value. The
128
+ # denominator is summed over the order's own lines rather than read from
129
+ # item_total: item_total is priced before discounts, so on a discounted
130
+ # order the shares would not add up to one and part of the refund would
131
+ # vanish. The `* 1.0` forces real division: SQLite divides two
132
+ # integer-valued decimals as integers, which silently rounds every share
133
+ # to zero.
123
134
  LINE_ITEM_REFUNDS_SUBQUERY = <<~SQL.squish.freeze
124
135
  SUM(
125
- COALESCE((SELECT SUM(r.amount) FROM %{refunds} r WHERE r.order_id = %{orders}.id), 0)
136
+ COALESCE((SELECT SUM(r.amount - r.tax_amount) FROM %{refunds} r WHERE r.order_id = %{orders}.id), 0)
126
137
  * CASE
127
138
  WHEN COALESCE((SELECT SUM(sibling.pre_tax_amount) FROM %{line_items} sibling
128
139
  WHERE sibling.order_id = %{orders}.id), 0) > 0
@@ -293,7 +304,7 @@ module Spree
293
304
  metric :gross_sales, sql: 'SUM(%{line_items}.price * %{line_items}.quantity)',
294
305
  base: :line_items, format: :money
295
306
  metric :discounts, sql: 'SUM(%{line_items}.discount_total)', base: :line_items, format: :money
296
- metric :returns, sql: REFUNDS_SUBQUERY, base: :orders, format: :money
307
+ metric :returns, sql: REFUNDED_GOODS_SUBQUERY, base: :orders, format: :money
297
308
  # Net of returns as well as discounts, so this is the figure a
298
309
  # merchant recognises as "what we actually sold".
299
310
  metric :net_sales, sql: "SUM(%{line_items}.pre_tax_amount) - #{LINE_ITEM_REFUNDS_SUBQUERY}",
@@ -303,7 +314,8 @@ module Spree
303
314
  # Every shopper-visible fee except duties, which the chain counts separately.
304
315
  metric :fees, sql: "SUM(COALESCE(%{orders}.fee_total, 0)) - #{DUTIES_SUBQUERY}",
305
316
  base: :orders, format: :money, suggests: 'net_sales'
306
- metric :taxes, sql: 'SUM(%{orders}.additional_tax_total + %{orders}.included_tax_total)',
317
+ # Net of the tax given back, as net_sales is net of the goods.
318
+ metric :taxes, sql: "SUM(%{orders}.additional_tax_total + %{orders}.included_tax_total) - #{REFUNDED_TAX_SUBQUERY}",
307
319
  base: :orders, format: :money, suggests: 'net_sales'
308
320
  # The order grand total, net of what came back.
309
321
  metric :total_sales, sql: "SUM(%{orders}.total) - #{REFUNDS_SUBQUERY}",
@@ -21,7 +21,7 @@ module Spree
21
21
  dimension = definition(name)
22
22
  # A row whose key is NULL is a real group — an order with no market or
23
23
  # channel — and needs a name a person can read, not a blank cell.
24
- return { id: nil, label: Spree.t('reporting.unassigned'), meta: {} } if raw.nil? && !plain?(dimension)
24
+ return { id: nil, label: I18n.t('spree.reporting.unassigned'), meta: {} } if raw.nil? && !plain?(dimension)
25
25
  return { id: raw.to_s, label: Schema.value_label(dimension, raw), meta: {} } if dimension.enumerated_values
26
26
  return raw unless dimension.hydrate
27
27
 
@@ -126,12 +126,20 @@ module Spree
126
126
  # year" means February to February, and an equal-day-count shift off a
127
127
  # 28-day February would land in January. ActiveSupport maps February 29
128
128
  # to February 28 in a non-leap year.
129
+ #
130
+ # A range of whole calendar months (a full quarter, `last_month`) shifts
131
+ # by the same number of months for the same reason: July – September is
132
+ # 92 days, and shifting it by days would start on March 31, putting a
133
+ # one-day March bucket at the front of the previous period.
129
134
  def previous_time_range
130
135
  first = time_range.first.in_time_zone(time_zone)
131
136
  last = time_range.last.in_time_zone(time_zone)
132
137
 
133
138
  if compare == 'previous_year'
134
139
  (first - 1.year)..(last - 1.year)
140
+ elsif first == first.beginning_of_month && last == last.end_of_month
141
+ months = (last.year * 12 + last.month) - (first.year * 12 + first.month) + 1
142
+ (first - months.months)..(last - months.months).end_of_month
135
143
  else
136
144
  days = (last.to_date - first.to_date).to_i + 1
137
145
  (first - days.days)..(last - days.days)
@@ -32,7 +32,7 @@ module Spree
32
32
  metrics: metrics.map { |m| metric_entry(m) },
33
33
  dimensions: dimensions.map { |d| dimension_entry(d, metrics) },
34
34
  time_range: {
35
- presets: (Query::PRESETS + Query::RELATIVE_PRESETS).map { |p| { name: p, label: translate('presets', p, :label) } },
35
+ presets: (Query::PRESETS + Query::RELATIVE_PRESETS).map { |p| { name: p, label: catalog_text('presets', p, :label) } },
36
36
  relative: %w[last_<n>_days last_<n>_weeks last_<n>_months],
37
37
  absolute: 'ISO 8601 dates or datetimes in `since` / `until`, resolved in the store timezone'
38
38
  },
@@ -50,7 +50,7 @@ module Spree
50
50
  # @param group [Symbol] :metrics | :dimensions
51
51
  # @return [String] localized label (humanized name when none registered)
52
52
  def label_for(group, name)
53
- translate(group, name, :label)
53
+ catalog_text(group, name, :label)
54
54
  end
55
55
 
56
56
  private
@@ -61,7 +61,7 @@ module Spree
61
61
  metrics.group_by { |metric| @registry.family_of(metric) }.map do |family, family_metrics|
62
62
  {
63
63
  name: family,
64
- label: translate('families', family, :label),
64
+ label: catalog_text('families', family, :label),
65
65
  metrics: family_metrics.map(&:name),
66
66
  dimensions: @registry.dimensions.values.
67
67
  select { |d| @allowed.call(d) && @registry.base!(d.base).family == family }.map(&:name)
@@ -73,8 +73,8 @@ module Spree
73
73
  {
74
74
  name: metric.name,
75
75
  family: @registry.family_of(metric),
76
- label: translate('metrics', metric.name, :label),
77
- description: translate('metrics', metric.name, :description),
76
+ label: catalog_text('metrics', metric.name, :label),
77
+ description: catalog_text('metrics', metric.name, :description),
78
78
  format: metric.format,
79
79
  currency: (metric.money? ? @store.default_currency : nil),
80
80
  derived: metric.derived?,
@@ -88,8 +88,8 @@ module Spree
88
88
  def dimension_entry(dimension, metrics)
89
89
  {
90
90
  name: dimension.name,
91
- label: translate('dimensions', dimension.name, :label),
92
- description: translate('dimensions', dimension.name, :description),
91
+ label: catalog_text('dimensions', dimension.name, :label),
92
+ description: catalog_text('dimensions', dimension.name, :description),
93
93
  type: dimension.type,
94
94
  grains: dimension.grains,
95
95
  lookup: dimension.lookup,
@@ -104,8 +104,8 @@ module Spree
104
104
  }.compact
105
105
  end
106
106
 
107
- def translate(group, name, facet)
108
- Spree.t("reporting.#{group}.#{name}.#{facet}", default: (facet == :label ? name.to_s.humanize : nil))
107
+ def catalog_text(group, name, facet)
108
+ I18n.t("spree.reporting.#{group}.#{name}.#{facet}", default: (facet == :label ? name.to_s.humanize : nil))
109
109
  end
110
110
 
111
111
 
@@ -117,7 +117,7 @@ module Spree
117
117
  # @param value [String]
118
118
  # @return [String]
119
119
  def self.value_label(dimension, value)
120
- Spree.t("reporting.values.#{dimension.name}.#{value}", default: value.to_s.humanize)
120
+ I18n.t("spree.reporting.values.#{dimension.name}.#{value}", default: value.to_s.humanize)
121
121
  end
122
122
  end
123
123
  end
@@ -0,0 +1,61 @@
1
+ # Registers core's staff password reset as an editable email for the length
2
+ # of an example, with a fixed sample, so specs in any engine can exercise the
3
+ # template editor without spree_emails' customer emails. Restores whatever was
4
+ # registered under the key before.
5
+ #
6
+ # RSpec.describe '...' do
7
+ # include_context 'with an editable email template'
8
+ # end
9
+ module Spree
10
+ module TestingSupport
11
+ class EmailTemplateSample
12
+ def self.record_type
13
+ nil
14
+ end
15
+
16
+ def initialize(store:, record_id: nil)
17
+ @store = store
18
+ end
19
+
20
+ def currency
21
+ @store.default_currency
22
+ end
23
+
24
+ def recent_records(limit: 5)
25
+ @store.orders.order(created_at: :desc).limit(limit).map do |order|
26
+ Spree::EmailTemplates::SampleRecord.new(id: order.prefixed_id, label: order.number, created_at: order.created_at)
27
+ end
28
+ end
29
+
30
+ def variables
31
+ {
32
+ user: { 'first_name' => 'Ann', 'email' => 'ann@example.com' },
33
+ order: { 'number' => 'R123456789', 'customer_name' => 'Ann Lee' },
34
+ reset_url: "#{@store.storefront_url}/reset-password?token=preview"
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ RSpec.shared_context 'with an editable email template' do
42
+ let(:editable_key) { 'spree/admin_user_mailer/password_reset_email' }
43
+
44
+ # Templates have per-language versions only in the store's languages.
45
+ before { allow_any_instance_of(Spree::Store).to receive(:supported_locales_list).and_return(%w[de en fr]) }
46
+
47
+ around do |example|
48
+ registry = Spree.editable_email_templates
49
+ previous = registry[editable_key]
50
+ previous_layout = registry['layouts/spree/base_mailer']
51
+ registry.register(editable_key, kind: :email, sample: 'Spree::TestingSupport::EmailTemplateSample')
52
+ registry.register('layouts/spree/base_mailer', kind: :layout)
53
+
54
+ begin
55
+ example.run
56
+ ensure
57
+ previous ? registry.register(previous.key, kind: previous.kind, sample: previous.sample) : registry.delete(editable_key)
58
+ previous_layout ? registry.register(previous_layout.key, kind: :layout) : registry.delete('layouts/spree/base_mailer')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module TestingSupport
3
+ module Emails
4
+ # The decoded text of every part of an email, HTML and plain text
5
+ # together. Rendered emails are quoted-printable on the wire, so the raw
6
+ # encoded body splits long lines and turns "=" into "=3D".
7
+ #
8
+ # @param message [Mail::Message, ActionMailer::MessageDelivery]
9
+ # @return [String]
10
+ def email_body(message)
11
+ message = message.message if message.respond_to?(:message) && !message.is_a?(Mail::Message)
12
+ parts = message.multipart? ? message.parts : [message]
13
+
14
+ parts.map(&:decoded).join("\n")
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ RSpec.configure do |config|
21
+ config.include Spree::TestingSupport::Emails
22
+ end