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
@@ -30,40 +30,43 @@ module Spree
30
30
  sweep_internal_duty_tax_lines(owner) if full_estimate
31
31
  return if items.empty?
32
32
 
33
- rates = Spree::TaxRate.for_store(owner.store).for_jurisdiction(owner.tax_country&.iso, owner.tax_address&.state_code).to_a
33
+ rates = rates_for(owner)
34
34
 
35
35
  items.group_by(&:class).each do |klass, group|
36
36
  # Replace-all set semantics per estimate: stale lines die with the
37
37
  # address/items change that triggered the re-estimate.
38
- Spree::TaxLine.where(tax_line_foreign_key(klass) => group.map(&:id)).delete_all
38
+ Spree::TaxLine.where(Spree::TaxLine.adjustable_key_for(klass) => group.map(&:id)).delete_all
39
39
  end
40
40
 
41
+ items.each { |item| tax_item(owner, item, rates, exemptions) }
42
+ end
43
+
44
+ # Gives back each credited unit's share of the TaxLines the sale actually
45
+ # charged (see Spree::TaxProvider::RecordedShare). TaxRate rows are not
46
+ # versioned, so this engine can never recompute at a past date —
47
+ # +tax_date+ is accepted and ignored, and the rows are the only record
48
+ # of what the customer paid.
49
+ def estimate_refund(order, items, amounts: nil, tax_date: nil)
50
+ Spree::TaxProvider::RecordedShare.new(order: order, items: items, amounts: amounts).call
51
+ end
52
+
53
+ # Taxed like any line of the order, under the replacement's own category
54
+ # and at the order's tax address. +tax_date+ is ignored for the reason
55
+ # given on #estimate.
56
+ #
57
+ # An exemption is read as it applied to the line being replaced: a
58
+ # certificate's per-line carve-out names that line, and the exchange line
59
+ # replacing it would otherwise match no carve-out and come out exempt.
60
+ def estimate_replacement(order, items, tax_date: nil, tax_identifier: nil, exemptions: [])
61
+ return if items.empty?
62
+
63
+ Spree::TaxLine.charges.where(exchange_line_item_id: items.map(&:id)).delete_all
64
+
65
+ rates = rates_for(order)
41
66
  items.each do |item|
42
- relevant_rates = rates.select { |rate| rate.tax_category_id == tax_category_id_for(item, owner) }
43
-
44
- # A matched rate always produces a row, zero-amount ones included:
45
- # a zero rate is a treatment ("this is zero-rated"), which reporting
46
- # and e-invoicing both need to see. No matched rate means no opinion,
47
- # and writes nothing.
48
- taxed = relevant_rates.map do |rate|
49
- jurisdiction = jurisdiction_for(rate, owner)
50
- [rate, jurisdiction, exemption_for(item, exemptions, jurisdiction)]
51
- end
67
+ next unless item.credited_quantity.to_i.positive?
52
68
 
53
- # An exempt item has no tax to back out of its price, so its whole
54
- # basis is pre-tax.
55
- exempt_rates = taxed.select { |_rate, _jurisdiction, exemption| exemption }.map(&:first)
56
- store_pre_tax_amount(item, relevant_rates - exempt_rates)
57
-
58
- taxed.each do |rate, jurisdiction, exemption|
59
- if exemption
60
- write_tax_line(owner, item, rate, 0, 'customer_exempt', jurisdiction,
61
- data: exemption_data(exemption, item))
62
- else
63
- write_tax_line(owner, item, rate, compute_tax(rate, item, relevant_rates),
64
- reason_for(rate), jurisdiction)
65
- end
66
- end
69
+ tax_item(order, item, rates, exemptions, exemption_subject: item.line_item || item)
67
70
  end
68
71
  end
69
72
 
@@ -107,12 +110,35 @@ module Spree
107
110
  owner.tax_lines.where(fee_id: duty_fee_ids, provider_id: 'internal').delete_all
108
111
  end
