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
@@ -24,15 +24,15 @@ module Spree
24
24
  def display_action
25
25
  case action
26
26
  when Spree::StoreCredit::CAPTURE_ACTION
27
- Spree.t('store_credit.captured')
27
+ I18n.t('spree.store_credit.captured')
28
28
  when Spree::StoreCredit::AUTHORIZE_ACTION
29
- Spree.t('store_credit.authorized')
29
+ I18n.t('spree.store_credit.authorized')
30
30
  when Spree::StoreCredit::ALLOCATION_ACTION
31
- Spree.t('store_credit.allocated')
31
+ I18n.t('spree.store_credit.allocated')
32
32
  when Spree::StoreCredit::ELIGIBLE_ACTION
33
- Spree.t('store_credit.eligible')
33
+ I18n.t('spree.store_credit.eligible')
34
34
  when Spree::StoreCredit::VOID_ACTION, Spree::StoreCredit::CREDIT_ACTION
35
- Spree.t('store_credit.credit')
35
+ I18n.t('spree.store_credit.credit')
36
36
  end
37
37
  end
38
38
 
@@ -9,6 +9,8 @@ module Spree
9
9
  # valid in one state and not the next is the ordinary case, and the country
10
10
  # and state columns say exactly where it holds.
11
11
  class TaxExemptionCertificate < Spree.base_class
12
+ publishes_event :verified
13
+
12
14
  has_prefix_id :cert
13
15
 
14
16
  include Spree::HasStatus
@@ -36,6 +36,8 @@ module Spree
36
36
  # directly. It is a habit of the gems that use it, not a rule this model
37
37
  # applies.
38
38
  class TaxIdentifier < Spree.base_class
39
+ publishes_event :number_changed
40
+
39
41
  has_prefix_id :txi
40
42
 
41
43
  # Statuses the platform records rather than the buyer chooses: nil means
@@ -150,7 +152,7 @@ module Spree
150
152
  # The kind as the dashboard shows it, falling back to the raw key so an
151
153
  # extension's kind reads as itself rather than blank.
152
154
  def kind_label
153
- Spree.t("tax_identifier_kinds.#{kind}", default: kind.to_s.humanize)
155
+ I18n.t("spree.tax_identifier_kinds.#{kind}", default: kind.to_s.humanize)
154
156
  end
155
157
 
156
158
  # The class registered for this kind, or nil when nothing is registered and
@@ -4,6 +4,11 @@ module Spree
4
4
  # per estimate. Snapshot columns (+rate+, +label+, +provider_id+) keep rows
5
5
  # self-describing after a TaxRate is deleted or when an external provider
6
6
  # computed them.
7
+ #
8
+ # Rows on a return, claim or exchange line are post-sale: the tax given back
9
+ # (+credit+), or the tax on an exchange's replacement. They are owned by the
10
+ # order like every other row but never count towards its totals — the order
11
+ # reads only {.sale} rows.
7
12
  class TaxLine < Spree.base_class
8
13
  include Spree::TypedAdjustmentLine
9
14
 
@@ -35,6 +40,13 @@ module Spree
35
40
  belongs_to :line_item, class_name: 'Spree::LineItem', optional: true
36
41
  belongs_to :fulfillment, class_name: 'Spree::Fulfillment', optional: true
37
42
  belongs_to :fee, class_name: 'Spree::Fee', optional: true
43
+ belongs_to :return_line_item, class_name: 'Spree::ReturnLineItem', optional: true
44
+ belongs_to :claim_line_item, class_name: 'Spree::ClaimLineItem', optional: true
45
+ belongs_to :exchange_line_item, class_name: 'Spree::ExchangeLineItem', optional: true
46
+
47
+ # The sale row a credit gives back — what caps repeated credits against one
48
+ # line, and the original a credit note refers to.
49
+ belongs_to :original_tax_line, class_name: 'Spree::TaxLine', optional: true
38
50
 
39
51
  # Tax included in price vs additional. Per-row (mixed regimes on one order).
40
52
  attribute :included, :boolean, default: false
@@ -46,20 +58,50 @@ module Spree
46
58
  inclusion: { in: ->(tax_line) { tax_line.class.taxability_reasons } },
