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
@@ -1,11 +1,14 @@
1
1
  module Spree
2
2
  # Shared shape of the typed adjustment rows (TaxLine, Discount, Fee):
3
3
  # dual-FK cart/order owner (exactly one), consolidated +metadata+ JSON
4
- # column, money display and localized amount parsing.
4
+ # column, money display and strict amount parsing.
5
5
  module TypedAdjustmentLine
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
+ include Spree::CanonicalDecimals
10
+
11
+ canonical_decimals :amount
9
12
  attribute :metadata, default: -> { {} }
10
13
 
11
14
  belongs_to :order, class_name: 'Spree::Order', optional: true
@@ -38,10 +41,6 @@ module Spree
38
41
  end
39
42
  end
40
43
 
41
- def amount=(amount)
42
- self[:amount] = Spree::LocalizedNumber.parse(amount)
43
- end
44
-
45
44
  def currency
46
45
  owner&.currency
47
46
  end
@@ -49,7 +48,7 @@ module Spree
49
48
  private
50
49
 
51
50
  def exactly_one_owner
52
- errors.add(:base, :exactly_one_of_cart_or_order, message: Spree.t('errors.messages.exactly_one_of_cart_or_order')) unless [order, cart].compact.one?
51
+ errors.add(:base, :exactly_one_of_cart_or_order, message: I18n.t('spree.errors.messages.exactly_one_of_cart_or_order')) unless [order, cart].compact.one?
53
52
  end
54
53
  end
55
54
  end
@@ -53,7 +53,7 @@ module Spree
53
53
 
54
54
  def reconcile_typed_association(association, rows)
55
55
  collection = public_send(association)
56
- kept_ids = rows.filter_map { |row| save_typed_association_row(collection, row) }
56
+ kept_ids = rows.each_with_index.filter_map { |row, index| save_typed_association_row(collection, row, "/#{association.to_s.split('_').last}/#{index}") }
57
57
  if kept_ids.any? || rows.empty?
58
58
  collection.where.not(id: kept_ids).destroy_all
59
59
  # `where.not(...).destroy_all` deletes the dropped rows from the DB but
@@ -64,7 +64,8 @@ module Spree
64
64
  end
65
65
  end
66
66
 
67
- def save_typed_association_row(collection, row)
67
+ # @param pointer [String] where the row sits in the request, e.g. `/rules/1`
68
+ def save_typed_association_row(collection, row, pointer)
68
69
  record = find_or_build_typed_association_row(collection, row)
69
70
  return nil unless record
70
71
 
@@ -78,12 +79,10 @@ module Spree
78
79
  deferred_ids, scalar_attrs = attrs.partition { |k, _| record.new_record? && k.to_s.end_with?('_ids') }
79
80
  record.assign_attributes(scalar_attrs.to_h) if scalar_attrs.any?
80
81
 
81
- preferences&.each do |key, value|
82
- next unless record.has_preference?(key.to_sym)
83
-
84
- record.set_preference(key.to_sym, decode_preference_value(key, value))
82
+ record.assign_preferences(preferences, pointer: "#{pointer}/preferences") if preferences
83
+ if calculator.present? && record.respond_to?(:assign_calculator_attributes)
84
+ record.assign_calculator_attributes(calculator, pointer: "#{pointer}/calculator")
85
85
  end
86
- record.assign_calculator_attributes(calculator) if calculator.present? && record.respond_to?(:assign_calculator_attributes)
87
86
 
88
87
  # Always save — `record.changed?` doesn't reflect preferences
89
88
  # (serialized hash) or calculator association changes.
@@ -93,17 +92,12 @@ module Spree
93
92
  record.save! if record.changed?
94
93
 
95
94
  record.id