109
112
 
110
- def tax_line_foreign_key(klass)
111
- case klass.name
112
- when 'Spree::LineItem' then :line_item_id
113
- when 'Spree::Fulfillment' then :fulfillment_id
114
- when 'Spree::Fee' then :fee_id
115
- else raise ArgumentError, "#{klass} is not taxable"
113
+ def rates_for(owner)
114
+ Spree::TaxRate.for_store(owner.store).for_jurisdiction(owner.tax_country&.iso, owner.tax_address&.state_code).to_a
115
+ end
116
+
117
+ def tax_item(owner, item, rates, exemptions, exemption_subject: item)
118
+ relevant_rates = rates.select { |rate| rate.tax_category_id == tax_category_id_for(item, owner) }
119
+
120
+ # A matched rate always produces a row, zero-amount ones included:
121
+ # a zero rate is a treatment ("this is zero-rated"), which reporting
122
+ # and e-invoicing both need to see. No matched rate means no opinion,
123
+ # and writes nothing.
124
+ taxed = relevant_rates.map do |rate|
125
+ jurisdiction = jurisdiction_for(rate, owner)
126
+ [rate, jurisdiction, exemption_for(exemption_subject, exemptions, jurisdiction)]
127
+ end
128
+
129
+ # An exempt item has no tax to back out of its price, so its whole
130
+ # basis is pre-tax.
131
+ exempt_rates = taxed.select { |_rate, _jurisdiction, exemption| exemption }.map(&:first)
132
+ store_pre_tax_amount(item, relevant_rates - exempt_rates)
133
+
134
+ taxed.each do |rate, jurisdiction, exemption|
135
+ if exemption
136
+ write_tax_line(owner, item, rate, 0, 'customer_exempt', jurisdiction,
137
+ data: exemption_data(exemption, exemption_subject))
138
+ else
139
+ write_tax_line(owner, item, rate, compute_tax(rate, item, relevant_rates, owner.currency),
140
+ reason_for(rate), jurisdiction)
141
+ end
116
142
  end
117
143
  end
118
144
 
@@ -143,16 +169,19 @@ module Spree
143
169
  item.respond_to?(:taxable_basis) ? item.taxable_basis : item.amount
144
170
  end
145
171
 
146
- def compute_tax(rate, item, relevant_rates)
172
+ # Rounded per line, half up, to the order's currency — the internal
173
+ # provider's choice; other providers round as their jurisdictions require.
174
+ def compute_tax(rate, item, relevant_rates, currency)
147
175
  basis = taxable_basis(item)
148
176
 
149
- if rate.included_in_price
150
- included_sum = relevant_rates.select(&:included_in_price).sum(&:amount)
151
- included_sum = rate.amount if included_sum.zero?
152
- (basis / (1 + included_sum) * rate.amount).round(2)
153
- else
154
- (basis * rate.amount).round(2)
155
- end
177
+ tax = if rate.included_in_price
178
+ included_sum = relevant_rates.select(&:included_in_price).sum(&:amount)
179
+ included_sum = rate.amount if included_sum.zero?
180
+ basis / (1 + included_sum) * rate.amount
181
+ else
182
+ basis * rate.amount
183
+ end
184
+ Spree::Money::Rounding.to_currency(tax, currency)
156
185
  end
157
186
 
158
187
  # Pre-tax amounts are stored so refund and reporting code can weigh
@@ -209,7 +238,7 @@ module Spree
209
238
  owner_key = owner.is_a?(Spree::Order) ? :order : :cart