47
59
  allow_nil: true
48
60
  validate :exactly_one_adjustable
61
+ validate :post_sale_owned_by_order
49
62
 
50
63
  # Tax reporting is the reason the treatment columns exist — "which country's
51
64
  # tax was this, and which sales were reverse-charged" has to be answerable.
52
65
  self.whitelisted_ransackable_attributes = %w[taxability_reason country_code state_code included provider_id]
53
66
 
67
+ POST_SALE_KEYS = %i[return_line_item_id claim_line_item_id exchange_line_item_id].freeze
68
+
69
+ ADJUSTABLE_KEYS = {
70
+ 'Spree::LineItem' => :line_item_id,
71
+ 'Spree::Fulfillment' => :fulfillment_id,
72
+ 'Spree::Fee' => :fee_id,
73
+ 'Spree::ReturnLineItem' => :return_line_item_id,
74
+ 'Spree::ClaimLineItem' => :claim_line_item_id,
75
+ 'Spree::ExchangeLineItem' => :exchange_line_item_id
76
+ }.freeze
77
+
54
78
  scope :included_in_price, -> { where(included: true) }
55
79
  scope :additional, -> { where(included: false) }
56
80
  scope :for_line_items, -> { where.not(line_item_id: nil) }
57
81
  scope :for_fulfillments, -> { where.not(fulfillment_id: nil) }
58
82
  scope :for_fees, -> { where.not(fee_id: nil) }
83
+ scope :sale, -> { where(POST_SALE_KEYS.index_with(nil)) }
84
+ scope :post_sale, -> { where.not(POST_SALE_KEYS.index_with(nil)) }
85
+ scope :credits, -> { where(credit: true) }
86
+ scope :charges, -> { where(credit: false) }
87
+
88
+ # The foreign key a row uses to point at an item of this class.
89
+ #
90
+ # @param klass [Class]
91
+ # @return [Symbol]
92
+ # @raise [ArgumentError] for a class that is not taxed
93
+ def self.adjustable_key_for(klass)
94
+ ADJUSTABLE_KEYS.fetch(klass.name) { raise ArgumentError, "#{klass} is not taxable" }
95
+ end
59
96
 
60
- # @return [Spree::LineItem, Spree::Fulfillment, Spree::Fee, nil]
97
+ # @return [Spree::LineItem, Spree::Fulfillment, Spree::Fee, Spree::ReturnLineItem,
98
+ # Spree::ClaimLineItem, Spree::ExchangeLineItem, nil]
61
99
  def adjustable
62
- line_item || fulfillment || fee
100
+ line_item || fulfillment || fee || return_line_item || claim_line_item || exchange_line_item
101
+ end
102
+
103
+ def post_sale?
104
+ POST_SALE_KEYS.any? { |key| self[key].present? }
63
105
  end
64
106
 
65
107
  def included?
@@ -73,7 +115,15 @@ module Spree
73
115
  private
74
116
 
75
117
  def exactly_one_adjustable
76
- errors.add(:base, :exactly_one_adjustable, message: Spree.t('errors.messages.exactly_one_adjustable')) unless [line_item, fulfillment, fee].compact.one?
118
+ adjustables = [line_item, fulfillment, fee, return_line_item, claim_line_item, exchange_line_item]
119
+ errors.add(:base, :exactly_one_adjustable, message: I18n.t('spree.errors.messages.exactly_one_adjustable')) unless adjustables.compact.one?
120
+ end
121
+
122
+ # Returns, claims and exchanges only exist on placed orders.
123
+ def post_sale_owned_by_order
124
+ return unless post_sale? && order.nil?
125
+
126
+ errors.add(:base, :exactly_one_of_cart_or_order, message: I18n.t('spree.errors.messages.exactly_one_of_cart_or_order'))
77
127
  end
78
128
  end
79
129
  end
@@ -3,12 +3,16 @@ module Spree
3
3
  # Contract for tax providers — the only sanctioned writers of
4
4
  # {Spree::TaxLine} rows. +estimate+ must use replace-all set semantics per
5
5
  # target item (delete the item's stale lines, insert fresh ones); that is