96
- end
97
-
98
- # Decode `*_ids` array preferences (`customer_group_ids`, `user_ids`,
99
- # …) from prefixed strings to raw PKs. Plain-scalar / non-id
100
- # preferences pass through unchanged.
101
- def decode_preference_value(key, value)
102
- return value unless key.to_s.end_with?('_ids') && value.is_a?(Array)
103
-
104
- value.map do |v|
105
- Spree::PrefixedId.prefixed_id?(v) ? Spree::PrefixedId.decode_prefixed_id(v) : v
106
- end
95
+ rescue ActiveRecord::RecordInvalid => e
96
+ # A row saved from the owner's `after_save` fails inside the owner's own
97
+ # save, and a plain `save` swallows the exception — so its errors are
98
+ # copied onto the owner, or the caller learns only that something failed.
99
+ e.record.errors.full_messages.each { |message| errors.add(:base, message) }
100
+ raise
107
101
  end
108
102
 
109
103
  def find_or_build_typed_association_row(collection, row)
@@ -1,6 +1,9 @@
1
1
  module Spree
2
2
  # Restates a VAT-inclusive price for a customer in another country: the
3
3
  # home-country VAT comes out, the destination's goes on.
4
+ #
5
+ # The including class answers +currency+, which the restated amount is
6
+ # rounded to.
4
7
  module VatPriceCalculation
5
8
  # Cheapest test first: a domestic sale needs no restatement whatever the
6
9
  # provider is, so the provider lookup (a constantize and an instantiation) is
@@ -11,7 +14,7 @@ module Spree
11
14
  return amount if amount.nil? || !outside_default_vat_zone?(price_options)
12
15
  return amount unless restatement_available?(price_options)
13
16
 
14
- round_to_two_places(add_foreign_vat_for(amount, price_options))
17
+ Spree::Money::Rounding.to_currency(add_foreign_vat_for(amount, price_options), currency)
15
18
  end
16
19
 
17
20
  private
@@ -66,7 +69,7 @@ module Spree
66
69
  lookup[:country] = country_from(price_options)
67
70
  end
68
71
 
69
- Spree::TaxRate.included_tax_amount_for(lookup).to_f
72
+ Spree::TaxRate.included_tax_amount_for(lookup).to_d
70
73
  end
71
74
 
72
75
  # +current_price_options+ is documented as an override point, so an app may
@@ -92,9 +95,5 @@ module Spree
92
95
 
93
96
  @default_tax_country = Spree::Current.store&.default_country
94
97
  end
95
-
96
- def round_to_two_places(amount)
97
- Spree::Money::Rounding.quantize(amount, 2)
98
- end
99
98
  end
100
99
  end
@@ -7,7 +7,7 @@ module Spree
7
7
  include Spree::HasCustomFields
8
8
  include Spree::Metadata
9
9
 
10
- serialize :preferences, type: Hash, coder: YAML, default: {}
10
+ serialize :preferences, coder: Spree::Metadata::HashSerializer, default: {}
11
11
 
