spree_core 6.0.0.beta4 → 6.0.0.rc1

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 (495) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/seller_payouts/sweep_seller_job.rb +2 -0
  10. data/app/mailers/spree/admin_user_mailer.rb +4 -19
  11. data/app/mailers/spree/base_mailer.rb +96 -13
  12. data/app/mailers/spree/email_template_mailer.rb +27 -0
  13. data/app/mailers/spree/export_mailer.rb +3 -4
  14. data/app/mailers/spree/import_mailer.rb +3 -4
  15. data/app/mailers/spree/invitation_mailer.rb +24 -7
  16. data/app/mailers/spree/seller_user_mailer.rb +4 -16
  17. data/app/mailers/spree/webhook_mailer.rb +2 -2
  18. data/app/models/concerns/spree/acted_by.rb +1 -1
  19. data/app/models/concerns/spree/admin_user_methods.rb +13 -1
  20. data/app/models/concerns/spree/api_typed.rb +91 -0
  21. data/app/models/concerns/spree/calculator/tiers.rb +76 -0
  22. data/app/models/concerns/spree/customer_methods.rb +15 -14
  23. data/app/models/concerns/spree/email_template_identity.rb +52 -0
  24. data/app/models/concerns/spree/parameterizable_name.rb +2 -0
  25. data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
  26. data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
  27. data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
  28. data/app/models/concerns/spree/preference_schema.rb +4 -12
  29. data/app/models/concerns/spree/product_scopes.rb +11 -1
  30. data/app/models/concerns/spree/provider_listing.rb +41 -0
  31. data/app/models/concerns/spree/publishable.rb +82 -14
  32. data/app/models/concerns/spree/purchase/addresses.rb +23 -10
  33. data/app/models/concerns/spree/purchase/currency.rb +1 -1
  34. data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
  35. data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
  36. data/app/models/concerns/spree/purchase/locale.rb +1 -1
  37. data/app/models/concerns/spree/purchase/payment_processing.rb +7 -3
  38. data/app/models/concerns/spree/purchase/purchase_order.rb +2 -2
  39. data/app/models/concerns/spree/purchase/totals.rb +49 -1
  40. data/app/models/concerns/spree/receivable.rb +1 -1
  41. data/app/models/concerns/spree/searchable.rb +0 -4
  42. data/app/models/concerns/spree/secret_preferences.rb +59 -0
  43. data/app/models/concerns/spree/single_store_resource.rb +1 -1
  44. data/app/models/concerns/spree/stores/email_branding.rb +31 -0
  45. data/app/models/concerns/spree/type_labels.rb +38 -0
  46. data/app/models/concerns/spree/typed_adjustment_line.rb +1 -1
  47. data/app/models/concerns/spree/typed_associations.rb +6 -0
  48. data/app/models/spree/address.rb +11 -1
  49. data/app/models/spree/adjusters/promotion.rb +1 -15
  50. data/app/models/spree/allowed_origin.rb +0 -1
  51. data/app/models/spree/api_key.rb +5 -6
  52. data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
  53. data/app/models/spree/base.rb +70 -22
  54. data/app/models/spree/calculator/flat_percent_item_total.rb +1 -3
  55. data/app/models/spree/calculator/flat_rate.rb +1 -3
  56. data/app/models/spree/calculator/flexi_rate.rb +1 -3
  57. data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
  58. data/app/models/spree/calculator/price_sack.rb +1 -3
  59. data/app/models/spree/calculator/shipping/digital_delivery.rb +1 -3
  60. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +1 -3
  61. data/app/models/spree/calculator/shipping/flat_rate.rb +1 -3
  62. data/app/models/spree/calculator/shipping/flexi_rate.rb +1 -3
  63. data/app/models/spree/calculator/shipping/per_item.rb +1 -3
  64. data/app/models/spree/calculator/shipping/price_sack.rb +1 -3
  65. data/app/models/spree/calculator/tiered_flat_rate.rb +6 -23
  66. data/app/models/spree/calculator/tiered_percent.rb +7 -25
  67. data/app/models/spree/cart.rb +78 -18
  68. data/app/models/spree/channel/gating.rb +1 -1
  69. data/app/models/spree/channel.rb +4 -4
  70. data/app/models/spree/claim.rb +21 -1
  71. data/app/models/spree/claim_line_item.rb +41 -6
  72. data/app/models/spree/collection.rb +0 -1
  73. data/app/models/spree/collection_rule.rb +5 -21
  74. data/app/models/spree/commission_line.rb +1 -1
  75. data/app/models/spree/commission_rate.rb +2 -3
  76. data/app/models/spree/commission_rate_value.rb +1 -1
  77. data/app/models/spree/commission_rule.rb +4 -11
  78. data/app/models/spree/commission_rules/product_rule.rb +1 -1
  79. data/app/models/spree/company_invitation.rb +1 -0
  80. data/app/models/spree/consent_record.rb +0 -1
  81. data/app/models/spree/credit_card.rb +1 -1
  82. data/app/models/spree/current.rb +23 -0
  83. data/app/models/spree/custom_field.rb +2 -2
  84. data/app/models/spree/custom_field_definition.rb +9 -23
  85. data/app/models/spree/custom_fields/boolean.rb +1 -1
  86. data/app/models/spree/custom_fields/json.rb +1 -1
  87. data/app/models/spree/data_feed/google.rb +0 -2
  88. data/app/models/spree/data_request.rb +1 -1
  89. data/app/models/spree/delivery_method.rb +30 -13
  90. data/app/models/spree/delivery_method_rule.rb +4 -11
  91. data/app/models/spree/delivery_rate.rb +4 -5
  92. data/app/models/spree/delivery_rate_provider/base.rb +1 -0
  93. data/app/models/spree/delivery_zone.rb +0 -1
  94. data/app/models/spree/delivery_zone_member.rb +4 -4
  95. data/app/models/spree/digital_asset.rb +7 -0
  96. data/app/models/spree/digital_asset_provider/base.rb +2 -0
  97. data/app/models/spree/digital_link.rb +2 -5
  98. data/app/models/spree/discount.rb +1 -1
  99. data/app/models/spree/email_template.rb +25 -0
  100. data/app/models/spree/email_template_draft.rb +13 -0
  101. data/app/models/spree/email_template_revision.rb +17 -0
  102. data/app/models/spree/email_templates/entry.rb +118 -0
  103. data/app/models/spree/email_templates/sample_record.rb +16 -0
  104. data/app/models/spree/emails/branding.rb +80 -0
  105. data/app/models/spree/exchange.rb +15 -1
  106. data/app/models/spree/exchange_line_item.rb +121 -5
  107. data/app/models/spree/export.rb +0 -1
  108. data/app/models/spree/exports/products.rb +10 -1
  109. data/app/models/spree/exports/report.rb +1 -1
  110. data/app/models/spree/fee.rb +4 -2
  111. data/app/models/spree/fulfillment.rb +53 -27
  112. data/app/models/spree/fulfillment_changer.rb +2 -2
  113. data/app/models/spree/fulfillment_item.rb +0 -4
  114. data/app/models/spree/fulfillment_provider/base.rb +1 -0
  115. data/app/models/spree/gateway/bogus.rb +4 -0
  116. data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
  117. data/app/models/spree/gateway.rb +5 -1
  118. data/app/models/spree/gift_card.rb +12 -1
  119. data/app/models/spree/gift_card_batch.rb +0 -1
  120. data/app/models/spree/import.rb +2 -7
  121. data/app/models/spree/import_mapping.rb +1 -2
  122. data/app/models/spree/import_row.rb +2 -2
  123. data/app/models/spree/import_schemas/products.rb +1 -0
  124. data/app/models/spree/integration.rb +8 -18
  125. data/app/models/spree/invitation.rb +5 -5
  126. data/app/models/spree/line_item.rb +13 -2
  127. data/app/models/spree/locale.rb +11 -8
  128. data/app/models/spree/market.rb +1 -0
  129. data/app/models/spree/newsletter_subscriber.rb +3 -0
  130. data/app/models/spree/order.rb +56 -138
  131. data/app/models/spree/order_group.rb +11 -2
  132. data/app/models/spree/order_inventory.rb +10 -5
  133. data/app/models/spree/order_promotion.rb +1 -1
  134. data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
  135. data/app/models/spree/order_routing/strategy/base.rb +4 -2
  136. data/app/models/spree/order_routing/strategy/rules.rb +1 -1
  137. data/app/models/spree/order_routing_rule.rb +6 -22
  138. data/app/models/spree/package_type.rb +1 -2
  139. data/app/models/spree/payment/gateway_options.rb +14 -52
  140. data/app/models/spree/payment/processing.rb +2 -4
  141. data/app/models/spree/payment.rb +20 -5
  142. data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
  143. data/app/models/spree/payment_method/store_credit.rb +4 -4
  144. data/app/models/spree/payment_method.rb +36 -4
  145. data/app/models/spree/payment_session.rb +5 -1
  146. data/app/models/spree/payment_setup_session.rb +1 -0
  147. data/app/models/spree/payout_provider/base.rb +3 -1
  148. data/app/models/spree/pickup_point_provider/base.rb +2 -0
  149. data/app/models/spree/price_list.rb +1 -2
  150. data/app/models/spree/price_rules/channel_rule.rb +1 -1
  151. data/app/models/spree/price_rules/customer_group_rule.rb +1 -1
  152. data/app/models/spree/product.rb +31 -7
  153. data/app/models/spree/product_submission.rb +0 -12
  154. data/app/models/spree/product_type.rb +0 -1
  155. data/app/models/spree/promotion/actions/create_line_items.rb +27 -17
  156. data/app/models/spree/promotion/rules/category.rb +0 -4
  157. data/app/models/spree/promotion/rules/channel.rb +0 -4
  158. data/app/models/spree/promotion/rules/country.rb +0 -4
  159. data/app/models/spree/promotion/rules/currency.rb +0 -4
  160. data/app/models/spree/promotion/rules/customer_group.rb +0 -4
  161. data/app/models/spree/promotion/rules/first_order.rb +0 -4
  162. data/app/models/spree/promotion/rules/item_total.rb +4 -8
  163. data/app/models/spree/promotion/rules/market.rb +0 -4
  164. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
  165. data/app/models/spree/promotion/rules/option_value.rb +0 -3
  166. data/app/models/spree/promotion/rules/product.rb +0 -4
  167. data/app/models/spree/promotion/rules/user.rb +0 -4
  168. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
  169. data/app/models/spree/promotion.rb +35 -6
  170. data/app/models/spree/promotion_action.rb +7 -14
  171. data/app/models/spree/promotion_action_line_item.rb +1 -1
  172. data/app/models/spree/promotion_handler/coupon.rb +59 -27
  173. data/app/models/spree/promotion_rule.rb +7 -14
  174. data/app/models/spree/purchase_order.rb +1 -0
  175. data/app/models/spree/quantity_rule.rb +1 -1
  176. data/app/models/spree/refund.rb +28 -3
  177. data/app/models/spree/return.rb +30 -2
  178. data/app/models/spree/return_line_item.rb +65 -14
  179. data/app/models/spree/role.rb +7 -7
  180. data/app/models/spree/saved_report.rb +1 -1
  181. data/app/models/spree/seller.rb +2 -1
  182. data/app/models/spree/seller_payout.rb +7 -0
  183. data/app/models/spree/seller_requirement.rb +4 -12
  184. data/app/models/spree/seller_requirement_custom_field.rb +1 -2
  185. data/app/models/spree/seller_requirement_submission.rb +2 -2
  186. data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
  187. data/app/models/spree/seller_transfer.rb +5 -0
  188. data/app/models/spree/shipping_label.rb +1 -0
  189. data/app/models/spree/stock/availability_validator.rb +2 -2
  190. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  191. data/app/models/spree/stock/packer.rb +2 -2
  192. data/app/models/spree/stock/splitter/weight.rb +1 -1
  193. data/app/models/spree/stock_level/custom_events.rb +6 -0
  194. data/app/models/spree/stock_level.rb +1 -1
  195. data/app/models/spree/stock_movement.rb +1 -1
  196. data/app/models/spree/stock_receipt_item.rb +1 -1
  197. data/app/models/spree/stock_reservation.rb +2 -2
  198. data/app/models/spree/stock_transfer.rb +4 -2
  199. data/app/models/spree/store.rb +33 -7
  200. data/app/models/spree/store_credit.rb +8 -9
  201. data/app/models/spree/store_credit_event.rb +5 -5
  202. data/app/models/spree/tax_exemption_certificate.rb +2 -0
  203. data/app/models/spree/tax_identifier.rb +3 -1
  204. data/app/models/spree/tax_line.rb +53 -3
  205. data/app/models/spree/tax_provider/base.rb +71 -14
  206. data/app/models/spree/tax_provider/internal.rb +59 -33
  207. data/app/models/spree/tax_provider/recorded_share.rb +114 -0
  208. data/app/models/spree/tax_rate.rb +1 -2
  209. data/app/models/spree/taxonomy.rb +0 -1
  210. data/app/models/spree/variant.rb +4 -3
  211. data/app/models/spree/variant_media.rb +1 -1
  212. data/app/models/spree/webhook_endpoint.rb +10 -13
  213. data/app/models/spree/wishlist.rb +1 -6
  214. data/app/models/spree/wishlist_item/custom_events.rb +6 -0
  215. data/app/presenters/spree/csv/customer_presenter.rb +1 -1
  216. data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
  217. data/app/presenters/spree/csv/order_line_item_presenter.rb +1 -1
  218. data/app/presenters/spree/csv/product_variant_presenter.rb +42 -12
  219. data/app/serializers/spree/emails/base_serializer.rb +14 -0
  220. data/app/serializers/spree/emails/export_serializer.rb +13 -0
  221. data/app/serializers/spree/emails/import_serializer.rb +19 -0
  222. data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
  223. data/app/serializers/spree/emails/store_serializer.rb +31 -0
  224. data/app/serializers/spree/emails/user_serializer.rb +12 -0
  225. data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
  226. data/app/services/spree/carts/create.rb +8 -2
  227. data/app/services/spree/carts/destroy.rb +2 -2
  228. data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
  229. data/app/services/spree/carts/split_by_seller.rb +1 -1
  230. data/app/services/spree/carts/update.rb +19 -5
  231. data/app/services/spree/catalogs/set_product_terms.rb +1 -1
  232. data/app/services/spree/categories/add_products.rb +13 -31
  233. data/app/services/spree/categories/remove_products.rb +12 -51
  234. data/app/services/spree/checkout/advance.rb +1 -4
  235. data/app/services/spree/collections/add_products.rb +10 -23
  236. data/app/services/spree/collections/remove_products.rb +7 -43
  237. data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
  238. data/app/services/spree/data_requests/create.rb +1 -1
  239. data/app/services/spree/deliveries/destroy.rb +2 -2
  240. data/app/services/spree/email_templates/check.rb +58 -0
  241. data/app/services/spree/email_templates/no_sample_record.rb +7 -0
  242. data/app/services/spree/email_templates/preview.rb +99 -0
  243. data/app/services/spree/emails/file_system.rb +23 -0
  244. data/app/services/spree/emails/filters.rb +84 -0
  245. data/app/services/spree/emails/liquid_context.rb +48 -0
  246. data/app/services/spree/emails/rendered_email.rb +14 -0
  247. data/app/services/spree/emails/renderer.rb +125 -0
  248. data/app/services/spree/emails/template.rb +51 -0
  249. data/app/services/spree/emails/template_data.rb +23 -0
  250. data/app/services/spree/emails/template_resolver.rb +109 -0
  251. data/app/services/spree/emails/text_converter.rb +72 -0
  252. data/app/services/spree/emails/translation_inliner.rb +134 -0
  253. data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
  254. data/app/services/spree/imports/row_processors/product_variant.rb +65 -34
  255. data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
  256. data/app/services/spree/locales.rb +4 -5
  257. data/app/services/spree/newsletter/subscribe.rb +8 -12
  258. data/app/services/spree/orders/build_fulfillments.rb +3 -2
  259. data/app/services/spree/orders/create.rb +2 -1
  260. data/app/services/spree/orders/discounts/create.rb +7 -4
  261. data/app/services/spree/orders/discounts/destroy.rb +1 -0
  262. data/app/services/spree/orders/discounts/update.rb +1 -0
  263. data/app/services/spree/orders/fees/create.rb +1 -0
  264. data/app/services/spree/orders/fees/destroy.rb +1 -0
  265. data/app/services/spree/orders/fees/update.rb +1 -0
  266. data/app/services/spree/orders/remove_line_item.rb +13 -3
  267. data/app/services/spree/orders/update.rb +10 -6
  268. data/app/services/spree/orders/update_item.rb +6 -3
  269. data/app/services/spree/orders/update_statuses.rb +3 -1
  270. data/app/services/spree/product_memberships.rb +61 -0
  271. data/app/services/spree/returns/eligibility_validator.rb +1 -1
  272. data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
  273. data/app/services/spree/seeds/commission_rates.rb +1 -1
  274. data/app/services/spree/seeds/digital_delivery.rb +1 -1
  275. data/app/services/spree/seeds/payment_methods.rb +2 -2
  276. data/app/services/spree/seeds/saved_reports.rb +3 -3
  277. data/app/services/spree/sellers/requirements.rb +1 -2
  278. data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
  279. data/app/services/spree/shipping_labels/store_file.rb +3 -3
  280. data/app/services/spree/stock_levels/correct.rb +3 -3
  281. data/app/services/spree/stock_reservations/reserve.rb +2 -3
  282. data/app/services/spree/store_credits/apply.rb +15 -13
  283. data/app/services/spree/stores/provision_defaults.rb +15 -14
  284. data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
  285. data/app/views/layouts/spree/base_mailer.html.erb +2 -43
  286. data/app/views/layouts/spree/base_mailer.liquid +79 -0
  287. data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
  288. data/app/views/spree/export_mailer/export_done.liquid +12 -0
  289. data/app/views/spree/import_mailer/import_done.liquid +15 -0
  290. data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
  291. data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
  292. data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
  293. data/app/views/spree/shared/_mailer_button.html.erb +4 -5
  294. data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
  295. data/app/views/spree/shared/_summary_row.liquid +8 -0
  296. data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
  297. data/app/workflows/spree/carts/add_item.rb +4 -4
  298. data/app/workflows/spree/carts/complete.rb +13 -3
  299. data/app/workflows/spree/carts/merge.rb +1 -1
  300. data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
  301. data/app/workflows/spree/carts/upsert_items.rb +18 -9
  302. data/app/workflows/spree/claims/approve.rb +5 -22
  303. data/app/workflows/spree/claims/cancel.rb +5 -21
  304. data/app/workflows/spree/claims/create.rb +15 -0
  305. data/app/workflows/spree/claims/deny.rb +7 -0
  306. data/app/workflows/spree/claims/resolve.rb +34 -31
  307. data/app/workflows/spree/customers/anonymize.rb +3 -4
  308. data/app/workflows/spree/customers/create.rb +1 -1
  309. data/app/workflows/spree/data_requests/fulfill.rb +2 -2
  310. data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
  311. data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
  312. data/app/workflows/spree/email_templates/publish.rb +70 -0
  313. data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
  314. data/app/workflows/spree/email_templates/revert.rb +29 -0
  315. data/app/workflows/spree/email_templates/save_draft.rb +85 -0
  316. data/app/workflows/spree/exchanges/approve.rb +5 -22
  317. data/app/workflows/spree/exchanges/cancel.rb +5 -21
  318. data/app/workflows/spree/exchanges/create.rb +6 -0
  319. data/app/workflows/spree/exchanges/fulfill.rb +75 -51
  320. data/app/workflows/spree/exchanges/receive.rb +6 -65
  321. data/app/workflows/spree/fulfillments/cancel.rb +1 -1
  322. data/app/workflows/spree/fulfillments/create.rb +51 -47
  323. data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
  324. data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
  325. data/app/workflows/spree/fulfillments/replacements.rb +59 -0
  326. data/app/workflows/spree/gift_cards/apply.rb +2 -2
  327. data/app/workflows/spree/orders/add_item.rb +5 -1
  328. data/app/workflows/spree/orders/cancel.rb +3 -3
  329. data/app/workflows/spree/orders/complete.rb +16 -8
  330. data/app/workflows/spree/payments/process.rb +2 -2
  331. data/app/workflows/spree/post_sale/approval.rb +35 -0
  332. data/app/workflows/spree/post_sale/cancellation.rb +44 -0
  333. data/app/workflows/spree/post_sale/receipt.rb +92 -0
  334. data/app/workflows/spree/products/nested_attributes.rb +1 -1
  335. data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
  336. data/app/workflows/spree/purchase_orders/close.rb +3 -31
  337. data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
  338. data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
  339. data/app/workflows/spree/purchase_orders/receive.rb +1 -1
  340. data/app/workflows/spree/purchase_orders/update.rb +5 -34
  341. data/app/workflows/spree/receivables/closing_short.rb +46 -0
  342. data/app/workflows/spree/receivables/draft_editing.rb +48 -0
  343. data/app/workflows/spree/refunds/create.rb +5 -2
  344. data/app/workflows/spree/refunds/order_payments.rb +14 -2
  345. data/app/workflows/spree/refunds/tax_credit.rb +67 -0
  346. data/app/workflows/spree/returns/approve.rb +5 -22
  347. data/app/workflows/spree/returns/cancel.rb +5 -22
  348. data/app/workflows/spree/returns/create.rb +8 -0
  349. data/app/workflows/spree/returns/receive.rb +5 -76
  350. data/app/workflows/spree/returns/refund.rb +31 -8
  351. data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
  352. data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
  353. data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
  354. data/app/workflows/spree/shipping_labels/record.rb +5 -5
  355. data/app/workflows/spree/shipping_labels/refund.rb +2 -2
  356. data/app/workflows/spree/stock_receipts/recording.rb +4 -4
  357. data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
  358. data/app/workflows/spree/stock_transfers/close.rb +3 -30
  359. data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
  360. data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
  361. data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
  362. data/app/workflows/spree/stock_transfers/receive.rb +1 -1
  363. data/app/workflows/spree/stock_transfers/update.rb +5 -33
  364. data/config/i18n-tasks.yml +32 -18
  365. data/config/initializers/alba.rb +7 -0
  366. data/config/language_names.csv +264 -0
  367. data/config/locales/ar.yml +811 -0
  368. data/config/locales/be.yml +855 -0
  369. data/config/locales/bg.yml +723 -0
  370. data/config/locales/ca.yml +689 -0
  371. data/config/locales/cs.yml +855 -0
  372. data/config/locales/da.yml +771 -0
  373. data/config/locales/de-CH.yml +248 -0
  374. data/config/locales/de.yml +835 -0
  375. data/config/locales/el.yml +769 -0
  376. data/config/locales/en.yml +37 -503
  377. data/config/locales/es-CL.yml +301 -0
  378. data/config/locales/es-EC.yml +186 -0
  379. data/config/locales/es-MX.yml +334 -0
  380. data/config/locales/es.yml +831 -0
  381. data/config/locales/et.yml +644 -0
  382. data/config/locales/fa.yml +709 -0
  383. data/config/locales/fi.yml +771 -0
  384. data/config/locales/fr.yml +830 -0
  385. data/config/locales/hu.yml +751 -0
  386. data/config/locales/id.yml +756 -0
  387. data/config/locales/it.yml +833 -0
  388. data/config/locales/ja.yml +726 -0
  389. data/config/locales/km.yml +759 -0
  390. data/config/locales/ko.yml +575 -0
  391. data/config/locales/ku.yml +753 -0
  392. data/config/locales/lt.yml +821 -0
  393. data/config/locales/lv.yml +685 -0
  394. data/config/locales/nb.yml +824 -0
  395. data/config/locales/ne.yml +823 -0
  396. data/config/locales/nl.yml +826 -0
  397. data/config/locales/pl.yml +835 -0
  398. data/config/locales/pt-BR.yml +547 -0
  399. data/config/locales/pt.yml +636 -0
  400. data/config/locales/ro.yml +750 -0
  401. data/config/locales/ru.yml +834 -0
  402. data/config/locales/sk.yml +680 -0
  403. data/config/locales/sl-SI.yml +600 -0
  404. data/config/locales/sv.yml +827 -0
  405. data/config/locales/th.yml +745 -0
  406. data/config/locales/tr.yml +777 -0
  407. data/config/locales/uk.yml +836 -0
  408. data/config/locales/vi.yml +768 -0
  409. data/config/locales/zh-CN.yml +786 -0
  410. data/config/locales/zh-TW.yml +834 -0
  411. data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
  412. data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
  413. data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
  414. data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
  415. data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
  416. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  417. data/lib/spree/checkout/default_requirements.rb +37 -13
  418. data/lib/spree/core/configuration.rb +1 -1
  419. data/lib/spree/core/controller_helpers/store.rb +1 -1
  420. data/lib/spree/core/dependencies.rb +5 -4
  421. data/lib/spree/core/emails/escaped_output.rb +60 -0
  422. data/lib/spree/core/engine.rb +48 -167
  423. data/lib/spree/core/permissions/default_catalog.rb +5 -0
  424. data/lib/spree/core/preferences/json_conversion.rb +182 -0
  425. data/lib/spree/core/preferences/masking.rb +4 -1
  426. data/lib/spree/core/preferences/preferable.rb +137 -83
  427. data/lib/spree/core/preferences/preferable_class_methods.rb +81 -146
  428. data/lib/spree/core/previews/import_preview.rb +10 -0
  429. data/lib/spree/core/tax/provider_error.rb +37 -0
  430. data/lib/spree/core/version.rb +1 -1
  431. data/lib/spree/core.rb +85 -370
  432. data/lib/spree/emails/editable_templates.rb +82 -0
  433. data/lib/spree/events/catalog.rb +189 -0
  434. data/lib/spree/events.rb +21 -1
  435. data/lib/spree/hooks.rb +0 -17
  436. data/lib/spree/i18n.rb +32 -15
  437. data/lib/spree/reporting/adapters/live.rb +6 -6
  438. data/lib/spree/reporting/default_vocabulary.rb +22 -10
  439. data/lib/spree/reporting/hydration.rb +1 -1
  440. data/lib/spree/reporting/query.rb +8 -0
  441. data/lib/spree/reporting/schema.rb +10 -10
  442. data/lib/spree/testing_support/editable_email_templates.rb +61 -0
  443. data/lib/spree/testing_support/emails.rb +22 -0
  444. data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
  445. data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
  446. data/lib/spree/testing_support/factories/order_factory.rb +2 -2
  447. data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
  448. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  449. data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
  450. data/lib/spree/testing_support/i18n.rb +3 -94
  451. data/lib/spree/testing_support/lifecycle_events.rb +1 -1
  452. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  453. data/lib/spree/testing_support/preferences.rb +0 -5
  454. data/lib/spree/testing_support/store.rb +9 -1
  455. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +45 -0
  456. data/lib/tasks/categories_and_collections.rake +3 -3
  457. data/lib/tasks/core.rake +6 -4
  458. data/lib/tasks/media.rake +3 -3
  459. data/lib/tasks/package_types.rake +1 -1
  460. data/lib/tasks/preferences_json.rake +101 -0
  461. data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
  462. metadata +188 -44
  463. data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
  464. data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
  465. data/app/models/spree/base_analytics_event_handler.rb +0 -54
  466. data/app/models/spree/invitations/store.rb +0 -6
  467. data/app/models/spree/preference.rb +0 -14
  468. data/app/models/spree/promotion_handler/page.rb +0 -25
  469. data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
  470. data/app/models/spree/stock/differentiator.rb +0 -45
  471. data/app/presenters/spree/filters/options_presenter.rb +0 -47
  472. data/app/services/spree/carts/empty.rb +0 -42
  473. data/app/services/spree/country_to_timezone.rb +0 -273
  474. data/app/services/spree/payments/create.rb +0 -92
  475. data/app/validators/spree/url_validator.rb +0 -23
  476. data/app/views/active_storage/blobs/_blob.html.erb +0 -14
  477. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
  478. data/app/views/spree/export_mailer/export_done.html.erb +0 -7
  479. data/app/views/spree/import_mailer/import_done.html.erb +0 -12
  480. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
  481. data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
  482. data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
  483. data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
  484. data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
  485. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
  486. data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
  487. data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
  488. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
  489. data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
  490. data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
  491. data/app/workflows/spree/returns/purchase_label.rb +0 -32
  492. data/lib/spree/analytics.rb +0 -11
  493. data/lib/spree/core/preferences/scoped_store.rb +0 -34
  494. data/lib/spree/core/preferences/store.rb +0 -105
  495. data/lib/spree/translation_migrations.rb +0 -52