6
- # what keeps tax correct after address or item changes. Commit/void/refund
7
- # are lifecycle no-ops for providers without a remote ledger.
6
+ # what keeps tax correct after address or item changes, and the post-sale
7
+ # +estimate_refund+ / +estimate_replacement+ follow the same rule.
8
+ # Commit/void/refund/commit_replacement are lifecycle no-ops for providers
9
+ # without a remote ledger.
8
10
  #
9
11
  # Providers are stateless and constructed without arguments, so anything
10
12
  # request-specific arrives as an argument rather than through the instance.
11
13
  class Base
14
+ extend Spree::ApiTyped
15
+
12
16
  # Domains this provider cannot handle, so a merchant is warned when
13
17
  # pairing it with a market instead of silently under-collecting. Declared
14
18
  # on the class because core reads it while presenting the choice, before
@@ -47,8 +51,8 @@ module Spree
47
51
  unsupported_capabilities.map do |capability|
48
52
  {
49
53
  key: capability.to_s,
50
- label: Spree.t("tax_capabilities.#{capability}.label", default: capability.to_s.humanize),
51
- description: Spree.t("tax_capabilities.#{capability}.description", default: nil)
54
+ label: I18n.t("spree.tax_capabilities.#{capability}.label", default: capability.to_s.humanize),
55
+ description: I18n.t("spree.tax_capabilities.#{capability}.description", default: nil)
52
56
  }.compact
53
57
  end
54
58
  end
@@ -61,7 +65,7 @@ module Spree
61
65
  # @return [Hash]
62
66
  def self.to_api_hash(store)
