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
@@ -5,6 +5,9 @@ module Spree
5
5
  has_prefix_id :dig
6
6
 
7
7
  publishes_lifecycle_events
8
+ publishes_event 'digital.created', deprecated_alias_of: 'digital_asset.created'
9
+ publishes_event 'digital.updated', deprecated_alias_of: 'digital_asset.updated'
10
+ publishes_event 'digital.deleted', deprecated_alias_of: 'digital_asset.deleted'
8
11
 
9
12
  # Provider config lives under one key in `metadata` so a provider owns its
10
13
  # own namespace without colliding with other developer metadata. The values
@@ -22,6 +25,10 @@ module Spree
22
25
  # and provider-backed assets (which carry no file) are exempt.
23
26
  DEFAULT_PROVIDER = 'Spree::DigitalAssetProvider::File'.freeze
24
27
 
28
+ # The API names a provider by its shorthand (`file`); the column keeps the
29
+ # class name.
30
+ normalizes :provider_type, with: ->(value) { Spree::ApiTyped.class_name_for(Spree.digital_asset_providers, value) }
31
+
25
32
  validate :provider_type_is_registered, if: -> { provider_type.present? }
26
33
  # Only ask a valid provider whether it needs a file — an unregistered
27
34
  # provider_type is already rejected above, and calling requires_attachment?
@@ -12,6 +12,8 @@ module Spree
12
12
  # into internal software and owns its own configuration. The base gives it
13
13
  # only the asset.
14
14
  class Base
15
+ extend Spree::ApiTyped
16
+
15
17
  attr_reader :digital_asset
16
18
 
17
19
  SETTING_TYPES = %i[string number boolean select].freeze
@@ -3,12 +3,9 @@ module Spree
3
3
  has_prefix_id :dl # Spree-specific: digital link
4
4
 
5
5
  publishes_lifecycle_events
6
+ publishes_event :downloaded
6
7
 
7
- if Rails::VERSION::STRING >= '7.1.0'
8
- has_secure_token on: :save
9
- else
10
- has_secure_token
11
- end
8
+ has_secure_token on: :save
12
9
 
13
10
  if defined?(Spree::Security::DigitalLinks)
14
11
  include Spree::Security::DigitalLinks
@@ -45,7 +45,7 @@ module Spree
45
45
  private
46
46
 
47
47
  def exactly_one_adjustable
48
- errors.add(:base, :exactly_one_adjustable, message: Spree.t('errors.messages.exactly_one_adjustable')) unless [line_item, fulfillment].compact.one?
48
+ errors.add(:base, :exactly_one_adjustable, message: I18n.t('spree.errors.messages.exactly_one_adjustable')) unless [line_item, fulfillment].compact.one?
49
49
  end
50
50
  end
51
51
  end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ # A store's published version of an editable email template (an email, the