@@ -2,13 +2,19 @@ module Spree
2
2
  module Exchanges
3
3
  # Ships the replacements and settles the difference in price.
4
4
  #
5
+ # Both halves of the swap carry tax: the units that came back are credited
6
+ # the tax the customer paid on them, and the replacements are taxed as a
7
+ # new sale. The difference between the two, tax included, is what settles.
8
+ #
5
9
  # The money branch mirrors Returns::Refund: a credit the customer is owed
6
10
  # can be store credit (an internal ledger write, inside the transaction)
7
- # or a gateway refund (outside it). A balance the customer owes is left
8
- # for the merchant to collect — charging a stored card without a fresh
9
- # authorization is not something core should do silently.
11
+ # or a gateway refund (outside it). A balance the customer owes is put on
12
+ # the order as a fee for the merchant to collect — charging a stored card
13
+ # without a fresh authorization is not something core should do silently.
10
14
  class Fulfill < Spree::Workflow
11
15
  include Spree::Refunds::OrderPayments
16
+ include Spree::Refunds::TaxCredit
17
+ include Spree::Fulfillments::Replacements
12
18
 
13
19
  hooks :validate, :before_settle, :after_fulfill
14
20
 
@@ -24,17 +30,22 @@ module Spree
24
30
 
25
31
  @fulfillments = []
