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
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ module PostSale
3
+ # The tax side of a line on a return, claim or exchange: the rows the tax
4
+ # provider writes on it (see Spree::TaxProvider::Base#estimate_refund).
5
+ #
6
+ # Including models answer +credited_quantity+ and +credited_worth+, which
7
+ # the provider reads, and +fold_tax_lines!+, which copies the rows written
8
+ # at opening onto the line's own columns.
9
+ module TaxedLine
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ has_many :tax_lines, class_name: 'Spree::TaxLine', dependent: :destroy
14
+ end
15
+
16
+ # Whether this line's tax goes through the provider when it is refunded.
17
+ # A line opened before refunds carried tax recorded none against a taxed
18
+ # sale line, and is refunded exactly as it was opened.
19
+ #
20
+ # Read from the columns, which are written once at opening, rather than
21
+ # the rows: settling rewrites or deletes the rows, and a settle whose
22
+ # money step then failed would otherwise leave the line looking like one
23
+ # that never carried tax.
24
+ #
25
+ # @return [Boolean]
26
+ def settles_tax?
27
+ recorded_tax_total.positive? || line_item.nil? || !line_item.tax_lines.exists?
28
+ end
29
+
30
+ # The tax this line gives back, as its credit rows stand now.
31
+ #
32
+ # @return [BigDecimal]
33
+ def credited_tax_total
34
+ tax_lines.credits.sum(:amount)
35
+ end
36
+
37
+ private
38
+
39
+ # The tax copied onto the line's columns when it opened.
40
+ def recorded_tax_total
41
+ tax_total
42
+ end
43
+
44
+ # @return [Array(BigDecimal, BigDecimal)] included and additional tax
45
+ def tax_totals_from(rows)
46
+ included, additional = rows.partition(&:included?)
47
+ [included.sum(0.to_d, &:amount), additional.sum(0.to_d, &:amount)]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,139 @@
1
+ module Spree
2
+ module PreferenceSchema
3
+ # Compiles a class's preference declarations into the JSON Schema of its
4
+ # `preferences` object — the contract the OpenAPI components, the SDK
5
+ # types, the `/…/types` endpoints and write validation all read, so none
6
+ # of them is written by hand.
7
+ module JsonSchema
8
+ DECIMAL_PATTERN = '^-?\d+(\.\d+)?$'.freeze
9
+
10
+ # Domain formats, with the pattern that makes each one checkable by any
11
+ # validator, not only those that know the format's name.
12
+ FORMATS = {
13
+ currency: { format: 'currency', pattern: '^[A-Z]{3}$' },
14
+ iso_country: { format: 'iso-country', pattern: '^[A-Z]{2}$' },
15
+ timezone: { format: 'timezone' },
16
+ locale: { format: 'locale' },
17
+ url: { format: 'uri' },
18
+ color: { format: 'color', pattern: '^#[0-9A-Fa-f]{6}$' }
19
+ }.freeze
20
+
21
+ # @param klass [Class] a class including {Spree::PreferenceSchema}
22
+ # @return [Hash] a JSON Schema object (string keys)
23
+ def self.for(klass)
24
+ fields = klass.preference_schema
25
+ definitions = klass.preference_definitions
26
+ properties = fields.to_h do |field|
27
+ [field[:key].to_s, property_schema(definitions.fetch(field[:key]), default: field[:default])]
28
+ end
29
+
30
+ { 'type' => 'object', 'properties' => properties, 'additionalProperties' => false }
31
+ end
32
+
33
+ # The published schema plus the deprecated preferences, which a write
34
+ # still accepts until their removal although no read returns them.
35
+ #
36
+ # @param klass [Class]
37
+ # @return [Hash]
38
+ def self.writable(klass)
39
+ deprecated = klass.preference_definitions.filter_map do |name, definition|
40
+ [name.to_s, property_schema(definition)] if definition[:deprecated] && !definition[:internal]
41
+ end
42
+ schema = klass.preference_json_schema
43
+ schema.merge('properties' => schema['properties'].merge(deprecated.to_h))
44
+ end
45
+
46
+ # The schema of one declared preference.
47
+ #
48
+ # @param definition [Hash] an entry of `preference_definitions`
49
+ # @param default [Object] the declared default, already evaluated
50
+ # @return [Hash]
51
+ def self.property_schema(definition, default: declared_default(definition))
52
+ schema = value_schema(definition)
53
+ choices = definition[:choices]
54
+ choices = choices.call if choices.respond_to?(:call)
55
+ schema['enum'] = choices.map { |choice| choice.is_a?(Symbol) ? choice.to_s : choice } if choices.present?
56
+ default = definition[:dynamic_default] ? nil : wire_default(default, definition)
57
+ unset_reads_null = default.nil? && !definition[:dynamic_default]
58
+
59
+ # A value nothing has set reads back as null, so a preference without
60
+ # a default is nullable on the wire whatever its declared type — and so
61
+ # is a secret, whose default is never sent.
62
+ if (definition[:nullable] || unset_reads_null || definition[:type] == :password) && schema['type']
63
+ schema['type'] = [schema['type'], 'null']
64
+ schema['enum'] += [nil] if schema['enum']
65
+ end
66
+ # A secret's default would leak alongside the masked value.
67
+ schema['default'] = default unless default.nil? || definition[:type] == :password
68
+ schema
69
+ end
70
+
71
+ # A default can be a block that reads the database (the default store's
72
+ # currency); one that cannot be read here is left out.
73
+ def self.declared_default(definition)
74
+ definition[:default].call
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
79
+ def self.value_schema(definition)
80
+ case definition[:type]
81
+ when :string then string_schema(definition[:format])
82
+ when :text then { 'type' => 'string', 'x-spree-widget' => 'textarea' }
83
+ when :boolean then { 'type' => 'boolean' }
84
+ when :integer then { 'type' => 'integer' }
85
+ when :decimal, :money then item_schema(definition[:type], definition)
86
+ when :password then { 'type' => 'string', 'x-spree-secret' => true }
87
+ when :date then { 'type' => 'string', 'format' => 'date' }
88
+ when :datetime then { 'type' => 'string', 'format' => 'date-time' }
89
+ when :array
90
+ schema = { 'type' => 'array' }
91
+ schema['items'] = item_schema(definition[:of], definition) if definition[:of]
92
+ schema
93
+ when :hash
94
+ schema = { 'type' => 'object' }
95
+ if definition[:keys] && definition[:values]
96
+ schema['propertyNames'] = definition[:keys] == :currency ? string_schema(:currency) : { 'type' => 'string' }
97
+ schema['additionalProperties'] = item_schema(definition[:values], definition)
98
+ end
99
+ schema
100
+ else
101
+ {}
102
+ end
103
+ end
104
+
105
+ def self.item_schema(item_type, definition)
106
+ case item_type
107
+ when :string then string_schema(definition[:format])
108
+ when :integer then { 'type' => 'integer' }
109
+ when :boolean then { 'type' => 'boolean' }
110
+ when :decimal then { 'type' => 'string', 'pattern' => DECIMAL_PATTERN }
111
+ when :money then { 'type' => 'string', 'format' => 'money', 'pattern' => DECIMAL_PATTERN }
112
+ when :id
113
+ prefix = Spree::Preferences::Preferable.preference_model(definition)._prefix_id_prefix
114
+ { 'type' => 'string', 'format' => 'prefixed-id', 'pattern' => "^#{prefix}_[A-Za-z0-9]+$", 'x-spree-prefix' => prefix }
115
+ when :object
116
+ properties = definition[:properties].to_h { |name, type| [name.to_s, item_schema(type, {})] }
117
+ { 'type' => 'object', 'properties' => properties, 'required' => properties.keys, 'additionalProperties' => false }
118
+ else
119
+ {}
120
+ end
121
+ end
122
+
123
+ def self.string_schema(format)
124
+ { 'type' => 'string' }.merge(FORMATS.fetch(format, {}).transform_keys(&:to_s))
125
+ end
126
+
127
+ # The default in the form the API takes it back: a decimal as its exact
128
+ # string without trailing zeros ("100", "0.23"), as every API decimal is.
129
+ def self.wire_default(default, definition)
130
+ return default if default.nil?
131
+ return Spree::Money::Rounding.format_decimal(default) if %i[decimal money].include?(definition[:type])
132
+
133
+ default.as_json
134
+ end
135
+
136
+ private_class_method :declared_default, :value_schema, :item_schema, :string_schema, :wire_default
137
+ end
138
+ end
139
+ end
@@ -50,12 +50,41 @@ module Spree
50
50
  # runtime. Each entry also caches `key_string` (frozen) so hot-path