3
+ # layout or a shared partial). Found before the template file whenever a
4
+ # customer email renders, so it is what customers receive. Reverting marks it
5
+ # reverted rather than deleting it, so its revisions stay reachable.
6
+ class EmailTemplate < Spree.base_class
7
+ has_prefix_id :etpl
8
+
9
+ include Spree::EmailTemplateIdentity
10
+ include Spree::HasStatus
11
+ include Spree::ActedBy
12
+
13
+ has_status :published, :reverted, default: :published
14
+
15
+ publishes_events :published, :reverted
16
+
17
+ acted_by :published_by
18
+ acted_by :reverted_by
19
+
20
+ has_many :revisions, -> { order(created_at: :desc) }, class_name: 'Spree::EmailTemplateRevision',
21
+ dependent: :destroy, inverse_of: :email_template
22
+
23
+ validates :published_at, presence: true
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ # Work in progress on an editable email template, one per store, template
3
+ # and language. Shared by every admin of the store: `lock_version` refuses a
4
+ # save made from a stale copy, and `updated_by` says who saved it last.
5
+ class EmailTemplateDraft < Spree.base_class
6
+ has_prefix_id :etdr
7
+
8
+ include Spree::EmailTemplateIdentity
9
+ include Spree::ActedBy
10
+
11
+ acted_by :updated_by
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ # One published version of a store's email template, kept so it can be
3
+ # restored. Append-only.
4
+ class EmailTemplateRevision < Spree.base_class
5
+ has_prefix_id :etrv
6
+
7
+ include Spree::ActedBy
8
+
9
+ belongs_to :email_template, class_name: 'Spree::EmailTemplate', inverse_of: :revisions
10
+
11
+ acted_by :published_by
12
+
13
+ validates :body, presence: true
14
+
15
+ delegate :store, :key, :locale, to: :email_template
16
+ end
17
+ end
@@ -0,0 +1,118 @@
1
+ module Spree
2
+ module EmailTemplates
3
+ # One editable email template for one store and language, as the editor
4
+ # shows it: what customers receive now, the draft in progress and
5
+ # Spree's default. For one language, Spree's translation keys are written
6
+ # out as text in it, so the merchant reads and edits plain words.
7
+ class Entry
8
+ include ActiveModel::Model
9
+ include ActiveModel::Attributes
10
+
11
+ attribute :definition
12
+ attribute :locale, :string, default: Spree::EmailTemplate::ANY_LOCALE
13
+ attribute :published
14
+ attribute :draft
15
+ attribute :customized_locales, default: -> { [] }
16
+
17
+ delegate :key, :kind, to: :definition
18
+
19
+ # @param store [Spree::Store]
20
+ # @param locale [String] a language code, or "any"
21
+ # @return [Array<Spree::EmailTemplates::Entry>] every editable template
22
+ def self.all(store, locale: Spree::EmailTemplate::ANY_LOCALE)
23
+ published = store.email_templates.published.where(locale: [locale, Spree::EmailTemplate::ANY_LOCALE]).
24
+ sort_by { |template| template.locale == locale ? 0 : 1 }.uniq(&:key).index_by(&:key)
25
+ drafts = store.email_template_drafts.where(locale: locale).includes(:updated_by).index_by(&:key)
26
+ customized = store.email_templates.published.pluck(:key, :locale).group_by(&:first)
27
+
28
+ Spree.editable_email_templates.map do |definition|
29
+ new(definition: definition, locale: locale, published: published[definition.key], draft: drafts[definition.key],
30
+ customized_locales: customized.fetch(definition.key, []).map(&:last).sort)
31
+ end
32
+ end
33
+
34
+ # @param store [Spree::Store]
35
+ # @param id [String] the template's key with dots, e.g. "spree.order_mailer.confirm_email"
36
+ # @param locale [String]
37
+ # @return [Spree::EmailTemplates::Entry]
38
+ # @raise [ActiveRecord::RecordNotFound] when no editable template has that id
39
+ def self.for_id(store, id, locale: Spree::EmailTemplate::ANY_LOCALE)
40
+ definition = Spree.editable_email_templates[id.to_s.tr('.', '/')]
41
+ raise ActiveRecord::RecordNotFound, "Couldn't find email template #{id}" unless definition
42
+
43
+ locale = locale.presence || Spree::EmailTemplate::ANY_LOCALE
44
+ new(
45
+ definition: definition, locale: locale,
46
+ published: store.email_templates.published.where(key: definition.key, locale: [locale, Spree::EmailTemplate::ANY_LOCALE]).
47
+ min_by { |template| template.locale == locale ? 0 : 1 },
48
+ draft: store.email_template_drafts.for_key(definition.key, locale).first,
49
+ customized_locales: store.email_templates.published.where(key: definition.key).order(:locale).pluck(:locale)
50
+ )
51
+ end
52
+
53
+ # The key with dots, so it fits in a URL segment.
54
+ def id
55
+ key.tr('/', '.')
56
+ end
57
+ alias prefixed_id id
58
+
59
+ def customized?
60
+ published.present?
61
+ end
62
+
63
+ # @return [Spree::Emails::Template, nil] the template file Spree or the app ships
64
+ def default
65
+ @default ||= Spree::Emails::TemplateResolver.for_mailers.find_default(key)
66
+ end
67
+
68
+ # @return [String, nil] the language of the published version customers
69
+ # receive: this one, or "any" when the version for every language applies
70
+ def published_locale
71
+ published&.locale
72
+ end
73
+
74
+ # What customers receive now.
75
+ def subject
76
+ readable(customized? ? published.subject : default&.subject, escape: false)
77
+ end
78
+
79
+ def body
80
+ readable(customized? ? published.body : default&.body)
81
+ end
82
+
83
+ def default_subject
84
+ readable(default&.subject, escape: false)
85
+ end
86
+
87
+ def default_body
88
+ readable(default&.body)
89
+ end
90
+
91
+ def base_subject
92
+ readable(outdated_version&.base_subject, escape: false)
93
+ end
94
+
95
+ def base_body
96
+ readable(outdated_version&.base_body)
97
+ end
98
+
99
+ # The default the store's version started from, when Spree's default has
100
+ # changed since, so the editor can show what changed.
101
+ #
102
+ # @return [Spree::EmailTemplate, Spree::EmailTemplateDraft, nil]
103
+ def outdated_version
104
+ # An open draft is what gets published next, so it alone decides.
105
+ version = draft || published
106
+ version if version&.default_changed?(default)
107
+ end
108
+
109
+ private
110
+
111
+ def readable(text, escape: true)
112
+ return text if locale == Spree::EmailTemplate::ANY_LOCALE
113
+
114
+ Spree::Emails::TranslationInliner.call(text, locale: locale, escape: escape)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module EmailTemplates
3
+ # A record of the store an email can be previewed with, as the editor's
4
+ # record picker lists it.
5
+ class SampleRecord
6
+ include ActiveModel::Model
7
+ include ActiveModel::Attributes
8
+
9
+ attribute :id, :string
10
+ attribute :label, :string
11
+ attribute :created_at, :datetime
12
+
13
+ alias prefixed_id id
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,80 @@
1
+ module Spree
2
+ module Emails
3
+ # How a store's customer emails look: its colors and font over the
4
+ # defaults the shipped layout was designed with. Templates read it as
5
+ # `store.branding`, so it outlives any edit or revert of a template.
6
+ class Branding
7
+ include ActiveModel::Model
8
+ include ActiveModel::Attributes
9
+
10
+ COLOR_FORMAT = /\A#\h{6}\z/
11
+
12
+ # Email-safe fonts, with fallback stacks for clients that do not load
13
+ # web fonts. Family names stay unquoted: templates escape what they print.
14
+ FONTS = {
15
+ 'inter' => { family: 'Inter, Helvetica, Arial, sans-serif', heading_family: 'Geist, Inter, Helvetica, Arial, sans-serif' },
16
+ 'system' => { family: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif' },
17
+ 'helvetica' => { family: 'Helvetica, Arial, sans-serif' },
18
+ 'georgia' => { family: 'Georgia, Times New Roman, Times, serif' },
19
+ 'roboto' => { family: 'Roboto, Helvetica, Arial, sans-serif', url: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700' },
20
+ 'lato' => { family: 'Lato, Helvetica, Arial, sans-serif', url: 'https://fonts.googleapis.com/css2?family=Lato:wght@400;700' },
21
+ 'merriweather' => { family: 'Merriweather, Georgia, serif', url: 'https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700' }
22
+ }.freeze
23
+
24
+ DEFAULT_FONT = 'inter'.freeze
25
+ DEFAULT_COLORS = {
26
+ background_color: '#FFFFFF', card_color: '#F6F6F6', text_color: '#726A6A', heading_color: '#332C2C'
27
+ }.freeze
28
+ COLORS = %i[accent_color background_color card_color text_color heading_color].freeze
29
+
30
+ COLORS.each { |color| attribute color, :string }
31
+ attribute :font, :string
32
+
33
+ validates(*COLORS, format: { with: COLOR_FORMAT }, allow_blank: true)
34
+ validates :font, inclusion: { in: FONTS.keys }, allow_blank: true
35
+
36
+ # What templates read as `store.branding`. A value that is not a color
37
+ # or a known font falls back to the default, so nothing else ever reaches
38
+ # an email's CSS, unsaved preview values included.
39
+ #
40
+ # @return [Hash{String => String, nil}]
41
+ def to_h
42
+ accent = color(:accent_color)
43
+ typeface_key = FONTS.key?(font) ? font : DEFAULT_FONT
44
+ typeface = FONTS[typeface_key]
45
+ colors = DEFAULT_COLORS.to_h { |name, default| [name, color(name) || default] }
46
+
47
+ {
48
+ **colors,
49
+ accent_color: accent,
50
+ link_color: accent || colors[:heading_color],
51
+ # Without an accent, buttons keep the outlined look the layout ships with.
52
+ button_color: accent || '#FFFFFF',
53
+ button_text_color: accent ? readable_text_on(accent) : '#1F2222',
54
+ button_border: accent ? 'none' : '1px solid #E8E9E9',
55
+ font: typeface_key,
56
+ font_family: typeface[:family],
57
+ heading_font_family: typeface[:heading_family] || typeface[:family],
58
+ font_url: typeface[:url]
59
+ }.stringify_keys
60
+ end
61
+
62
+ private
63
+
64
+ def color(name)
65
+ value = public_send(name).to_s
66
+ value.upcase if value.match?(COLOR_FORMAT)
67
+ end
68
+
69
+ # Black or white, whichever reads better on the color (WCAG relative luminance).
70
+ def readable_text_on(hex)
71
+ red, green, blue = hex.delete('#').scan(/../).map do |channel|
72
+ value = channel.to_i(16) / 255.0
73
+ value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055)**2.4
74
+ end
75
+ luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue)
76
+ luminance > 0.179 ? '#000000' : '#FFFFFF'
77
+ end
78
+ end
79
+ end
80
+ end
@@ -17,13 +17,14 @@ module Spree
17
17
  include Spree::HasStatus
