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,87 @@
1
+ module Spree
2
+ module Purchase
3
+ # One-release bridges for the names renamed in 6.0, shared by
4
+ # {Spree::Cart} and {Spree::Order}. Before 6.0 a cart was an incomplete
5
+ # order, so extensions written against 5.x (payment gateways, checkout
6
+ # plugins) call the legacy names on carts too. Every legacy name warns and
7
+ # forwards to its replacement. Removed in 6.1.
8
+ #
9
+ # Renamed columns are also registered with +alias_attribute+, so the legacy
10
+ # name keeps working in +where+ / +find_by+ (those paths do not warn).
11
+ module DeprecatedAliases
12
+ extend ActiveSupport::Concern
13
+
14
+ RENAMED_ATTRIBUTES = {
15
+ promo_total: :discount_total,
16
+ item_count: :total_quantity,
17
+ shipment_total: :delivery_total,
18
+ ship_total: :delivery_total,
19
+ special_instructions: :customer_note
20
+ }.freeze
21
+
22
+ RENAMED_METHODS = {
23
+ display_promo_total: :display_discount_total,
24
+ display_shipment_total: :display_delivery_total,
25
+ display_ship_total: :display_delivery_total,
26
+ shipping_discount: :fulfillment_discount,
27
+ delivery_required?: :delivery_step_required?,
28
+ requires_ship_address?: :shipping_address_required?,
29
+ set_shipments_cost: :set_fulfillments_cost,
30
+ ensure_updated_shipments: :ensure_updated_fulfillments,
31
+ create_proposed_fulfillments: :rebuild_fulfillments!,
32
+ create_proposed_shipments: :rebuild_fulfillments!,
33
+ associate_user!: :associate_customer!,
34
+ update_with_updater!: :recalculate_totals!,
35
+ all_line_items: :line_items
36
+ }.freeze
37
+
38
+ # 5.x took a store argument; payment methods now always come from the
39
+ # purchase's own store.
40
+ RENAMED_PAYMENT_METHOD_READERS = %i[
41
+ available_payment_methods
42
+ collect_payment_methods
43
+ collect_frontend_payment_methods
44
+ ].freeze
45
+
46
+ included do
47
+ include Spree::DeprecatedCustomerAlias
48
+
49
+ RENAMED_ATTRIBUTES.each do |legacy_name, current_name|
50
+ alias_attribute legacy_name, current_name
51
+ end
52
+ end
53
+
54
+ RENAMED_ATTRIBUTES.each do |legacy_name, current_name|
55
+ define_method(legacy_name) do
56
+ warn_deprecated_name(legacy_name, current_name)
57
+ public_send(current_name)
58
+ end
59
+
60
+ define_method(:"#{legacy_name}=") do |value|
61
+ warn_deprecated_name(:"#{legacy_name}=", :"#{current_name}=")
62
+ public_send(:"#{current_name}=", value)
63
+ end
64
+ end
65
+
66
+ RENAMED_METHODS.each do |legacy_name, current_name|
67
+ define_method(legacy_name) do |*args, **options, &block|
68
+ warn_deprecated_name(legacy_name, current_name)
69
+ public_send(current_name, *args, **options, &block)
70
+ end
71
+ end
72
+
73
+ RENAMED_PAYMENT_METHOD_READERS.each do |legacy_name|
74
+ define_method(legacy_name) do |*|
75
+ warn_deprecated_name(legacy_name, :payment_methods)
76
+ payment_methods
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def warn_deprecated_name(legacy_name, current_name)
83
+ Spree::Deprecation.warn("#{self.class.name}##{legacy_name} is deprecated and will be removed in Spree 6.1. Use ##{current_name} instead.")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -51,16 +51,22 @@ module Spree
51
51
 
52
52
  payment = payments.checkout.store_credits.where(source: gift_card.store_credits).first
53
53
  return unless payment
54
+ # Nothing is left for the card to pay, and a store credit cannot hold
55
+ # a zero amount, so the card is released rather than resized.
56
+ return remove_gift_card unless total.positive?
54
57
 
55
58
  gift_card.with_lock do