210
239
  Spree::TaxLine.create!(
211
240
  {
212
- tax_line_foreign_key(item.class) => item.id,
241
+ Spree::TaxLine.adjustable_key_for(item.class) => item.id,
213
242
  owner_key => owner,
214
243
  tax_rate: rate,
215
244
  amount: amount,
@@ -0,0 +1,114 @@
1
+ module Spree
2
+ module TaxProvider
3
+ # The tax a return, claim or exchange line gives back, read off what the
4
+ # sale actually charged: each credited unit's part of every TaxLine on the
5
+ # original line, row by row.
6
+ #
7
+ # Read from the rows rather than recomputed from TaxRate configuration,
8
+ # because the customer is owed back what they paid. Rates are not
9
+ # versioned, so a rate edited since the sale would otherwise refund a
10
+ # figure nobody was charged.
11
+ #
12
+ # Internal's answer to +estimate_refund+. Public so any other provider can
13
+ # answer with it too, rather than quoting the credit itself.
14
+ class RecordedShare
15
+ # @param order [Spree::Order]
16
+ # @param items [Array<Spree::ReturnLineItem, Spree::ClaimLineItem, Spree::ExchangeLineItem>]
17
+ # @param amounts [Hash{Object => BigDecimal}, nil] see Base#estimate_refund
18
+ def initialize(order:, items:, amounts: nil)
19
+ @order = order
20
+ @items = Array(items)
21
+ @amounts = amounts
22
+ end
23
+
24
+ # Replaces each item's credit rows.
25
+ #
26
+ # @return [void]
27
+ def call
28
+ items.each do |item|
29
+ key = Spree::TaxLine.adjustable_key_for(item.class)
30
+ Spree::TaxLine.credits.where(key => item.id).delete_all
31
+ credit(item, key)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :order, :items, :amounts
38
+
39
+ def credit(item, key)
40
+ share = share_of_line(item)
41
+ return if share.zero?
42
+
43
+ sources = item.line_item.tax_lines.order(:id).to_a
44
+ sources.zip(shares_for(sources, share)).each do |source, cents|
45
+ write(item, key, source, cents)
46
+ end
47
+ end
48
+
49
+ # The part of the original line this item gives back: its credited units,
50
+ # and of those only the share of their worth the customer is refunded.
51
+ def share_of_line(item)
52
+ quantity = item.line_item&.quantity.to_i
53
+ units = item.credited_quantity.to_i
54
+ return 0 if quantity.zero? || units.zero?
55
+
56
+ Rational(units, quantity) * refunded_fraction(item)
57
+ end
58
+
59
+ def refunded_fraction(item)
60
+ return 1 if amounts.nil?
61
+
62
+ worth = item.credited_worth.to_d
63
+ return 0 unless worth.positive?
64
+
65
+ [Rational(amounts.fetch(item, 0).to_d) / Rational(worth), 1].min
66
+ end
67
+
68
+ # The rows' total share rounded once, then divided by largest remainder,
69
+ # so the credit lands on the cent and each rate keeps its part of it. A
70
+ # row is never credited past what earlier credits left of it, or three
71
+ # single-unit returns of one line could give back a cent more than it
72
+ # charged.
73
+ def shares_for(sources, share)
74
+ weights = sources.map { |source| to_minor_units(source.amount) }
75
+ return Array.new(sources.size, 0) if weights.sum.zero?
76
+
77
+ target = (Rational(weights.sum) * share).round(half: :up)
78
+ allotted = Spree::Adjusters::LargestRemainder.largest_remainder_shares(target, weights)
79
+
80
+ sources.zip(allotted).map { |source, cents| [cents, remaining_cents(source)].min }
81
+ end
82
+
83
+ def remaining_cents(source)
84
+ credited = Spree::TaxLine.credits.where(original_tax_line_id: source.id).sum(:amount)
85
+ [to_minor_units(source.amount) - to_minor_units(credited), 0].max
86
+ end
87
+
88
+ # Zero-amount rows are copied too: a zero-rated or exempt sale is a
89
+ # treatment the credit note has to repeat.
90
+ def write(item, key, source, cents)
91
+ Spree::TaxLine.create!(
92
+ key => item.id,
93
+ order: order,
94
+ credit: true,
95
+ original_tax_line: source,
96
+ tax_rate_id: source.tax_rate_id,
97
+ amount: Spree::Money::Rounding.from_minor_units(cents, order.currency),
98
+ rate: source.rate,
99
+ label: source.label,
100
+ included: source.included,
101
+ provider_id: source.provider_id,
102
+ taxability_reason: source.taxability_reason,
103
+ country_code: source.country_code,
104
+ state_code: source.state_code,
105
+ data: source.data
106
+ )
107
+ end
108
+
109
+ def to_minor_units(amount)
110
+ Spree::Money::Rounding.to_minor_units(amount, order.currency)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -15,6 +15,10 @@ module Spree
15
15
  include Spree::HasCustomFields
16
16
  include Spree::Metadata
17
17
  include Spree::SingleStoreResource
18
+ include Spree::CanonicalDecimals
19
+
20
+ # A fraction (0.07125), held to the column's five decimals.
21
+ canonical_decimals :amount, places: 5
18
22
 
19
23
  with_options inverse_of: :tax_rates do
20
24
  belongs_to :tax_category,
@@ -56,17 +60,36 @@ module Spree
56
60
  ->(category) { where(tax_category_id: category.try(:id)) }
57
61
  scope :included_in_price, -> { where(included_in_price: true) }
58
62
 
59
- self.whitelisted_ransackable_attributes = %w[amount country_code state_code tax_category_id included_in_price name]
63
+ self.whitelisted_ransackable_attributes = %w[amount rate country_code state_code tax_category_id included_in_price name]
64
+ ransack_alias :rate, :amount
65
+ search_by :name
60
66
 
61
- # Virtual attribute for percentage display in admin forms
62
- def amount_percentage
63
- return nil if amount.nil?
67
+ # The rate as a fraction (0.23); the column is +amount+.
68
+ alias_attribute :rate, :amount
69
+
70
+ # The rate as a percentage (23), what a merchant types.
71
+ #
72
+ # @return [BigDecimal, nil]
73
+ def rate_percent
74
+ amount && amount * 100
75
+ end
76
+
77
+ # @param value [String, Numeric, nil]
78
+ def rate_percent=(value)
79
+ parsed = Spree::Money::Rounding.parse_decimal(value)
80
+ self.amount = parsed && parsed / 100
81
+ end
64
82
 
65
- (amount * 100).round(2)
83
+ # @deprecated Use {#rate_percent}; removed in 6.1.
84
+ def amount_percentage
85
+ Spree::Deprecation.warn('Spree::TaxRate#amount_percentage is deprecated and will be removed in Spree 6.1. Use #rate_percent instead.')
86
+ rate_percent
66
87
  end
67
88
 
89
+ # @deprecated Use {#rate_percent=}; removed in 6.1.
68
90
  def amount_percentage=(value)
69
- self.amount = value.present? ? (value.to_f / 100) : nil
91
+ Spree::Deprecation.warn('Spree::TaxRate#amount_percentage= is deprecated and will be removed in Spree 6.1. Use #rate_percent= instead.')
92
+ self.rate_percent = value
70
93
  end
71
94
 
72
95
  # The included-in-price rate to back out of a gross price.
@@ -100,8 +123,7 @@ module Spree
100
123
  #
101
124
  # @return [String]
102
125
  def adjustment_label
103
- Spree.t included_in_price? ? :including_tax : :excluding_tax,
104
- scope: 'adjustment_labels.tax_rates',
126
+ I18n.t "spree.adjustment_labels.tax_rates.#{included_in_price? ? :including_tax : :excluding_tax}",
105
127
  name: name.presence || tax_category.name,
106
128
  amount: amount_for_label
107
129
  end
@@ -10,7 +10,7 @@ module Spree
10
10
  # prefix (ctg), and model_name are all Spree::Category. Only the constant differs,
11
11
  # so is_a?, STI, polymorphic *_type strings, and class_name: references keep
12
12
  # resolving to Spree::Category. The warning below fires through Spree's deprecator
13
- # when this file loads (at boot under eager load, on first reference otherwise).
13
+ # when this file loads (on first reference; it is kept out of eager loading).
14
14
  Taxon = Category
15
15
 
16
16
  Spree::Deprecation.warn('Spree::Taxon is deprecated and will be removed in Spree 6.1. Use Spree::Category instead.')
@@ -12,7 +12,6 @@ module Spree
12
12
 
13
13
  has_many :taxons, class_name: 'Spree::Category', inverse_of: :taxonomy
14
14
  has_one :root, -> { where parent_id: nil }, class_name: 'Spree::Category', dependent: :destroy
15
- belongs_to :store, class_name: 'Spree::Store'
16
15
 
17
16
  default_scope { order("#{table_name}.position, #{table_name}.created_at") }
18
17
  end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ # What {Spree::UpdateCheck} last learned about newer releases. Never
3
+ # persisted.
4
+ class UpdateStatus
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+
8
+ attribute :current_version, :string
9
+ attribute :enabled, :boolean, default: true
10
+ # Nil until the first check has finished.
11
+ attribute :checked_at, :datetime
12
+ # Newer releases, newest first: hashes with +version+, +url+ and +published_at+.
13
+ attribute :releases, default: -> { [] }
14
+
15
+ # @return [Hash, nil]
16
+ def latest_release
17
+ releases.first
18
+ end
19
+
20
+ # @return [Boolean]
21
+ def update_available?
22
+ latest_release.present?
23
+ end
24
+ end
25
+ end
@@ -8,6 +8,7 @@ module Spree
8
8
  include Spree::MemoizedData
9
9
  include Spree::HasCustomFields
10
10
  include Spree::Metadata
11
+ include Spree::CanonicalDecimals
11
12
  include Spree::HasExternalReferences
12
13
  include Spree::Searchable
13
14
  include Spree::StorePreferences
@@ -231,6 +232,10 @@ module Spree
231
232
 
232
233
  scope :not_deleted, -> { where("#{Spree::Variant.quoted_table_name}.deleted_at IS NULL") }
233
234
 
235
+ # The variants of a product relation, as a subquery. Unordered, because a
236
+ # listing's sort would otherwise land inside the IN.
237
+ scope :for_products, ->(products) { where(product_id: products.reorder(nil).select(Spree::Product.arel_table[:id])) }
238
+
234
239
  # Variants the shop sells in this currency. Base prices only: a price
235
240
  # list's rows are what one audience pays under an agreement, and a ladder
236
241
  # holds several rows for one variant, so a contract-only variant is not
@@ -285,17 +290,9 @@ module Spree
285
290
  relation.where(conditions.reduce(:or)).distinct
286
291
  end
287
292
 
288
- # Backward compatibility alias — remove in Spree 6.0
289
- scope :multi_search, ->(*args) { search(*args) }
290
-
291
- # FIXME: cost price should be represented with DisplayMoney class
292
- LOCALIZED_NUMBERS = %w(cost_price weight depth width height)
293
-
294
- LOCALIZED_NUMBERS.each do |m|
295
- define_method("#{m}=") do |argument|
296
- self[m] = Spree::LocalizedNumber.parse(argument) if argument.present?
297
- end
298
- end
293
+ # A blank value leaves these as they were rather than clearing them.
294
+ canonical_decimals :cost_price, places: Spree::Money::Rounding::UNIT_PRICE_DECIMALS, ignore_blank: true
295
+ canonical_decimals :weight, :depth, :width, :height, places: nil, ignore_blank: true
299
296
 
300
297
  accepts_nested_attributes_for(
301
298
  :stock_levels,
@@ -330,6 +327,7 @@ module Spree
330
327
  carton_package_type_id carton_weight cartons_per_pallet]
331
328
  self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku search
332
329
  available_at_stock_location)
330
+ self.ransackable_scope_types = { 'available_at_stock_location' => 'id' }
333
331
  self.private_ransackable_attributes = { store: %w[cost_price cost_currency deleted_at] }
334
332
 
335
333
  def self.product_name_or_sku_cont(query)
@@ -665,7 +663,7 @@ module Spree
665
663
  def price_in(currency)
666
664
  currency = currency&.upcase
667
665
 
668
- price = if prices.loaded? && prices.any?
666
+ price = if read_association_in_memory?(:prices) && prices.any?
669
667
  prices.detect { |p| p.currency == currency && p.price_list_id.nil? }
670
668
  else
671
669
  prices.base_prices.find_by(currency: currency)
@@ -35,7 +35,7 @@ module Spree
35
35
  return if asset.product&.id == variant.product_id
36
36
 
37
37
  errors.add(:asset, :variant_product_mismatch,
38
- message: Spree.t('errors.messages.variant_product_mismatch'))
38
+ message: I18n.t('spree.errors.messages.variant_product_mismatch'))
39
39
  end