18
18
  include Spree::HasCustomFields
19
19
  include Spree::Metadata
20
+ include Spree::PostSale::Taxation
20
21
 
21
22
  publishes_lifecycle_events
23
+ publishes_events :approved, :canceled, :fulfilled, :received, :requested
22
24
 
23
25
  has_status :requested, :approved, :received, :fulfilled, :canceled,
24
26
  default: :requested
25
27
 
26
- belongs_to :store, class_name: 'Spree::Store'
27
28
  belongs_to :order, class_name: 'Spree::Order', inverse_of: :exchanges
28
29
  belongs_to :stock_location, class_name: 'Spree::StockLocation'
29
30
  belongs_to :reason, class_name: 'Spree::ReturnReason', optional: true, inverse_of: :exchanges
@@ -53,5 +54,18 @@ module Spree
53
54
  def display_price_difference
54
55
  Spree::Money.new(price_difference, currency: currency)
55
56
  end
57
+
58
+ def taxed_lines
59
+ exchange_line_items
60
+ end
61
+
62
+ private
63
+
64
+ # The replacement is a sale of its own, taxed as the order would be today
65
+ # rather than read off the original line, whose category it may not share.
66
+ def estimate_tax(lines, amounts: nil)
67
+ super
68
+ order.tax_provider.estimate_replacement(order, lines, **order.tax_estimate_inputs)
69
+ end
56
70
  end