56
- new_amount = [gift_card.amount_remaining + payment.amount, total].min
59
+ # A locking read: a plain reload can return the transaction's older
60
+ # snapshot, and a resize committed since would be applied again.
61
+ payment.lock!
62
+ new_amount = [gift_card.spendable_amount + payment.amount, total].min
57
63
  next if payment.amount == new_amount
58
64
 
59
65
  difference = new_amount - payment.amount
60
- # update_column bypasses Payment#max_amount validation which can
61
- # fail on stale in-memory state; bounds enforced via min() above.
62
- payment.update_column(:amount, new_amount)
63
66
  payment.source.update_column(:amount, new_amount)
67
+ # Skips Payment#max_amount, which can fail on stale in-memory state;
68
+ # min() above bounds the amount.
69
+ payment.update_store_credit_amount!(new_amount)
64
70
  gift_card.amount_used += difference
65
71
  gift_card.save!
66
72
  end
@@ -22,7 +22,7 @@ module Spree
22
22
  return if locale.blank? || store.blank?
23
23
 
24
24
  unless store.supported_locales_list.include?(locale)
25
- errors.add(:locale, :locale_not_supported_by_store, message: Spree.t(:locale_not_supported_by_store))
25
+ errors.add(:locale, :locale_not_supported_by_store, message: I18n.t('spree.locale_not_supported_by_store'))
26
26
  end
27
27
  end
28
28
  end
@@ -31,18 +31,22 @@ module Spree
31
31
  payments.pending
32
32
  end
33
33
 
34
- # Active front-end payment methods available for this record.
34
+ # Active front-end payment methods available for this record. Store
35
+ # credit is never one of them: it is applied through its own endpoint
36
+ # before the buyer chooses how to pay the rest.
35
37
  #
36
38
  # @return [Array<Spree::PaymentMethod>]
37
39
  def payment_methods
38
- store.payment_methods.active.storefront_visible.select { |payment_method| payment_method.available_for_order?(self) }
40
+ store.payment_methods.active.storefront_visible.select do |payment_method|
41
+ !payment_method.store_credit? && payment_method.available_for_order?(self)
42
+ end
39
43
  end
40
44
 
41
45
  # Free checkouts have no money to collect.
42
46
  #
43
47
  # @return [Boolean]
44
48
  def payment_required?
45
- total.to_f > 0.0
49
+ total.to_d.positive?
46
50
  end
47
51
 
48
52
  # Whether a confirm/review pass is expected before completion —
@@ -71,7 +75,7 @@ module Spree
71
75
  # Don't run if there are authorized payments
72
76
  return if pending_payments.any? && unprocessed_payments.empty?
73
77
  # Never complete without a successfully processed payment.
74
- raise Spree::Core::GatewayError, Spree.t(:no_payment_found) if unprocessed_payments.empty?
78
+ raise Spree::Core::GatewayError, I18n.t('spree.no_payment_found') if unprocessed_payments.empty?
75
79
 
76
80
  unprocessed_payments.each do |payment|
77
81
  break if settled_payment_total >= total
@@ -17,8 +17,8 @@ module Spree
17
17
  MAX_PO_DOCUMENT_SIZE = 10.megabytes
18
18
 
19
19
  # What a purchase order plausibly arrives as: an export from the buyer's
20
- # procurement system, a scan, or a photo of one. Matches the list the
21
- # seller document requirement accepts — the same kinds of paperwork.
20
+ # procurement system (often a spreadsheet or CSV), a scan, or a photo of
21
+ # one.
22
22
  PO_DOCUMENT_CONTENT_TYPES = %w[
23
23
  application/pdf
24
24
  image/jpeg
@@ -27,6 +27,10 @@ module Spree
27
27
  image/webp
28
28
  application/msword
29
29
  application/vnd.openxmlformats-officedocument.wordprocessingml.document
30
+ application/vnd.ms-excel
31
+ application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
32
+ application/vnd.oasis.opendocument.spreadsheet
33
+ text/csv
30
34
  ].freeze
31
35
 
32
36
  included do
@@ -36,6 +40,7 @@ module Spree
36
40
  # internal cost codes, and is served only through an authenticated
37
41
  # download action.
38
42
  has_one_attached :po_document, service: Spree.private_storage_service_name
43
+ signed_id_attachments :po_document
39
44
 
40
45
  validates :po_document,