63
67
  {
64
- id: name,
68
+ id: api_type,
65
69
  name: display_name,
66
70
  available: available_for_store?(store),
67
71
  unsupported_capabilities: unsupported_capability_details,
@@ -128,19 +132,72 @@ module Spree
128
132
  nil
129
133
  end
130
134
 
135
+ # Works out the tax given back on lines of a return, claim or exchange,
136
+ # and writes it as credit TaxLine rows on them. The customer is refunded
137
+ # exactly what these rows hold, so this — not +refund+ — decides the
138
+ # figure.
139
+ #
140
+ # Each item credits its +credited_quantity+, and zero means no rows.
141
+ # Replace-all per item, as +estimate+: the rows are rewritten when the
142
+ # warehouse counts what arrived and again when the money goes back.
143
+ #
144
+ # A provider may quote the credit, for example at the original
145
+ # +tax_date+, or give back the recorded share of what the sale charged —
146
+ # {Spree::TaxProvider::RecordedShare}, which Internal uses. Either way
147
+ # it answers in the same rows.
148
+ #
149
+ # @param order [Spree::Order] a placed order
150
+ # @param items [Array<Spree::ReturnLineItem, Spree::ClaimLineItem, Spree::ExchangeLineItem>]
151
+ # @param amounts [Hash{Object => BigDecimal}, nil] the money refunded per
152
+ # item when it is less than the item's worth — a restocking fee, a
153
+ # part refund. Tax is then credited in the same proportion. Nil
154
+ # credits every item's full worth.
155
+ # @param tax_date [Time, nil] the date whose rates apply; nil = order.completed_at
156
+ # @return [void]
157
+ # @raise [Spree::Tax::ProviderError] when the credit could not be worked out
158
+ def estimate_refund(order, items, amounts: nil, tax_date: nil)
159
+ raise NotImplementedError, "Please implement 'estimate_refund' in your tax provider: #{self.class.name}"
160
+ end
161
+
162
+ # Taxes the replacement an exchange sends out, as a new sale, and writes
163
+ # it as charge TaxLine rows on the exchange lines. Never touches the
164
+ # order's own rows, which stay as the sale was placed. Replace-all per
165
+ # item; each item is taxed on its +taxable_basis+ under its
166
+ # +tax_category_id+, and one whose +credited_quantity+ is zero ships
167
+ # nothing and ends with no rows.
168
+ #
169
+ # @param order [Spree::Order] a placed order
170
+ # @param items [Array<Spree::ExchangeLineItem>]
171
+ # @param tax_date [Time, nil] date whose rates apply; nil = now
172
+ # @param tax_identifier [Spree::TaxIdentifier, nil] the order's frozen registration
173
+ # @param exemptions [Array] exemption evidence to apply
174
+ # @return [void]
175
+ # @raise [Spree::Tax::ProviderError] when the tax could not be worked out
176
+ def estimate_replacement(order, items, tax_date: nil, tax_identifier: nil, exemptions: [])
177
+ raise NotImplementedError, "Please implement 'estimate_replacement' in your tax provider: #{self.class.name}"
178
+ end
179
+
180
+ # Files an exchange's replacement as a sale. Called once the exchange is
181
+ # fulfilled; must be idempotent, keyed on the exchange. No-op for a
182
+ # provider without a remote ledger.
183
+ #
184
+ # @param order [Spree::Order]
185
+ # @param items [Array<Spree::ExchangeLineItem>]
186
+ # @return [void]
187
+ def commit_replacement(order, items); end
188
+
131
189
  # Reports a partial credit against the committed document, keyed to the
132
- # original transaction rather than voiding and re-committing it.
190
+ # original transaction rather than voiding and re-committing it. Called
191
+ # after the money has moved.
133
192
  #
134
- # +amount+ is what the customer was actually refunded, which is not always
135
- # the returned lines' worth: an admin may keep a restocking fee or refund
136
- # a goodwill figure of their own. A provider must credit no more tax than
137
- # that refund carries — crediting every returned line when only part of
138
- # their value went back would reclaim tax the merchant never repaid, and a
139
- # return is marked refunded once, so nothing later corrects it. Where the
140
- # amount is short, allocate proportionally.
193
+ # By then each item's credit rows hold what went back (see
194
+ # +estimate_refund+), and a provider must credit no more than they do:
195
+ # the customer was repaid that tax and no more. +amount+ is the money
196
+ # refunded, which may be less than the items are worth when an admin kept
197
+ # a restocking fee or refunded a goodwill figure of their own.
141
198
  #
142
199
  # @param order [Spree::Order]
143
- # @param return_items [Array<Spree::ReturnLineItem>] the returned lines
200
+ # @param return_items [Array<Spree::ReturnLineItem, Spree::ClaimLineItem, Spree::ExchangeLineItem>]
144
201
  # @param amount [BigDecimal, nil] the refund issued; nil = the lines' full worth
145
202
  # @param tax_date [Time, nil] the original supply date; nil = order.completed_at
146
203
  # @return [void]
@@ -30,40 +30,43 @@ module Spree
30
30
  sweep_internal_duty_tax_lines(owner) if full_estimate
31
31
  return if items.empty?
32
32
 
33
- rates = Spree::TaxRate.for_store(owner.store).for_jurisdiction(owner.tax_country&.iso, owner.tax_address&.state_code).to_a
33
+ rates = rates_for(owner)
34
34
 
35
35
  items.group_by(&:class).each do |klass, group|
36
36
  # Replace-all set semantics per estimate: stale lines die with the
37
37
  # address/items change that triggered the re-estimate.
38
- Spree::TaxLine.where(tax_line_foreign_key(klass) => group.map(&:id)).delete_all
38
+ Spree::TaxLine.where(Spree::TaxLine.adjustable_key_for(klass) => group.map(&:id)).delete_all
39
39
  end
40
40
 
41
+ items.each { |item| tax_item(owner, item, rates, exemptions) }
42
+ end
43
+
44
+ # Gives back each credited unit's share of the TaxLines the sale actually
45
+ # charged (see Spree::TaxProvider::RecordedShare). TaxRate rows are not
46
+ # versioned, so this engine can never recompute at a past date —
47
+ # +tax_date+ is accepted and ignored, and the rows are the only record
48
+ # of what the customer paid.
49
+ def estimate_refund(order, items, amounts: nil, tax_date: nil)
50
+ Spree::TaxProvider::RecordedShare.new(order: order, items: items, amounts: amounts).call
51
+ end
52
+
53
+ # Taxed like any line of the order, under the replacement's own category
54
+ # and at the order's tax address. +tax_date+ is ignored for the reason
55
+ # given on #estimate.
56
+ #
57
+ # An exemption is read as it applied to the line being replaced: a
58
+ # certificate's per-line carve-out names that line, and the exchange line
59
+ # replacing it would otherwise match no carve-out and come out exempt.
60
+ def estimate_replacement(order, items, tax_date: nil, tax_identifier: nil, exemptions: [])
61
+ return if items.empty?
62
+
63
+ Spree::TaxLine.charges.where(exchange_line_item_id: items.map(&:id)).delete_all
64
+
65
+ rates = rates_for(order)
41
66
  items.each do |item|
42
- relevant_rates = rates.select { |rate| rate.tax_category_id == tax_category_id_for(item, owner) }
43
-
44
- # A matched rate always produces a row, zero-amount ones included:
45
- # a zero rate is a treatment ("this is zero-rated"), which reporting
46
- # and e-invoicing both need to see. No matched rate means no opinion,
47
- # and writes nothing.
48
- taxed = relevant_rates.map do |rate|
49
- jurisdiction = jurisdiction_for(rate, owner)
50
- [rate, jurisdiction, exemption_for(item, exemptions, jurisdiction)]
51
- end
67
+ next unless item.credited_quantity.to_i.positive?
52
68
 
53
- # An exempt item has no tax to back out of its price, so its whole
54
- # basis is pre-tax.
55
- exempt_rates = taxed.select { |_rate, _jurisdiction, exemption| exemption }.map(&:first)
56
- store_pre_tax_amount(item, relevant_rates - exempt_rates)
57
-
58
- taxed.each do |rate, jurisdiction, exemption|
59
- if exemption
60
- write_tax_line(owner, item, rate, 0, 'customer_exempt', jurisdiction,
61
- data: exemption_data(exemption, item))
62
- else
63
- write_tax_line(owner, item, rate, compute_tax(rate, item, relevant_rates),
64
- reason_for(rate), jurisdiction)
65
- end
66
- end
69
+ tax_item(order, item, rates, exemptions, exemption_subject: item.line_item || item)
67
70
  end
68
71
  end
69
72
 
@@ -107,12 +110,35 @@ module Spree
107
110
  owner.tax_lines.where(fee_id: duty_fee_ids, provider_id: 'internal').delete_all
108
111
  end
109
112
 
110
- def tax_line_foreign_key(klass)
111
- case klass.name
112
- when 'Spree::LineItem' then :line_item_id
113
- when 'Spree::Fulfillment' then :fulfillment_id
114
- when 'Spree::Fee' then :fee_id
115
- else raise ArgumentError, "#{klass} is not taxable"
113
+ def rates_for(owner)
114
+ Spree::TaxRate.for_store(owner.store).for_jurisdiction(owner.tax_country&.iso, owner.tax_address&.state_code).to_a
115
+ end
116
+
117
+ def tax_item(owner, item, rates, exemptions, exemption_subject: item)
118
+ relevant_rates = rates.select { |rate| rate.tax_category_id == tax_category_id_for(item, owner) }
119
+
120
+ # A matched rate always produces a row, zero-amount ones included:
121
+ # a zero rate is a treatment ("this is zero-rated"), which reporting
122
+ # and e-invoicing both need to see. No matched rate means no opinion,
123
+ # and writes nothing.
124
+ taxed = relevant_rates.map do |rate|
125
+ jurisdiction = jurisdiction_for(rate, owner)
126
+ [rate, jurisdiction, exemption_for(exemption_subject, exemptions, jurisdiction)]
127
+ end
128
+
129
+ # An exempt item has no tax to back out of its price, so its whole
130
+ # basis is pre-tax.
131
+ exempt_rates = taxed.select { |_rate, _jurisdiction, exemption| exemption }.map(&:first)
132
+ store_pre_tax_amount(item, relevant_rates - exempt_rates)
133
+
134
+ taxed.each do |rate, jurisdiction, exemption|
135
+ if exemption
136
+ write_tax_line(owner, item, rate, 0, 'customer_exempt', jurisdiction,
137
+ data: exemption_data(exemption, exemption_subject))
138
+ else
139
+ write_tax_line(owner, item, rate, compute_tax(rate, item, relevant_rates),
140
+ reason_for(rate), jurisdiction)
141
+ end
116
142
  end
117
143
  end
118
144
 
@@ -209,7 +235,7 @@ module Spree
209
235
  owner_key = owner.is_a?(Spree::Order) ? :order : :cart
210
236
  Spree::TaxLine.create!(
211
237
  {
212
- tax_line_foreign_key(item.class) => item.id,
238
+ Spree::TaxLine.adjustable_key_for(item.class) => item.id,
213
239
  owner_key => owner,
214
240
  tax_rate: rate,
215
241
  amount: amount,
@@ -0,0 +1,114 @@
1
+ module Spree
2
+ module TaxProvider
3
+ # The tax a return, claim or exchange line gives back, read off what the
4
+ # sale actually charged: each credited unit's part of every TaxLine on the
5
+ # original line, row by row.
6
+ #
7
+ # Read from the rows rather than recomputed from TaxRate configuration,
8
+ # because the customer is owed back what they paid. Rates are not
9
+ # versioned, so a rate edited since the sale would otherwise refund a
10
+ # figure nobody was charged.
11
+ #
12
+ # Internal's answer to +estimate_refund+. Public so any other provider can
13
+ # answer with it too, rather than quoting the credit itself.
14
+ class RecordedShare
15
+ # @param order [Spree::Order]
16
+ # @param items [Array<Spree::ReturnLineItem, Spree::ClaimLineItem, Spree::ExchangeLineItem>]
17
+ # @param amounts [Hash{Object => BigDecimal}, nil] see Base#estimate_refund
18
+ def initialize(order:, items:, amounts: nil)
19
+ @order = order
20
+ @items = Array(items)
21
+ @amounts = amounts
22
+ end
23
+
24
+ # Replaces each item's credit rows.
25
+ #
26
+ # @return [void]
27
+ def call
28
+ items.each do |item|
29
+ key = Spree::TaxLine.adjustable_key_for(item.class)
30
+ Spree::TaxLine.credits.where(key => item.id).delete_all
31
+ credit(item, key)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :order, :items, :amounts
38
+
39
+ def credit(item, key)
40
+ share = share_of_line(item)
41
+ return if share.zero?
42
+
43
+ sources = item.line_item.tax_lines.order(:id).to_a
44
+ sources.zip(shares_for(sources, share)).each do |source, cents|
45
+ write(item, key, source, cents)
46
+ end
47
+ end
48
+
49
+ # The part of the original line this item gives back: its credited units,
50
+ # and of those only the share of their worth the customer is refunded.
51
+ def share_of_line(item)
52
+ quantity = item.line_item&.quantity.to_i
53
+ units = item.credited_quantity.to_i
54
+ return 0 if quantity.zero? || units.zero?
55
+
56
+ Rational(units, quantity) * refunded_fraction(item)
57
+ end
58
+
59
+ def refunded_fraction(item)
60
+ return 1 if amounts.nil?
61
+
62
+ worth = item.credited_worth.to_d
63
+ return 0 unless worth.positive?
64
+
65
+ [Rational(amounts.fetch(item, 0).to_d) / Rational(worth), 1].min
66
+ end
67
+
68
+ # The rows' total share rounded once, then divided by largest remainder,
69
+ # so the credit lands on the cent and each rate keeps its part of it. A
70
+ # row is never credited past what earlier credits left of it, or three
71
+ # single-unit returns of one line could give back a cent more than it
72
+ # charged.
73
+ def shares_for(sources, share)
74
+ weights = sources.map { |source| to_minor_units(source.amount) }
75
+ return Array.new(sources.size, 0) if weights.sum.zero?
76
+
77
+ target = (Rational(weights.sum) * share).round(half: :up)
78
+ allotted = Spree::Adjusters::LargestRemainder.largest_remainder_shares(target, weights)
79
+
80
+ sources.zip(allotted).map { |source, cents| [cents, remaining_cents(source)].min }
81
+ end
82
+
83
+ def remaining_cents(source)
84
+ credited = Spree::TaxLine.credits.where(original_tax_line_id: source.id).sum(:amount)
85
+ [to_minor_units(source.amount) - to_minor_units(credited), 0].max
86
+ end
87
+
88
+ # Zero-amount rows are copied too: a zero-rated or exempt sale is a
89
+ # treatment the credit note has to repeat.
90
+ def write(item, key, source, cents)
91
+ Spree::TaxLine.create!(
92
+ key => item.id,
93
+ order: order,
94
+ credit: true,
95
+ original_tax_line: source,
96
+ tax_rate_id: source.tax_rate_id,
97
+ amount: Spree::Money::Rounding.from_minor_units(cents, order.currency),
98
+ rate: source.rate,
99
+ label: source.label,
100
+ included: source.included,
101
+ provider_id: source.provider_id,
102
+ taxability_reason: source.taxability_reason,
103
+ country_code: source.country_code,
104
+ state_code: source.state_code,
105
+ data: source.data
106
+ )
107
+ end
108
+
109
+ def to_minor_units(amount)
110
+ Spree::Money::Rounding.to_minor_units(amount, order.currency)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -100,8 +100,7 @@ module Spree
100
100
  #
101
101
  # @return [String]
102
102
  def adjustment_label
103
- Spree.t included_in_price? ? :including_tax : :excluding_tax,
104
- scope: 'adjustment_labels.tax_rates',
103
+ I18n.t "spree.adjustment_labels.tax_rates.#{included_in_price? ? :including_tax : :excluding_tax}",
105
104
  name: name.presence || tax_category.name,
106
105
  amount: amount_for_label
107
106
  end
@@ -12,7 +12,6 @@ module Spree
12
12
 
13
13
  has_many :taxons, class_name: 'Spree::Category', inverse_of: :taxonomy
14
14
  has_one :root, -> { where parent_id: nil }, class_name: 'Spree::Category', dependent: :destroy
15
- belongs_to :store, class_name: 'Spree::Store'
16
15
 
17
16
  default_scope { order("#{table_name}.position, #{table_name}.created_at") }
18
17
  end
@@ -231,6 +231,10 @@ module Spree
231
231
 
232
232
  scope :not_deleted, -> { where("#{Spree::Variant.quoted_table_name}.deleted_at IS NULL") }
233
233
 
234
+ # The variants of a product relation, as a subquery. Unordered, because a
235
+ # listing's sort would otherwise land inside the IN.
236
+ scope :for_products, ->(products) { where(product_id: products.reorder(nil).select(Spree::Product.arel_table[:id])) }
237
+
234
238
  # Variants the shop sells in this currency. Base prices only: a price
235
239
  # list's rows are what one audience pays under an agreement, and a ladder
236
240
  # holds several rows for one variant, so a contract-only variant is not
@@ -285,9 +289,6 @@ module Spree
285
289
  relation.where(conditions.reduce(:or)).distinct
286
290
  end
287
291
 
288
- # Backward compatibility alias — remove in Spree 6.0
289
- scope :multi_search, ->(*args) { search(*args) }
290
-
291
292
  # FIXME: cost price should be represented with DisplayMoney class
292
293
  LOCALIZED_NUMBERS = %w(cost_price weight depth width height)
293
294
 
@@ -35,7 +35,7 @@ module Spree
35
35
  return if asset.product&.id == variant.product_id
36
36
 
37
37
  errors.add(:asset, :variant_product_mismatch,
38
- message: Spree.t('errors.messages.variant_product_mismatch'))
38
+ message: I18n.t('spree.errors.messages.variant_product_mismatch'))
39
39
  end
40
40
 
41
41
  def refresh_variant_thumbnail
@@ -13,7 +13,6 @@ module Spree
13
13
 
14
14
  encrypts :secret_key, deterministic: true if Rails.configuration.active_record.encryption.include?(:primary_key)
15
15
 
16
- belongs_to :store, class_name: 'Spree::Store'
17
16
  has_many :webhook_deliveries, class_name: 'Spree::WebhookDelivery', dependent: :destroy_async
18
17
 
19
18
  validates :url, presence: true
@@ -50,18 +49,16 @@ module Spree
50
49
  # Number of consecutive failed deliveries before auto-disabling
51
50
  AUTO_DISABLE_THRESHOLD = 15
52
51
 
53
- # Events whose payload carries a live customer credential (a password
54
- # reset token). Receiving one is as good as holding the customer's account,
55
- # so they reach only an endpoint that names them — never through `*` or a
56
- # pattern — and the API asks for customer write access to point one at them.
57
- CREDENTIAL_EVENTS = %w[customer.password_reset_requested].freeze
58
-
59
- # Check if this endpoint is subscribed to a specific event
52
+ # Check if this endpoint is subscribed to a specific event.
53
+ #
54
+ # Events whose payload carries a live credential (a password reset token)
55
+ # reach only an endpoint that names them — never through `*` or a pattern —
56
+ # since receiving one is as good as holding the account.
60
57
  #
61
58
  # @param event_name [String] the event name to check
62
59
  # @return [Boolean]
63
60
  def subscribed_to?(event_name)
64
- return subscriptions.to_a.include?(event_name) if CREDENTIAL_EVENTS.include?(event_name)
61
+ return subscriptions.to_a.include?(event_name) if Spree::Events.catalog.credential?(event_name)
65
62
  return true if subscriptions.blank? || subscriptions.include?('*')
66
63
 
67
64
  subscriptions.any? do |subscription|
@@ -74,11 +71,11 @@ module Spree
74
71
  end
75
72
  end
76
73
 
77
- # Whether this endpoint receives customer credentials.
74
+ # The credential-carrying events this endpoint names.
78
75
  #
79
- # @return [Boolean]
80
- def receives_credentials?
81
- (subscriptions.to_a & CREDENTIAL_EVENTS).any?
76
+ # @return [Array<Spree::Events::Catalog::Entry>]
77
+ def credential_subscriptions
78
+ Spree::Events.catalog.credential_events(subscriptions.to_a)
82
79
  end
83
80
 
84
81
  # Returns all events this endpoint is subscribed to
@@ -6,15 +6,10 @@ module Spree
6
6
 
7
7
  publishes_lifecycle_events
8
8
 
9
- if Rails::VERSION::STRING >= '7.1.0'
10
- has_secure_token on: :save
11
- else
12
- has_secure_token
13
- end
9
+ has_secure_token on: :save
14
10
 
15
11
  belongs_to :customer, class_name: "::#{Spree.customer_class}", touch: true
16
12
  include Spree::DeprecatedCustomerAlias
17
- belongs_to :store, class_name: 'Spree::Store'
18
13
 
19
14
  has_many :wishlist_items, class_name: 'Spree::WishlistItem', dependent: :destroy
20
15
  has_many :wished_items, class_name: 'Spree::WishlistItem', inverse_of: :wishlist, deprecated: true
@@ -11,6 +11,12 @@ module Spree
11
11
  LEGACY_EVENT_PREFIX = 'wished_item'
12
12
  LEGACY_EVENT_SUFFIXES = %w[created updated deleted].freeze
13
13
 
14
+ def self.included(base)
15
+ LEGACY_EVENT_SUFFIXES.each do |suffix|
16
+ base.publishes_event("#{LEGACY_EVENT_PREFIX}.#{suffix}", deprecated_alias_of: "#{base.event_prefix}.#{suffix}")
17
+ end
18
+ end
19
+
14
20
  # Wraps publishing rather than the three callbacks that call it, so the
15
21
  # decisions Publishable makes — whether events are enabled at all,
16
22
  # whether a touch-only update is worth announcing — are made once and the
@@ -35,7 +35,7 @@ module Spree
35
35
  customer.first_name,
36
36
  customer.last_name,
37
37
  customer.email,
38
- customer.accepts_email_marketing ? Spree.t(:say_yes) : Spree.t(:say_no),
38
+ customer.accepts_email_marketing ? I18n.t('spree.say_yes') : I18n.t('spree.say_no'),
39
39
  customer.address&.company,
40
40
  customer.address&.address1,
41
41
  customer.address&.address2,
@@ -25,7 +25,7 @@ module Spree
25
25
  newsletter_subscriber.email,
26
26
  newsletter_subscriber.customer&.full_name,
27
27
  newsletter_subscriber.customer_id,
28
- newsletter_subscriber.verified? ? Spree.t(:say_yes) : Spree.t(:say_no),
28
+ newsletter_subscriber.verified? ? I18n.t('spree.say_yes') : I18n.t('spree.say_no'),
29
29
  newsletter_subscriber.verified_at,
30
30
  newsletter_subscriber.created_at,
31
31
  newsletter_subscriber.updated_at