26
32
  @refunds = []
27
- step :ensure_fulfillable
33
+ step :ensure_received
34
+ step :settle_tax
35
+ step :ensure_refund_method
28
36
  run_hooks :validate
29
37
 
30
38
  ApplicationRecord.transaction do
31
39
  step :build_replacement_fulfillments
32
40
  run_hooks :before_settle
33
41
  step :issue_store_credit if credit_due? && internal_refund?
42
+ step :charge_balance if balance_owed?
34
43
  step :mark_fulfilled
35
44
  end
36
45
 
37
46
  external_step :refund_at_gateway if credit_due? && !internal_refund?
47
+ external_step :refund_tax
48
+ external_step :commit_replacement
38
49
 
39
50
  step :recalculate_order
40
51
  run_hooks :after_fulfill
@@ -54,6 +65,10 @@ module Spree
54
65
  settled_difference.negative?
55
66
  end
56
67
 
68
+ def balance_owed?
69
+ settled_difference.positive?
70
+ end
71
+
57
72
  def credit_amount
58
73
  settled_difference.abs
59
74
  end
@@ -63,64 +78,44 @@ module Spree
63
78
  # exchange that asked for more than arrived would pay credit for goods
64
79
  # nobody returned.
65
80
  def settled_difference
66
- @settled_difference ||= exchange.exchange_line_items.sum(0.to_d) do |line|
67
- next 0.to_d if line.quantity.to_i.zero?
81
+ @settled_difference ||= received_lines.sum(0.to_d) { |line| line.settled_charge - line.settled_credit }
82
+ end
68
83
 
