spree_core 6.0.0.beta4 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (575) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/app/helpers/spree/images_helper.rb +0 -57
  4. data/app/jobs/spree/base_job.rb +21 -0
  5. data/app/jobs/spree/carts/reap_expired_job.rb +2 -2
  6. data/app/jobs/spree/collections/regenerate_time_based_job.rb +6 -3
  7. data/app/jobs/spree/orders/finalize_stale_drafts_job.rb +8 -5
  8. data/app/jobs/spree/payments/cancel_unused_sessions_job.rb +61 -0
  9. data/app/jobs/spree/search_provider/index_job.rb +4 -0
  10. data/app/jobs/spree/seller_payouts/sweep_seller_job.rb +2 -0
  11. data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
  12. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  13. data/app/mailers/spree/base_mailer.rb +97 -13
  14. data/app/mailers/spree/email_template_mailer.rb +27 -0
  15. data/app/mailers/spree/export_mailer.rb +3 -4
  16. data/app/mailers/spree/import_mailer.rb +3 -4
  17. data/app/mailers/spree/invitation_mailer.rb +24 -7
  18. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  19. data/app/mailers/spree/webhook_mailer.rb +2 -2
  20. data/app/models/concerns/spree/acted_by.rb +1 -1
  21. data/app/models/concerns/spree/admin_user_methods.rb +15 -1
  22. data/app/models/concerns/spree/api_typed.rb +91 -0
  23. data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
  24. data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
  25. data/app/models/concerns/spree/calculator/tiers.rb +63 -0
  26. data/app/models/concerns/spree/canonical_decimals.rb +61 -0
  27. data/app/models/concerns/spree/customer_methods.rb +23 -18
  28. data/app/models/concerns/spree/derived_number.rb +11 -13
  29. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  30. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  31. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  32. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  33. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  34. data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
  35. data/app/models/concerns/spree/preference_schema.rb +62 -32
  36. data/app/models/concerns/spree/product_scopes.rb +16 -2
  37. data/app/models/concerns/spree/provider_listing.rb +41 -0
  38. data/app/models/concerns/spree/publishable.rb +82 -14
  39. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  40. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  41. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  42. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  43. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  44. data/app/models/concerns/spree/purchase/payment_processing.rb +8 -4
  45. data/app/models/concerns/spree/purchase/purchase_order.rb +9 -4
  46. data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
  47. data/app/models/concerns/spree/purchase/totals.rb +49 -8
  48. data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
  49. data/app/models/concerns/spree/receivable.rb +1 -1
  50. data/app/models/concerns/spree/searchable.rb +0 -4
  51. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  52. data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
  53. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  54. data/app/models/concerns/spree/stores/email_branding.rb +32 -0
  55. data/app/models/concerns/spree/stores/markets.rb +5 -2
  56. data/app/models/concerns/spree/type_labels.rb +38 -0
  57. data/app/models/concerns/spree/typed_adjustment_line.rb +5 -6
  58. data/app/models/concerns/spree/typed_associations.rb +12 -18
  59. data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
  60. data/app/models/spree/address.rb +12 -2
  61. data/app/models/spree/adjusters/largest_remainder.rb +14 -1
  62. data/app/models/spree/adjusters/promotion.rb +4 -19
  63. data/app/models/spree/allowed_origin.rb +3 -1
  64. data/app/models/spree/api_key.rb +5 -6
  65. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  66. data/app/models/spree/base.rb +99 -22
  67. data/app/models/spree/calculator/flat_percent_item_total.rb +2 -4
  68. data/app/models/spree/calculator/flat_rate.rb +3 -5
  69. data/app/models/spree/calculator/flexi_rate.rb +4 -6
  70. data/app/models/spree/calculator/percent_on_line_item.rb +2 -2
  71. data/app/models/spree/calculator/price_sack.rb +5 -7
  72. data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -5
  73. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +7 -7
  74. data/app/models/spree/calculator/shipping/flat_rate.rb +5 -7
  75. data/app/models/spree/calculator/shipping/flexi_rate.rb +4 -6
  76. data/app/models/spree/calculator/shipping/per_item.rb +3 -5
  77. data/app/models/spree/calculator/shipping/price_sack.rb +5 -7
  78. data/app/models/spree/calculator/tiered_flat_rate.rb +7 -24
  79. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  80. data/app/models/spree/cart.rb +78 -18
  81. data/app/models/spree/catalog.rb +1 -0
  82. data/app/models/spree/catalog_price.rb +0 -7
  83. data/app/models/spree/catalog_price_tier.rb +0 -8
  84. data/app/models/spree/category.rb +3 -1
  85. data/app/models/spree/channel/gating.rb +3 -2
  86. data/app/models/spree/channel.rb +6 -4
  87. data/app/models/spree/claim.rb +22 -1
  88. data/app/models/spree/claim_line_item.rb +41 -6
  89. data/app/models/spree/collection.rb +2 -1
  90. data/app/models/spree/collection_rule.rb +5 -21
  91. data/app/models/spree/commission_line.rb +1 -1
  92. data/app/models/spree/commission_rate.rb +7 -4
  93. data/app/models/spree/commission_rate_value.rb +1 -1
  94. data/app/models/spree/commission_rule.rb +4 -11
  95. data/app/models/spree/commission_rules/category_rule.rb +2 -5
  96. data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
  97. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  98. data/app/models/spree/commission_rules/seller_rule.rb +2 -5
  99. data/app/models/spree/company.rb +1 -0
  100. data/app/models/spree/company_invitation.rb +1 -0
  101. data/app/models/spree/consent_record.rb +0 -1
  102. data/app/models/spree/credit_card.rb +1 -1
  103. data/app/models/spree/current.rb +23 -0
  104. data/app/models/spree/custom_field.rb +2 -2
  105. data/app/models/spree/custom_field_definition.rb +20 -35
  106. data/app/models/spree/custom_fields/boolean.rb +1 -1
  107. data/app/models/spree/custom_fields/json.rb +1 -1
  108. data/app/models/spree/customer_group.rb +1 -0
  109. data/app/models/spree/data_feed/google.rb +0 -2
  110. data/app/models/spree/data_request.rb +2 -1
  111. data/app/models/spree/delivery_method.rb +36 -14
  112. data/app/models/spree/delivery_method_rule.rb +4 -11
  113. data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
  114. data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
  115. data/app/models/spree/delivery_profile.rb +1 -0
  116. data/app/models/spree/delivery_rate.rb +7 -8
  117. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  118. data/app/models/spree/delivery_zone.rb +1 -2
  119. data/app/models/spree/delivery_zone_member.rb +4 -4
  120. data/app/models/spree/digital_asset.rb +7 -0
  121. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  122. data/app/models/spree/digital_link.rb +2 -5
  123. data/app/models/spree/discount.rb +1 -1
  124. data/app/models/spree/email_template.rb +25 -0
  125. data/app/models/spree/email_template_draft.rb +13 -0
  126. data/app/models/spree/email_template_revision.rb +17 -0
  127. data/app/models/spree/email_templates/entry.rb +118 -0
  128. data/app/models/spree/email_templates/sample_record.rb +16 -0
  129. data/app/models/spree/emails/branding.rb +80 -0
  130. data/app/models/spree/exchange.rb +16 -1
  131. data/app/models/spree/exchange_line_item.rb +121 -5
  132. data/app/models/spree/export.rb +0 -1
  133. data/app/models/spree/exports/products.rb +10 -1
  134. data/app/models/spree/exports/report.rb +3 -3
  135. data/app/models/spree/fee.rb +4 -2
  136. data/app/models/spree/file_upload.rb +155 -0
  137. data/app/models/spree/fulfillment.rb +66 -30
  138. data/app/models/spree/fulfillment_changer.rb +2 -2
  139. data/app/models/spree/fulfillment_group.rb +6 -1
  140. data/app/models/spree/fulfillment_item.rb +0 -4
  141. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  142. data/app/models/spree/gateway/bogus.rb +6 -0
  143. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  144. data/app/models/spree/gateway.rb +6 -2
  145. data/app/models/spree/gift_card.rb +18 -8
  146. data/app/models/spree/gift_card_batch.rb +3 -5
  147. data/app/models/spree/import.rb +5 -7
  148. data/app/models/spree/import_mapping.rb +1 -2
  149. data/app/models/spree/import_row.rb +2 -2
  150. data/app/models/spree/import_schemas/products.rb +1 -0
  151. data/app/models/spree/integration.rb +9 -19
  152. data/app/models/spree/invitation.rb +5 -5
  153. data/app/models/spree/line_item.rb +15 -4
  154. data/app/models/spree/locale.rb +11 -8
  155. data/app/models/spree/market.rb +8 -2
  156. data/app/models/spree/media.rb +1 -9
  157. data/app/models/spree/newsletter_subscriber.rb +4 -0
  158. data/app/models/spree/option_type.rb +2 -1
  159. data/app/models/spree/option_value.rb +2 -1
  160. data/app/models/spree/order.rb +60 -142
  161. data/app/models/spree/order_group.rb +18 -4
  162. data/app/models/spree/order_inventory.rb +10 -5
  163. data/app/models/spree/order_promotion.rb +1 -1
  164. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  165. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  166. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  167. data/app/models/spree/order_routing_rule.rb +6 -22
  168. data/app/models/spree/package_type.rb +2 -2
  169. data/app/models/spree/payment/gateway_options.rb +15 -53
  170. data/app/models/spree/payment/processing.rb +2 -4
  171. data/app/models/spree/payment.rb +21 -13
  172. data/app/models/spree/payment_method/check.rb +2 -0
  173. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  174. data/app/models/spree/payment_method/store_credit.rb +15 -13
  175. data/app/models/spree/payment_method.rb +78 -6
  176. data/app/models/spree/payment_session.rb +5 -1
  177. data/app/models/spree/payment_setup_session.rb +1 -0
  178. data/app/models/spree/payout_provider/base.rb +3 -1
  179. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  180. data/app/models/spree/policy.rb +1 -0
  181. data/app/models/spree/price.rb +17 -14
  182. data/app/models/spree/price_adjustment_tier.rb +4 -0
  183. data/app/models/spree/price_list.rb +14 -9
  184. data/app/models/spree/price_rules/channel_rule.rb +3 -6
  185. data/app/models/spree/price_rules/customer_group_rule.rb +3 -6
  186. data/app/models/spree/price_rules/market_rule.rb +2 -5
  187. data/app/models/spree/price_rules/user_rule.rb +1 -3
  188. data/app/models/spree/product/channels.rb +9 -2
  189. data/app/models/spree/product.rb +54 -30
  190. data/app/models/spree/product_submission.rb +0 -12
  191. data/app/models/spree/product_type.rb +1 -1
  192. data/app/models/spree/promotion/actions/create_line_items.rb +28 -18
  193. data/app/models/spree/promotion/rules/category.rb +1 -5
  194. data/app/models/spree/promotion/rules/channel.rb +2 -9
  195. data/app/models/spree/promotion/rules/country.rb +3 -8
  196. data/app/models/spree/promotion/rules/currency.rb +1 -5
  197. data/app/models/spree/promotion/rules/customer_group.rb +2 -9
  198. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  199. data/app/models/spree/promotion/rules/item_total.rb +6 -10
  200. data/app/models/spree/promotion/rules/market.rb +2 -9
  201. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  202. data/app/models/spree/promotion/rules/option_value.rb +2 -6
  203. data/app/models/spree/promotion/rules/product.rb +1 -5
  204. data/app/models/spree/promotion/rules/user.rb +0 -4
  205. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  206. data/app/models/spree/promotion.rb +36 -6
  207. data/app/models/spree/promotion_action.rb +7 -14
  208. data/app/models/spree/promotion_action_line_item.rb +1 -1
  209. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  210. data/app/models/spree/promotion_rule.rb +7 -14
  211. data/app/models/spree/purchase_order.rb +3 -0
  212. data/app/models/spree/quantity_rule.rb +1 -1
  213. data/app/models/spree/refund.rb +41 -18
  214. data/app/models/spree/return.rb +31 -2
  215. data/app/models/spree/return_line_item.rb +65 -14
  216. data/app/models/spree/role.rb +7 -7
  217. data/app/models/spree/saved_report.rb +2 -1
  218. data/app/models/spree/search_provider/base.rb +4 -0
  219. data/app/models/spree/seller.rb +10 -2
  220. data/app/models/spree/seller_payout.rb +8 -0
  221. data/app/models/spree/seller_requirement.rb +5 -12
  222. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  223. data/app/models/spree/seller_requirement_submission.rb +3 -2
  224. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  225. data/app/models/spree/seller_transfer.rb +6 -0
  226. data/app/models/spree/shipping_label.rb +2 -0
  227. data/app/models/spree/stock/availability_validator.rb +2 -2
  228. data/app/models/spree/stock/estimator.rb +1 -1
  229. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  230. data/app/models/spree/stock/packer.rb +2 -2
  231. data/app/models/spree/stock/splitter/weight.rb +1 -1
  232. data/app/models/spree/stock_level/custom_events.rb +6 -0
  233. data/app/models/spree/stock_level.rb +3 -1
  234. data/app/models/spree/stock_location.rb +1 -0
  235. data/app/models/spree/stock_movement.rb +1 -1
  236. data/app/models/spree/stock_receipt_item.rb +1 -1
  237. data/app/models/spree/stock_reservation.rb +2 -2
  238. data/app/models/spree/stock_transfer.rb +6 -2
  239. data/app/models/spree/store.rb +70 -19
  240. data/app/models/spree/store_credit.rb +13 -13
  241. data/app/models/spree/store_credit_event.rb +5 -5
  242. data/app/models/spree/supplier.rb +1 -0
  243. data/app/models/spree/tax_category.rb +1 -0
  244. data/app/models/spree/tax_exemption_certificate.rb +3 -0
  245. data/app/models/spree/tax_identifier.rb +3 -1
  246. data/app/models/spree/tax_line.rb +53 -3
  247. data/app/models/spree/tax_provider/base.rb +71 -14
  248. data/app/models/spree/tax_provider/internal.rb +70 -41
  249. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  250. data/app/models/spree/tax_rate.rb +30 -8
  251. data/app/models/spree/taxon.rb +1 -1
  252. data/app/models/spree/taxonomy.rb +0 -1
  253. data/app/models/spree/update_status.rb +25 -0
  254. data/app/models/spree/variant.rb +10 -12
  255. data/app/models/spree/variant_media.rb +1 -1
  256. data/app/models/spree/webhook_delivery.rb +1 -0
  257. data/app/models/spree/webhook_endpoint.rb +46 -13
  258. data/app/models/spree/wishlist.rb +1 -6
  259. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  260. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  261. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  262. data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
  263. data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
  264. data/app/presenters/spree/csv/product_variant_presenter.rb +59 -20
  265. data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
  266. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  267. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  268. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  269. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  270. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  271. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  272. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  273. data/app/services/spree/carts/create.rb +8 -2
  274. data/app/services/spree/carts/destroy.rb +2 -2
  275. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  276. data/app/services/spree/carts/split_by_seller.rb +1 -1
  277. data/app/services/spree/carts/update.rb +22 -8
  278. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  279. data/app/services/spree/categories/add_products.rb +13 -31
  280. data/app/services/spree/categories/remove_products.rb +12 -51
  281. data/app/services/spree/checkout/advance.rb +1 -4
  282. data/app/services/spree/collections/add_products.rb +10 -23
  283. data/app/services/spree/collections/remove_products.rb +7 -43
  284. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  285. data/app/services/spree/data_requests/create.rb +1 -1
  286. data/app/services/spree/deliveries/destroy.rb +2 -2
  287. data/app/services/spree/email_templates/check.rb +58 -0
  288. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  289. data/app/services/spree/email_templates/preview.rb +99 -0
  290. data/app/services/spree/emails/file_system.rb +23 -0
  291. data/app/services/spree/emails/filters.rb +84 -0
  292. data/app/services/spree/emails/liquid_context.rb +48 -0
  293. data/app/services/spree/emails/rendered_email.rb +14 -0
  294. data/app/services/spree/emails/renderer.rb +125 -0
  295. data/app/services/spree/emails/template.rb +51 -0
  296. data/app/services/spree/emails/template_data.rb +23 -0
  297. data/app/services/spree/emails/template_resolver.rb +109 -0
  298. data/app/services/spree/emails/text_converter.rb +72 -0
  299. data/app/services/spree/emails/translation_inliner.rb +134 -0
  300. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  301. data/app/services/spree/imports/row_processors/product_variant.rb +77 -34
  302. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  303. data/app/services/spree/locales.rb +43 -10
  304. data/app/services/spree/newsletter/subscribe.rb +8 -12
  305. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  306. data/app/services/spree/orders/create.rb +5 -4
  307. data/app/services/spree/orders/discounts/create.rb +14 -11
  308. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  309. data/app/services/spree/orders/discounts/update.rb +1 -0
  310. data/app/services/spree/orders/fees/create.rb +1 -0
  311. data/app/services/spree/orders/fees/destroy.rb +1 -0
  312. data/app/services/spree/orders/fees/update.rb +1 -0
  313. data/app/services/spree/orders/remove_line_item.rb +13 -3
  314. data/app/services/spree/orders/update.rb +10 -6
  315. data/app/services/spree/orders/update_item.rb +6 -3
  316. data/app/services/spree/orders/update_statuses.rb +3 -1
  317. data/app/services/spree/prices/bulk_upsert.rb +32 -25
  318. data/app/services/spree/product_memberships.rb +61 -0
  319. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  320. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  321. data/app/services/spree/seeds/commission_rates.rb +1 -1
  322. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  323. data/app/services/spree/seeds/payment_methods.rb +2 -2
  324. data/app/services/spree/seeds/saved_reports.rb +3 -3
  325. data/app/services/spree/seeds/stores.rb +1 -1
  326. data/app/services/spree/sellers/requirements.rb +1 -2
  327. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  328. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  329. data/app/services/spree/stock_levels/correct.rb +3 -3
  330. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  331. data/app/services/spree/store_credits/apply.rb +15 -13
  332. data/app/services/spree/stores/provision_defaults.rb +23 -25
  333. data/app/services/spree/update_check.rb +160 -0
  334. data/app/services/spree/uploads/assign_stores.rb +92 -0
  335. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  336. data/app/validators/spree/attachment_content_type_validator.rb +19 -2
  337. data/app/validators/spree/locale_codes_validator.rb +22 -0
  338. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  339. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  340. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  341. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  342. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  343. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  344. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  345. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  346. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  347. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  348. data/app/views/spree/shared/_summary_row.liquid +8 -0
  349. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  350. data/app/workflows/spree/carts/add_item.rb +4 -4
  351. data/app/workflows/spree/carts/complete.rb +13 -3
  352. data/app/workflows/spree/carts/merge.rb +1 -1
  353. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  354. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  355. data/app/workflows/spree/claims/approve.rb +5 -22
  356. data/app/workflows/spree/claims/cancel.rb +5 -21
  357. data/app/workflows/spree/claims/create.rb +15 -0
  358. data/app/workflows/spree/claims/deny.rb +7 -0
  359. data/app/workflows/spree/claims/resolve.rb +34 -31
  360. data/app/workflows/spree/customers/anonymize.rb +3 -4
  361. data/app/workflows/spree/customers/create.rb +1 -1
  362. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  363. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  364. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  365. data/app/workflows/spree/email_templates/publish.rb +70 -0
  366. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  367. data/app/workflows/spree/email_templates/revert.rb +29 -0
  368. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  369. data/app/workflows/spree/exchanges/approve.rb +5 -22
  370. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  371. data/app/workflows/spree/exchanges/create.rb +6 -0
  372. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  373. data/app/workflows/spree/exchanges/receive.rb +6 -65
  374. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  375. data/app/workflows/spree/fulfillments/create.rb +53 -49
  376. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  377. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  378. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  379. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  380. data/app/workflows/spree/orders/add_item.rb +5 -1
  381. data/app/workflows/spree/orders/cancel.rb +3 -3
  382. data/app/workflows/spree/orders/complete.rb +16 -8
  383. data/app/workflows/spree/payments/capture.rb +20 -5
  384. data/app/workflows/spree/payments/process.rb +5 -3
  385. data/app/workflows/spree/post_sale/approval.rb +35 -0
  386. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  387. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  388. data/app/workflows/spree/price_lists/update.rb +4 -4
  389. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  390. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  391. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  392. data/app/workflows/spree/purchase_orders/create.rb +3 -1
  393. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  394. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  395. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  396. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  397. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  398. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  399. data/app/workflows/spree/refunds/create.rb +5 -2
  400. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  401. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  402. data/app/workflows/spree/returns/approve.rb +5 -22
  403. data/app/workflows/spree/returns/cancel.rb +5 -22
  404. data/app/workflows/spree/returns/create.rb +8 -0
  405. data/app/workflows/spree/returns/receive.rb +5 -76
  406. data/app/workflows/spree/returns/refund.rb +31 -8
  407. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  408. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  409. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  410. data/app/workflows/spree/shipping_labels/record.rb +7 -7
  411. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  412. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  413. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  414. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  415. data/app/workflows/spree/stock_transfers/create.rb +3 -1
  416. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  417. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  418. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  419. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  420. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  421. data/config/currency_exponents.json +180 -0
  422. data/config/i18n-tasks.yml +32 -18
  423. data/config/initializers/alba.rb +7 -0
  424. data/config/language_names.csv +264 -0
  425. data/config/locales/ar.yml +811 -0
  426. data/config/locales/be.yml +855 -0
  427. data/config/locales/bg.yml +723 -0
  428. data/config/locales/ca.yml +689 -0
  429. data/config/locales/content_locales.rb +9 -0
  430. data/config/locales/cs.yml +855 -0
  431. data/config/locales/da.yml +771 -0
  432. data/config/locales/de-CH.yml +248 -0
  433. data/config/locales/de.yml +835 -0
  434. data/config/locales/el.yml +769 -0
  435. data/config/locales/en.yml +47 -505
  436. data/config/locales/es-CL.yml +301 -0
  437. data/config/locales/es-EC.yml +186 -0
  438. data/config/locales/es-MX.yml +334 -0
  439. data/config/locales/es.yml +831 -0
  440. data/config/locales/et.yml +644 -0
  441. data/config/locales/fa.yml +709 -0
  442. data/config/locales/fi.yml +771 -0
  443. data/config/locales/fr.yml +830 -0
  444. data/config/locales/hu.yml +751 -0
  445. data/config/locales/id.yml +756 -0
  446. data/config/locales/it.yml +833 -0
  447. data/config/locales/ja.yml +726 -0
  448. data/config/locales/km.yml +759 -0
  449. data/config/locales/ko.yml +575 -0
  450. data/config/locales/ku.yml +753 -0
  451. data/config/locales/lt.yml +821 -0
  452. data/config/locales/lv.yml +685 -0
  453. data/config/locales/nb.yml +824 -0
  454. data/config/locales/ne.yml +823 -0
  455. data/config/locales/nl.yml +826 -0
  456. data/config/locales/pl.yml +835 -0
  457. data/config/locales/pt-BR.yml +547 -0
  458. data/config/locales/pt.yml +636 -0
  459. data/config/locales/ro.yml +750 -0
  460. data/config/locales/ru.yml +834 -0
  461. data/config/locales/sk.yml +680 -0
  462. data/config/locales/sl-SI.yml +600 -0
  463. data/config/locales/sv.yml +827 -0
  464. data/config/locales/th.yml +745 -0
  465. data/config/locales/tr.yml +777 -0
  466. data/config/locales/uk.yml +836 -0
  467. data/config/locales/vi.yml +768 -0
  468. data/config/locales/zh-CN.yml +786 -0
  469. data/config/locales/zh-TW.yml +834 -0
  470. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  471. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  472. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  473. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  474. data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
  475. data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
  476. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  477. data/lib/generators/spree/dummy/templates/rails/test.rb +26 -0
  478. data/lib/spree/checkout/default_requirements.rb +37 -13
  479. data/lib/spree/core/configuration.rb +19 -1
  480. data/lib/spree/core/controller_helpers/store.rb +1 -1
  481. data/lib/spree/core/dependencies.rb +5 -4
  482. data/lib/spree/core/emails/escaped_output.rb +60 -0
  483. data/lib/spree/core/engine.rb +72 -165
  484. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  485. data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
  486. data/lib/spree/core/preferences/json_conversion.rb +186 -0
  487. data/lib/spree/core/preferences/masking.rb +55 -2
  488. data/lib/spree/core/preferences/preferable.rb +343 -103
  489. data/lib/spree/core/preferences/preferable_class_methods.rb +188 -136
  490. data/lib/spree/core/previews/import_preview.rb +10 -0
  491. data/lib/spree/core/ransack_configuration.rb +13 -2
  492. data/lib/spree/core/tax/provider_error.rb +37 -0
  493. data/lib/spree/core/version.rb +1 -1
  494. data/lib/spree/core.rb +91 -373
  495. data/lib/spree/emails/editable_templates.rb +82 -0
  496. data/lib/spree/events/catalog.rb +189 -0
  497. data/lib/spree/events.rb +21 -1
  498. data/lib/spree/hooks.rb +0 -17
  499. data/lib/spree/i18n.rb +32 -15
  500. data/lib/spree/iso_data.rb +7 -0
  501. data/lib/spree/localized_number.rb +6 -0
  502. data/lib/spree/migrations.rb +4 -1
  503. data/lib/spree/money.rb +196 -15
  504. data/lib/spree/reporting/adapters/base.rb +7 -6
  505. data/lib/spree/reporting/adapters/live.rb +6 -6
  506. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  507. data/lib/spree/reporting/hydration.rb +1 -1
  508. data/lib/spree/reporting/query.rb +8 -0
  509. data/lib/spree/reporting/registry.rb +13 -0
  510. data/lib/spree/reporting/schema.rb +10 -10
  511. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  512. data/lib/spree/testing_support/emails.rb +22 -0
  513. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  514. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  515. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  516. data/lib/spree/testing_support/factories/product_factory.rb +9 -0
  517. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  518. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  519. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  520. data/lib/spree/testing_support/i18n.rb +3 -94
  521. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  522. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  523. data/lib/spree/testing_support/preferences.rb +0 -5
  524. data/lib/spree/testing_support/store.rb +9 -1
  525. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +67 -1
  526. data/lib/spree/uploads.rb +101 -0
  527. data/lib/tasks/categories_and_collections.rake +3 -3
  528. data/lib/tasks/core.rake +6 -4
  529. data/lib/tasks/markets.rake +2 -1
  530. data/lib/tasks/media.rake +3 -3
  531. data/lib/tasks/migrate_users_to_customers.rake +23 -18
  532. data/lib/tasks/money.rake +46 -0
  533. data/lib/tasks/package_types.rake +1 -1
  534. data/lib/tasks/preferences_json.rake +101 -0
  535. data/lib/tasks/seller_sample_data.rake +2 -2
  536. data/lib/tasks/store_settings.rake +13 -6
  537. data/lib/tasks/typed_adjustments_migration.rake +1 -1
  538. data/lib/tasks/uploads.rake +19 -0
  539. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  540. metadata +219 -46
  541. data/app/jobs/spree/products/touch_categories_job.rb +0 -15
  542. data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
  543. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  544. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  545. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  546. data/app/models/spree/invitations/store.rb +0 -6
  547. data/app/models/spree/preference.rb +0 -14
  548. data/app/models/spree/promotion_handler/page.rb +0 -25
  549. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  550. data/app/models/spree/stock/differentiator.rb +0 -45
  551. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  552. data/app/services/spree/carts/empty.rb +0 -42
  553. data/app/services/spree/country_to_timezone.rb +0 -273
  554. data/app/services/spree/payments/create.rb +0 -92
  555. data/app/validators/spree/url_validator.rb +0 -23
  556. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  557. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  558. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  559. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  560. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  561. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  562. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  563. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  564. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  565. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  566. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  567. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  568. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  569. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  570. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  571. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  572. data/lib/spree/analytics.rb +0 -11
  573. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  574. data/lib/spree/core/preferences/store.rb +0 -105
  575. data/lib/spree/translation_migrations.rb +0 -52