12
12
  NO_ZIPCODE_ISO_CODES ||= [
13
13
  'AO', 'AG', 'AW', 'BS', 'BZ', 'BJ', 'BM', 'BO', 'BW', 'BF', 'BI', 'CM', 'CF', 'KM', 'CG',
@@ -27,6 +27,10 @@ module Spree
27
27
  # those attributes depending of the logic of their applications
28
28
  ADDRESS_FIELDS = %w(first_name last_name company address1 address2 city state postal_code country phone)
29
29
 
30
+ # The columns a delivery quote reads. A change to anything else (a phone
31
+ # number, the recipient's name) leaves the parcel going where it was.
32
+ DESTINATION_FIELDS = %w(address1 address2 city state_code postal_code country_code).freeze
33
+
30
34
  # Beyond identity and ownership, three groups are excluded because they
31
35
  # cannot tell two addresses apart. Latitude and longitude are derived from
32
36
  # the address, so an entry whose geocoding has already run would otherwise
@@ -202,7 +206,7 @@ module Spree
202
206
  scope.first
203
207
  end
204
208
 
205
- self.whitelisted_ransackable_attributes = ADDRESS_FIELDS + %w[country_code state_code]
209
+ self.whitelisted_ransackable_attributes = ADDRESS_FIELDS - %w[country state] + %w[country_code state_code]
206
210
  self.whitelisted_ransackable_associations = %w[owner]
207
211
 
208
212
  def self.required_fields
@@ -379,6 +383,12 @@ module Spree
379
383
  attributes.except(*EXCLUDED_KEYS_FOR_COMPARISON)
380
384
  end
381
385
 
386
+ # @return [Boolean] whether the last save moved this address somewhere
387
+ # else, as opposed to correcting who receives the parcel
388
+ def saved_change_to_destination?
389
+ saved_changes.keys.intersect?(DESTINATION_FIELDS)
390
+ end
391
+
382
392
  # A country alone doesn't make an address — a new one is pre-filled with the
383
393
  # store's default, so both the code and the association are ignored here.
384
394
  def empty?
@@ -1,6 +1,7 @@
1
1
  module Spree
2
2
  module Adjusters
3
- # Largest-remainder apportionment of an integer cent total across weights.
3
+ # Largest-remainder apportionment of an integer total, in the currency's
4
+ # minor units, across weights.
4
5
  # Shared by promotion order-level distribution and manual admin discounts
5
6
  # so both split identically (sum of shares always equals the total).
6
7
  module LargestRemainder
@@ -19,6 +20,18 @@ module Spree
19
20
  end
20
21
  shares
21
22
  end
23
+
24
+ # Splits an amount across weights in the currency's minor units, so the
25
+ # shares are amounts that sum exactly to the one given.
26
+ #
27
+ # @param amount [BigDecimal]
28
+ # @param weights [Array<Numeric>] proportional bases (sum must be > 0)
29
+ # @param currency [String]
30
+ # @return [Array<BigDecimal>]
31
+ def apportion(amount, weights, currency)
32
+ units = Spree::Money::Rounding.to_minor_units(amount, currency)
33
+ largest_remainder_shares(units, weights).map { |share| Spree::Money::Rounding.from_minor_units(share, currency) }
34
+ end
22
35
  end
23
36
  end
24
37
  end
@@ -65,22 +65,8 @@ module Spree
65
65
  end
66
66
  end
67
67
 
68
- # The owner's PERSISTED coupon code keeps its promotion in candidacy
69
- # even before it ever applied — the discount activates on the exact
70
- # recalculation where the cart first qualifies, and deactivates the
71
- # same way (Shopify-parity for cart-level discount codes). In-memory
72
- # assignments deliberately don't participate: unsaved codes belong to
73
- # the explicit PromotionHandler::Coupon path.
74
68
  def coupon_promotions
75
- return [] unless order.class.respond_to?(:column_names) && order.class.column_names.include?('coupon_code')
76
-
77
- code = order.read_attribute(:coupon_code)
78
- return [] if code.blank?
79
-
80
- promotion = order.store.promotions.active.with_coupon_code(code)
81
- return [] if promotion.nil? || promotion.usage_limit_exceeded?(order)
82
-
83
- [promotion]
69
+ Spree::Promotion.held_by_saved_coupon_code(order)
84
70
  end
85
71
 
86
72
  # A coupon promotion that produced rows becomes an applied promotion —
@@ -154,7 +140,7 @@ module Spree
154
140
  end
155
141
  end
156
142
 
157
- # Largest-remainder distribution (in cents) of the order-wide winner over
143
+ # Largest-remainder distribution (in minor units) of the order-wide winner over
158
144
  # each line's remaining discounted base, so shares always sum exactly to
159
145
  # the promotion amount and no line goes below zero.
160
146
  def distribute_over_line_items(chosen)
@@ -163,11 +149,10 @@ module Spree
163
149
  bases_sum = bases.sum
164
150
  return if bases_sum <= 0
165
151
 
166
- total_cents = [chosen[:amount].abs, bases_sum].min * 100
167
- shares = Spree::Adjusters::LargestRemainder.largest_remainder_shares(total_cents.round, bases)
152
+ shares = Spree::Adjusters::LargestRemainder.apportion([chosen[:amount].abs, bases_sum].min, bases, order.currency)
168
153
 
169
154
  line_items.each_with_index do |line_item, index|
170
- amount = -BigDecimal(shares[index]) / 100
155
+ amount = -shares[index]
171
156
  next if amount.zero?
172
157
 
173
158
  persist_discount(line_item, chosen, amount)
@@ -10,7 +10,9 @@ module Spree
10
10
 
11
11
  include Spree::SingleStoreResource
12
12
 
13
- belongs_to :store, class_name: 'Spree::Store'
13
+ self.whitelisted_ransackable_attributes = %w[origin]
14
+ search_by :origin
15
+
14
16
 
15
17
  validates :origin, presence: true
16
18
  validates :origin, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
@@ -44,7 +44,6 @@ module Spree
44
44
  publishable? ? token : @plaintext_token
45
45
  end
46
46
 
47
- belongs_to :store, class_name: 'Spree::Store'
48
47
  # Optional single-channel binding for publishable keys. A bound key
49
48
  # server-assigns the request's channel (see Api::V3::ChannelResolution),
50
49
  # making channel identity unforgeable instead of client-asserted via the
@@ -162,23 +161,23 @@ module Spree
162
161
  return if invalid.empty?
163
162
 
164
163
  errors.add(:scopes, :unknown_scopes, scopes: invalid.join(', '),
165
- message: Spree.t('errors.messages.unknown_scopes', scopes: invalid.join(', ')))
164
+ message: I18n.t('spree.errors.messages.unknown_scopes', scopes: invalid.join(', ')))
166
165
  end