40
40
 
41
41
  def refresh_variant_thumbnail
@@ -25,6 +25,7 @@ module Spree
25
25
 
26
26
  # Ransack configuration
27
27
  self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at created_at]
28
+ search_by :event_name
28
29
 
29
30
  # Event subjects whose permission is not found by class name alone.
30
31
  PAYLOAD_SUBJECT_CLASSES = {
@@ -13,7 +13,6 @@ module Spree
13
13
 
14
14
  encrypts :secret_key, deterministic: true if Rails.configuration.active_record.encryption.include?(:primary_key)
15
15
 
16
- belongs_to :store, class_name: 'Spree::Store'
17
16
  has_many :webhook_deliveries, class_name: 'Spree::WebhookDelivery', dependent: :destroy_async
18
17
 
19
18
  validates :url, presence: true
@@ -31,11 +30,45 @@ module Spree
31
30
  before_save :clear_disabled_state_when_reactivated
32
31
 
33
32
  self.whitelisted_ransackable_attributes = %w[name url active]
33
+ search_by :url
34
34
 
35
35
  scope :active, -> { where(active: true) }
36
36
  scope :inactive, -> { where(active: false) }
37
37
  scope :enabled, -> { active.where(disabled_at: nil) }
38
38
 
39
+ # Whether +url+ targets a host the operator trusts despite it resolving to
40
+ # a private address — e.g. a storefront Service in the same Kubernetes
41
+ # cluster. Set via +Spree::Api::Config.webhooks_allowed_internal_hosts+
42
+ # (SPREE_WEBHOOKS_ALLOWED_INTERNAL_HOSTS): an entry matches that exact
43
+ # host, or with a leading dot (".svc.cluster.local") any host under it.
44
+ # Matching is case-insensitive. Operator config only, so a store admin
45
+ # can never point a webhook at an internal address.
46
+ #
47
+ # @param url [String]
48
+ # @return [Boolean]
49
+ def self.allowed_internal_host?(url)
50
+ host = URI.parse(url.to_s).hostname&.downcase
51
+ return false if host.blank?
52
+
53
+ allowed_internal_hosts.any? do |entry|
54
+ entry.start_with?('.') ? host.end_with?(entry) : host == entry
55
+ end
56
+ rescue URI::InvalidURIError
57
+ false
58
+ end
59
+
60
+ # Spree::Api::Config only exists when spree_api is loaded; without it no
61
+ # webhook is ever delivered, so there is nothing to allow.
62
+ def self.allowed_internal_hosts
63
+ return [] unless defined?(Spree::Api::Config)
64
+
65
+ Array(Spree::Api::Config.webhooks_allowed_internal_hosts).filter_map do |entry|
66
+ entry = entry.to_s.strip.downcase
67
+ entry if entry.present? && entry != '.'
68
+ end
69
+ end
70
+ private_class_method :allowed_internal_hosts
71
+
39
72
  # Returns the plaintext `secret_key` only on the create response.
40
73
  #
41
74
  # `@reveal_secret_in_response` is set by the `after_create` callback above
@@ -50,18 +83,16 @@ module Spree
50
83
  # Number of consecutive failed deliveries before auto-disabling
51
84
  AUTO_DISABLE_THRESHOLD = 15
52
85
 
53
- # Events whose payload carries a live customer credential (a password
54
- # reset token). Receiving one is as good as holding the customer's account,
55
- # so they reach only an endpoint that names them — never through `*` or a
56
- # pattern — and the API asks for customer write access to point one at them.
57
- CREDENTIAL_EVENTS = %w[customer.password_reset_requested].freeze
58
-
59
- # Check if this endpoint is subscribed to a specific event
86
+ # Check if this endpoint is subscribed to a specific event.
87
+ #
88
+ # Events whose payload carries a live credential (a password reset token)
89
+ # reach only an endpoint that names them — never through `*` or a pattern —
90
+ # since receiving one is as good as holding the account.
60
91
  #
61
92
  # @param event_name [String] the event name to check
62
93
  # @return [Boolean]
63
94
  def subscribed_to?(event_name)
64
- return subscriptions.to_a.include?(event_name) if CREDENTIAL_EVENTS.include?(event_name)
95
+ return subscriptions.to_a.include?(event_name) if Spree::Events.catalog.credential?(event_name)
65
96
  return true if subscriptions.blank? || subscriptions.include?('*')
66
97
 
67
98
  subscriptions.any? do |subscription|
@@ -74,11 +105,11 @@ module Spree
74
105
  end
75
106
  end
76
107
 
77
- # Whether this endpoint receives customer credentials.
108
+ # The credential-carrying events this endpoint names.
78
109
  #
79
- # @return [Boolean]
80
- def receives_credentials?
81
- (subscriptions.to_a & CREDENTIAL_EVENTS).any?
110
+ # @return [Array<Spree::Events::Catalog::Entry>]
111
+ def credential_subscriptions
112
+ Spree::Events.catalog.credential_events(subscriptions.to_a)
82
113
  end
83
114
 
84
115
  # Returns all events this endpoint is subscribed to
@@ -168,6 +199,8 @@ module Spree
168
199
  end
169
200
 
170
201
  def url_must_not_resolve_to_private_ip
202
+ return if self.class.allowed_internal_host?(url)
203
+
171
204
  uri = URI.parse(url)
172
205
  blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
173
206
  addresses = Resolv.getaddresses(uri.host)
@@ -6,15 +6,10 @@ module Spree
6
6
 
7
7
  publishes_lifecycle_events
8
8
 
9
- if Rails::VERSION::STRING >= '7.1.0'
10
- has_secure_token on: :save
11
- else
12
- has_secure_token
13
- end
9
+ has_secure_token on: :save
14
10
 
15
11
  belongs_to :customer, class_name: "::#{Spree.customer_class}", touch: true
16
12
  include Spree::DeprecatedCustomerAlias
17
- belongs_to :store, class_name: 'Spree::Store'
18
13
 
19
14
  has_many :wishlist_items, class_name: 'Spree::WishlistItem', dependent: :destroy
20
15
  has_many :wished_items, class_name: 'Spree::WishlistItem', inverse_of: :wishlist, deprecated: true
@@ -11,6 +11,12 @@ module Spree
11
11
  LEGACY_EVENT_PREFIX = 'wished_item'
12
12
  LEGACY_EVENT_SUFFIXES = %w[created updated deleted].freeze
13
13
 
14
+ def self.included(base)
15
+ LEGACY_EVENT_SUFFIXES.each do |suffix|
16
+ base.publishes_event("#{LEGACY_EVENT_PREFIX}.#{suffix}", deprecated_alias_of: "#{base.event_prefix}.#{suffix}")
17
+ end
18
+ end
19
+
14
20
  # Wraps publishing rather than the three callbacks that call it, so the
15
21
  # decisions Publishable makes — whether events are enabled at all,
16
22
  # whether a touch-only update is worth announcing — are made once and the
@@ -35,7 +35,7 @@ module Spree
35
35
  customer.first_name,
36
36
  customer.last_name,
37
37
  customer.email,
38
- customer.accepts_email_marketing ? Spree.t(:say_yes) : Spree.t(:say_no),
38
+ customer.accepts_email_marketing ? I18n.t('spree.say_yes') : I18n.t('spree.say_no'),
39
39
  customer.address&.company,
40
40
  customer.address&.address1,
41
41
  customer.address&.address2,
@@ -25,7 +25,7 @@ module Spree
25
25
  newsletter_subscriber.email,
26
26
  newsletter_subscriber.customer&.full_name,
27
27
  newsletter_subscriber.customer_id,
28
- newsletter_subscriber.verified? ? Spree.t(:say_yes) : Spree.t(:say_no),
28
+ newsletter_subscriber.verified? ? I18n.t('spree.say_yes') : I18n.t('spree.say_no'),
29
29
  newsletter_subscriber.verified_at,
30
30
  newsletter_subscriber.created_at,
31
31
  newsletter_subscriber.updated_at
@@ -96,16 +96,16 @@ module Spree
96
96
  index.zero? ? order.email : nil,
97
97
  index.zero? ? order.status : nil,
98
98
  index.zero? ? order.currency : nil,
99
- index.zero? ? order.item_total.to_f : nil,
100
- index.zero? ? order.shipment_total.to_f : nil,
101
- index.zero? ? order.tax_total.to_f : nil,
99
+ index.zero? ? money(order.item_total) : nil,
100
+ index.zero? ? money(order.delivery_total) : nil,
101
+ index.zero? ? money(order.tax_total) : nil,
102
102
  index.zero? ? order.included_tax_total.positive? : nil,
103
103
  index.zero? ? (order.discount_total.negative? || line_item.discount_total.negative?) : nil,
104
104
  index.zero? ? order.has_free_shipping? : nil,
105
- index.zero? ? order.discount_total.abs : nil,
105
+ index.zero? ? money(order.discount_total.abs) : nil,
106
106
  index.zero? ? order.promo_code : nil,
107
- index.zero? ? order.payments.store_credits.sum(:amount).abs : nil,
108
- index.zero? ? order.total.to_f : nil,
107
+ index.zero? ? money(order.payments.store_credits.sum(:amount).abs) : nil,
108
+ index.zero? ? money(order.total) : nil,
109
109
  index.zero? ? order.delivery_method&.name : nil,
110
110
  index.zero? ? order.total_weight.to_f : nil,
111
111
  index.zero? ? order.payments.valid&.first&.display_source_name : nil,
@@ -160,6 +160,10 @@ module Spree
160
160
 
161
161
  private
162
162
 
163
+ def money(amount)
164
+ Spree::Money::Rounding.format(amount, order.currency)
165
+ end
166
+
163
167
  # Drops the cells whose headers were omitted, matching each value to its
164
168
  # header by position so the row and the header list stay aligned.
165
169
  def reject_omitted(csv)
@@ -26,15 +26,10 @@ module Spree
26
26
 
27
27
  private
28
28
 
29
- # The currency's own precision, a dot for the decimal mark and nothing
30
- # else — "18.00", never "$18.00", "18.0" or "18,00". Fixed rather than
31
- # localized because the file is read back by the import under the
32
- # store's locale, not the exporting admin's, and a comma written here
33
- # would re-import as a hundred times the price.
29
+ # A plain decimal ("18.00", never "$18.00" or "18,00"): the import reads
30
+ # the file back and accepts nothing else.
34
31
  def amount_string(amount)
35
- return nil if amount.nil?
36
-
37
- format("%.#{Spree::Money::Rounding.precision(price.currency)}f", amount)
32
+ Spree::Money::Rounding.format(amount, price.currency, unit_price: true)
38
33
  end
39
34
  end
40
35
  end