69
- line.price_difference.to_d / line.quantity.to_i * line.received_quantity.to_i
70
- end
84
+ def received_lines
85
+ @received_lines ||= exchange.exchange_line_items.select { |line| line.received_quantity.to_i.positive? }
71
86
  end
72
87
 
73
- def ensure_fulfillable
74
- if credit_due? && !Spree::RefundMethods.valid?(refund_method)
75
- # :base, not :refund_method — it is a workflow argument, not an
76
- # attribute, and ActiveModel raises when an error names one that
77
- # does not exist on the record.
78
- exchange.errors.add(:base, :invalid_refund_method,
79
- message: Spree.t('errors.messages.invalid_refund_method'))
80
- failure(exchange)
81
- end
88
+ def ensure_received
82
89
  failure(exchange, :not_received) unless exchange.received?
83
90
  end
84
91
 
92
+ # Rewrites both halves' rows for the units that arrived, before any money
93
+ # moves, so the difference settled is the one the rows record.
94
+ def settle_tax
95
+ with_tax_provider(exchange) { exchange.settle_tax! }
96
+ end
97
+
98
+ def ensure_refund_method
99
+ return unless credit_due? && !Spree::RefundMethods.valid?(refund_method)
100
+
101
+ # :base, not :refund_method — it is a workflow argument, not an
102
+ # attribute, and ActiveModel raises when an error names one that
103
+ # does not exist on the record.
104
+ exchange.errors.add(:base, :invalid_refund_method,
105
+ message: I18n.t('spree.errors.messages.invalid_refund_method'))
106
+ failure(exchange)
107
+ end
108
+
85
109
  # Only lines that actually came back are replaced — a customer who