51
51
  # serializers don't allocate `pref.to_s` per request.
52
52
  def preference_schema
53
- @preference_schema ||= compute_preference_schema
53
+ return @preference_schema if @preference_schema
54
54
 
55
- # An empty list to callers either way; only the memo tells the two
56
- # apart, so a schema that could not be computed is retried rather than
57
- # settled on.
58
- @preference_schema || []
55
+ fields, complete = compute_preference_schema
56
+ # Settled on only when every default could be read; one computed
57
+ # before the database was up is retried next time.
58
+ @preference_schema = fields if complete
59
+ fields
60
+ end
61
+
62
+ # The JSON Schema of this class's `preferences` object (see
63
+ # {Spree::PreferenceSchema::JsonSchema}). Memoized like
64
+ # {#preference_schema}.
65
+ #
66
+ # @return [Hash]
67
+ def preference_json_schema
68
+ return @preference_json_schema if @preference_json_schema
69
+
70
+ schema = Spree::PreferenceSchema::JsonSchema.for(self)
71
+ @preference_json_schema = schema if @preference_schema
72
+ schema
73
+ end
74
+
75
+ # Checks a `preferences` payload against {#preference_json_schema}, which
76
+ # also accepts deprecated preferences until their removal.
77
+ #
78
+ # @param values [Hash{String => Object}]
79
+ # @return [Array<Hash{Symbol => String}>] `{ pointer:, message: }` per failure, empty when it matches
80
+ def preference_failures(values)
81
+ # The published schema plus deprecated preferences, which a write
82
+ # still accepts until their removal. Memoized like the published one.
83
+ schemer = @preference_schemer || JSONSchemer.schema(Spree::PreferenceSchema::JsonSchema.writable(self))
84
+ @preference_schemer ||= schemer if @preference_schema
85
+ schemer.validate(values).map do |error|
86
+ { pointer: error['data_pointer'], message: error['error'] }
87
+ end
59
88
  end