57
71
  end
@@ -1,7 +1,13 @@
1
1
  module Spree
2
2
  # One swap on a {Spree::Exchange}: a quantity of one variant going back,
3
3
  # the same quantity of another coming out.
4
+ #
5
+ # Both halves are taxed: the units coming back are credited the tax the
6
+ # customer paid on them (`original_*`), and the replacement is taxed as a new
7
+ # sale (`new_*`), each for the announced quantity.
4
8
  class ExchangeLineItem < Spree.base_class
9
+ include Spree::PostSale::TaxedLine
10
+
5
11
  has_prefix_id :eli
6
12
 
7
13
  belongs_to :exchange, class_name: 'Spree::Exchange', inverse_of: :exchange_line_items
@@ -19,24 +25,134 @@ module Spree
19
25
 
20
26
  delegate :order, :currency, to: :exchange
21
27
 
22
- # What the customer paid for the units coming back, after discounts —
23
- # not the list price, which would over-credit a discounted line.
28
+ extend Spree::DisplayMoney
29
+ money_methods :original_price, :new_variant_price, :price_difference, :original_tax_total, :new_tax_total
30
+
31
+ # What the customer paid for the units coming back, after discounts and
32
+ # with the tax charged on top — not the list price, which would
33
+ # over-credit a discounted line.
24
34
  def original_price