86
110
  # returned two of three items gets two replacements.
87
111
  def build_replacement_fulfillments
88
- units = exchange.exchange_line_items.filter_map do |line|
89
- next if line.received_quantity.to_i.zero?
90
-
91
- exchange.order.fulfillment_items.new(
92
- variant: line.new_variant,
93
- quantity: line.received_quantity,
94
- line_item: line.line_item,
95
- order: exchange.order,
96
- status: 'on_hand'
97
- )
112
+ items = received_lines.map do |line|
113
+ { variant: line.new_variant, quantity: line.received_quantity, line_item: line.line_item }
98
114
  end
99
115
 
100
- failure(exchange, :nothing_to_fulfill) if units.empty?
116
+ failure(exchange, :nothing_to_fulfill) if items.empty?
101
117
 
102
- @fulfillments = Spree::Stock::Coordinator.new(exchange.order, units).fulfillments
103
- if @fulfillments.flat_map(&:fulfillment_items).sum(&:quantity) != units.sum(&:quantity)
104
- failure(exchange, :replacement_out_of_stock)
105
- end
106
-
107
- exchange.order.fulfillments += @fulfillments
108
- exchange.order.save!
109
- @fulfillments.each { |fulfillment| allocate_replacement_stock(fulfillment) }
110
- end
111
-
112
- # The replacement is promised the moment it exists, exactly as placement
113
- # promises an order's own fulfillments. Without this the fulfillment holds
114
- # nothing, and dispatch then writes no movement at all — an unallocated
115
- # fulfillment is indistinguishable from one created before typed
116
- # movements, so the goods would leave the shelf untouched and unrecorded.
117
- def allocate_replacement_stock(fulfillment)
118
- fulfillment.manifest.each do |item|
119
- next unless item.variant.track_inventory?
120
- next unless item.quantity.positive?
121
-
122
- fulfillment.stock_location.allocate(item.variant, item.quantity, fulfillment)
123
- end
118
+ @fulfillments = build_replacements(exchange, items)
124
119
  end
125
120
 
126
121
  def issue_store_credit
@@ -138,10 +133,39 @@ module Spree
138
133
  order: exchange.order,
139
134
  amount: credit_amount,
140
135
  record: exchange,
141
- refunder: refunder
136
+ refunder: refunder,
137
+ tax_amount: received_lines.sum(0.to_d, &:settled_tax)
142
138
  )
143
139
  end
144
140
 
141
+ # Puts what the customer owes on the order, which then shows it as due
142
+ # for the merchant to collect through the order's payments.
143
+ def charge_balance
144
+ result = Spree.order_fee_create_service.call(
145
+ order: exchange.order,
146
+ attributes: {
147
+ kind: 'exchange',
148
+ amount: settled_difference,
149
+ label: I18n.t('spree.exchange_fee_label', number: exchange.number),
150
+ metadata: { 'exchange_id' => exchange.prefixed_id }
151
+ }
152
+ )
153
+
154
+ failure(exchange, result.error.value) if result.failure?
155
+ end
156
+
157
+ def refund_tax
158
+ file_tax_credit(exchange, received_lines, received_lines.sum(0.to_d, &:settled_credit))
159
+ end
160
+
161
+ # Reported rather than raised, as the credit is: the goods have shipped.
162
+ def commit_replacement
163
+ order = exchange.order
164
+ order.tax_provider.commit_replacement(order, received_lines)
165
+ rescue Spree::Tax::ProviderError => error
166
+ Rails.error.report(error, handled: true, context: tax_report_context(exchange), source: 'spree.post_sale')
167
+ end
168
+
145
169
  def mark_fulfilled
