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.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/app/helpers/spree/images_helper.rb +0 -57
- data/app/jobs/spree/base_job.rb +21 -0
- data/app/jobs/spree/carts/reap_expired_job.rb +2 -2
- data/app/jobs/spree/collections/regenerate_time_based_job.rb +6 -3
- data/app/jobs/spree/orders/finalize_stale_drafts_job.rb +8 -5
- data/app/jobs/spree/payments/cancel_unused_sessions_job.rb +61 -0
- data/app/jobs/spree/search_provider/index_job.rb +4 -0
- data/app/jobs/spree/seller_payouts/sweep_seller_job.rb +2 -0
- data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
- data/app/mailers/spree/admin_user_mailer.rb +4 -19
- data/app/mailers/spree/base_mailer.rb +97 -13
- data/app/mailers/spree/email_template_mailer.rb +27 -0
- data/app/mailers/spree/export_mailer.rb +3 -4
- data/app/mailers/spree/import_mailer.rb +3 -4
- data/app/mailers/spree/invitation_mailer.rb +24 -7
- data/app/mailers/spree/seller_user_mailer.rb +4 -16
- data/app/mailers/spree/webhook_mailer.rb +2 -2
- data/app/models/concerns/spree/acted_by.rb +1 -1
- data/app/models/concerns/spree/admin_user_methods.rb +15 -1
- data/app/models/concerns/spree/api_typed.rb +91 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
- data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
- data/app/models/concerns/spree/calculator/tiers.rb +63 -0
- data/app/models/concerns/spree/canonical_decimals.rb +61 -0
- data/app/models/concerns/spree/customer_methods.rb +23 -18
- data/app/models/concerns/spree/derived_number.rb +11 -13
- data/app/models/concerns/spree/email_template_identity.rb +52 -0
- data/app/models/concerns/spree/parameterizable_name.rb +2 -0
- data/app/models/concerns/spree/payment_session_transitions.rb +8 -2
- data/app/models/concerns/spree/post_sale/taxation.rb +54 -0
- data/app/models/concerns/spree/post_sale/taxed_line.rb +51 -0
- data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
- data/app/models/concerns/spree/preference_schema.rb +62 -32
- data/app/models/concerns/spree/product_scopes.rb +16 -2
- data/app/models/concerns/spree/provider_listing.rb +41 -0
- data/app/models/concerns/spree/publishable.rb +82 -14
- data/app/models/concerns/spree/purchase/addresses.rb +23 -10
- data/app/models/concerns/spree/purchase/currency.rb +1 -1
- data/app/models/concerns/spree/purchase/deprecated_aliases.rb +87 -0
- data/app/models/concerns/spree/purchase/gift_cards.rb +10 -4
- data/app/models/concerns/spree/purchase/locale.rb +1 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +8 -4
- data/app/models/concerns/spree/purchase/purchase_order.rb +9 -4
- data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
- data/app/models/concerns/spree/purchase/totals.rb +49 -8
- data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
- data/app/models/concerns/spree/receivable.rb +1 -1
- data/app/models/concerns/spree/searchable.rb +0 -4
- data/app/models/concerns/spree/secret_preferences.rb +59 -0
- data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
- data/app/models/concerns/spree/single_store_resource.rb +1 -1
- data/app/models/concerns/spree/stores/email_branding.rb +32 -0
- data/app/models/concerns/spree/stores/markets.rb +5 -2
- data/app/models/concerns/spree/type_labels.rb +38 -0
- data/app/models/concerns/spree/typed_adjustment_line.rb +5 -6
- data/app/models/concerns/spree/typed_associations.rb +12 -18
- data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
- data/app/models/spree/address.rb +12 -2
- data/app/models/spree/adjusters/largest_remainder.rb +14 -1
- data/app/models/spree/adjusters/promotion.rb +4 -19
- data/app/models/spree/allowed_origin.rb +3 -1
- data/app/models/spree/api_key.rb +5 -6
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +5 -5
- data/app/models/spree/base.rb +99 -22
- data/app/models/spree/calculator/flat_percent_item_total.rb +2 -4
- data/app/models/spree/calculator/flat_rate.rb +3 -5
- data/app/models/spree/calculator/flexi_rate.rb +4 -6
- data/app/models/spree/calculator/percent_on_line_item.rb +2 -2
- data/app/models/spree/calculator/price_sack.rb +5 -7
- data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -5
- data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +7 -7
- data/app/models/spree/calculator/shipping/flat_rate.rb +5 -7
- data/app/models/spree/calculator/shipping/flexi_rate.rb +4 -6
- data/app/models/spree/calculator/shipping/per_item.rb +3 -5
- data/app/models/spree/calculator/shipping/price_sack.rb +5 -7
- data/app/models/spree/calculator/tiered_flat_rate.rb +7 -24
- data/app/models/spree/calculator/tiered_percent.rb +7 -25
- data/app/models/spree/cart.rb +78 -18
- data/app/models/spree/catalog.rb +1 -0
- data/app/models/spree/catalog_price.rb +0 -7
- data/app/models/spree/catalog_price_tier.rb +0 -8
- data/app/models/spree/category.rb +3 -1
- data/app/models/spree/channel/gating.rb +3 -2
- data/app/models/spree/channel.rb +6 -4
- data/app/models/spree/claim.rb +22 -1
- data/app/models/spree/claim_line_item.rb +41 -6
- data/app/models/spree/collection.rb +2 -1
- data/app/models/spree/collection_rule.rb +5 -21
- data/app/models/spree/commission_line.rb +1 -1
- data/app/models/spree/commission_rate.rb +7 -4
- data/app/models/spree/commission_rate_value.rb +1 -1
- data/app/models/spree/commission_rule.rb +4 -11
- data/app/models/spree/commission_rules/category_rule.rb +2 -5
- data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
- data/app/models/spree/commission_rules/product_rule.rb +1 -1
- data/app/models/spree/commission_rules/seller_rule.rb +2 -5
- data/app/models/spree/company.rb +1 -0
- data/app/models/spree/company_invitation.rb +1 -0
- data/app/models/spree/consent_record.rb +0 -1
- data/app/models/spree/credit_card.rb +1 -1
- data/app/models/spree/current.rb +23 -0
- data/app/models/spree/custom_field.rb +2 -2
- data/app/models/spree/custom_field_definition.rb +20 -35
- data/app/models/spree/custom_fields/boolean.rb +1 -1
- data/app/models/spree/custom_fields/json.rb +1 -1
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/data_feed/google.rb +0 -2
- data/app/models/spree/data_request.rb +2 -1
- data/app/models/spree/delivery_method.rb +36 -14
- data/app/models/spree/delivery_method_rule.rb +4 -11
- data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
- data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
- data/app/models/spree/delivery_profile.rb +1 -0
- data/app/models/spree/delivery_rate.rb +7 -8
- data/app/models/spree/delivery_rate_provider/base.rb +1 -0
- data/app/models/spree/delivery_zone.rb +1 -2
- data/app/models/spree/delivery_zone_member.rb +4 -4
- data/app/models/spree/digital_asset.rb +7 -0
- data/app/models/spree/digital_asset_provider/base.rb +2 -0
- data/app/models/spree/digital_link.rb +2 -5
- data/app/models/spree/discount.rb +1 -1
- data/app/models/spree/email_template.rb +25 -0
- data/app/models/spree/email_template_draft.rb +13 -0
- data/app/models/spree/email_template_revision.rb +17 -0
- data/app/models/spree/email_templates/entry.rb +118 -0
- data/app/models/spree/email_templates/sample_record.rb +16 -0
- data/app/models/spree/emails/branding.rb +80 -0
- data/app/models/spree/exchange.rb +16 -1
- data/app/models/spree/exchange_line_item.rb +121 -5
- data/app/models/spree/export.rb +0 -1
- data/app/models/spree/exports/products.rb +10 -1
- data/app/models/spree/exports/report.rb +3 -3
- data/app/models/spree/fee.rb +4 -2
- data/app/models/spree/file_upload.rb +155 -0
- data/app/models/spree/fulfillment.rb +66 -30
- data/app/models/spree/fulfillment_changer.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +6 -1
- data/app/models/spree/fulfillment_item.rb +0 -4
- data/app/models/spree/fulfillment_provider/base.rb +1 -0
- data/app/models/spree/gateway/bogus.rb +6 -0
- data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
- data/app/models/spree/gateway.rb +6 -2
- data/app/models/spree/gift_card.rb +18 -8
- data/app/models/spree/gift_card_batch.rb +3 -5
- data/app/models/spree/import.rb +5 -7
- data/app/models/spree/import_mapping.rb +1 -2
- data/app/models/spree/import_row.rb +2 -2
- data/app/models/spree/import_schemas/products.rb +1 -0
- data/app/models/spree/integration.rb +9 -19
- data/app/models/spree/invitation.rb +5 -5
- data/app/models/spree/line_item.rb +15 -4
- data/app/models/spree/locale.rb +11 -8
- data/app/models/spree/market.rb +8 -2
- data/app/models/spree/media.rb +1 -9
- data/app/models/spree/newsletter_subscriber.rb +4 -0
- data/app/models/spree/option_type.rb +2 -1
- data/app/models/spree/option_value.rb +2 -1
- data/app/models/spree/order.rb +60 -142
- data/app/models/spree/order_group.rb +18 -4
- data/app/models/spree/order_inventory.rb +10 -5
- data/app/models/spree/order_promotion.rb +1 -1
- data/app/models/spree/order_routing/has_strategy_preference.rb +1 -1
- data/app/models/spree/order_routing/strategy/base.rb +4 -2
- data/app/models/spree/order_routing/strategy/rules.rb +1 -1
- data/app/models/spree/order_routing_rule.rb +6 -22
- data/app/models/spree/package_type.rb +2 -2
- data/app/models/spree/payment/gateway_options.rb +15 -53
- data/app/models/spree/payment/processing.rb +2 -4
- data/app/models/spree/payment.rb +21 -13
- data/app/models/spree/payment_method/check.rb +2 -0
- data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
- data/app/models/spree/payment_method/store_credit.rb +15 -13
- data/app/models/spree/payment_method.rb +78 -6
- data/app/models/spree/payment_session.rb +5 -1
- data/app/models/spree/payment_setup_session.rb +1 -0
- data/app/models/spree/payout_provider/base.rb +3 -1
- data/app/models/spree/pickup_point_provider/base.rb +2 -0
- data/app/models/spree/policy.rb +1 -0
- data/app/models/spree/price.rb +17 -14
- data/app/models/spree/price_adjustment_tier.rb +4 -0
- data/app/models/spree/price_list.rb +14 -9
- data/app/models/spree/price_rules/channel_rule.rb +3 -6
- data/app/models/spree/price_rules/customer_group_rule.rb +3 -6
- data/app/models/spree/price_rules/market_rule.rb +2 -5
- data/app/models/spree/price_rules/user_rule.rb +1 -3
- data/app/models/spree/product/channels.rb +9 -2
- data/app/models/spree/product.rb +54 -30
- data/app/models/spree/product_submission.rb +0 -12
- data/app/models/spree/product_type.rb +1 -1
- data/app/models/spree/promotion/actions/create_line_items.rb +28 -18
- data/app/models/spree/promotion/rules/category.rb +1 -5
- data/app/models/spree/promotion/rules/channel.rb +2 -9
- data/app/models/spree/promotion/rules/country.rb +3 -8
- data/app/models/spree/promotion/rules/currency.rb +1 -5
- data/app/models/spree/promotion/rules/customer_group.rb +2 -9
- data/app/models/spree/promotion/rules/first_order.rb +0 -4
- data/app/models/spree/promotion/rules/item_total.rb +6 -10
- data/app/models/spree/promotion/rules/market.rb +2 -9
- data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
- data/app/models/spree/promotion/rules/option_value.rb +2 -6
- data/app/models/spree/promotion/rules/product.rb +1 -5
- data/app/models/spree/promotion/rules/user.rb +0 -4
- data/app/models/spree/promotion/rules/user_logged_in.rb +0 -4
- data/app/models/spree/promotion.rb +36 -6
- data/app/models/spree/promotion_action.rb +7 -14
- data/app/models/spree/promotion_action_line_item.rb +1 -1
- data/app/models/spree/promotion_handler/coupon.rb +59 -27
- data/app/models/spree/promotion_rule.rb +7 -14
- data/app/models/spree/purchase_order.rb +3 -0
- data/app/models/spree/quantity_rule.rb +1 -1
- data/app/models/spree/refund.rb +41 -18
- data/app/models/spree/return.rb +31 -2
- data/app/models/spree/return_line_item.rb +65 -14
- data/app/models/spree/role.rb +7 -7
- data/app/models/spree/saved_report.rb +2 -1
- data/app/models/spree/search_provider/base.rb +4 -0
- data/app/models/spree/seller.rb +10 -2
- data/app/models/spree/seller_payout.rb +8 -0
- data/app/models/spree/seller_requirement.rb +5 -12
- data/app/models/spree/seller_requirement_custom_field.rb +1 -2
- data/app/models/spree/seller_requirement_submission.rb +3 -2
- data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
- data/app/models/spree/seller_transfer.rb +6 -0
- data/app/models/spree/shipping_label.rb +2 -0
- data/app/models/spree/stock/availability_validator.rb +2 -2
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
- data/app/models/spree/stock/packer.rb +2 -2
- data/app/models/spree/stock/splitter/weight.rb +1 -1
- data/app/models/spree/stock_level/custom_events.rb +6 -0
- data/app/models/spree/stock_level.rb +3 -1
- data/app/models/spree/stock_location.rb +1 -0
- data/app/models/spree/stock_movement.rb +1 -1
- data/app/models/spree/stock_receipt_item.rb +1 -1
- data/app/models/spree/stock_reservation.rb +2 -2
- data/app/models/spree/stock_transfer.rb +6 -2
- data/app/models/spree/store.rb +70 -19
- data/app/models/spree/store_credit.rb +13 -13
- data/app/models/spree/store_credit_event.rb +5 -5
- data/app/models/spree/supplier.rb +1 -0
- data/app/models/spree/tax_category.rb +1 -0
- data/app/models/spree/tax_exemption_certificate.rb +3 -0
- data/app/models/spree/tax_identifier.rb +3 -1
- data/app/models/spree/tax_line.rb +53 -3
- data/app/models/spree/tax_provider/base.rb +71 -14
- data/app/models/spree/tax_provider/internal.rb +70 -41
- data/app/models/spree/tax_provider/recorded_share.rb +114 -0
- data/app/models/spree/tax_rate.rb +30 -8
- data/app/models/spree/taxon.rb +1 -1
- data/app/models/spree/taxonomy.rb +0 -1
- data/app/models/spree/update_status.rb +25 -0
- data/app/models/spree/variant.rb +10 -12
- data/app/models/spree/variant_media.rb +1 -1
- data/app/models/spree/webhook_delivery.rb +1 -0
- data/app/models/spree/webhook_endpoint.rb +46 -13
- data/app/models/spree/wishlist.rb +1 -6
- data/app/models/spree/wishlist_item/custom_events.rb +6 -0
- data/app/presenters/spree/csv/customer_presenter.rb +1 -1
- data/app/presenters/spree/csv/newsletter_subscriber_presenter.rb +1 -1
- data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
- data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
- data/app/presenters/spree/csv/product_variant_presenter.rb +59 -20
- data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
- data/app/serializers/spree/emails/base_serializer.rb +14 -0
- data/app/serializers/spree/emails/export_serializer.rb +13 -0
- data/app/serializers/spree/emails/import_serializer.rb +19 -0
- data/app/serializers/spree/emails/invitation_serializer.rb +28 -0
- data/app/serializers/spree/emails/store_serializer.rb +31 -0
- data/app/serializers/spree/emails/user_serializer.rb +12 -0
- data/app/serializers/spree/emails/webhook_endpoint_serializer.rb +16 -0
- data/app/services/spree/carts/create.rb +8 -2
- data/app/services/spree/carts/destroy.rb +2 -2
- data/app/services/spree/carts/remove_out_of_stock_items.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +1 -1
- data/app/services/spree/carts/update.rb +22 -8
- data/app/services/spree/catalogs/set_product_terms.rb +1 -1
- data/app/services/spree/categories/add_products.rb +13 -31
- data/app/services/spree/categories/remove_products.rb +12 -51
- data/app/services/spree/checkout/advance.rb +1 -4
- data/app/services/spree/collections/add_products.rb +10 -23
- data/app/services/spree/collections/remove_products.rb +7 -43
- data/app/services/spree/coupon_codes/bulk_generate.rb +8 -14
- data/app/services/spree/data_requests/create.rb +1 -1
- data/app/services/spree/deliveries/destroy.rb +2 -2
- data/app/services/spree/email_templates/check.rb +58 -0
- data/app/services/spree/email_templates/no_sample_record.rb +7 -0
- data/app/services/spree/email_templates/preview.rb +99 -0
- data/app/services/spree/emails/file_system.rb +23 -0
- data/app/services/spree/emails/filters.rb +84 -0
- data/app/services/spree/emails/liquid_context.rb +48 -0
- data/app/services/spree/emails/rendered_email.rb +14 -0
- data/app/services/spree/emails/renderer.rb +125 -0
- data/app/services/spree/emails/template.rb +51 -0
- data/app/services/spree/emails/template_data.rb +23 -0
- data/app/services/spree/emails/template_resolver.rb +109 -0
- data/app/services/spree/emails/text_converter.rb +72 -0
- data/app/services/spree/emails/translation_inliner.rb +134 -0
- data/app/services/spree/imports/row_processors/price_list_price.rb +10 -10
- data/app/services/spree/imports/row_processors/product_variant.rb +77 -34
- data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
- data/app/services/spree/locales.rb +43 -10
- data/app/services/spree/newsletter/subscribe.rb +8 -12
- data/app/services/spree/orders/build_fulfillments.rb +3 -2
- data/app/services/spree/orders/create.rb +5 -4
- data/app/services/spree/orders/discounts/create.rb +14 -11
- data/app/services/spree/orders/discounts/destroy.rb +1 -0
- data/app/services/spree/orders/discounts/update.rb +1 -0
- data/app/services/spree/orders/fees/create.rb +1 -0
- data/app/services/spree/orders/fees/destroy.rb +1 -0
- data/app/services/spree/orders/fees/update.rb +1 -0
- data/app/services/spree/orders/remove_line_item.rb +13 -3
- data/app/services/spree/orders/update.rb +10 -6
- data/app/services/spree/orders/update_item.rb +6 -3
- data/app/services/spree/orders/update_statuses.rb +3 -1
- data/app/services/spree/prices/bulk_upsert.rb +32 -25
- data/app/services/spree/product_memberships.rb +61 -0
- data/app/services/spree/returns/eligibility_validator.rb +1 -1
- data/app/services/spree/search_provider/custom_field_schema.rb +2 -2
- data/app/services/spree/seeds/commission_rates.rb +1 -1
- data/app/services/spree/seeds/digital_delivery.rb +1 -1
- data/app/services/spree/seeds/payment_methods.rb +2 -2
- data/app/services/spree/seeds/saved_reports.rb +3 -3
- data/app/services/spree/seeds/stores.rb +1 -1
- data/app/services/spree/sellers/requirements.rb +1 -2
- data/app/services/spree/shipping_labels/confirm_refund.rb +1 -1
- data/app/services/spree/shipping_labels/store_file.rb +3 -3
- data/app/services/spree/stock_levels/correct.rb +3 -3
- data/app/services/spree/stock_reservations/reserve.rb +2 -3
- data/app/services/spree/store_credits/apply.rb +15 -13
- data/app/services/spree/stores/provision_defaults.rb +23 -25
- data/app/services/spree/update_check.rb +160 -0
- data/app/services/spree/uploads/assign_stores.rb +92 -0
- data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
- data/app/validators/spree/attachment_content_type_validator.rb +19 -2
- data/app/validators/spree/locale_codes_validator.rb +22 -0
- data/app/views/layouts/spree/base_mailer.html.erb +2 -43
- data/app/views/layouts/spree/base_mailer.liquid +79 -0
- data/app/views/spree/admin_user_mailer/password_reset_email.liquid +14 -0
- data/app/views/spree/export_mailer/export_done.liquid +12 -0
- data/app/views/spree/import_mailer/import_done.liquid +15 -0
- data/app/views/spree/invitation_mailer/invitation_accepted.liquid +9 -0
- data/app/views/spree/invitation_mailer/invitation_email.liquid +13 -0
- data/app/views/spree/seller_user_mailer/password_reset_email.liquid +14 -0
- data/app/views/spree/shared/_mailer_button.html.erb +4 -5
- data/app/views/spree/shared/_mailer_hero.html.erb +6 -5
- data/app/views/spree/shared/_summary_row.liquid +8 -0
- data/app/views/spree/webhook_mailer/endpoint_disabled.liquid +21 -0
- data/app/workflows/spree/carts/add_item.rb +4 -4
- data/app/workflows/spree/carts/complete.rb +13 -3
- data/app/workflows/spree/carts/merge.rb +1 -1
- data/app/workflows/spree/carts/recalculate_totals.rb +5 -0
- data/app/workflows/spree/carts/upsert_items.rb +18 -9
- data/app/workflows/spree/claims/approve.rb +5 -22
- data/app/workflows/spree/claims/cancel.rb +5 -21
- data/app/workflows/spree/claims/create.rb +15 -0
- data/app/workflows/spree/claims/deny.rb +7 -0
- data/app/workflows/spree/claims/resolve.rb +34 -31
- data/app/workflows/spree/customers/anonymize.rb +3 -4
- data/app/workflows/spree/customers/create.rb +1 -1
- data/app/workflows/spree/data_requests/fulfill.rb +2 -2
- data/app/workflows/spree/deliveries/update_tracking.rb +1 -1
- data/app/workflows/spree/email_templates/discard_draft.rb +20 -0
- data/app/workflows/spree/email_templates/publish.rb +70 -0
- data/app/workflows/spree/email_templates/restore_revision.rb +33 -0
- data/app/workflows/spree/email_templates/revert.rb +29 -0
- data/app/workflows/spree/email_templates/save_draft.rb +85 -0
- data/app/workflows/spree/exchanges/approve.rb +5 -22
- data/app/workflows/spree/exchanges/cancel.rb +5 -21
- data/app/workflows/spree/exchanges/create.rb +6 -0
- data/app/workflows/spree/exchanges/fulfill.rb +75 -51
- data/app/workflows/spree/exchanges/receive.rb +6 -65
- data/app/workflows/spree/fulfillments/cancel.rb +1 -1
- data/app/workflows/spree/fulfillments/create.rb +53 -49
- data/app/workflows/spree/fulfillments/fulfill.rb +23 -27
- data/app/workflows/spree/fulfillments/mark_delivered.rb +1 -1
- data/app/workflows/spree/fulfillments/replacements.rb +59 -0
- data/app/workflows/spree/gift_cards/apply.rb +2 -2
- data/app/workflows/spree/orders/add_item.rb +5 -1
- data/app/workflows/spree/orders/cancel.rb +3 -3
- data/app/workflows/spree/orders/complete.rb +16 -8
- data/app/workflows/spree/payments/capture.rb +20 -5
- data/app/workflows/spree/payments/process.rb +5 -3
- data/app/workflows/spree/post_sale/approval.rb +35 -0
- data/app/workflows/spree/post_sale/cancellation.rb +44 -0
- data/app/workflows/spree/post_sale/receipt.rb +92 -0
- data/app/workflows/spree/price_lists/update.rb +4 -4
- data/app/workflows/spree/products/nested_attributes.rb +1 -1
- data/app/workflows/spree/purchase_orders/cancel.rb +1 -1
- data/app/workflows/spree/purchase_orders/close.rb +3 -31
- data/app/workflows/spree/purchase_orders/create.rb +3 -1
- data/app/workflows/spree/purchase_orders/mark_draft.rb +2 -2
- data/app/workflows/spree/purchase_orders/mark_ordered.rb +2 -2
- data/app/workflows/spree/purchase_orders/receive.rb +1 -1
- data/app/workflows/spree/purchase_orders/update.rb +5 -34
- data/app/workflows/spree/receivables/closing_short.rb +46 -0
- data/app/workflows/spree/receivables/draft_editing.rb +48 -0
- data/app/workflows/spree/refunds/create.rb +5 -2
- data/app/workflows/spree/refunds/order_payments.rb +14 -2
- data/app/workflows/spree/refunds/tax_credit.rb +67 -0
- data/app/workflows/spree/returns/approve.rb +5 -22
- data/app/workflows/spree/returns/cancel.rb +5 -22
- data/app/workflows/spree/returns/create.rb +8 -0
- data/app/workflows/spree/returns/receive.rb +5 -76
- data/app/workflows/spree/returns/refund.rb +31 -8
- data/app/workflows/spree/seller_requirement_submissions/create.rb +1 -2
- data/app/workflows/spree/seller_transfers/reverse.rb +28 -1
- data/app/workflows/spree/shipping_labels/purchase.rb +8 -8
- data/app/workflows/spree/shipping_labels/record.rb +7 -7
- data/app/workflows/spree/shipping_labels/refund.rb +2 -2
- data/app/workflows/spree/stock_receipts/recording.rb +4 -4
- data/app/workflows/spree/stock_transfers/cancel.rb +2 -2
- data/app/workflows/spree/stock_transfers/close.rb +3 -30
- data/app/workflows/spree/stock_transfers/create.rb +3 -1
- data/app/workflows/spree/stock_transfers/mark_draft.rb +1 -1
- data/app/workflows/spree/stock_transfers/mark_in_transit.rb +3 -3
- data/app/workflows/spree/stock_transfers/mark_ready.rb +2 -2
- data/app/workflows/spree/stock_transfers/receive.rb +1 -1
- data/app/workflows/spree/stock_transfers/update.rb +5 -33
- data/config/currency_exponents.json +180 -0
- data/config/i18n-tasks.yml +32 -18
- data/config/initializers/alba.rb +7 -0
- data/config/language_names.csv +264 -0
- data/config/locales/ar.yml +811 -0
- data/config/locales/be.yml +855 -0
- data/config/locales/bg.yml +723 -0
- data/config/locales/ca.yml +689 -0
- data/config/locales/content_locales.rb +9 -0
- data/config/locales/cs.yml +855 -0
- data/config/locales/da.yml +771 -0
- data/config/locales/de-CH.yml +248 -0
- data/config/locales/de.yml +835 -0
- data/config/locales/el.yml +769 -0
- data/config/locales/en.yml +47 -505
- data/config/locales/es-CL.yml +301 -0
- data/config/locales/es-EC.yml +186 -0
- data/config/locales/es-MX.yml +334 -0
- data/config/locales/es.yml +831 -0
- data/config/locales/et.yml +644 -0
- data/config/locales/fa.yml +709 -0
- data/config/locales/fi.yml +771 -0
- data/config/locales/fr.yml +830 -0
- data/config/locales/hu.yml +751 -0
- data/config/locales/id.yml +756 -0
- data/config/locales/it.yml +833 -0
- data/config/locales/ja.yml +726 -0
- data/config/locales/km.yml +759 -0
- data/config/locales/ko.yml +575 -0
- data/config/locales/ku.yml +753 -0
- data/config/locales/lt.yml +821 -0
- data/config/locales/lv.yml +685 -0
- data/config/locales/nb.yml +824 -0
- data/config/locales/ne.yml +823 -0
- data/config/locales/nl.yml +826 -0
- data/config/locales/pl.yml +835 -0
- data/config/locales/pt-BR.yml +547 -0
- data/config/locales/pt.yml +636 -0
- data/config/locales/ro.yml +750 -0
- data/config/locales/ru.yml +834 -0
- data/config/locales/sk.yml +680 -0
- data/config/locales/sl-SI.yml +600 -0
- data/config/locales/sv.yml +827 -0
- data/config/locales/th.yml +745 -0
- data/config/locales/tr.yml +777 -0
- data/config/locales/uk.yml +836 -0
- data/config/locales/vi.yml +768 -0
- data/config/locales/zh-CN.yml +786 -0
- data/config/locales/zh-TW.yml +834 -0
- data/db/migrate/20260925000001_add_replacement_to_spree_fulfillment_items.rb +5 -0
- data/db/migrate/20261001000001_add_post_sale_tax.rb +30 -0
- data/db/migrate/20261001000001_convert_preferences_to_json.rb +166 -0
- data/db/migrate/20261001000001_create_spree_email_templates.rb +52 -0
- data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
- data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
- data/lib/generators/spree/dummy/dummy_generator.rb +0 -9
- data/lib/generators/spree/dummy/templates/rails/test.rb +26 -0
- data/lib/spree/checkout/default_requirements.rb +37 -13
- data/lib/spree/core/configuration.rb +19 -1
- data/lib/spree/core/controller_helpers/store.rb +1 -1
- data/lib/spree/core/dependencies.rb +5 -4
- data/lib/spree/core/emails/escaped_output.rb +60 -0
- data/lib/spree/core/engine.rb +72 -165
- data/lib/spree/core/permissions/default_catalog.rb +5 -0
- data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
- data/lib/spree/core/preferences/json_conversion.rb +186 -0
- data/lib/spree/core/preferences/masking.rb +55 -2
- data/lib/spree/core/preferences/preferable.rb +343 -103
- data/lib/spree/core/preferences/preferable_class_methods.rb +188 -136
- data/lib/spree/core/previews/import_preview.rb +10 -0
- data/lib/spree/core/ransack_configuration.rb +13 -2
- data/lib/spree/core/tax/provider_error.rb +37 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +91 -373
- data/lib/spree/emails/editable_templates.rb +82 -0
- data/lib/spree/events/catalog.rb +189 -0
- data/lib/spree/events.rb +21 -1
- data/lib/spree/hooks.rb +0 -17
- data/lib/spree/i18n.rb +32 -15
- data/lib/spree/iso_data.rb +7 -0
- data/lib/spree/localized_number.rb +6 -0
- data/lib/spree/migrations.rb +4 -1
- data/lib/spree/money.rb +196 -15
- data/lib/spree/reporting/adapters/base.rb +7 -6
- data/lib/spree/reporting/adapters/live.rb +6 -6
- data/lib/spree/reporting/default_vocabulary.rb +22 -10
- data/lib/spree/reporting/hydration.rb +1 -1
- data/lib/spree/reporting/query.rb +8 -0
- data/lib/spree/reporting/registry.rb +13 -0
- data/lib/spree/reporting/schema.rb +10 -10
- data/lib/spree/testing_support/editable_email_templates.rb +61 -0
- data/lib/spree/testing_support/emails.rb +22 -0
- data/lib/spree/testing_support/factories/cart_factory.rb +1 -1
- data/lib/spree/testing_support/factories/email_template_factory.rb +28 -0
- data/lib/spree/testing_support/factories/order_factory.rb +2 -2
- data/lib/spree/testing_support/factories/product_factory.rb +9 -0
- data/lib/spree/testing_support/factories/refund_factory.rb +1 -1
- data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
- data/lib/spree/testing_support/factories/variant_factory.rb +1 -1
- data/lib/spree/testing_support/i18n.rb +3 -94
- data/lib/spree/testing_support/lifecycle_events.rb +1 -1
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/preferences.rb +0 -5
- data/lib/spree/testing_support/store.rb +9 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +67 -1
- data/lib/spree/uploads.rb +101 -0
- data/lib/tasks/categories_and_collections.rake +3 -3
- data/lib/tasks/core.rake +6 -4
- data/lib/tasks/markets.rake +2 -1
- data/lib/tasks/media.rake +3 -3
- data/lib/tasks/migrate_users_to_customers.rake +23 -18
- data/lib/tasks/money.rake +46 -0
- data/lib/tasks/package_types.rake +1 -1
- data/lib/tasks/preferences_json.rake +101 -0
- data/lib/tasks/seller_sample_data.rake +2 -2
- data/lib/tasks/store_settings.rake +13 -6
- data/lib/tasks/typed_adjustments_migration.rake +1 -1
- data/lib/tasks/uploads.rake +19 -0
- data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
- metadata +219 -46
- data/app/jobs/spree/products/touch_categories_job.rb +0 -15
- data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
- data/app/models/acts_as_taggable_on/tag_decorator.rb +0 -10
- data/app/models/concerns/spree/has_image_alt_text.rb +0 -18
- data/app/models/spree/base_analytics_event_handler.rb +0 -54
- data/app/models/spree/invitations/store.rb +0 -6
- data/app/models/spree/preference.rb +0 -14
- data/app/models/spree/promotion_handler/page.rb +0 -25
- data/app/models/spree/promotion_handler/promotion_duplicator.rb +0 -60
- data/app/models/spree/stock/differentiator.rb +0 -45
- data/app/presenters/spree/filters/options_presenter.rb +0 -47
- data/app/services/spree/carts/empty.rb +0 -42
- data/app/services/spree/country_to_timezone.rb +0 -273
- data/app/services/spree/payments/create.rb +0 -92
- data/app/validators/spree/url_validator.rb +0 -23
- data/app/views/active_storage/blobs/_blob.html.erb +0 -14
- data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +0 -8
- data/app/views/spree/export_mailer/export_done.html.erb +0 -7
- data/app/views/spree/import_mailer/import_done.html.erb +0 -12
- data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +0 -4
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +0 -12
- data/app/views/spree/seller_user_mailer/password_reset_email.html.erb +0 -8
- data/app/views/spree/shared/_base_mailer_footer.html.erb +0 -19
- data/app/views/spree/shared/_base_mailer_header.html.erb +0 -8
- data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +0 -399
- data/app/views/spree/shared/_mailer_logo.html.erb +0 -41
- data/app/views/spree/shared/_purchased_items_styles.html.erb +0 -119
- data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +0 -51
- data/app/views/spree/webhook_mailer/endpoint_disabled.text.erb +0 -10
- data/app/workflows/spree/fulfillments/purchase_label.rb +0 -40
- data/app/workflows/spree/returns/purchase_label.rb +0 -32
- data/lib/spree/analytics.rb +0 -11
- data/lib/spree/core/preferences/scoped_store.rb +0 -34
- data/lib/spree/core/preferences/store.rb +0 -105
- data/lib/spree/translation_migrations.rb +0 -52
|
@@ -47,6 +47,25 @@ module Spree
|
|
|
47
47
|
value.is_a?(String) && value.start_with?(TOKEN)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Money is written to at least the decimals of the currency the record
|
|
51
|
+
# names (a calculator's `currency` preference), keeping up to four like
|
|
52
|
+
# a unit price, so a per-item amount below a cent survives a round trip.
|
|
53
|
+
# A money preference with no currency of its own, such as an order-total
|
|
54
|
+
# rule that applies to any currency, keeps its exact decimal.
|
|
55
|
+
def self.wire_value(preferable, key, type, value)
|
|
56
|
+
case type
|
|
57
|
+
when :password then mask(value)
|
|
58
|
+
when :money
|
|
59
|
+
# Stored as the JSON text of a decimal.
|
|
60
|
+
amount = value.is_a?(String) ? BigDecimal(value, exception: false) : value
|
|
61
|
+
return value unless amount.is_a?(Numeric)
|
|
62
|
+
|
|
63
|
+
currency = preferable.money_preference_currency(key)
|
|
64
|
+
currency.present? ? Spree::Money::Rounding.format(amount, currency, unit_price: true) : Spree::Money::Rounding.format_decimal(amount)
|
|
65
|
+
else value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
50
69
|
# Serializes a Preferable's `preferences` hash for the wire,
|
|
51
70
|
# masking `:password` values. Keys are stringified to match the
|
|
52
71
|
# wire shape expected by JSON clients — schema entries built by
|
|
@@ -59,11 +78,45 @@ module Spree
|
|
|
59
78
|
def self.serialize(preferable)
|
|
60
79
|
return {} if preferable.nil?
|
|
61
80
|
|
|
81
|
+
definitions = preferable.class.preference_definitions
|
|
62
82
|
preferable.preference_schema.each_with_object({}) do |field, hash|
|
|
63
|
-
value
|
|
64
|
-
|
|
83
|
+
# The stored value only, never the default — an unset secret must not
|
|
84
|
+
# reveal what it would fall back to — read through the record,
|
|
85
|
+
# because a secret lives in its own column.
|
|
86
|
+
value = preferable.stored_preference(field[:key]) { nil }
|
|
87
|
+
definition = definitions.fetch(field[:key])
|
|
88
|
+
hash[field[:key_string] || field[:key].to_s] =
|
|
89
|
+
if definition[:of] == :id
|
|
90
|
+
prefixed_ids(value, definition)
|
|
91
|
+
elsif %i[password money].include?(field[:type])
|
|
92
|
+
wire_value(preferable, field[:key], field[:type], value)
|
|
93
|
+
elsif definition[:keys] == :currency && definition[:values] == :money
|
|
94
|
+
money_by_currency(preferable.wire_preference_value(value, definition))
|
|
95
|
+
else
|
|
96
|
+
preferable.wire_preference_value(value, definition)
|
|
97
|
+
end
|
|
65
98
|
end
|
|
66
99
|
end
|
|
100
|
+
|
|
101
|
+
# Amounts keyed by currency (`{ "EUR" => "12.5" }`), each written with
|
|
102
|
+
# the decimals of its own currency, as every money value is.
|
|
103
|
+
def self.money_by_currency(value)
|
|
104
|
+
return value unless value.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
value.to_h do |currency, amount|
|
|
107
|
+
decimal = BigDecimal(amount.to_s, exception: false)
|
|
108
|
+
[currency, decimal ? Spree::Money::Rounding.format(decimal, currency, unit_price: true) : amount]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Ids are stored as raw primary keys and leave as prefixed ids, the form
|
|
113
|
+
# every write accepts.
|
|
114
|
+
def self.prefixed_ids(value, definition)
|
|
115
|
+
return value unless value.is_a?(Array)
|
|
116
|
+
|
|
117
|
+
model = Spree::Preferences::Preferable.preference_model(definition)
|
|
118
|
+
value.map { |id| Spree::PrefixedId.prefixed_id?(id.to_s) ? id : model.prefixed_id_for(id) }
|
|
119
|
+
end
|
|
67
120
|
end
|
|
68
121
|
end
|
|
69
122
|
end
|
|
@@ -1,84 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Typed model preferences, each a Rails store accessor on the `preferences`
|
|
2
|
+
# JSON column — `secret_preferences` for a `:password` one (see
|
|
3
|
+
# {Spree::SecretPreferences}). Included by `Spree::Base`.
|
|
2
4
|
#
|
|
3
|
-
# A class including Preferable must implement #preferences which should return
|
|
4
|
-
# an object responding to .fetch(key), []=(key, val), and .delete(key).
|
|
5
|
-
#
|
|
6
|
-
# The generated writer method performs typecasting before assignment into the
|
|
7
|
-
# preferences object.
|
|
8
|
-
#
|
|
9
|
-
# Examples:
|
|
10
|
-
#
|
|
11
|
-
# # Spree::Base includes Preferable and defines preferences as a serialized
|
|
12
|
-
# # column.
|
|
13
5
|
# class Settings < Spree::Base
|
|
14
6
|
# preference :color, :string, default: 'red'
|
|
15
7
|
# preference :temperature, :integer, default: 21
|
|
16
8
|
# end
|
|
17
9
|
#
|
|
18
10
|
# s = Settings.new
|
|
19
|
-
# s.preferred_color
|
|
20
|
-
# s.preferred_temperature #
|
|
21
|
-
#
|
|
22
|
-
# s.
|
|
23
|
-
# s.preferred_color # => 'blue'
|
|
24
|
-
#
|
|
25
|
-
# # Typecasting is performed on assignment
|
|
26
|
-
# s.preferred_temperature = '24'
|
|
27
|
-
# s.preferred_temperature # => 24
|
|
28
|
-
#
|
|
29
|
-
# # Modifications have been made to the .preferences hash
|
|
30
|
-
# s.preferences #=> {color: 'blue', temperature: 24}
|
|
31
|
-
#
|
|
32
|
-
# # Save the changes. All handled by activerecord
|
|
33
|
-
# s.save!
|
|
11
|
+
# s.preferred_color # => 'red'
|
|
12
|
+
# s.preferred_temperature = '24' # cast on assignment
|
|
13
|
+
# s.preferred_temperature # => 24
|
|
14
|
+
# s.preferences # => { 'color' => 'red', 'temperature' => 24 }
|
|
34
15
|
|
|
35
16
|
require 'spree/core/preferences/preferable_class_methods'
|
|
36
17
|
|
|
37
18
|
module Spree::Preferences::Preferable
|
|
38
19
|
extend ActiveSupport::Concern
|
|
39
20
|
|
|
21
|
+
BOOLEAN_TYPE = ActiveModel::Type::Boolean.new
|
|
22
|
+
INTEGER_TYPE = ActiveModel::Type::Integer.new
|
|
23
|
+
STRING_TYPE = ActiveModel::Type::String.new
|
|
24
|
+
|
|
40
25
|
included do
|
|
41
|
-
serialize :preferences,
|
|
26
|
+
serialize :preferences, coder: Spree::Metadata::HashSerializer
|
|
42
27
|
extend Spree::Preferences::PreferableClassMethods
|
|
28
|
+
|
|
29
|
+
# The preferences read and written under their plain names (see `exposes_preferences`).
|
|
30
|
+
class_attribute :exposed_preference_names, instance_accessor: false, default: Set.new.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# JSON keeps decimals and times as strings; the declared type turns them
|
|
34
|
+
# back into a BigDecimal and a time. Anything else, or a string that does
|
|
35
|
+
# not parse, comes back as stored.
|
|
36
|
+
#
|
|
37
|
+
# @param value [Object] a value as JSON stores it
|
|
38
|
+
# @param type [Symbol] the preference's declared type
|
|
39
|
+
# @return [Object]
|
|
40
|
+
def self.restore_value(value, type)
|
|
41
|
+
return value unless value.is_a?(String)
|
|
42
|
+
|
|
43
|
+
case type
|
|
44
|
+
when :decimal, :money then BigDecimal(value, exception: false) || value
|
|
45
|
+
when :datetime then Time.zone.iso8601(value)
|
|
46
|
+
else value
|
|
47
|
+
end
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The record class an `of: :id` preference points at. Declared as a class
|
|
53
|
+
# name or a block, so a declaration never loads another model, nor fixes a
|
|
54
|
+
# configurable class like `Spree.customer_class`, at class load.
|
|
55
|
+
#
|
|
56
|
+
# @param definition [Hash] a preference definition
|
|
57
|
+
# @return [Class]
|
|
58
|
+
def self.preference_model(definition)
|
|
59
|
+
model = definition[:model]
|
|
60
|
+
model.respond_to?(:call) ? model.call : model.to_s.constantize
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
def get_preference(name)
|
|
46
64
|
has_preference! name
|
|
47
|
-
|
|
65
|
+
public_send(:"preferred_#{name}")
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
def set_preference(name, value)
|
|
51
69
|
has_preference! name
|
|
52
|
-
|
|
70
|
+
public_send(:"preferred_#{name}=", value)
|
|
53
71
|
end
|
|
54
72
|
|
|
55
73
|
def preference_type(name)
|
|
56
|
-
|
|
57
|
-
send self.class.preference_type_getter_method(name)
|
|
74
|
+
preference_definition(name)[:type]
|
|
58
75
|
end
|
|
59
76
|
|
|
60
77
|
def preference_default(name)
|
|
61
|
-
|
|
62
|
-
send self.class.preference_default_getter_method(name)
|
|
78
|
+
instance_exec(&preference_definition(name)[:default])
|
|
63
79
|
end
|
|
64
80
|
|
|
65
81
|
def preference_deprecated(name)
|
|
66
|
-
|
|
67
|
-
send(self.class.preference_deprecated_getter_method(name))
|
|
82
|
+
preference_definition(name)[:deprecated]
|
|
68
83
|
end
|
|
69
84
|
|
|
70
85
|
# Whether this preference is written by Spree rather than supplied by the
|
|
71
86
|
# operator — a webhook secret a provider issues, say. Kept out of the
|
|
72
87
|
# schema, so nothing offers it as a field to fill in.
|
|
73
88
|
def preference_internal(name)
|
|
74
|
-
|
|
75
|
-
getter = self.class.preference_internal_getter_method(name)
|
|
76
|
-
# A preference declared before this option existed has no such reader.
|
|
77
|
-
# Treated as not internal rather than raising, so one old declaration
|
|
78
|
-
# cannot take a whole class's preference schema down with it.
|
|
79
|
-
return false unless respond_to?(getter)
|
|
80
|
-
|
|
81
|
-
send(getter)
|
|
89
|
+
preference_definition(name)[:internal]
|
|
82
90
|
end
|
|
83
91
|
|
|
84
92
|
# The fixed set this preference's value must come from, when it has one.
|
|
@@ -87,14 +95,26 @@ module Spree::Preferences::Preferable
|
|
|
87
95
|
#
|
|
88
96
|
# @return [Array, nil]
|
|
89
97
|
def preference_choices(name)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# Treated as unconstrained rather than raising, so one old declaration
|
|
94
|
-
# cannot take a whole class's preference schema down with it.
|
|
95
|
-
return unless respond_to?(getter)
|
|
98
|
+
choices = preference_definition(name)[:choices]
|
|
99
|
+
choices.respond_to?(:call) ? choices.call : choices
|
|
100
|
+
end
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
# The currency a `:money` preference is in: its declared `currency:` source
|
|
103
|
+
# (another preference, or a method of the record), else the record's own
|
|
104
|
+
# `currency` preference. Values about to be assigned win over stored ones,
|
|
105
|
+
# so a request changing both the amount and its currency is read as one.
|
|
106
|
+
#
|
|
107
|
+
# @param name [Symbol, String]
|
|
108
|
+
# @param pending [Hash{String => Object}] values not yet assigned
|
|
109
|
+
# @return [String, nil] nil for an amount in any currency
|
|
110
|
+
def money_preference_currency(name, pending = {})
|
|
111
|
+
declared = preference_definition(name)[:currency]
|
|
112
|
+
source = declared || :currency
|
|
113
|
+
# A currency sent in the same payload wins, even a blank one clearing it.
|
|
114
|
+
return pending[source.to_s].presence if pending.key?(source.to_s)
|
|
115
|
+
return get_preference(source).presence if has_preference?(source)
|
|
116
|
+
|
|
117
|
+
public_send(source).presence if declared && respond_to?(source)
|
|
98
118
|
end
|
|
99
119
|
|
|
100
120
|
def has_preference!(name)
|
|
@@ -102,92 +122,310 @@ module Spree::Preferences::Preferable
|
|
|
102
122
|
end
|
|
103
123
|
|
|
104
124
|
def has_preference?(name)
|
|
105
|
-
|
|
125
|
+
self.class.preference_definitions.key?(name.to_sym)
|
|
106
126
|
end
|
|
107
127
|
|
|
108
128
|
def defined_preferences
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
self.class.preference_definitions.keys
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def default_preferences
|
|
133
|
+
defined_preferences.index_with { |name| preference_default(name) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Every preference value, secrets included — what `preferences` alone held
|
|
137
|
+
# before secrets moved to their own column. Declared preferences read
|
|
138
|
+
# through their `preferred_*` reader, so a default applies and decimals and
|
|
139
|
+
# times come back typed rather than in the string form JSON stores.
|
|
140
|
+
#
|
|
141
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
|
142
|
+
def preference_values
|
|
143
|
+
(preferences || {}).to_h.with_indifferent_access.merge(defined_preferences.index_with { |name| get_preference(name) })
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The stored value of a preference, before its default applies. A secret
|
|
147
|
+
# found in `preferences` wins over the encrypted column: it is either a
|
|
148
|
+
# newer value assigned as part of a whole hash, waiting for the save that
|
|
149
|
+
# moves it across, or one the upgrade has not moved yet — the writer always
|
|
150
|
+
# removes that copy, so it is never stale.
|
|
151
|
+
#
|
|
152
|
+
# @param name [Symbol, String]
|
|
153
|
+
# @return [Object] the stored value, or the block's result when nothing is stored
|
|
154
|
+
def stored_preference(name)
|
|
155
|
+
if secret_preference?(name)
|
|
156
|
+
(preferences || {}).fetch(name) { (secret_preferences || {}).fetch(name) { return yield } }
|
|
157
|
+
else
|
|
158
|
+
(preferences || {}).fetch(name) { yield }
|
|
115
159
|
end
|
|
116
160
|
end
|
|
117
161
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
162
|
+
# Fills in the declared default of every preference this record has no
|
|
163
|
+
# stored value for, so a preference added to the class after the row was
|
|
164
|
+
# saved never reads as missing and the raw `preferences` hash always holds
|
|
165
|
+
# every value. Secrets are left out: their defaults are read through the
|
|
166
|
+
# `preferred_*` reader, never copied into storage.
|
|
167
|
+
#
|
|
168
|
+
# Filling in a default is not a change to a stored record: a loaded record
|
|
169
|
+
# stays clean, so `with_lock` still accepts it, and the defaults are written
|
|
170
|
+
# with its next real save.
|
|
171
|
+
def backfill_default_preferences
|
|
172
|
+
secrets = self.class.secret_preference_names
|
|
173
|
+
missing = defined_preferences.reject { |name| preferences.key?(name) || secrets.include?(name) }
|
|
174
|
+
return if missing.empty?
|
|
175
|
+
|
|
176
|
+
already_changed = attribute_changed?(:preferences)
|
|
177
|
+
self.preferences = preferences.merge(missing.index_with { |name| stored_default(name) })
|
|
178
|
+
clear_attribute_change(:preferences) if persisted? && !already_changed
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Writes a `preferences` payload an API client sent. The whole payload is
|
|
182
|
+
# checked against the type's schema first, so nothing is written when any
|
|
183
|
+
# value is wrong. Then each value goes through its typed writer. A secret
|
|
184
|
+
# sent back masked, as it was read, keeps the stored one; `null` clears it.
|
|
185
|
+
#
|
|
186
|
+
# @param values [Hash, ActionController::Parameters]
|
|
187
|
+
# @param pointer [String] where the preferences object sits in the request,
|
|
188
|
+
# e.g. `/rules/1/preferences`, so a failure names its place
|
|
189
|
+
# @raise [Spree::Preferences::InvalidPreferences] naming every value that does not match
|
|
190
|
+
# @return [void]
|
|
191
|
+
def assign_preferences(values, pointer: '/preferences')
|
|
192
|
+
values = values.respond_to?(:to_unsafe_h) ? values.to_unsafe_h : values.to_h
|
|
193
|
+
values = values.deep_stringify_keys
|
|
194
|
+
failures = self.class.preference_failures(values)
|
|
195
|
+
failures += money_precision_failures(values) if failures.empty?
|
|
196
|
+
raise Spree::Preferences::InvalidPreferences.new(failures, prefix: pointer) if failures.any?
|
|
197
|
+
|
|
198
|
+
values.each do |key, value|
|
|
199
|
+
next if secret_preference?(key) && Spree::Preferences::Masking.masked?(value)
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
set_preference(key, value)
|
|
203
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
204
|
+
raise Spree::Preferences::InvalidPreferences.new([{ pointer: "/#{key}", message: e.message }], prefix: pointer)
|
|
205
|
+
end
|
|
122
206
|
end
|
|
123
207
|
end
|
|
124
208
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
209
|
+
# A stored value in the form the schema describes. Rows written before the
|
|
210
|
+
# declarations were typed can hold a number where an exact decimal string
|
|
211
|
+
# is declared (an `amounts` hash, a decimal default from YAML), or a string
|
|
212
|
+
# where a number or boolean is; read back as they are, a client sending
|
|
213
|
+
# them unchanged would be refused.
|
|
214
|
+
#
|
|
215
|
+
# @param value [Object] the stored value
|
|
216
|
+
# @param definition [Hash] its preference definition
|
|
217
|
+
# @return [Object]
|
|
218
|
+
def wire_preference_value(value, definition)
|
|
219
|
+
return BigDecimal(value.to_s).as_json if %i[decimal money].include?(definition[:type]) && value.is_a?(Numeric)
|
|
220
|
+
return nil if value == '' && definition[:nullable] && %i[integer boolean].include?(definition[:type])
|
|
221
|
+
return cast_preference_item(value, :integer) if definition[:type] == :integer && value.is_a?(String)
|
|
222
|
+
return convert_preference_value(value, :boolean, nullable: definition[:nullable]) if definition[:type] == :boolean && value.is_a?(String)
|
|
223
|
+
return value if value.nil? || (definition[:type] == :array && !value.is_a?(Array))
|
|
224
|
+
|
|
225
|
+
cast_preference_contents(value, definition)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Names of the preferences the last save changed, secrets included.
|
|
229
|
+
#
|
|
230
|
+
# @return [Array<Symbol>]
|
|
231
|
+
def previously_changed_preference_names
|
|
232
|
+
defined_preferences.select { |name| public_send(:"saved_change_to_preferred_#{name}?") }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
# A `:money` value with more decimals than its currency has is refused, as
|
|
238
|
+
# every other amount on the API is. The schema has already checked the
|
|
239
|
+
# format; it cannot know the currency.
|
|
240
|
+
def money_precision_failures(values)
|
|
241
|
+
values.filter_map do |key, value|
|
|
242
|
+
next unless value.is_a?(String) && has_preference?(key) && preference_type(key) == :money
|
|
243
|
+
|
|
244
|
+
currency = money_preference_currency(key, values)
|
|
245
|
+
amount = BigDecimal(value, exception: false)
|
|
246
|
+
next if currency.blank? || amount.nil?
|
|
247
|
+
|
|
248
|
+
places = Spree::Money::Rounding.precision(currency)
|
|
249
|
+
next if amount.round(places) == amount
|
|
250
|
+
|
|
251
|
+
{ pointer: "/#{key}", message: I18n.t('spree.errors.messages.money_preference_too_many_decimals', count: places, currency: currency) }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def preference_definition(name)
|
|
256
|
+
has_preference! name
|
|
257
|
+
self.class.preference_definitions[name.to_sym]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# A declared default in the form JSON stores, as the writer would store it,
|
|
261
|
+
# so a decimal default reads back as a BigDecimal and setting the same
|
|
262
|
+
# amount is not a change.
|
|
263
|
+
def stored_default(name)
|
|
264
|
+
default = preference_default(name)
|
|
265
|
+
type = preference_type(name)
|
|
266
|
+
return default if default.nil? || %i[decimal money datetime].exclude?(type)
|
|
267
|
+
|
|
268
|
+
convert_preference_value(default, type, nullable: true).as_json
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Only a class that stores secrets can declare one, so the names say it all.
|
|
272
|
+
def secret_preference?(name)
|
|
273
|
+
self.class.secret_preference_names.include?(name.to_sym)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def cast_preference(name, value)
|
|
277
|
+
definition = preference_definition(name)
|
|
278
|
+
parse_on_set = definition[:parse_on_set]
|
|
279
|
+
if parse_on_set.is_a?(Proc)
|
|
280
|
+
# Procs that accept more than one arg opt into receiving the owning
|
|
281
|
+
# record so they can scope (e.g. `normalize_id_preference` rejecting
|
|
282
|
+
# cross-store IDs). `arity.abs > 1` covers both the `(value, owner)` and
|
|
283
|
+
# `(value, owner = nil)` shapes.
|
|
284
|
+
value = parse_on_set.arity.abs > 1 ? parse_on_set.call(value, self) : parse_on_set.call(value)
|
|
285
|
+
end
|
|
286
|
+
# Spree 6.0 bridge, removed with incomplete declarations in 6.1: an id list
|
|
287
|
+
# an extension declared without `of: :id` still has its prefixed ids
|
|
288
|
+
# decoded, as the API did for every `*_ids` key before.
|
|
289
|
+
if definition[:type] == :array && definition[:of].nil? && name.to_s.end_with?('_ids') && value.is_a?(Array)
|
|
290
|
+
value = value.map { |id| (Spree::PrefixedId.decode_prefixed_id(id) if Spree::PrefixedId.prefixed_id?(id)) || id }
|
|
291
|
+
end
|
|
292
|
+
value = convert_preference_value(value, definition[:type], nullable: definition[:nullable])
|
|
293
|
+
value = cast_preference_contents(value, definition)
|
|
294
|
+
# Decimals and times are kept in the string form JSON stores, so a record
|
|
295
|
+
# read back from the database and one just written compare equal and
|
|
296
|
+
# setting the same value is not a change. The reader restores them.
|
|
297
|
+
value = value.as_json if %i[decimal money datetime].include?(definition[:type])
|
|
298
|
+
|
|
299
|
+
Spree::Deprecation.warn("`#{name}` is deprecated. #{definition[:deprecated]}") if definition[:deprecated]
|
|
300
|
+
value
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Casts what a typed `:array` or `:hash` holds to its declared item type, in
|
|
304
|
+
# the form JSON stores. A value that does not cast is left as it came, so
|
|
305
|
+
# validation can name it rather than a silent zero hiding it.
|
|
306
|
+
def cast_preference_contents(value, definition)
|
|
307
|
+
case definition[:type]
|
|
308
|
+
when :array
|
|
309
|
+
case definition[:of]
|
|
310
|
+
when nil then value
|
|
311
|
+
when :id then decode_preference_ids(value, definition)
|
|
312
|
+
when :object then value.map { |item| cast_preference_object(item, definition[:properties]) }
|
|
313
|
+
else split_preference_list(value).map { |item| cast_preference_item(item, definition[:of]) }
|
|
314
|
+
end
|
|
315
|
+
when :hash
|
|
316
|
+
return value unless definition[:values] && value.is_a?(Hash)
|
|
317
|
+
|
|
318
|
+
value.to_h do |key, item|
|
|
319
|
+
key = definition[:keys] == :currency ? key.to_s.upcase : key.to_s
|
|
320
|
+
[key, cast_preference_item(item, definition[:values])]
|
|
129
321
|
end
|
|
130
|
-
|
|
322
|
+
else
|
|
323
|
+
value
|
|
324
|
+
end
|
|
131
325
|
end
|
|
132
326
|
|
|
133
|
-
|
|
134
|
-
|
|
327
|
+
# A comma-separated string is a list too: what a plain text field sends.
|
|
328
|
+
def split_preference_list(values)
|
|
329
|
+
values.flat_map { |item| item.is_a?(String) ? item.split(',') : [item] }
|
|
330
|
+
.map { |item| item.is_a?(String) ? item.strip : item }
|
|
331
|
+
.reject { |item| item.respond_to?(:empty?) && item.empty? }
|
|
135
332
|
end
|
|
136
333
|
|
|
137
|
-
def
|
|
138
|
-
|
|
334
|
+
def cast_preference_item(value, item_type)
|
|
335
|
+
case item_type
|
|
336
|
+
when :string then value.to_s
|
|
337
|
+
when :integer then Integer(value.to_s, 10, exception: false) || value
|
|
338
|
+
when :decimal, :money then BigDecimal(value.to_s, exception: false)&.as_json || value
|
|
339
|
+
when :boolean then BOOLEAN_TYPE.cast(value)
|
|
340
|
+
else value
|
|
341
|
+
end
|
|
139
342
|
end
|
|
140
343
|
|
|
141
|
-
def
|
|
142
|
-
|
|
344
|
+
def cast_preference_object(item, properties)
|
|
345
|
+
return item unless item.respond_to?(:to_h) && !item.is_a?(Array)
|
|
346
|
+
|
|
347
|
+
item.to_h.stringify_keys.slice(*properties.keys.map(&:to_s)).to_h do |key, value|
|
|
348
|
+
[key, cast_preference_item(value, properties[key.to_sym])]
|
|
349
|
+
end
|
|
143
350
|
end
|
|
144
351
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
352
|
+
# Turns a list of record ids into the raw primary keys the preference
|
|
353
|
+
# stores. A prefixed id must carry the declared model's prefix — a
|
|
354
|
+
# channel's id never decodes into a market id — and every id must belong to
|
|
355
|
+
# the record's scope, so a rule cannot point at another store's records.
|
|
356
|
+
#
|
|
357
|
+
# @raise [ActiveRecord::RecordNotFound] naming the ids that were not found
|
|
358
|
+
def decode_preference_ids(values, definition)
|
|
359
|
+
model = Spree::Preferences::Preferable.preference_model(definition)
|
|
360
|
+
ids = split_preference_list(values).map(&:to_s).map do |id|
|
|
361
|
+
Spree::PrefixedId.prefixed_id?(id) ? (model.decode_prefixed_id(id)&.to_s || id) : id
|
|
362
|
+
end
|
|
363
|
+
return ids if ids.empty?
|
|
364
|
+
|
|
365
|
+
relation = definition[:scope] ? definition[:scope].call(self) : model
|
|
366
|
+
found = relation.where(id: ids.reject { |id| Spree::PrefixedId.prefixed_id?(id) }).pluck(:id).map(&:to_s).to_set
|
|
367
|
+
missing = ids.reject { |id| found.include?(id) }
|
|
368
|
+
if missing.any?
|
|
369
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{model.name} with id=#{missing.join(',')}", model.name)
|
|
370
|
+
end
|
|
149
371
|
|
|
150
|
-
|
|
372
|
+
ids
|
|
373
|
+
end
|
|
151
374
|
|
|
152
|
-
|
|
375
|
+
# Assigns a new hash rather than calling the store accessor's writer, which
|
|
376
|
+
# skips writing nil to a missing key — leaving the default in force when nil
|
|
377
|
+
# was meant. Rails still sees no change when the value is the one stored.
|
|
378
|
+
def write_preference(name, value)
|
|
379
|
+
if secret_preference?(name)
|
|
380
|
+
# A copy assigned as part of a whole hash would otherwise outlive the
|
|
381
|
+
# value written here and be moved over it on save.
|
|
382
|
+
self.preferences = preferences.except(name) if preferences&.key?(name)
|
|
383
|
+
self.secret_preferences = (secret_preferences || {}).with_indifferent_access.merge(name => value)
|
|
384
|
+
else
|
|
385
|
+
self.preferences = (preferences || {}).with_indifferent_access.merge(name => value)
|
|
386
|
+
end
|
|
153
387
|
end
|
|
154
388
|
|
|
155
|
-
|
|
389
|
+
# Whitespace alone is no number or boolean, so for those it is blank too; a
|
|
390
|
+
# string keeps it.
|
|
391
|
+
def blank_preference_value?(value, type = nil)
|
|
392
|
+
return true if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
393
|
+
|
|
394
|
+
%i[integer boolean].include?(type) && value.is_a?(String) && value.strip.empty?
|
|
395
|
+
end
|
|
156
396
|
|
|
157
397
|
def convert_preference_value(value, type, nullable: false)
|
|
158
398
|
case type
|
|
399
|
+
# Strings, integers and booleans cast the way Rails casts attributes (and
|
|
400
|
+
# `has_json` its keys). Our own rule on top: a blank value stays nil for a
|
|
401
|
+
# nullable preference, so it can fall back to another, and becomes the
|
|
402
|
+
# type's empty value otherwise.
|
|
159
403
|
when :string, :text
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
163
|
-
nil
|
|
404
|
+
if blank_preference_value?(value)
|
|
405
|
+
nullable ? nil : ''
|
|
164
406
|
else
|
|
165
|
-
value
|
|
407
|
+
STRING_TYPE.cast(value)
|
|
166
408
|
end
|
|
167
409
|
when :password
|
|
168
410
|
value.to_s
|
|
169
|
-
when :decimal
|
|
170
|
-
|
|
411
|
+
when :decimal, :money
|
|
412
|
+
# A number or canonical decimal text only: "1,599.99" raises rather
|
|
413
|
+
# than being read as 1.
|
|
414
|
+
decimal_value = value.is_a?(String) ? value.presence : value
|
|
171
415
|
decimal_value ||= 0 unless nullable
|
|
172
|
-
decimal_value.
|
|
416
|
+
decimal_value.nil? ? nil : Spree::Money::Rounding.parse_decimal(decimal_value)
|
|
173
417
|
when :integer
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
418
|
+
if blank_preference_value?(value, :integer)
|
|
419
|
+
nullable ? nil : 0
|
|
420
|
+
else
|
|
421
|
+
INTEGER_TYPE.cast(value) || 0
|
|
422
|
+
end
|
|
177
423
|
when :boolean
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
182
|
-
nil
|
|
183
|
-
elsif value.is_a?(FalseClass) ||
|
|
184
|
-
value.nil? ||
|
|
185
|
-
value == 0 ||
|
|
186
|
-
value&.to_s =~ /^(f|false|0)$/i ||
|
|
187
|
-
(value.respond_to?(:empty?) && value.empty?)
|
|
188
|
-
false
|
|
424
|
+
# An explicit false is kept: it is neither nil nor empty.
|
|
425
|
+
if blank_preference_value?(value, :boolean)
|
|
426
|
+
nullable ? nil : false
|
|
189
427
|
else
|
|
190
|
-
|
|
428
|
+
BOOLEAN_TYPE.cast(value)
|
|
191
429
|
end
|
|
192
430
|
when :array
|
|
193
431
|
value.is_a?(Array) ? value : Array.wrap(value)
|
|
@@ -195,6 +433,8 @@ module Spree::Preferences::Preferable
|
|
|
195
433
|
case value.class.to_s
|
|
196
434
|
when 'Hash'
|
|
197
435
|
value
|
|
436
|
+
when 'ActionController::Parameters'
|
|
437
|
+
value.to_h
|
|
198
438
|
when 'String'
|
|
199
439
|
# only works with hashes whose keys are strings
|
|
200
440
|
JSON.parse value.gsub('=>', ':')
|
|
@@ -221,7 +461,7 @@ module Spree::Preferences::Preferable
|
|
|
221
461
|
value.to_time
|
|
222
462
|
end
|
|
223
463
|
# Kept as the `yyyy-MM-dd` string it arrived as rather than coerced to a
|
|
224
|
-
# Time. Preferences are
|
|
464
|
+
# Time. Preferences are stored as JSON, and what a date *means* depends on
|
|
225
465
|
# the store's timezone, which this method has no access to: coercing here
|
|
226
466
|
# would freeze the value against the server's zone and move the operator's
|
|
227
467
|
# deadline by hours. The reader owns that (see
|