@@ -13,11 +13,8 @@ module Spree
13
13
  # decodes them on write, so eligibility compares against raw
14
14
  # `channel_id` rows. The scope confines the existence check to the
15
15
  # method's store, so cross-store channel IDs can't sneak in.
16
- preference :channel_ids, :array, default: [],
17
- parse_on_set: normalize_id_preference(
18
- klass: Spree::Channel,
19
- scope: ->(rule) { rule.store.channels }
20
- )
16
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
17
+ scope: ->(rule) { rule.store.channels }
21
18
 
22
19
  def channels
23
20
  return [] if preferred_channel_ids.blank?
@@ -4,8 +4,8 @@ module Spree
4
4
  # owner's currency). Empty preferences pass — half-configured rules fail
5
5
  # open per-rule, matching the PromotionRule/PriceRule convention.
6
6
  class ItemTotalRule < Spree::DeliveryMethodRule
7
- preference :minimum_amount, :decimal, default: nil, nullable: true
8
- preference :maximum_amount, :decimal, default: nil, nullable: true
7
+ preference :minimum_amount, :money, default: nil, nullable: true
8
+ preference :maximum_amount, :money, default: nil, nullable: true
9
9
 
10
10
  def eligible?(package)
11
11
  total = package.item_total
@@ -13,6 +13,7 @@ module Spree
13
13
  include Spree::HasListPosition