146
170
  exchange.update!(status: 'fulfilled', fulfilled_at: Time.current)
147
171
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  # Records what came back. Same partial-receipt shape as returns: the
4
4
  # warehouse enters actual quantities and flags anything unsellable.
5
5
  class Receive < Spree::Workflow
6
+ include Spree::PostSale::Receipt
7
+
6
8
  hooks :validate, :before_restock, :after_receive
7
9
 
8
10
  # @param exchange [Spree::Exchange]
@@ -10,76 +12,15 @@ module Spree
10
12
  # @param received_by [Object, nil]
11
13
  def perform(exchange:, items: nil, received_by: nil)
12
14
  super
13
-
14
- step :ensure_receivable
15
- step :normalize_items
16
- run_hooks :validate
17
-
18
- ApplicationRecord.transaction do
19
- step :record_receipt
20
- run_hooks :before_restock
21
- step :restock_resellable_items
22
- step :mark_received
23
- end
24
-
25
- run_hooks :after_receive
26
- exchange.publish_event('exchange.received')
27
- success(exchange.reload)
15
+ receive
28
16
  end
29
17
 
30
18
  private
31
19
 
32
- def ensure_receivable
33
- failure(exchange, :not_approved) unless exchange.approved?
34
- end
35
-
36
- def normalize_items
37
- @normalized_items =
38
- if items.nil?
39
- exchange.exchange_line_items.map do |line|
40
- { exchange_line_item: line, quantity: line.quantity, resellable: true }
41
- end
42
- else
43
- items.map { |item| normalize_item(item) }
44
- end
45
-
46
- failure(exchange, :no_items_received) if @normalized_items.sum { |item| item[:quantity] }.zero?
47
- end
48
-
49
- def normalize_item(item)
50
- line = item[:exchange_line_item]
51
- quantity = item[:quantity].to_i
52
-
53
- failure(exchange, :item_not_on_exchange) unless line&.exchange_id == exchange.id
54
- failure(exchange, :invalid_quantity) if quantity.negative? || quantity > line.quantity
55
-
56
- { exchange_line_item: line, quantity: quantity, resellable: item.fetch(:resellable, true) }
57
- end
58
-
59
- def record_receipt
60
- @normalized_items.each do |item|
61
- item[:exchange_line_item].update!(
62
- received_quantity: item[:quantity],
63
- resellable: item[:resellable]
64
- )
65
- end
66
- end
67
-
68
- def restock_resellable_items
69
- @normalized_items.each do |item|
70
- next unless item[:resellable]
71
- next unless item[:quantity].positive?
72
-
73
- exchange.stock_location.restock(
74
- item[:exchange_line_item].original_variant,
75
- item[:quantity],
76
- exchange
77
- )
78
- end
79
- end
20
+ def post_sale_record = exchange
80
21
 
81
- def mark_received
82
- exchange.update!(status: 'received', received_at: Time.current)
22
+ def restock_variant(line)
23
+ line.original_variant
83
24
  end
84
25
  end
85
26
  end
@@ -45,7 +45,7 @@ module Spree
45
45
  def ensure_cancelable
46
46
  return if fulfillment.can_cancel?
47
47
 
48
- failure(fulfillment, Spree.t('fulfillments.errors.cannot_cancel'))
48
+ failure(fulfillment, I18n.t('spree.fulfillments.errors.cannot_cancel'))
49
49
  end
50
50
 
51
51
  # Withdraws the promise this fulfillment held. Nothing physical comes
@@ -1,6 +1,6 @@
1
1
  module Spree
2
2
  module Fulfillments
3
- # Manually creates a fulfillment (Spree::Shipment) on a completed order,
3
+ # Manually creates a fulfillment (Spree::Fulfillment) on a completed order,
4
4
  # bypassing order routing. Moves the requested quantities of each line
5
5
  # item's not-yet-shipped inventory units out of their current shipments
6
6
  # into the new fulfillment, mirroring externally-managed fulfillment
@@ -13,12 +13,19 @@ module Spree
13
13
  # new fulfillment, unless the caller provides its own +cost+ /
14
14
  # +delivery_method+.
15
15
  #
16
+ # Order-wide, replacement units sent by an exchange or a claim are never
17
+ # moved: they belong to the record that sent them, not to the goods the
18
+ # customer bought on the line they are filed under. Given a
19
+ # +source_fulfillment+, units move only out of that one, replacements
20
+ # included — splitting a parcel, or re-sending a canceled one, moves what
21
+ # that parcel holds.
22
+ #
16
23
  # The resulting cost is frozen only for fulfillments registered with
17
- # status: 'shipped' (rate refresh skips shipped shipments). Pending/ready
18
- # fulfillments participate in the standard rate machinery — this workflow
19
- # re-prices them from the delivery method calculators before the final
20
- # totals recalculation (repricing a completed order's delivery is an
21
- # explicit post-placement edit, never a side effect of a totals refresh).
24
+ # status: 'shipped' (rate refresh skips shipped shipments). A pending new
25
+ # fulfillment participates in the standard rate machinery — this workflow
26
+ # re-prices it from the delivery method calculators before the final
27
+ # totals recalculation. Only the new fulfillment is re-priced; the order's
28
+ # other fulfillments keep their cost.
22
29
  #
23
30
  # In the workflow tier for its hooks and its transaction discipline:
24
31
  # 3PL and courier integrations need to contribute provider payload and
@@ -35,9 +42,11 @@ module Spree
35
42
  # @param order [Spree::Order] completed order to fulfill
36
43
  # @param stock_location [Spree::StockLocation] location the fulfillment ships from
37
44
  # @param items [Array<Hash>, nil] `[{ line_item: Spree::LineItem, quantity: Integer }]`;
38
- # nil fulfills every not-yet-shipped unit on the order
45
+ # nil moves every not-yet-shipped unit, drawn as +source_fulfillment+ describes
46
+ # @param source_fulfillment [Spree::Fulfillment, nil] the only fulfillment units are moved from;
47
+ # nil takes the customer's bought units from every unshipped fulfillment on the order
39
48
  # @param tracking [String, nil] carrier tracking number
40
- # @param delivery_method [Spree::ShippingMethod, nil] carrier; stored as the selected rate.
49
+ # @param delivery_method [Spree::DeliveryMethod, nil] carrier; stored as the selected rate.
41
50
  # Defaults to the delivery method of the drained source fulfillment(s)
42
51
  # @param cost [String, Numeric, nil] explicit shipping cost (e.g. the 3PL's price).
43
52
  # Defaults to the summed cost of the drained source fulfillment(s), keeping the
@@ -47,7 +56,7 @@ module Spree
47
56
  # @param status [String, nil] pass 'shipped' to register an already-shipped fulfillment
48
57
  # @param metadata [Hash, nil] metadata stored on the fulfillment
49
58
  # @return [Spree::ServiceModule::Result] the created shipment on success
50
- def perform(order:, stock_location:, items: nil, tracking: nil, delivery_method: nil, cost: nil, status: nil, metadata: nil)
59
+ def perform(order:, stock_location:, items: nil, source_fulfillment: nil, tracking: nil, delivery_method: nil, cost: nil, status: nil, metadata: nil)
51
60
  super
52
61
 
53
62
  step :ensure_valid_status
@@ -83,7 +92,7 @@ module Spree
83
92
  def ensure_valid_status
84
93
  return if status.nil? || status == 'shipped'
85
94
 