25
- return 0 if line_item.nil? || line_item.quantity.to_i.zero?
35
+ original_amount_for(quantity) + original_additional_tax_total
36
+ end
37
+
38
+ def original_pre_tax_amount
39
+ original_amount_for(quantity) - original_included_tax_total
40
+ end
26
41
 
27
- (line_item.amount / line_item.quantity) * quantity.to_i
42
+ def original_tax_total
43
+ original_included_tax_total + original_additional_tax_total
28
44
  end
29
45
 
46
+ # What the replacement costs, tax on top included.
30
47
  def new_variant_price
31
- new_variant.price_in(currency)&.amount.to_d * quantity.to_i
48
+ replacement_amount_for(quantity) + new_additional_tax_total
49
+ end
50
+
51
+ def new_pre_tax_amount
52
+ replacement_amount_for(quantity) - new_included_tax_total
53
+ end
54
+
55
+ def new_tax_total
56
+ new_included_tax_total + new_additional_tax_total
32
57
  end
33
58
 
34
59
  def price_difference
35
60
  new_variant_price - original_price
36
61
  end
37
62
 
63
+ # The replacement keeps the deal the customer had on what it replaces:
64
+ # its price carries the original line's discount in the same proportion,
65
+ # so swapping a size of a discounted item costs nothing.
66
+ #
67
+ # @param units [Integer]
68
+ # @return [BigDecimal]
69
+ def replacement_amount_for(units)
70
+ price = new_variant&.price_in(currency)&.amount.to_d
71
+
72
+ Spree::Money::Rounding.to_currency(price * units.to_i * discount_ratio, currency)
73
+ end
74
+
75
+ # The units this swap covers: none once canceled, what arrived once the
76
+ # warehouse has counted, what was announced before.
77
+ #
78
+ # @return [Integer]
79
+ def credited_quantity
80
+ return 0 if exchange.canceled?
81
+
82
+ exchange.received? || exchange.fulfilled? ? received_quantity.to_i : quantity.to_i
83
+ end
84
+
85
+ # @return [BigDecimal] what the units coming back are worth, tax included
86
+ def credited_worth
87
+ units = credited_quantity
88
+ return 0.to_d if quantity.to_i.zero?
89
+
90
+ original_amount_for(units) +
91
+ Spree::Money::Rounding.to_currency((original_additional_tax_total / quantity) * units, currency)
92
+ end
93
+
94
+ # What the provider taxes the replacement on (see
95
+ # Spree::TaxProvider::Base#estimate_replacement).
96
+ def taxable_basis
97
+ replacement_amount_for(credited_quantity)
98
+ end
99
+
100
+ def tax_category_id
101
+ new_variant&.tax_category_id
102
+ end
103
+
104
+ # What the units that arrived are credited, from the rows as they stand.
105
+ #
106
+ # @return [BigDecimal]
107
+ def settled_credit
108
+ original_amount_for(received_quantity) + tax_lines.credits.additional.sum(:amount)
109
+ end
110
+
111
+ # What their replacements cost, from the rows as they stand.
112
+ #
113
+ # @return [BigDecimal]
114
+ def settled_charge
115
+ replacement_amount_for(received_quantity) + tax_lines.charges.additional.sum(:amount)
116
+ end
117
+
118
+ # The tax inside {#settled_credit} less the tax inside {#settled_charge}.
119
+ #
120
+ # @return [BigDecimal]
121
+ def settled_tax
122
+ credited_tax_total - tax_lines.charges.sum(:amount)
123
+ end
124
+
125
+ # @return [void]
126
+ def fold_tax_lines!
127
+ rows = tax_lines.reload.to_a
128
+ original_included, original_additional = tax_totals_from(rows.select(&:credit?))
129
+ new_included, new_additional = tax_totals_from(rows.reject(&:credit?))
130
+
131
+ update_columns(
132
+ original_included_tax_total: original_included,
133
+ original_additional_tax_total: original_additional,
134
+ new_included_tax_total: new_included,
135
+ new_additional_tax_total: new_additional
136
+ )
137
+ end
138
+
38
139
  private