167
166
 
168
167
  def scopes_immutable
169
- errors.add(:scopes, :api_key_scopes_immutable, message: Spree.t(:api_key_scopes_immutable))
168
+ errors.add(:scopes, :api_key_scopes_immutable, message: I18n.t('spree.api_key_scopes_immutable'))
170
169
  end
171
170
 
172
171
  def channel_binding_valid
173
172
  if secret?
174
- errors.add(:channel, :api_key_channel_publishable_only, message: Spree.t(:api_key_channel_publishable_only))
173
+ errors.add(:channel, :api_key_channel_publishable_only, message: I18n.t('spree.api_key_channel_publishable_only'))
175
174
  elsif channel && channel.store_id != store_id
176
- errors.add(:channel, :api_key_channel_must_belong_to_store, message: Spree.t(:api_key_channel_must_belong_to_store))
175
+ errors.add(:channel, :api_key_channel_must_belong_to_store, message: I18n.t('spree.api_key_channel_must_belong_to_store'))
177
176
  end
178
177
  end
179
178
 
180
179
  def channel_immutable
181
- errors.add(:channel, :api_key_channel_immutable, message: Spree.t(:api_key_channel_immutable))
180
+ errors.add(:channel, :api_key_channel_immutable, message: I18n.t('spree.api_key_channel_immutable'))
182
181
  end
183
182
 
184
183
  # Generates the token on creation. For publishable keys, stores the raw token
@@ -129,7 +129,7 @@ module Spree
129
129
  # Password login is not available through an OIDC provider — the browser
130
130
  # redirect is the only entry point.
131
131
  def authenticate
132
- failure(Spree.t('errors.messages.oidc_requires_redirect'))
132
+ failure(I18n.t('spree.errors.messages.oidc_requires_redirect'))
133
133
  end
134
134
 
135
135
  # Completes the login started by +#authorization_url+: exchanges the
@@ -138,13 +138,13 @@ module Spree
138
138
  # @return [Spree::ServiceModule::Result]
139
139
  def callback
140
140
  code = params[:code]