86
- failure(nil, Spree.t('fulfillments.errors.invalid_status'))
95
+ failure(nil, I18n.t('spree.fulfillments.errors.invalid_status'))
87
96
  end
88
97
 
89
98
  # parse_cost returns a failure Result for an unparseable value; step
@@ -93,8 +102,8 @@ module Spree
93
102
  end
94
103
 
95
104
  def ensure_order_fulfillable
96
- failure(nil, Spree.t('fulfillments.errors.order_not_completed')) unless order.completed?
97
- failure(nil, Spree.t('fulfillments.errors.order_canceled')) if order.canceled?
105
+ failure(nil, I18n.t('spree.fulfillments.errors.order_not_completed')) unless order.completed?
106
+ failure(nil, I18n.t('spree.fulfillments.errors.order_canceled')) if order.canceled?
98
107
  end
99
108
 
100
109
  def build_fulfillment
@@ -126,38 +135,32 @@ module Spree
126
135
  mark_shipped(fulfillment) if status == 'shipped'
127
136
  end
128
137
 
129
- def reprice_and_recalculate
130
- reprice_pending_fulfillments(order.reload)
131
- order.recalculate_totals!
132
- end
133
-
134
- # Moved here from the old updater completed-order branch: pending/ready
135
- # fulfillments re-price from backoffice-visible delivery methods;
136
- # fulfilled ones keep their frozen cost.
137
- #
138
138
  # A caller that priced this parcel itself is honoured rather than
139
- # re-quoted.
140
- def reprice_pending_fulfillments(order)
141
- order.fulfillments.each do |candidate|
142
- next unless candidate.persisted?
143
- next if candidate.fulfilled?
144
- next if @requested_cost && candidate.id == fulfillment.id
145
-
146
- candidate.refresh_rates(Spree::DeliveryMethod::BACKOFFICE)
147
- candidate.update_amounts
139
+ # re-quoted, and a shipped one keeps its frozen cost.
140
+ def reprice_and_recalculate
141
+ unless @requested_cost || fulfillment.fulfilled?
142
+ fulfillment.reload.refresh_rates(Spree::DeliveryMethod::BACKOFFICE)
143
+ fulfillment.update_amounts
148
144
  end
145
+
146
+ order.reload.recalculate_totals!
149
147
  end
150
148
 
151
149
  # Units that can still be moved into a manual fulfillment: on-hand or
152
- # backordered units sitting in shipments that haven't shipped. A
153
- # canceled shipment's units are included — cancellation kills that
154
- # attempt, not the obligation to ship, so the goods are still owed and
155
- # a fresh fulfillment is the only way left to send them (the canceled
156
- # record itself can never be fulfilled). Loaded in one query, on-hand
157
- # first so moved units stay shippable, grouped by line item for both
158
- # validation and moving.
150
+ # backordered units sitting in shipments that haven't shipped — the
151
+ # source's own units when one is named, otherwise the customer's bought
152
+ # units across the order. A canceled shipment's units are included —
153
+ # cancellation kills that attempt, not the obligation to ship, so the
154
+ # goods are still owed and a fresh fulfillment is the only way left to
155
+ # send them (the canceled record itself can never be fulfilled). Loaded
156
+ # in one query, bought before replacement (a request names only the
157
+ # line) and on-hand first so moved units stay shippable, grouped by line
158
+ # item for both validation and moving.
159
159
  def fulfillable_units(order)
160
- order.fulfillment_items.
160
+ units = order.fulfillment_items
161
+ units = source_fulfillment ? units.where(fulfillment_id: source_fulfillment.id).order(:replacement) : units.where(replacement: false)
162
+
163
+ units.
161
164
  on_hand_or_backordered.
162
165
  joins(:fulfillment).
163
166
  merge(Spree::Fulfillment.ready_or_pending.or(Spree::Fulfillment.canceled)).
@@ -174,12 +177,12 @@ module Spree
174
177
  quantity = available_for.call(line_item)
175
178
  { line_item: line_item, quantity: quantity } if quantity.positive?
176
179
  end
177
- failure(nil, Spree.t('fulfillments.errors.no_items_to_fulfill')) if derived.empty?
180
+ failure(nil, I18n.t('spree.fulfillments.errors.no_items_to_fulfill')) if derived.empty?
178
181
 
179
182
  return derived
180
183
  end
181
184
 
182
- failure(nil, Spree.t('fulfillments.errors.no_items_to_fulfill')) if items.empty?
185
+ failure(nil, I18n.t('spree.fulfillments.errors.no_items_to_fulfill')) if items.empty?
183
186
 
184
187
  # Merge duplicate line item entries, then validate quantities.
185
188
  merged = items.group_by { |item| item[:line_item].id }.values.map do |grouped|
@@ -191,14 +194,14 @@ module Spree
191
194
  quantity = item[:quantity]
192
195
 
193
196
  unless quantity.positive?
194
- failure(nil, Spree.t('fulfillments.errors.invalid_quantity', item: line_item.prefixed_id))
197
+ failure(nil, I18n.t('spree.fulfillments.errors.invalid_quantity', item: line_item.prefixed_id))
195
198
  end
196
199
 
197
200
  available = available_for.call(line_item)
198
201
  if quantity > available
199
202
  failure(
200
203
  nil,
201
- Spree.t('fulfillments.errors.insufficient_quantity',
204
+ I18n.t('spree.fulfillments.errors.insufficient_quantity',
202
205
  item: line_item.prefixed_id, requested: quantity, available: available)
203
206
  )
204
207
  end
@@ -212,7 +215,7 @@ module Spree
212
215
  # carries each moved unit's allocation across with it so the promise
213
216
  # follows the fulfillment that will ship it.
214
217
  #
215
- # @return [Array<Spree::Shipment>] the shipments units were taken from
218
+ # @return [Array<Spree::Fulfillment>] the shipments units were taken from
216
219
  def move_units(order, fulfillment, requested, units_by_line_item)
217
220
  source_shipments = []
218
221
  stock_moves = Hash.new(0)
@@ -231,7 +234,8 @@ module Spree
231
234
  status: unit.status,
232
235
  variant_id: unit.variant_id,
233
236
  line_item_id: unit.line_item_id,
234
- order_id: order.id
237
+ order_id: order.id,
238
+ replacement: unit.replacement
235
239
  )
236
240
  target.pending = unit.pending
237
241
  # The quantity column has a database default of 1, so a fresh
@@ -282,7 +286,7 @@ module Spree
282
286
  # deleted along with the shipment. The delivery method lookup costs
283
287
  # queries, so it is skipped when the caller provided its own.
284
288
  #
285
- # @return [Hash] `{ cost: BigDecimal, delivery_method: Spree::ShippingMethod or nil }`
289
+ # @return [Hash] `{ cost: BigDecimal, delivery_method: Spree::DeliveryMethod or nil }`
286
290
  def destroy_drained_shipments(source_shipments, capture_delivery_method:)
287
291
  inherited = { cost: 0, delivery_method: nil }
288
292
 
@@ -325,11 +329,11 @@ module Spree
325
329
  return if cost.blank?
326
330
 
327
331
  parsed = cost.is_a?(String) ? BigDecimal(cost.strip) : cost
328
- failure(nil, Spree.t('fulfillments.errors.invalid_cost')) if parsed.negative?
332
+ failure(nil, I18n.t('spree.fulfillments.errors.invalid_cost')) if parsed.negative?
329
333
 
330
334
  parsed
331
335
  rescue ArgumentError