39
140
 
141
+ def original_amount_for(units)
142
+ line_item&.discounted_amount_for(units) || 0.to_d
143
+ end
144
+
145
+ def discount_ratio
146
+ amount = line_item&.amount.to_d
147
+ return 1 unless amount.positive?
148
+
149
+ [line_item.discounted_amount, 0].max / amount
150
+ end
151
+
152
+ def recorded_tax_total
153
+ original_tax_total + new_tax_total
154
+ end
155
+
40
156
  def set_original_variant_from_line_item
41
157
  self.original_variant ||= line_item&.variant
42
158
  end
@@ -30,7 +30,6 @@ module Spree
30
30
  #
31
31
  # Associations
32
32
  #
33
- belongs_to :store, class_name: 'Spree::Store'
34
33
  # Optional so secret-API-key callers (apps / server-to-server) can create
35
34
  # exports without a human user attached. The email notification is
36
35
  # skipped for these — apps poll instead.
@@ -9,13 +9,22 @@ module Spree
9
9
  end
10
10
 
11
11
  def variant_includes
12
- [:images, :prices, :stock_levels, :stock_locations, { option_values: [:option_type] }]
12
+ [:images, :prices, { stock_levels: :stock_location, option_values: [:option_type] }]
13
13
  end
14
14
 
15
15
  def multi_line_csv?
16
16
  true
17
17
  end
18
18
 
19
+ # The seller scopes which locations the inventory columns cover; their
20
+ # default, worked out once per export, carries the full row's stock.
21
+ def to_csv_options
22
+ @to_csv_options ||= {
23
+ seller: seller,
24
+ default_stock_location: Spree::StockLocation.owned_by(store_id: store.id, seller_id: seller&.id).active.order_default.first || store.default_stock_location
25
+ }
26
+ end
27
+
19
28
  # when doing full product export, we want to exclude archived products
20
29
  def scope
21
30
  if search_params.nil?
@@ -52,7 +52,7 @@ module Spree
52
52
  totals = query.metrics.map { |m| result.totals[m.name][:value] }
53
53
  # With a breakdown the totals close the file under a "Total" label in
54
54
  # the first dimension column; without one the totals are the only row.
55
- label = query.dimensions.empty? ? [] : [Spree.t('reporting.export.total')] + Array.new(query.dimensions.size - 1, '')
55
+ label = query.dimensions.empty? ? [] : [I18n.t('spree.reporting.export.total')] + Array.new(query.dimensions.size - 1, '')
56
56
  csv << Spree::CSV::FormulaSanitizer.row(label + totals)
57
57
  end
58
58
  end
@@ -10,8 +10,10 @@ module Spree
10
10
  # Extensions may append their own kinds — the list is validated, not frozen.
11
11
  # `duty` rows are written by a duties provider's adjuster and snapshot the
12
12
  # classification they were calculated from in `metadata` — never re-derive
13
- # a duty from the live catalog.
14
- KINDS = %w[surcharge handling gift_wrap cod payment duty]
13
+ # a duty from the live catalog. `exchange` is what a customer owes when an
14
+ # exchange's replacement costs more, put on the order for the merchant to
15
+ # collect.
16
+ KINDS = %w[surcharge handling gift_wrap cod payment duty exchange]
15
17
 
16
18
  has_prefix_id :fee
17
19