14
14
 
15
15
  has_prefix_id :fp
16
+ search_by :name
16
17
 
17
18
  acts_as_list scope: :store
18
19
 
@@ -40,9 +40,8 @@ module Spree
40
40
 
41
41
  return price if tax_rate.nil? || tax_amount.zero? || !tax_rate.show_rate_in_label
42
42
 
43
- Spree.t(
44
- tax_rate.included_in_price? ? :including_tax : :excluding_tax,
45
- scope: 'shipping_rates.display_price',
43
+ I18n.t(
44
+ "spree.shipping_rates.display_price.#{tax_rate.included_in_price? ? :including_tax : :excluding_tax}",
46
45
  price: price,
47
46
  tax_amount: display_tax_amount,
48
47
  tax_rate_name: tax_rate.name
@@ -81,9 +80,9 @@ module Spree
81
80
  @tax_amount ||= if tax_rate.nil?
82
81
  BigDecimal(0)
83
82
  elsif tax_rate.included_in_price?
84
- (cost / (1 + tax_rate.amount) * tax_rate.amount).round(2)
83
+ Spree::Money::Rounding.to_currency(cost / (1 + tax_rate.amount) * tax_rate.amount, currency)
85
84
  else
86
- (cost * tax_rate.amount).round(2)
85
+ Spree::Money::Rounding.to_currency(cost * tax_rate.amount, currency)
87
86
  end
88
87
  end
89
88
 
@@ -147,18 +146,18 @@ module Spree
147
146
  def display_delivery_range
148
147
  return unless delivery_range
149
148
 
150
- Spree.t(:display_delivery_range, delivery_range: delivery_range)
149
+ I18n.t('spree.display_delivery_range', delivery_range: delivery_range)
151
150
  end
152
151
 
153
152
  private
154
153
 
155
154
  # What every money display on this rate reads instead of an amount.
156
155
  def quoted_after_review
157
- Spree.t('delivery_rates.quoted_after_review')
156
+ I18n.t('spree.delivery_rates.quoted_after_review')
158
157
  end
159
158
 
160
159
  def discount_amount
161
- fulfillment.discounts.promotion.sum(:amount)
160
+ fulfillment.promotion_discount_total
162
161
  end
163
162
  end
164
163
  end
@@ -13,6 +13,7 @@ module Spree
13
13
  # store integration — they never store credentials themselves.
14
14
  class Base
15
15
  include Spree::IntegrationBackedProvider
16
+ extend Spree::ApiTyped
16
17
 
17
18
  attr_reader :delivery_method
18
19
 
@@ -8,7 +8,6 @@ module Spree
8
8
  # 2026-03-16 "Consolidate metadata") — write-only developer escape hatch.
9
9
  attribute :metadata, default: -> { {} }
10
10
 
11
- belongs_to :store, class_name: 'Spree::Store'
12
11
  # Zones are destination sets owned by a delivery profile; within it they
13
12
  # hang off an origin group, so the same profile can quote different
14
13
  # tables per warehouse. The profile's methods bind to at most one zone.
@@ -34,7 +33,7 @@ module Spree
34
33
  validate :origin_group_must_belong_to_profile,
35
34
  if: -> { delivery_origin_group_id_changed? || delivery_profile_id_changed? }
36
35
 
37
- self.whitelisted_ransackable_attributes = %w[name]
36
+ self.whitelisted_ransackable_attributes = %w[name delivery_profile_id]
38
37
 
39
38
  # @param address [Spree::Address, nil]
40
39
  # @return [Boolean] whether any member of this zone matches the address
@@ -78,21 +78,21 @@ module Spree
78
78
 
79
79
  def postal_definition
80
80
  if postal_code_prefix.present?
81
- errors.add(:postal_code_prefix, :delivery_zone_prefix_and_range_exclusive, message: Spree.t('errors.messages.delivery_zone_prefix_and_range_exclusive')) if postal_range?
81
+ errors.add(:postal_code_prefix, :delivery_zone_prefix_and_range_exclusive, message: I18n.t('spree.errors.messages.delivery_zone_prefix_and_range_exclusive')) if postal_range?
82
82
  return
83
83
  end
84
84
 
85
85
  if postal_code_from.blank? || postal_code_to.blank?
86
- errors.add(:base, :delivery_zone_postal_definition_required, message: Spree.t('errors.messages.delivery_zone_postal_definition_required'))
86
+ errors.add(:base, :delivery_zone_postal_definition_required, message: I18n.t('spree.errors.messages.delivery_zone_postal_definition_required'))
87
87
  return
88
88
  end
89
89
 
90
90
  unless country_code.present? && range_capable_country_codes.include?(country_code)
91
- errors.add(:base, :delivery_zone_range_not_supported, message: Spree.t('errors.messages.delivery_zone_range_not_supported'))
91
+ errors.add(:base, :delivery_zone_range_not_supported, message: I18n.t('spree.errors.messages.delivery_zone_range_not_supported'))
92
92
  return
93
93
  end
94
94
 
95
- errors.add(:postal_code_to, :delivery_zone_range_inverted, message: Spree.t('errors.messages.delivery_zone_range_inverted')) if postal_code_to < postal_code_from
95
+ errors.add(:postal_code_to, :delivery_zone_range_inverted, message: I18n.t('spree.errors.messages.delivery_zone_range_inverted')) if postal_code_to < postal_code_from
96
96
  end
97
97
  end
98
98
  end
@@ -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
@@ -41,6 +42,7 @@ module Spree
41
42
  delegate :currency, to: :order
42
43
 
43
44
  self.whitelisted_ransackable_attributes = %w[number status created_at]
45
+ search_by :number
44
46
  self.whitelisted_ransackable_associations = %w[order reason]
45
47
 
46
48
  # Positive when the replacements cost more than what came back (the
@@ -53,5 +55,18 @@ module Spree
53
55
  def display_price_difference
54
56
  Spree::Money.new(price_difference, currency: currency)
55
57
  end
58
+
59
+ def taxed_lines
60
+ exchange_line_items
61
+ end
62
+
63
+ private
64
+
65
+ # The replacement is a sale of its own, taxed as the order would be today
66
+ # rather than read off the original line, whose category it may not share.
67
+ def estimate_tax(lines, amounts: nil)
68
+ super
69
+ order.tax_provider.estimate_replacement(order, lines, **order.tax_estimate_inputs)
70
+ end
56
71
  end
57
72
  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.