60
89
 
61
90
  # Wire-safe variant of `preference_schema` with `:password`
@@ -77,6 +106,10 @@ module Spree
77
106
  wire = { key: field[:key], type: field[:type], default: field[:default] }
78
107
  wire[:choices] = field[:choices] if field[:choices].present?
79
108
  wire[:default] = nil if field[:type] == :password
109
+ # A decimal default goes out as the decimal string the API takes back.
110
+ if %i[decimal money].include?(field[:type]) && field[:default].is_a?(Numeric)
111
+ wire[:default] = Spree::Money::Rounding.format_decimal(field[:default])
112
+ end
80
113
  wire.freeze
81
114
  end.freeze
82
115
  end
@@ -93,35 +126,26 @@ module Spree
93
126
  .freeze
94
127
  end
95
128
 
96
- # Declared order first, so a form reads the way its author grouped the
97
- # fields; anything the macro did not record (an association writer that
98
- # looks like a preference) keeps its existing place at the end.
99
- def ordered_preferences(instance)
100
- defined = instance.defined_preferences
101
- return defined unless respond_to?(:declared_preference_order)
102
-
103
- declared = declared_preference_order & defined
104
- declared + (defined - declared)
105
- end
106
-
129
+ # @return [Array(Array<Hash>, Boolean)] the fields, and whether their
130
+ # defaults could all be read
107
131
  def compute_preference_schema
108
132
  # Only instantiation is guarded. `new` touches the database to read
109
133
  # the column list, so it fails whenever the schema is asked for before
110
- # a connection exists — at boot, or in a rake task on an empty
111
- # database. Describing the preferences themselves is pure Ruby, so an
112
- # error there is a real bug: left inside the rescue it would return an
113
- # empty schema and quietly strip every field from the admin form.
114
- # Nil rather than an empty list, so `preference_schema`'s `||=` asks
115
- # again next time. Memoizing the failure would leave a class describing
116
- # no preferences for the life of the process because it was first asked
117
- # before the database was up.
134
+ # a connection exists — at boot, in a rake task on an empty database,
135
+ # or while generating the SDK types. An uninitialized instance still
136
+ # describes every preference, since the declarations are pure Ruby;
137
+ # only a default read from the database is missing, so that result is
138
+ # not memoized. An error describing the preferences themselves is a
139
+ # real bug and is left to raise.
140
+ complete = true
118
141
  instance = begin