41
46
  size: { less_than_or_equal_to: MAX_PO_DOCUMENT_SIZE },
@@ -83,8 +88,8 @@ module Spree
83
88
  return if stored_size <= MAX_PO_DOCUMENT_SIZE
84
89
 
85
90
  errors.add(
86
- :po_document, :po_document_too_large, message: Spree.t(
87
- :po_document_too_large,
91
+ :po_document, :po_document_too_large, message: I18n.t(
92
+ 'spree.po_document_too_large',
88
93
  size: ActiveSupport::NumberHelper.number_to_human_size(MAX_PO_DOCUMENT_SIZE)
89
94
  )
90
95
  )
@@ -58,7 +58,7 @@ module Spree
58
58
 
59
59
  # @return [BigDecimal]
60
60
  def total_applied_store_credit
61
- if payments.loaded?
61
+ if read_association_in_memory?(:payments)
62
62
  payments.
63
63
  find_all(&:store_credit?).
64
64
  reject(&:has_invalid_status?).
@@ -68,10 +68,32 @@ module Spree
68
68
  end
69
69
  end
70
70
 
71
+ # Balance still to collect, never negative. Store credit already applied
72
+ # but not yet captured is taken off; captured credit is a completed
73
+ # payment, so the balance has counted it already.
74
+ #
75
+ # @return [BigDecimal]
76
+ def amount_due
77
+ [outstanding_balance - uncaptured_store_credit, 0].max
78
+ end
79
+
80
+ # What store credit paid, apart from gift cards. A gift card is redeemed
81
+ # as store credit, but it is reported on its own as +gift_card_total+, so
82
+ # a summary showing both rows deducts each amount once.
83
+ #
84
+ # @return [BigDecimal]
85
+ def store_credit_total
86
+ total_applied_store_credit - gift_card_total
87
+ end
88
+
71
89
  def using_store_credit?
72
90
  total_applied_store_credit > 0
73
91
  end
74
92
 
93
+ def display_store_credit_total
94
+ Spree::Money.new(-store_credit_total, currency: currency)
95
+ end
96
+
75
97
  def display_total_applicable_store_credit
76
98
  Spree::Money.new(-total_applicable_store_credit, currency: currency)
77
99
  end
@@ -95,6 +117,19 @@ module Spree
95
117
  def display_total_minus_store_credits
96
118
  Spree::Money.new(total_minus_store_credits, currency: currency)
97
119
  end
120
+
121
+ private
122
+
123
+ # @return [BigDecimal]
124
+ def uncaptured_store_credit
125
+ if read_association_in_memory?(:payments)
126
+ payments.
127
+ select { |payment| payment.store_credit? && !payment.has_invalid_status? && !payment.completed? }.
128
+ sum(BigDecimal('0'), &:amount)
129
+ else
130
+ payments.store_credits.valid.where.not(status: 'completed').sum(:amount)
131
+ end
132
+ end
98
133
  end
99
134
  end
100
135
  end
@@ -10,9 +10,37 @@ module Spree
10
10
  line_items.sum(:quantity)
11
11
  end
12
12
 
13
+ # What promotion calculators take a share of: the line amounts less the
14
+ # free gifts, so a gift never enlarges a percentage off the order.
15
+ #
13
16
  # @return [BigDecimal]
14
17
  def amount
15
- line_items.sum(BigDecimal('0'), &:amount)
18
+ line_items.sum(BigDecimal('0'), &:amount) - gift_amount
19
+ end
20
+
21
+ # The list price of the units gift promotions give away. The gift keeps
22
+ # its price on its line and is paid for by a discount, so anything
23
+ # measuring what the shopper spends has to take it out.
24
+ #
25
+ # @param promotion [Spree::Promotion, nil] the promotion doing the
26
+ # measuring; its own gifts are always taken out, since a gift cannot
27
+ # be what qualifies the promotion giving it away
28
+ # @param own_gift_only [Boolean] true while deciding whether another
29
+ # promotion gives its gift, so two gift promotions with spend
30
+ # thresholds do not each wait on the other
31
+ # @return [BigDecimal]
32
+ def gift_amount(promotion: nil, own_gift_only: false)
33
+ return BigDecimal('0') if line_items.empty?
34
+
35
+ excluded_gift_promo_actions = gift_promo_actions(promotion, own_gift_only).select do |action|
36
+ action.promotion_id == promotion&.id || action.gives_away?(self)
37
+ end
38
+ return BigDecimal('0') if excluded_gift_promo_actions.empty?
39
+
40
+ line_items.sum(BigDecimal('0')) do |line_item|
41
+ gifted_quantity = excluded_gift_promo_actions.sum { |action| action.gifted_quantity_of(line_item) }
42
+ Spree::Money::Rounding.to_currency(line_item.price * [gifted_quantity, line_item.quantity].min, line_item.currency)
43
+ end
16
44
  end
17
45
 
18
46
  # Re-sums what the customer has actually paid, and nothing else. A
@@ -47,13 +75,6 @@ module Spree
47
75
  outstanding_balance != 0
48
76
  end
49
77
 
50
- # Balance still to collect after applied store credit, never negative.
51
- #
52
- # @return [BigDecimal]
53
- def amount_due
54
- [outstanding_balance - total_applied_store_credit, 0].max
55
- end
56
-
57
78
  # @return [Boolean]
58
79
  def paid?
59
80
  total.positive? && payment_total >= total
@@ -78,6 +99,26 @@ module Spree
78
99
 
79
100
  private
80
101
 
102
+ # Other promotions' gifts are looked for among the promotions a
103
+ # recalculation writes discounts for, so a gift leaves the measure
104
+ # exactly when its discount can be written.
105
+ def gift_promo_actions(promotion, own_gift_only)
106
+ own = promotion ? promotion.actions.grep(Spree::Promotion::Actions::CreateLineItems) : []
107
+ return own if own_gift_only
108
+
109
+ discounting_ids = promotion_ids + Spree::Promotion.held_by_saved_coupon_code(self).map(&:id)
110
+ return own if discounting_ids.empty?
111
+
112
+ gifting_cart_variants = Spree::PromotionActionLineItem.
113
+ where(variant_id: line_items.map(&:variant_id)).
114
+ select(:promotion_action_id)
115
+ others = Spree::Promotion::Actions::CreateLineItems.
116
+ where(promotion_id: discounting_ids, id: gifting_cart_variants).
117
+ includes(:promotion, :promotion_action_line_items)
118
+
119
+ (own + others.to_a).uniq(&:id)
120
+ end
121
+
81
122
  # Completed payments less their refunds, as a scalar subquery so the
82
123
  # sum and the write are one statement.
83
124
  #
@@ -11,6 +11,12 @@ module Spree::RansackableAttributes
11
11
  class_attribute :whitelisted_ransackable_attributes
12
12
  class_attribute :whitelisted_ransackable_scopes
13
13
 
14
+ # `{ 'price_between' => %w[decimal decimal], 'in_categories' => { list: 'id' } }`
15
+ # — the arguments a ransackable scope takes, which Ruby cannot report for
16
+ # a `scope` lambda. A scope taking none is `'boolean'`; an undeclared one
17
+ # is published as taking one text value.
18
+ class_attribute :ransackable_scope_types, default: {}
19
+
14
20
  # Associations the Store API may filter through. Empty by default: every
15
21
  # hop is a join into data the storefront was never meant to query, and a
16
22
  # join back to the model it came from lets one filter chain them forever.
@@ -21,6 +27,9 @@ module Spree::RansackableAttributes
21
27
  class_attribute :private_ransackable_attributes, default: {}
22
28
  class_attribute :private_ransackable_scopes, default: {}
23
29
 
30
+ # Attributes the `search` filter matches, set with {.search_by}.
31
+ class_attribute :search_attributes, default: [], instance_accessor: false
32
+
24
33
  class_attribute :default_ransackable_attributes
25
34
  # `position` is included so any `acts_as_list` model is sortable by it
26
35
  # without each subclass having to opt in. Ransack ignores attributes
@@ -48,10 +57,47 @@ module Spree::RansackableAttributes
48
57
  def self.ransackable_scopes(auth_object = nil)
49
58
  base = (whitelisted_ransackable_scopes || []).map(&:to_s)
50
59
  base |= Spree.ransack.custom_scopes_for(self).map(&:to_s)
60
+ base |= %w[search] if search_attributes.any? && search_attributes.all? { |path| ransackable_path?(path, auth_object) }
51
61
  return base unless Spree::RansackableAttributes.restricted_audience?(auth_object)
52
62
 
53
63
  base - Array(private_ransackable_scopes[auth_object.to_sym])
54
64
  end
65
+
66
+ # Declares the `search` filter (`q[search]=term`): a case-insensitive
67
+ # partial match on any of the given attributes, which may reach through
68
+ # associations (`variant_sku`). It runs as Ransack's `_cont` on those
69
+ # attributes, so translated names match in the current locale. An
70
+ # audience that may not filter on every one of them does not get it.
71
+ #
72
+ # search_by :name, :code
73
+ #
74
+ # @param attributes [Array<Symbol, String>]
75
+ def self.search_by(*attributes)
76
+ self.search_attributes = attributes.map(&:to_s).freeze
77
+ scope :search, ->(query) {
78
+ query.blank? ? all : ransack("#{search_attributes.join('_or_')}_cont" => query.to_s.strip).result
79
+ }
80
+ end
81
+
82
+ # Whether an attribute, or one reached through associations
83
+ # (`variant_product_name`), is filterable by this audience.
84
+ #
85
+ # @param path [String]
86
+ # @param auth_object [Symbol, nil]
87
+ # @return [Boolean]
88
+ def self.ransackable_path?(path, auth_object = nil, depth = 0)
89
+ return true if ransackable_attributes(auth_object).include?(path)
90
+ return false if depth >= 2
91
+
92
+ ransackable_associations(auth_object).any? do |name|
93
+ next false unless path.start_with?("#{name}_")
94
+
95
+ reflection = reflect_on_association(name.to_sym)
96
+ next false if reflection.nil? || reflection.polymorphic? || !reflection.klass.respond_to?(:ransackable_path?)
97
+
98
+ reflection.klass.ransackable_path?(path.delete_prefix("#{name}_"), auth_object, depth + 1)
99
+ end
100
+ end
55
101
  end
56
102
 
57
103
  # @param auth_object [Symbol, nil] the Ransack auth object a search ran with
@@ -97,7 +97,7 @@ module Spree
97
97
  variant_ids = items.reject(&:marked_for_destruction?).filter_map(&:variant_id)
98
98
  return if variant_ids.uniq.size == variant_ids.size
99
99
 
100
- errors.add(:items, :duplicate_variant, message: Spree.t('errors.messages.duplicate_variant'))
100
+ errors.add(:items, :duplicate_variant, message: I18n.t('spree.errors.messages.duplicate_variant'))
101
101
  end
102
102
  end
103
103
  end
@@ -25,10 +25,6 @@ module Spree
25
25
  model_class.arel_table[attribute.to_sym].lower.matches("%#{escaped}%", '\\')
26
26
  end
27
27
  end
28
-
29
- # Backward compatibility aliases — remove in Spree 6.0
30
- def self.sanitize_query_for_multi_search(query) = sanitize_query_for_search(query)
31
- def self.multi_search_condition(model_class, attribute, query) = search_condition(model_class, attribute, query)
32
28
  end
33
29
  end
34
30
  end
@@ -0,0 +1,59 @@
1
+ module Spree
2
+ # Stores a model's `:password` preferences in a separate `secret_preferences`
3
+ # text column, encrypted with Active Record encryption whenever keys are
4
+ # configured, so the `preferences` column never holds a secret. Declarations
5
+ # are unchanged: `preference :secret_key, :password` and
6
+ # `preferred_secret_key` read and write the encrypted column.
7
+ #
8
+ # Values written before encryption was enabled stay readable and are
9
+ # encrypted on the next save, or at once by
10
+ # `spree:upgrade:encrypt_secret_preferences`.
11
+ module SecretPreferences
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ serialize :secret_preferences, coder: TextHashSerializer
16
+
17
+ if ActiveRecord::Encryption.config.has_primary_key?
18
+ encrypts :secret_preferences, support_unencrypted_data: true
19
+ end
20
+
21
+ before_save :move_secret_preferences
22
+ end
23
+
24
+ # The column is `text`, since ciphertext cannot live in a JSON column. An
25
+ # empty column stays nil rather than `{}`: encrypted attributes compare the
26
+ # stored value with the loaded one, so `{}` would mark every record without
27
+ # secrets as changed.
28
+ class TextHashSerializer
29
+ def self.dump(hash)
30
+ ActiveSupport::JSON.encode(hash) unless hash.nil?
31
+ end
32
+
33
+ def self.load(json)
34
+ Spree::Metadata::HashSerializer.load(ActiveSupport::JSON.decode(json)) unless json.nil?
35
+ end
36
+ end
37
+
38
+ # Both columns as they should be stored, when `preferences` holds a secret
39
+ # — one assigned as part of a whole hash, or one the upgrade has not moved
40
+ # yet. A secret found there is the newer value and wins.
41
+ #
42
+ # @return [Hash{Symbol => Hash}, nil] `preferences:` and `secret_preferences:`, or nil when there is nothing to move
43
+ def separated_secret_preferences
44
+ secrets = (preferences || {}).slice(*self.class.secret_preference_names)
45
+ return if secrets.empty?
46
+
47
+ { preferences: preferences.except(*secrets.keys), secret_preferences: (secret_preferences || {}).merge(secrets) }
48
+ end
49
+
50
+ private
51
+
52
+ # Keeps a secret out of the plain `preferences` column when the record is
53
+ # saved.
54
+ def move_secret_preferences
55
+ separated = separated_secret_preferences
56
+ assign_attributes(separated) if separated
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ # Gives attachment slots their API write name, `<slot>_signed_id`
3
+ # (docs/plans/6.0-uploads-and-file-ownership.md).
4
+ #
5
+ # signed_id_attachments :logo, :mailer_logo
6
+ # store.update(logo_signed_id: upload.signed_id) # attaches
7
+ # store.update(logo_signed_id: nil) # removes
8
+ #
9
+ # A declared slot also refuses a file stored with the other visibility:
10
+ # attaching never moves a file between storage services, so a public upload
11
+ # attached to a private slot would stay readable at a public URL.
12
+ module SignedIdAttachments
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ class_attribute :signed_id_attachment_slots, instance_writer: false, default: []
17
+ end
18
+
19
+ class_methods do
20
+ # @param slots [Array<Symbol>] `has_one_attached` slot names
21
+ # @return [void]
22
+ def signed_id_attachments(*slots)
23
+ validate :signed_id_attachments_on_their_storage if signed_id_attachment_slots.empty?
24
+ self.signed_id_attachment_slots += slots
25
+
26
+ slots.each do |slot|
27
+ define_method(:"#{slot}_signed_id=") { |signed_id| public_send(:"#{slot}=", signed_id) }
28
+ end
29
+ end
30
+
31
+ # The name the API reports +attribute+'s errors under: `logo_signed_id`
32
+ # for the `logo` slot, so a client reads them back under the field it sent.
33
+ #
34
+ # @param attribute [Symbol, String]
35
+ # @return [String, nil] nil when +attribute+ is not a declared slot
36
+ def signed_id_attribute_for(attribute)
37
+ "#{attribute}_signed_id" if signed_id_attachment_slots.include?(attribute.to_sym)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def signed_id_attachments_on_their_storage
44
+ signed_id_attachment_slots.each do |slot|
45
+ blob = attachment_changes[slot.to_s].try(:blob)
46
+ expected_service = self.class.reflect_on_attachment(slot)&.options&.dig(:service_name)
47
+ next if blob.nil? || expected_service.nil? || blob.service_name.to_s == expected_service.to_s
48
+
49
+ visibility = expected_service.to_s == Spree.private_storage_service_name.to_s ? 'private' : 'public'
50
+ errors.add(slot, :wrong_visibility, message: I18n.t('spree.upload_wrong_visibility', visibility: visibility))
51
+ end
52
+ end
53
+ end
54
+ end
@@ -19,7 +19,7 @@ module Spree
19
19
 
20
20
  def ensure_store_association_is_not_changed
21
21
  if store_id_changed? && persisted?
22
- errors.add(:store, :store_association_can_not_be_changed, message: Spree.t('errors.messages.store_association_can_not_be_changed'))
22
+ errors.add(:store, :store_association_can_not_be_changed, message: I18n.t('spree.errors.messages.store_association_can_not_be_changed'))
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module Stores
3
+ # The colors and font of a store's customer emails (see Spree::Emails::Branding).
4
+ # All optional: a blank one keeps the shipped design.
5
+ module EmailBranding
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ Spree::Emails::Branding::COLORS.each { |name| preference :"email_#{name}", :string, format: :color }
10
+ preference :email_font, :string, choices: -> { Spree::Emails::Branding::FONTS.keys }
11
+
12
+ validate :email_branding_valid
13
+ end
14
+
15
+ # @param overrides [Hash] unsaved values, e.g. to preview them
16
+ # @return [Spree::Emails::Branding]
17
+ def email_branding(overrides = {})
18
+ saved = Spree::Emails::Branding.attribute_names.to_h { |name| [name, public_send(:"preferred_email_#{name}")] }
19
+ Spree::Emails::Branding.new(saved.merge(overrides.to_h.stringify_keys.slice(*saved.keys)))
20
+ end
21
+
22
+ private
23
+
24
+ def email_branding_valid
25
+ branding = email_branding
26
+ return if branding.valid?
27
+
28
+ branding.errors.each { |error| errors.add(:"preferred_email_#{error.attribute}", error.type) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -7,6 +7,9 @@ module Spree
7
7
  has_many :markets, class_name: 'Spree::Market', dependent: :destroy
8
8
  has_one :default_market, -> { default }, class_name: 'Spree::Market'
9
9
 
10
+ normalizes :default_locale, with: ->(value) { value.to_s.strip.presence }
11
+ validates_with Spree::LocaleCodesValidator, attributes: %i[default_locale supported_locales]
12
+
10
13
  after_create :ensure_default_market
11
14
  after_update :sync_default_market_settings
12
15
  end
@@ -145,7 +148,7 @@ module Spree
145
148
  markets.create!(
146
149
  name: country.name,
147
150
  currency: read_attribute(:default_currency) || iso_country&.currency_code || 'USD',
148
- default_locale: read_attribute(:default_locale) || iso_country&.languages_official&.first || 'en',
151
+ default_locale: read_attribute(:default_locale) || Spree::Locales.listed(iso_country&.languages_official&.first) || 'en',
149
152
  supported_locales: read_attribute(:supported_locales),
150
153
  default: true,
151
154
  bootstrap_default: true,
@@ -190,7 +193,7 @@ module Spree
190
193
  end
191
194
 
192
195
  def legacy_supported_locales_list
193
- (read_attribute(:supported_locales).to_s.split(',') << default_locale).compact.uniq.sort
196
+ (read_attribute(:supported_locales).to_s.split(',').map(&:strip) << default_locale).compact_blank.uniq.sort
194
197
  end
195
198
  end
196
199
  end
@@ -0,0 +1,38 @@
1
+ module Spree
2
+ # Names and describes each kind in a family of typed subclasses (promotion
3
+ # rules, collection rules, …) from one translation scope keyed by
4
+ # +api_type+, so the `/types` endpoints and dashboard pickers label
5
+ # extension kinds the same way as built-in ones.
6
+ #
7
+ # class Spree::PromotionRule < Spree.base_class
8
+ # include Spree::TypeLabels
9
+ # self.type_labels_scope = 'spree.promotion_rule_types'
10
+ # end
11
+ #
12
+ # Spree::Promotion::Rules::Currency.human_name # => spree.promotion_rule_types.currency.name
13
+ module TypeLabels
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ class_attribute :type_labels_scope, instance_writer: false
18
+ end
19
+
20
+ class_methods do
21
+ # @return [String] the kind's name, falling back to its humanized +api_type+
22
+ def human_name
23
+ I18n.t("#{type_labels_scope}.#{api_type}.name", default: api_type.titleize)
24
+ end
25
+
26
+ # @return [String] what the kind does, empty when it has no translation
27
+ def human_description
28
+ I18n.t("#{type_labels_scope}.#{api_type}.description", default: '')
29
+ end
30
+ end
31
+
32
+ # @return [String]
33
+ def human_name = self.class.human_name
34
+
35
+ # @return [String]
36
+ def human_description = self.class.human_description
37
+ end
38
+ end