141
- return authentication_failure(Spree.t('errors.messages.oidc_missing_code')) if code.blank?
141
+ return authentication_failure(I18n.t('spree.errors.messages.oidc_missing_code')) if code.blank?
142
142
 
143
143
  claims = verify_id_token(exchange_code_for_tokens(code).fetch('id_token'))
144
144
  resolve_user(claims)
145
145
  rescue StandardError => e
146
146
  Rails.logger.error("[Spree] OIDC callback failed for #{config[:label] || config[:issuer]}: #{e.message}")
147
- authentication_failure(Spree.t('errors.messages.oidc_authentication_failed'))
147
+ authentication_failure(I18n.t('spree.errors.messages.oidc_authentication_failed'))
148
148
  end
149
149
 
150
150
  # True when the last +#callback+ failed because no account here is
@@ -168,7 +168,7 @@ module Spree
168
168
  # every member of a corporate tenant would become store staff.
169
169
  def resolve_user(claims)
170
170
  subject = claims['sub']
171
- return authentication_failure(Spree.t('errors.messages.oidc_authentication_failed')) if subject.blank?
171
+ return authentication_failure(I18n.t('spree.errors.messages.oidc_authentication_failed')) if subject.blank?
172
172
 
173
173
  identity = Spree::UserIdentity.find_by(
174
174
  provider: registry_key,
@@ -191,7 +191,7 @@ module Spree
191
191
 
192
192
  def not_provisioned_failure
193
193
  @account_not_provisioned = true
194
- failure(Spree.t('errors.messages.oidc_account_not_provisioned'))
194
+ failure(I18n.t('spree.errors.messages.oidc_account_not_provisioned'))
195
195
  end
196
196
 
197
197
  # Only an email the provider asserts as verified may claim an existing
@@ -9,6 +9,7 @@ class Spree::Base < ApplicationRecord
9
9
  include Spree::HasNumber
10
10
  include Spree::HasIsoGeography
11
11
  include Spree::TypedAssociations
12
+ include Spree::SignedIdAttachments
12
13
  include Spree::ValidatesStoreUniqueness
13
14
 
14
15
  # Extra writable attributes contributed by extensions, appended to the v3
@@ -23,24 +24,20 @@ class Spree::Base < ApplicationRecord
23
24
  # rather than assigning, so extensions don't clobber each other.
24
25
  class_attribute :additional_permitted_attributes, instance_writer: false, default: [].freeze
25
26
 
27
+ # The Seller API's counterpart, kept separate because sellers write a
28
+ # narrower set than operators: an attribute an extension makes writable for
29
+ # admins stays admin-only unless it is also declared here.
30
+ #
31
+ # Spree::Product.additional_seller_permitted_attributes += [:brand_id]
32
+ class_attribute :additional_seller_permitted_attributes, instance_writer: false, default: [].freeze
33
+
26
34
  # Backfills preferences added to the class after this row was last saved, so
27
- # a reader never sees nil for a newly defined preference. Assigning
28
- # unconditionally would dirty every record on load: `preferences` is a
29
- # YAML-serialized Hash, so the dirty check compares the serialized string and
30
- # the merge reorders keys even when nothing changed — which is enough to make
31
- # `with_lock` refuse the record ("unpersisted changes").
35
+ # a reader never sees nil for a newly defined preference. Only missing keys
36
+ # are assigned: assigning unconditionally would dirty every record on load,
37
+ # which is enough to make `with_lock` refuse the record ("unpersisted
38
+ # changes").
32
39
  after_initialize do
33
- if has_attribute?(:preferences) && !preferences.nil?
34
- missing = default_preferences.except(*preferences.keys)
35
- self.preferences = preferences.merge(missing) if missing.any?
36
- end
37
- end
38
-
39
- # only for backwards compatibility with Kaminari
40
- if defined?(Kaminari) && Kaminari.config.page_method_name != :page
41
- def self.page(num)
42
- send Kaminari.config.page_method_name, num
43
- end
40
+ backfill_default_preferences if has_attribute?(:preferences) && !preferences.nil?
44
41
  end
45
42
 
46
43
  self.abstract_class = true
@@ -72,6 +69,18 @@ class Spree::Base < ApplicationRecord
72
69
  true
73
70
  end
74
71
 
72
+ # Whether to answer from an association's rows rather than a query of its
73
+ # own (a SUM, an EXISTS). True when the rows are loaded already, or when
74
+ # this record came from a list loaded with +preload_associations_lazily+:
75
+ # loading them then fetches the rows for the whole list in one query, where
76
+ # an aggregate would run once per record.
77
+ #
78
+ # @param name [Symbol] the association
79
+ # @return [Boolean]
80
+ def read_association_in_memory?(name)
81
+ association(name).loaded? || lazy_preload_context.present?
82
+ end
83
+
75
84
  # @see Spree.mysql? — the single definition; this reads it so a model can
76
85
  # branch without reaching for the connection itself.
77
86
  def mysql_adapter?
@@ -115,9 +124,10 @@ class Spree::Base < ApplicationRecord
115
124
  # read through the parent (a plain `Spree::Export` row, a factory that sets
116
125
  # `type` as an attribute, a query on the base relation).
117
126
  #
118
- # Resolves against `available_types` where the class defines it, so an
119
- # unrecognized column value passes through untouched rather than being
120
- # constantized.
127
+ # Resolves against the class's type registry where it has one, so an
128
+ # unrecognized column value (an extension no longer installed) is never
129
+ # constantized — it is shortened the same way instead, never sent as a class
130
+ # name.
121
131
  #
122
132
  # @param type [String, nil] value of the STI `type` column
123
133
  # @return [String]
@@ -125,9 +135,49 @@ class Spree::Base < ApplicationRecord
125
135
  return api_type if type.blank?
126
136
 
127
137
  type = type.to_s
128
- return type unless respond_to?(:available_types)
138
+ api_type_registry.find { |klass| klass.to_s == type }&.api_type || type.demodulize.underscore
139
+ end
140
+
141
+ # Inverse of {.api_type_for}: the STI `type` column value for a wire
142
+ # shorthand, or nil when no registered subclass answers to it. Lookup is
143
+ # registry-driven, so a request can never name a class outside the family.
144
+ #
145
+ # @param api_type [String, nil] e.g. `"orders"`
146
+ # @return [String, nil] e.g. `"Spree::Exports::Orders"`
147
+ def self.class_name_for_api_type(api_type)
148
+ return nil if api_type.blank?
149
+
150
+ api_type_registry.find { |klass| klass.api_type == api_type.to_s }&.to_s
151
+ end
129
152
 
130
- available_types.find { |klass| klass.to_s == type }&.api_type || type
153
+ # The subclasses a typed family accepts — `registered_subclasses` for the
154
+ # preference-schema families, `available_types` for exports and imports.
155
+ #
156
+ # @return [Array<Class>]
157
+ def self.api_type_registry
158
+ if subclass_registry
159
+ registered_subclasses
160
+ elsif respond_to?(:available_types)
161
+ Array(available_types)
162
+ else
163
+ []
164
+ end
165
+ end
166
+
167
+ # How a filter on one of this model's columns turns the short name a client
168
+ # sends (`purchase_order`) into the class name the column stores, or nil
169
+ # when the column does not store class names. Covers the STI column of a
170
+ # typed family and polymorphic `*_type` columns; a model with another such
171
+ # column overrides this.
172
+ #
173
+ # @param attribute [String]
174
+ # @return [Proc, nil] short name → stored class name
175
+ def self.api_type_resolver(attribute)
176
+ if attribute == inheritance_column && api_type_registry.any?
177
+ ->(api_type) { class_name_for_api_type(api_type) }
178
+ elsif reflect_on_all_associations(:belongs_to).any? { |reflection| reflection.polymorphic? && reflection.foreign_type == attribute }
179
+ ->(api_type) { Spree::Base.polymorphic_type_for(api_type) }
180
+ end
131
181
  end
132
182
 
133
183
  # Shorthand for a *polymorphic* `*_type` column (`owner_type`,
@@ -138,10 +188,37 @@ class Spree::Base < ApplicationRecord
138
188
  #
139
189
  # @param type [String, Class, nil] value of the polymorphic type column
140
190
  # @return [String, nil]
191
+ #
192
+ # The configured customer and admin user classes always answer `customer`
193
+ # and `admin_user`, so a host app's own `User` class does not change the
194
+ # contract.
141
195
  def self.polymorphic_api_type(type)
142
196
  return nil if type.blank?
143
197
 
144
- type.to_s.demodulize.underscore
198
+ type = type.to_s
199
+ return 'customer' if type == Spree.customer_class(constantize: false)
200
+ return 'admin_user' if type == Spree.admin_user_class(constantize: false)
201
+
202
+ type.demodulize.underscore
203
+ end
204
+
205
+ # Inverse of {.polymorphic_api_type}: the class name a polymorphic `*_type`
206
+ # column stores for a wire shorthand. Matches against `candidates` when the
207
+ # caller knows what the column may reference (always prefer that); otherwise
208
+ # against the configured customer and admin user classes, then the
209
+ # `Spree::` model of that name. Never constantizes the input.
210
+ #
211
+ # @param api_type [String, nil] e.g. `"purchase_order"`
212
+ # @param candidates [Array<Class, String>, nil] classes the column may hold
213
+ # @return [String, nil] e.g. `"Spree::PurchaseOrder"`
214
+ def self.polymorphic_type_for(api_type, candidates = nil)
215
+ token = api_type.to_s
216
+ return nil unless token.match?(/\A[a-z][a-z0-9_]*\z/)
217
+
218
+ candidates ||= [Spree.customer_class(constantize: false),
219
+ Spree.admin_user_class(constantize: false),
220
+ "Spree::#{token.camelize}"]
221
+ candidates.map(&:to_s).find { |name| polymorphic_api_type(name) == token }
145
222
  end
146
223
 
147
224
  # Prefixed id for a polymorphic reference, encoded from the columns without
@@ -1,15 +1,13 @@
1
- require_dependency 'spree/calculator'
2
-
3
1
  module Spree
4
2
  class Calculator::FlatPercentItemTotal < Calculator
5
3
  preference :flat_percent, :decimal, default: 0
6
4
 
7
5
  def self.description
8
- Spree.t(:flat_percent)
6
+ I18n.t('spree.flat_percent')
9
7
  end
10
8
 
11
9
  def compute(object)
12
- computed_amount = (object.amount * preferred_flat_percent / 100).round(2)
10
+ computed_amount = Spree::Money::Rounding.to_currency(object.amount * preferred_flat_percent / 100, object.try(:currency))
13
11
 
14
12
  # We don't want to cause the promotion adjustments to push the order into a negative total.
15
13
  if computed_amount > object.amount
@@ -1,13 +1,11 @@
1
- require_dependency 'spree/calculator'
2
-
3
1
  module Spree
4
2
  class Calculator::FlatRate < Calculator
5
- preference :amount, :decimal, default: 0
6
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
3
+ preference :amount, :money, default: 0
4
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
7
5
  preference :apply_only_on_full_priced_items, :boolean, default: false
8
6
 
9
7
  def self.description
10
- Spree.t(:flat_rate_per_order)
8
+ I18n.t('spree.flat_rate_per_order')
11
9
  end
12
10
 
13
11
  def compute(object = nil)
@@ -1,15 +1,13 @@
1
- require_dependency 'spree/calculator'
2
-
3
1
  module Spree
4
2
  class Calculator::FlexiRate < Calculator
5
- preference :first_item, :decimal, default: 0.0
6
- preference :additional_item, :decimal, default: 0.0
3
+ preference :first_item, :money, default: 0.0
4
+ preference :additional_item, :money, default: 0.0
7
5
  preference :max_items, :integer, default: 0
8
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
6
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
9
7
  preference :apply_only_on_full_priced_items, :boolean, default: false
10
8
 
11
9
  def self.description
12
- Spree.t(:flexible_rate)
10
+ I18n.t('spree.flexible_rate')
13
11
  end
14
12
 
15
13
  def self.available?(_object)
@@ -5,13 +5,13 @@ module Spree
5
5
  preference :apply_only_on_full_priced_items, :boolean, default: false
6
6
 
7
7
  def self.description
8
- Spree.t(:percent_per_item)
8
+ I18n.t('spree.percent_per_item')
9
9
  end
10
10
 
11
11
  def compute(object)
12
12
  return 0 if preferred_apply_only_on_full_priced_items && object.variant.compare_at_amount_in(object.currency).present?
13
13
 
14
- computed_amount = (object.amount * preferred_percent / 100).round(2)
14
+ computed_amount = Spree::Money::Rounding.to_currency(object.amount * preferred_percent / 100, object.currency)
15
15
 
16
16
  # We don't want to cause the promotion adjustments to push the order into a negative total.
17
17
  if computed_amount > object.amount
@@ -1,14 +1,12 @@
1
- require_dependency 'spree/calculator'
2
-
3
1
  module Spree
4
2
  class Calculator::PriceSack < Calculator
5
- preference :minimal_amount, :decimal, default: 0
6
- preference :normal_amount, :decimal, default: 0
7
- preference :discount_amount, :decimal, default: 0
8
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
3
+ preference :minimal_amount, :money, default: 0
4
+ preference :normal_amount, :money, default: 0
5
+ preference :discount_amount, :money, default: 0
6
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
9
7
 
10
8
  def self.description
11
- Spree.t(:price_sack)
9
+ I18n.t('spree.price_sack')
12
10
  end
13
11
 
14
12
  # as object we always get line items, as calculable we have Coupon, ShippingMethod
@@ -1,15 +1,13 @@
1
- require_dependency 'spree/shipping_calculator'
2
-
3
1
  module Spree
4
2
  module Calculator::Shipping
5
3
  class DigitalDelivery < ShippingCalculator
6
4
  include Spree::Calculator::CurrencyAmounts
7
5
 
8
- preference :amount, :decimal, default: 0
9
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
6
+ preference :amount, :money, default: 0
7
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
10
8
 
11
9
  def self.description
12
- Spree.t('digital.digital_delivery')
10
+ I18n.t('spree.digital.digital_delivery')
13
11
  end
14
12
 
15
13
  def compute_package(package = nil)
@@ -1,21 +1,21 @@
1
- require_dependency 'spree/shipping_calculator'
2
-
3
1
  module Spree
4
2
  module Calculator::Shipping
5
3
  class FlatPercentItemTotal < ShippingCalculator
6
4
  preference :flat_percent, :decimal, default: 0
7
5
 
8
6
  def self.description
9
- Spree.t(:flat_percent)
7
+ I18n.t('spree.flat_percent')
10
8
  end
11
9
 
12
10
  def compute_package(package)
13
- compute_from_price(total(package.contents))
11
+ compute_from_price(total(package.contents), package.currency)
14
12
  end
15
13
 
16
- def compute_from_price(price)
17
- value = price * BigDecimal(preferred_flat_percent.to_s) / 100.0
18
- (value * 100).round.to_f / 100
14
+ # @param price [BigDecimal]
15
+ # @param currency [String, nil] the amount is rounded to its minor unit
16
+ # @return [BigDecimal]
17
+ def compute_from_price(price, currency = nil)
18
+ Spree::Money::Rounding.to_currency(price * BigDecimal(preferred_flat_percent.to_s) / 100, currency)
19
19
  end
20
20
  end
21
21
  end