119
142
  new
120
143
  rescue StandardError
121
- return nil
144
+ complete = false
145
+ allocate
122
146
  end
123
147
 
124
- ordered_preferences(instance).filter_map do |pref|
148
+ fields = instance.defined_preferences.filter_map do |pref|
125
149
  next if instance.preference_deprecated(pref)
126
150
  # Written by Spree, not supplied by the operator — a value a
127
151
  # provider hands back after we register something with it. Offering
@@ -137,6 +161,7 @@ module Spree
137
161
  choices: instance.preference_choices(pref)
138
162
  }.compact.freeze
139
163
  end
164
+ [fields, complete]
140
165
  end
141
166
 
142
167
  # Builds a `parse_on_set:` lambda for `preference :foo_ids, :array`
@@ -161,6 +186,9 @@ module Spree
161
186
  # relation builder; defaults to the unscoped `klass`.
162
187
  # @return [Proc] suitable for the `parse_on_set:` preference option.
163
188
  def normalize_id_preference(klass: nil, scope: nil)
189
+ Spree::Deprecation.warn(
190
+ '`normalize_id_preference` is deprecated. Declare the list as `preference :name, :array, of: :id, model:, scope:`.'
191
+ )
164
192
  lambda do |values, owner = nil|
165
193
  raw = Array(values).flat_map { |v| v.to_s.split(',') }.compact_blank.map(&:strip)
166
194
  next raw unless klass
@@ -189,11 +217,10 @@ module Spree
189
217
  registered_subclasses.find { |klass| klass.api_type == shorthand.to_s }
190
218
  end
191
219
 
192
- # Returns a `[{ type:, label:, description:, preference_schema: }]`
193
- # array for every concrete subclass in `subclasses`. Sorted by label
194
- # for stable output. Uses `serialized_preference_schema` so
195
- # `:password` defaults are redacted — `/types` is an unauthenticated
196
- # discovery surface and must never leak gateway-shipped defaults.
220
+ # Returns a `[{ type:, label:, description:, schema: }]` array for every
221
+ # registered subclass, sorted by label for stable output. `schema` is
222
+ # the JSON Schema of the subclass's `preferences`, which never carries a
223
+ # secret's default.
197
224
  #
198
225
  # `label` and `description` are a fallback, not the admin UI's copy:
199
226
  # they resolve in the request's locale, which is the store's rather
@@ -210,12 +237,15 @@ module Spree
210
237
  # Asking for only one left promotion types with a null description
211
238
  # and no fallback for a client without its own translation.
212
239
  description: subclass_description(klass),
213
- preference_schema: klass.respond_to?(:serialized_preference_schema) ? klass.serialized_preference_schema : []
240
+ schema: klass.preference_json_schema
214
241
  }
215
242
  # Only present when true, so families without the concept keep
216
243
  # their wire shape byte-identical. Pickers stop offering a
217
244
  # superseded kind; existing rows still render and match.
218
245
  entry[:superseded] = true if klass.respond_to?(:superseded?) && klass.superseded?
246
+ # Provider families (payment methods) add their gallery logo and
247
+ # setup guide; other families keep their wire shape unchanged.
248
+ entry.merge!(klass.provider_listing) if klass.respond_to?(:provider_listing)
219
249
  entry
220
250
  end.sort_by { |entry| entry[:label] }
221
251
  end
@@ -20,7 +20,8 @@ module Spree
20
20
  .group("#{Variant.table_name}.product_id")
21
21
  .to_sql
22
22
 
23
- price_expr = Arel.sql('COALESCE(price_agg.agg_price, 999999999)')
23
+ # Above any price decimal(19,4) holds, so an unpriced product sorts last.
24
+ price_expr = Arel.sql('COALESCE(price_agg.agg_price, 1000000000000000)')
24
25
 
25
26
  joins("LEFT JOIN (#{price_agg_sql}) AS price_agg ON price_agg.product_id = #{Product.table_name}.id").