332
- failure(nil, Spree.t('fulfillments.errors.invalid_cost'))
336
+ failure(nil, I18n.t('spree.fulfillments.errors.invalid_cost'))
333
337
  end
334
338
 
335
339
  # Registers an externally-completed fulfillment: backorders are filled
@@ -56,9 +56,12 @@ module Spree
56
56
 
57
57
  # The split has to land before the provider is involved: the label is
58
58
  # for the actual parcel, and a partial shipment's parcel only exists
59
- # after the split.
60
- ApplicationRecord.transaction do
59
+ # after the split. requires_new: #call rescues a failure, so joining a
60
+ # caller's open transaction (the API's order lock) would let it commit
61
+ # a split the shelf check refused.
62
+ ApplicationRecord.transaction(requires_new: true) do
61
63
  step :split_off_requested_units
64
+ step :ensure_shelf_can_cover_dispatch
62
65
  step :apply_tracking
63
66
  end
64
67
 
@@ -67,7 +70,7 @@ module Spree
67
70
  # box, hand it over), and the ordering that lets the shipped email
68
71
  # carry the tracking number the provider discovered instead of racing
69
72
  # it. No-op when a label was already bought through
70
- # Spree::Fulfillments::PurchaseLabel (the flow where a failure is
73
+ # Spree::ShippingLabels::Purchase (the flow where a failure is
71
74
  # loud), or when the provider has no dispatch mechanics (Manual). A
72
75
  # failure here still degrades to "no label yet" — a carrier outage
73
76
  # must never stop a merchant recording a parcel that physically left.
@@ -102,11 +105,10 @@ module Spree
102
105
  end
103
106
 
104
107
  def ensure_fulfillable
105
- failure(@source, Spree.t('fulfillments.errors.cannot_fulfill')) unless @source.can_fulfill?
108
+ failure(@source, I18n.t('spree.fulfillments.errors.cannot_fulfill')) unless @source.can_fulfill?
106
109
 
107
110
  ensure_order_placed
108
111
  ensure_ready_to_hand_over
109
- ensure_shelf_can_cover_dispatch
110
112
  ensure_requested_units_available
111
113
  end
112
114
 
@@ -116,7 +118,7 @@ module Spree
116
118
  def ensure_order_placed
117
119
  return unless @source.order&.draft?
118
120
 
119
- failure(@source, Spree.t('fulfillments.errors.order_draft'))
121
+ failure(@source, I18n.t('spree.fulfillments.errors.order_draft'))
120
122
  end
121
123
 
122
124
  # What the pending/ready statuses used to encode, asked once at the
@@ -132,7 +134,7 @@ module Spree
132
134
  return if order.nil?
133
135
 
134
136
  if @source.fulfillment_items.any?(&:backordered?)
135
- failure(@source, Spree.t('fulfillments.errors.backordered_units'))
137
+ failure(@source, I18n.t('spree.fulfillments.errors.backordered_units'))
136
138
  end
137
139
 
138
140
  # An order that owes nothing is ready to hand over, which the old
@@ -152,7 +154,7 @@ module Spree
152
154
  pending = order.settlement_pending_payments
153
155
  return if pending.any? && pending.all? { |payment| payment.payment_method&.capture_at_checkout? == false }
154
156
 
155
- failure(@source, Spree.t('fulfillments.errors.order_not_paid'))
157
+ failure(@source, I18n.t('spree.fulfillments.errors.order_not_paid'))
156
158
  end
157
159
 
158
160
  # The shelf has to be able to cover what is about to leave it. A dispatch
@@ -163,7 +165,10 @@ module Spree
163
165
  #
164
166
  # Refused here rather than at the stock write so it renders as a 422 the
165
167
  # merchant can act on, instead of a validation exception surfacing from
166
- # inside the dispatch transaction.
168
+ # inside the dispatch transaction. Checked after the split, which the
169
+ # refusal rolls back, so a partial dispatch is measured on the units that
170
+ # actually moved: a line's request can move a replacement of another
171
+ # variant.
167
172
  def ensure_shelf_can_cover_dispatch
168
173
  return if force
169
174
 
@@ -173,12 +178,12 @@ module Spree
173
178
  variants = Spree::Variant.with_deleted.where(id: quantities.keys).index_by(&:id)
174
179
 
175
180
  quantities.each do |variant_id, quantity|
176
- stock_level = @source.stock_location.stock_level(variant_id)
181
+ stock_level = @fulfillment.stock_location.stock_level(variant_id)
177
182
  next if stock_level.nil? || stock_level.count_on_hand >= quantity
178
183
 
179
184
  failure(
180
185
  @source,
181
- Spree.t('fulfillments.errors.insufficient_stock_on_hand',
186
+ I18n.t('spree.fulfillments.errors.insufficient_stock_on_hand',
182
187
  item: variants[variant_id]&.name,
183
188
  on_hand: stock_level.count_on_hand,
184
189
  requested: quantity)
@@ -186,28 +191,18 @@ module Spree
186
191
  end
187
192
  end
188
193
 
189
- # What each variant will actually ship: its outstanding promise, capped
190
- # by what this dispatch covers. A partial dispatch splits first and the
191
- # split carries min(promise, moved quantity), so the two agree.
194
+ # What each variant will actually ship: the parcel's outstanding promise,
195
+ # capped by the units it holds.
192
196
  #
193
197
  # @return [Hash{Integer => Integer}]
194
198
  def dispatch_quantity_by_variant
195
- held = @source.fulfillment_items.group(:variant_id).sum(:quantity)
196
-
197
- allocated = @source.allocated_quantities
199
+ allocated = @fulfillment.allocated_quantities
198
200
  return {} if allocated.empty?
199
201
 
200
- wanted =
201
- if @requested.empty?
202
- held
203
- else
204
- @requested.each_with_object(Hash.new(0)) do |item, totals|
205
- totals[item[:line_item].variant_id] += item[:quantity]
206
- end
207
- end
202
+ held = @fulfillment.fulfillment_items.group(:variant_id).sum(:quantity)
208
203
 
209
204
  allocated.each_with_object({}) do |(variant_id, promised), totals|
210
- quantity = [promised, wanted[variant_id].to_i].min
205
+ quantity = [promised, held[variant_id].to_i].min
211
206
  totals[variant_id] = quantity if quantity.positive?
212
207
  end
213
208
  end
@@ -226,7 +221,7 @@ module Spree
226
221
 
227
222
  failure(
228
223
  @source,
229
- Spree.t('fulfillments.errors.not_in_fulfillment',
224
+ I18n.t('spree.fulfillments.errors.not_in_fulfillment',
230
225
  item: item[:line_item].prefixed_id, requested: item[:quantity], available: held)
231
226
  )
232
227
  end
@@ -244,6 +239,7 @@ module Spree
244
239
  order: @source.order,
245
240
  stock_location: @source.stock_location,
246
241
  items: @requested,
242
+ source_fulfillment: @source,
247
243
  delivery_method: @source.delivery_method,
248
244
  # Delivery was bought once, at checkout. Charging the rate again
249
245
  # would bill the customer for the warehouse's decision.
@@ -45,7 +45,7 @@ module Spree
45
45
  def ensure_deliverable
46
46
  return if fulfillment.can_mark_delivered?
47
47
 
48
- failure(fulfillment, Spree.t('fulfillments.errors.cannot_mark_delivered'))
48
+ failure(fulfillment, I18n.t('spree.fulfillments.errors.cannot_mark_delivered'))
49
49
  end
50
50
 
51
51
  # A human saying "it arrived" outranks a carrier that never sent the