26
27
  select("#{Product.table_name}.*").
@@ -109,6 +110,9 @@ module Spree
109
110
  #
110
111
  # Spree::Product.in_taxon(taxon).count(distinct: true)
111
112
  scope :in_taxon, ->(taxon) {
113
+ taxon = Spree::Category.find_by_prefix_id(taxon) if taxon.is_a?(String)
114
+ next none if taxon.nil?
115
+
112
116
  joins(:product_categories).
113
117
  where("#{Spree::ProductCategory.table_name}.category_id" => taxon.cached_self_and_descendants_ids).distinct
114
118
  }
@@ -156,7 +160,8 @@ module Spree
156
160
  where(Spree::ProductCategory.table_name => { category_id: taxon_ids }).
157
161
  select("#{Product.table_name}.*", "#{min_position_sql} AS min_taxon_position").
158
162
  group("#{Product.table_name}.id").
159
- order(Arel.sql("#{min_position_sql} ASC"))
163
+ # Ties fall back to id, so the order holds across queries and pages.
164
+ order(Arel.sql("#{min_position_sql} ASC"), arel_table[:id].asc)
160
165
  }
161
166
 
162
167
  scope :with_option_value, ->(option, value) {
@@ -218,6 +223,15 @@ module Spree
218
223
  channel = Spree::Current.channel
219
224
  return where(discontinue_on: [nil, Time.current.beginning_of_minute..]) unless channel
220
225
 
226
+ not_discontinued_on(channel)
227
+ end
228
+
229
+ # Published on the channel and not yet unpublished from it — a channel
230
+ # publication's end date is the product's discontinuation there.
231
+ #
232
+ # @param channel [Spree::Channel]
233
+ # @return [ActiveRecord::Relation<Spree::Product>]
234
+ def self.not_discontinued_on(channel)
221
235
  for_channel(channel).where(Spree::ProductPublication.table_name => { unpublished_at: [nil, Time.current.beginning_of_minute..] })
222
236
  end
223
237
 
@@ -0,0 +1,41 @@
1
+ module Spree
2
+ # What a provider shows on its gallery card in the dashboard: a logo and a
3
+ # link to its setup guide. Shared by integrations and payment methods so a
4
+ # provider gem declares both the same way whichever family it extends.
5
+ #
6
+ # class SpreeStripe::Gateway < Spree::Gateway
7
+ # def self.logo_url = 'https://example.com/stripe.png'
8
+ # def self.docs_url = 'https://spreecommerce.org/docs/integrations/payments/stripe'
9
+ # end
10
+ module ProviderListing
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ # Logo shown on the gallery card: an absolute URL to publicly hosted
15
+ # brand assets, or a `data:` URI for gems that want to be self-contained
16
+ # (works air-gapped, no CSP domain to allowlist). Anything an `<img src>`
17
+ # accepts. Deliberately not an asset-pipeline path — provider gems must
18
+ # not force an asset pipeline onto headless API hosts. Hosted logos are a
19
+ # courtesy, not a guarantee: the dashboard falls back to a letter avatar
20
+ # when unset or unreachable.
21
+ #
22
+ # @return [String, nil]
23
+ def logo_url
24
+ nil
25
+ end
26
+
27
+ # Absolute URL of the provider's setup guide, linked from its gallery
28
+ # card. A third-party gem points at its own documentation.
29
+ #
30
+ # @return [String, nil]
31
+ def docs_url
32
+ nil
33
+ end
34
+
35
+ # @return [Hash] the listing attributes merged into a type-discovery entry
36
+ def provider_listing
37
+ { logo_url: logo_url, docs_url: docs_url }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -36,9 +36,12 @@ module Spree
36
36
  module Publishable
37
37
  extend ActiveSupport::Concern
38
38
 
39
+ LIFECYCLE_ACTIONS = { create: :created, update: :updated, delete: :deleted }.freeze
40
+
39
41
  included do
40
42
  class_attribute :publish_events, default: true
41
43
  class_attribute :lifecycle_events_enabled, default: false
44
+ class_attribute :lifecycle_event_actions, default: []
42
45
 
43
46
  after_update :mark_real_update_for_events
44
47
  end
@@ -76,6 +79,9 @@ module Spree
76
79
  events &= Array(options[:only]) if options[:only]
77
80
  events -= Array(options[:except]) if options[:except]
78
81
 
82
+ self.lifecycle_event_actions = events.map { |event| LIFECYCLE_ACTIONS.fetch(event) }
83
+ lifecycle_event_actions.each { |action| publishes_event(action) }
84
+
79
85
  if events.include?(:create)
80
86
  after_commit :publish_create_event, on: :create, if: :should_publish_events?
81
87
  end
@@ -90,6 +96,51 @@ module Spree
90
96
  end
91
97
  end
92
98
 
99
+ # A subclass publishes its lifecycle events under its own prefix, which may
100
+ # differ from its parent's, so they are declared again for it.
101
+ def inherited(subclass)
102
+ super
103
+ subclass.lifecycle_event_actions.each { |action| subclass.publishes_event(action) }
104
+ end
105
+
106
+ # Declare events this model publishes, each carrying this model's Store
107
+ # serializer as its payload.
108
+ #
109
+ # @param names [Array<Symbol, String>] actions (`:placed`, prefixed with
110
+ # {event_prefix}) or full dotted names (`'customer.password_reset'`)
111
+ # @param options [Hash] see {publishes_event}
112
+ # @return [void]
113
+ #
114
+ # @example
115
+ # publishes_events :placed, :paid, :shipped
116
+ def publishes_events(*names, **options)
117
+ names.each { |name| publishes_event(name, **options) }
118
+ end
119
+
120
+ # Declare one event this model publishes. Publishing a name no model
121
+ # declared raises in development and test.
122
+ #
123
+ # @param name [Symbol, String] an action or a full dotted name
124
+ # @param serializer [String, nil] payload serializer class name, when the
125
+ # payload is not this model's Store serializer
126
+ # @param credential [String, nil] for a payload carrying a live credential,
127
+ # the permission key needed to point a webhook endpoint at it; such an
128
+ # event reaches only endpoints that name it outright
129
+ # @param deprecated_alias_of [String, nil] the event this one duplicates
130
+ # @param webhook [Boolean] false for an event no webhook endpoint may
131
+ # receive, even one subscribed to `*`
132
+ # @return [void]
133
+ #
134
+ # @example
135
+ # publishes_event :canceled, serializer: 'Spree::Api::V3::OrderCanceledEventSerializer'
136
+ # publishes_event 'customer.password_reset_requested', serializer: '...', credential: 'write_customers'
137
+ def publishes_event(name, serializer: nil, credential: nil, deprecated_alias_of: nil, webhook: true)
138
+ Spree::Events.catalog.declare(
139
+ self, name,
140
+ serializer: serializer, credential: credential, deprecated_alias_of: deprecated_alias_of, webhook: webhook
141
+ )
142
+ end
143
+
93
144
  # Disable lifecycle events for this model
94
145
  #
95
146
  # @example
@@ -145,10 +196,13 @@ module Spree
145
196
  # order.publish_event('order.placed', metadata: { user_id: 1 })
146
197
  #
147
198
  def publish_event(event_name, payload = nil, metadata = {})
199
+ # Checked even while events are disabled, so every spec that reaches a
200
+ # publish also proves the event is declared.
201
+ Spree::Events.catalog.verify_declared!(event_name)
148
202
  return unless Spree::Events.enabled?
149
203
 
150
204
  @_current_event_name = event_name
151
- payload ||= event_payload
205
+ payload ||= event_payload_for(event_name)
152
206
  Spree::Events.publish(event_name, payload, metadata)
153
207
  ensure
154
208
  @_current_event_name = nil
@@ -161,20 +215,19 @@ module Spree
161
215
  #
162
216
  # @return [Hash]
163
217
  def event_payload
164
- serializer = event_serializer_class
165
-
166
- unless serializer
167
- return {
168
- id: respond_to?(:prefixed_id) ? prefixed_id : id,
169
- created_at: created_at&.iso8601,
170
- updated_at: updated_at&.iso8601
171
- }
172
- end
218
+ build_event_payload(event_serializer_class)
219
+ end
173
220
 
174
- # Use as_json to ensure all values are JSON-safe primitives.
175
- # Alba's to_h can return raw Ruby objects (e.g., Spree::Money) which
176
- # ActiveJob cannot serialize for async event subscribers.
177
- serializer.new(self, params: event_serializer_params).to_h.as_json
221
+ # The payload for one event, built with the serializer the catalog declares
222
+ # for it, falling back to this model's Store serializer.
223
+ #
224
+ # @param event_name [String]
225
+ # @param params [Hash] extra serializer params the event serializer reads,
226
+ # e.g. the reset token of a password reset request
227
+ # @return [Hash]
228
+ def event_payload_for(event_name, **params)
229
+ serializer = Spree::Events.catalog.find(event_name)&.serializer_name&.safe_constantize
230
+ build_event_payload(serializer || event_serializer_class, params)
178
231
  end
179
232
 
180
233
  # Find the event serializer class for this model
@@ -223,6 +276,21 @@ module Spree
223
276
 
224
277
  private
225
278
 
279
+ def build_event_payload(serializer, params = {})
280
+ unless serializer
281
+ return {
282
+ id: respond_to?(:prefixed_id) ? prefixed_id : id,
283
+ created_at: created_at&.iso8601,
284
+ updated_at: updated_at&.iso8601
285
+ }
286
+ end
287
+
288
+ # Use as_json to ensure all values are JSON-safe primitives.
289
+ # Alba's to_h can return raw Ruby objects (e.g., Spree::Money) which
290
+ # ActiveJob cannot serialize for async event subscribers.
291
+ serializer.new(self, params: event_serializer_params.merge(params)).to_h.as_json
292
+ end
293
+
226
294
  # Build params for V3 serializers
227
295
  #
228
296
  # @return [Hash]
@@ -59,17 +59,17 @@ module Spree
59
59
 
60
60
  return false if preferred_stock_location_id.present?
61
61
 
62
- # An address is needed when any item's profile ships to one —
63
- # answered by the profile kind, resolved once per profile.
64
- profile_requires_address = Hash.new do |cache, profile|
65
- cache[profile] = profile.present? && profile.requires_shipping_address?
66
- end
62
+ items_require_shipping_address?
63
+ end
67
64
 
68
- # Both profile paths are preloaded: the variant's own override, and the
69
- # product's for the variants that inherit it.
70
- line_items.includes(variant: [:delivery_profile, { product: :delivery_profile }]).any? do |line_item|
71
- profile_requires_address[line_item.variant&.resolved_delivery_profile]
72
- end
65
+ # Whether delivery can be proposed yet: once there is a destination (a
66
+ # shipping address, or a pickup intent for a purchase that never gets
67
+ # one), or straight away when no item ships to an address. Quoted
68
+ # without a destination, only pickup qualifies.
69
+ #
70
+ # @return [Boolean]
71
+ def can_propose_deliveries?
72
+ ship_address.present? || preferred_stock_location_id.present? || !items_require_shipping_address?
73
73
  end
74
74
 
75
75
  # @deprecated The shipping address is canonical — use +use_shipping+ to
@@ -174,6 +174,19 @@ module Spree
174
174
 
175
175
  private
176
176
 
177
+ def items_require_shipping_address?
178
+ # Answered by the profile kind, resolved once per profile.
179
+ profile_requires_address = Hash.new do |cache, profile|
180
+ cache[profile] = profile.present? && profile.requires_shipping_address?
181
+ end
182
+
183
+ # Both profile paths are preloaded: the variant's own override, and the
184
+ # product's for the variants that inherit it.
185
+ line_items.includes(variant: [:delivery_profile, { product: :delivery_profile }]).any? do |line_item|
186
+ profile_requires_address[line_item.variant&.resolved_delivery_profile]
187
+ end
188
+ end
189
+
177
190
  # The address behind an id a client may select, or nil when the id names
178
191
  # one this purchase has no business shipping to.
179
192
  #
@@ -15,7 +15,7 @@ module Spree
15
15
 
16
16
  supported_codes = store.supported_currencies_list.map(&:iso_code)
17
17
  unless supported_codes.include?(currency)
18
- errors.add(:currency, :currency_not_supported_by_store, message: Spree.t(:currency_not_supported_by_store))
18
+ errors.add(:currency, :currency_not_supported_by_store, message: I18n.t('spree.currency_not_supported_by_store'))
19
19
  end
20
20
  end
21
21