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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# One Liquid email template: a file on disk, with optional front matter
|
|
4
|
+
# holding its subject, or a store's saved version with the subject in its
|
|
5
|
+
# own field.
|
|
6
|
+
class Template
|
|
7
|
+
FRONT_MATTER = /\A---\s*\n(.*?)\n---\s*(?:\n|\z)/m
|
|
8
|
+
|
|
9
|
+
attr_reader :key, :path
|
|
10
|
+
|
|
11
|
+
# @param key [String] the template key, e.g. "spree/order_mailer/confirm_email"
|
|
12
|
+
# @param path [String, nil] the file on disk
|
|
13
|
+
# @param subject [String, nil] the subject, for a template not read from a file
|
|
14
|
+
# @param body [String, nil] the body, for a template not read from a file
|
|
15
|
+
def initialize(key:, path: nil, subject: nil, body: nil)
|
|
16
|
+
@key = key
|
|
17
|
+
@path = path
|
|
18
|
+
@subject = subject
|
|
19
|
+
@body = body
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] whether this is a store's saved template or a draft, rather than a file
|
|
23
|
+
def stored?
|
|
24
|
+
path.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] the subject line as Liquid
|
|
28
|
+
def subject
|
|
29
|
+
stored? ? @subject : front_matter['subject']
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String] the template without its front matter
|
|
33
|
+
def body
|
|
34
|
+
stored? ? @body.to_s : source.sub(FRONT_MATTER, '')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def source
|
|
40
|
+
@source ||= File.read(path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def front_matter
|
|
44
|
+
@front_matter ||= begin
|
|
45
|
+
match = source.match(FRONT_MATTER)
|
|
46
|
+
match ? YAML.safe_load(match[1]).to_h : {}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Turns a record into the JSON an email template reads: the serializer's
|
|
4
|
+
# output, the same data a renderer outside Ruby would receive. Shared by
|
|
5
|
+
# mailers and by the sample data the template editor previews with.
|
|
6
|
+
# Download links and other bearer tokens stay out of it.
|
|
7
|
+
module TemplateData
|
|
8
|
+
# @param object [Object, nil] the record
|
|
9
|
+
# @param serializer [Class] an email serializer
|
|
10
|
+
# @param store [Spree::Store]
|
|
11
|
+
# @param currency [String, nil] defaults to the store's
|
|
12
|
+
# @param params [Hash] serializer params over the defaults
|
|
13
|
+
# @return [Hash, nil]
|
|
14
|
+
def self.call(object, serializer, store:, currency: nil, **params)
|
|
15
|
+
return if object.nil?
|
|
16
|
+
|
|
17
|
+
params = { store: store, currency: currency || store.default_currency, locale: I18n.locale.to_s,
|
|
18
|
+
storefront_url: store.storefront_url.to_s.chomp('/'), hide_credentials: true }.merge(params)
|
|
19
|
+
JSON.parse(serializer.new(object, params: params).serialize)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Finds the Liquid template behind an email key.
|
|
4
|
+
#
|
|
5
|
+
# With a store, an editable template (see Spree.editable_email_templates)
|
|
6
|
+
# is looked up in the store first: its published version in the email's
|
|
7
|
+
# language, then its version for every language. Otherwise, and for every
|
|
8
|
+
# other template, the app's own file wins over the one a gem ships.
|
|
9
|
+
# `drafts` puts unsaved templates in front of everything, for previews and
|
|
10
|
+
# for checking a draft before it is published.
|
|
11
|
+
class TemplateResolver
|
|
12
|
+
KEY_FORMAT = %r{\A[a-z0-9_]+(?:/[a-z0-9_]+)*\z}
|
|
13
|
+
|
|
14
|
+
# @return [Spree::Store, nil] the store whose published templates apply
|
|
15
|
+
attr_reader :store
|
|
16
|
+
|
|
17
|
+
class InvalidKey < ArgumentError; end
|
|
18
|
+
|
|
19
|
+
# @param options [Hash] see {#initialize}
|
|
20
|
+
# @return [Spree::Emails::TemplateResolver] searching the directories Spree's mailers render from
|
|
21
|
+
def self.for_mailers(**options)
|
|
22
|
+
new(Spree::BaseMailer.view_paths.paths.map(&:path), **options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param view_paths [Array<String, Pathname>] directories to search, highest precedence first
|
|
26
|
+
# @param store [Spree::Store, nil] the store whose published templates apply
|
|
27
|
+
# @param locale [String, Symbol, nil] the email's language
|
|
28
|
+
# @param drafts [Hash{String => Spree::Emails::Template}] unsaved templates by key
|
|
29
|
+
def initialize(view_paths, store: nil, locale: nil, drafts: {})
|
|
30
|
+
@view_paths = view_paths.map(&:to_s).uniq
|
|
31
|
+
@store = store
|
|
32
|
+
@locale = locale.to_s
|
|
33
|
+
@drafts = drafts
|
|
34
|
+
@stored = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param key [String] e.g. "spree/order_mailer/confirm_email"
|
|
38
|
+
# @return [Spree::Emails::Template, nil]
|
|
39
|
+
def find(key)
|
|
40
|
+
validate!(key)
|
|
41
|
+
@drafts[key] || stored(key) || find_file(key, '.liquid')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The default: what answers for the key without the store's templates or drafts.
|
|
45
|
+
#
|
|
46
|
+
# @param key [String]
|
|
47
|
+
# @return [Spree::Emails::Template, nil]
|
|
48
|
+
def find_default(key)
|
|
49
|
+
validate!(key)
|
|
50
|
+
Spree.editable_email_templates[key]&.kind == :partial ? partial_file(key) : find_file(key, '.liquid')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A hand-written plain-text part. A store's saved template has none, so
|
|
54
|
+
# its text is always generated from its HTML.
|
|
55
|
+
#
|
|
56
|
+
# @param key [String]
|
|
57
|
+
# @return [Spree::Emails::Template, nil]
|
|
58
|
+
def find_text(key)
|
|
59
|
+
validate!(key)
|
|
60
|
+
return if @drafts[key] || stored(key)
|
|
61
|
+
|
|
62
|
+
find_file(key, '.text.liquid')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param name [String] a partial name, e.g. "spree/shared/line_item"
|
|
66
|
+
# @return [Spree::Emails::Template, nil] the store's version, or "spree/shared/_line_item.liquid"
|
|
67
|
+
def find_partial(name)
|
|
68
|
+
validate!(name)
|
|
69
|
+
@drafts[name] || stored(name) || partial_file(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Boolean] whether a store's published template answered any lookup so far
|
|
73
|
+
def stored_templates_used?
|
|
74
|
+
@stored.values.any?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
attr_reader :view_paths
|
|
80
|
+
|
|
81
|
+
def stored(key)
|
|
82
|
+
return unless @store && Spree.editable_email_templates.include?(key)
|
|
83
|
+
return @stored[key] if @stored.key?(key)
|
|
84
|
+
|
|
85
|
+
record = @store.email_templates.published.where(key: key, locale: [@locale, Spree::EmailTemplate::ANY_LOCALE]).
|
|
86
|
+
min_by { |template| template.locale == Spree::EmailTemplate::ANY_LOCALE ? 1 : 0 }
|
|
87
|
+
@stored[key] = record && Template.new(key: key, subject: record.subject, body: record.body)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def partial_file(name)
|
|
91
|
+
*folders, file = name.split('/')
|
|
92
|
+
find_file([*folders, "_#{file}"].join('/'), '.liquid')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def find_file(key, extension)
|
|
96
|
+
view_paths.each do |view_path|
|
|
97
|
+
path = File.join(view_path, "#{key}#{extension}")
|
|
98
|
+
return Template.new(key: key, path: path) if File.file?(path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate!(key)
|
|
105
|
+
raise InvalidKey, "Invalid email template name: #{key.inspect}" unless key.to_s.match?(KEY_FORMAT)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Turns a rendered email's HTML into its plain-text part, keeping each
|
|
4
|
+
# link as "label (url)". Generating the text rather than writing it by
|
|
5
|
+
# hand means it can never drift from the HTML.
|
|
6
|
+
class TextConverter
|
|
7
|
+
BLOCK_ELEMENTS = %w[p div h1 h2 h3 h4 h5 h6 table tr ul ol li section blockquote].freeze
|
|
8
|
+
SKIPPED_ELEMENTS = %w[head style script title img].freeze
|
|
9
|
+
HIDDEN_STYLE = /display\s*:\s*none/i
|
|
10
|
+
|
|
11
|
+
# @param html [String]
|
|
12
|
+
# @return [String]
|
|
13
|
+
def self.call(html)
|
|
14
|
+
new(html).call
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(html)
|
|
18
|
+
@document = Nokogiri::HTML(html)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String]
|
|
22
|
+
def call
|
|
23
|
+
output = +''
|
|
24
|
+
walk(@document.at('body') || @document, output)
|
|
25
|
+
|
|
26
|
+
output.lines.map { |line| line.squeeze(' ').strip }.join("\n").gsub(/\n{3,}/, "\n\n").strip + "\n"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def walk(node, output)
|
|
32
|
+
node.children.each do |child|
|
|
33
|
+
if child.text?
|
|
34
|
+
output << child.text.gsub(/\s+/, ' ')
|
|
35
|
+
elsif child.element?
|
|
36
|
+
element(child, output)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def element(node, output)
|
|
42
|
+
name = node.name.downcase
|
|
43
|
+
return if SKIPPED_ELEMENTS.include?(name) || node['style'].to_s.match?(HIDDEN_STYLE)
|
|
44
|
+
|
|
45
|
+
case name
|
|
46
|
+
when 'br' then output << "\n"
|
|
47
|
+
when 'a' then link(node, output)
|
|
48
|
+
when 'td', 'th'
|
|
49
|
+
walk(node, output)
|
|
50
|
+
output << ' '
|
|
51
|
+
else
|
|
52
|
+
block = BLOCK_ELEMENTS.include?(name)
|
|
53
|
+
output << "\n" if block
|
|
54
|
+
walk(node, output)
|
|
55
|
+
output << "\n" if block
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def link(node, output)
|
|
60
|
+
label = +''
|
|
61
|
+
walk(node, label)
|
|
62
|
+
label = label.squeeze(' ').strip
|
|
63
|
+
href = node['href'].to_s.strip.delete_prefix('mailto:')
|
|
64
|
+
|
|
65
|
+
# A link with no text is a linked image, which has nothing to say in text.
|
|
66
|
+
return if label.blank?
|
|
67
|
+
|
|
68
|
+
output << (href.blank? || href == label ? label : "#{label} (#{href})")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Turns Spree's translation keys in a template into readable text in one
|
|
4
|
+
# language, for a merchant who starts editing it:
|
|
5
|
+
#
|
|
6
|
+
# {{ 'order_mailer.confirm_email.dear_customer' | t: name: order.customer_name }}
|
|
7
|
+
# # => Dear {{ order.customer_name }},
|
|
8
|
+
#
|
|
9
|
+
# {% assign heading = 'order_mailer.confirm_email.order_summary' | t: number: order.number %}
|
|
10
|
+
# # => {% capture heading %}Order {{ order.number }} Summary{% endcapture %}
|
|
11
|
+
#
|
|
12
|
+
# A use it cannot turn into text exactly (a plural form, a missing
|
|
13
|
+
# translation, a value it does not recognize) is left as it was, so the
|
|
14
|
+
# template still renders the same.
|
|
15
|
+
class TranslationInliner
|
|
16
|
+
# Tags are found with simple patterns and read with string handling, so
|
|
17
|
+
# matching stays linear on any input a merchant pastes in.
|
|
18
|
+
OUTPUT_TAG = /\{\{([^{}]*)\}\}/
|
|
19
|
+
ASSIGN_TAG = /\{%([^{}%]*)%\}/
|
|
20
|
+
TRANSLATED = /\A'([a-z0-9_.]+)'\s*\|\s*t\b/
|
|
21
|
+
ASSIGNED = /\Aassign\s+(\w+)\s*=\s*/
|
|
22
|
+
INTERPOLATION = /%\{(\w+)\}/
|
|
23
|
+
|
|
24
|
+
# @param source [String, nil] a template's subject or body
|
|
25
|
+
# @param locale [String, Symbol] the language to write the text in
|
|
26
|
+
# @param escape [Boolean] whether the text is HTML: false for a subject,
|
|
27
|
+
# which renders unescaped as a mail header
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
def self.call(source, locale:, escape: true)
|
|
30
|
+
new(locale, escape).call(source)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(locale, escape = true)
|
|
34
|
+
@locale = locale
|
|
35
|
+
@escape = escape
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(source)
|
|
39
|
+
return source if source.blank?
|
|
40
|
+
|
|
41
|
+
source.
|
|
42
|
+
gsub(ASSIGN_TAG) { |tag| inline_assign(markup(Regexp.last_match[1])) || tag }.
|
|
43
|
+
gsub(OUTPUT_TAG) { |tag| inline_output(markup(Regexp.last_match[1])) || tag }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# A tag's content without whitespace control dashes or padding.
|
|
49
|
+
def markup(content)
|
|
50
|
+
content.delete_prefix('-').delete_suffix('-').strip
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `'key' | t`, `'key' | t: name: value` and `'key' | t | filter`, split into
|
|
54
|
+
# the key, its arguments and the filters after it.
|
|
55
|
+
def translation_call(markup)
|
|
56
|
+
match = TRANSLATED.match(markup)
|
|
57
|
+
return unless match
|
|
58
|
+
|
|
59
|
+
rest = match.post_match.strip
|
|
60
|
+
if rest.start_with?(':')
|
|
61
|
+
arguments, filters = rest.delete_prefix(':').split('|', 2)
|
|
62
|
+
[match[1], arguments.strip, filters&.strip]
|
|
63
|
+
elsif rest.start_with?('|')
|
|
64
|
+
[match[1], nil, rest.delete_prefix('|').strip]
|
|
65
|
+
elsif rest.empty?
|
|
66
|
+
[match[1], nil, nil]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inline_output(markup)
|
|
71
|
+
key, arguments, filters = translation_call(markup)
|
|
72
|
+
text = key && translation(key)
|
|
73
|
+
return unless text
|
|
74
|
+
|
|
75
|
+
if filters.present?
|
|
76
|
+
return if arguments.present? || text.include?("'")
|
|
77
|
+
|
|
78
|
+
"{{ '#{text}' | #{filters} }}"
|
|
79
|
+
else
|
|
80
|
+
interpolate(text, arguments) { |literal| literal_text(literal) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def inline_assign(markup)
|
|
85
|
+
assigned = ASSIGNED.match(markup)
|
|
86
|
+
return unless assigned
|
|
87
|
+
|
|
88
|
+
variable = assigned[1]
|
|
89
|
+
key, arguments, filters = translation_call(assigned.post_match)
|
|
90
|
+
text = key && filters.nil? && translation(key)
|
|
91
|
+
return unless text
|
|
92
|
+
|
|
93
|
+
if arguments.blank? && text.exclude?("'") && text !~ INTERPOLATION
|
|
94
|
+
"{% assign #{variable} = '#{text}' %}"
|
|
95
|
+
else
|
|
96
|
+
body = interpolate(text, arguments) { |literal| literal_text(literal) }
|
|
97
|
+
body && "{% capture #{variable} %}#{body}{% endcapture %}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Literal text is escaped by the caller's block; each `%{name}` becomes
|
|
102
|
+
# the Liquid expression the template passed for it.
|
|
103
|
+
def interpolate(text, arguments)
|
|
104
|
+
values = parse_arguments(arguments)
|
|
105
|
+
return unless values
|
|
106
|
+
return if text.scan(INTERPOLATION).flatten.any? { |name| !values.key?(name) }
|
|
107
|
+
|
|
108
|
+
text.split(INTERPOLATION).each_with_index.map do |part, index|
|
|
109
|
+
index.odd? ? "{{ #{values[part]} }}" : yield(part)
|
|
110
|
+
end.join
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def literal_text(literal)
|
|
114
|
+
@escape ? ERB::Util.html_escape(literal) : literal
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_arguments(arguments)
|
|
118
|
+
return {} if arguments.blank?
|
|
119
|
+
|
|
120
|
+
arguments.split(/,\s*(?=\w+\s*:)/).to_h do |pair|
|
|
121
|
+
name, expression = pair.split(':', 2).map(&:strip)
|
|
122
|
+
return nil if name.blank? || expression.blank?
|
|
123
|
+
|
|
124
|
+
[name, expression]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def translation(key)
|
|
129
|
+
text = I18n.t(key, scope: :spree, locale: @locale, default: nil)
|
|
130
|
+
text if text.is_a?(String) && !text.include?('{{') && !text.include?('{%')
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -22,7 +22,7 @@ module Spree
|
|
|
22
22
|
# the product's update either way
|
|
23
23
|
def process!
|
|
24
24
|
price_list = cached_lookup(:price_list_import_list) { import.price_list }
|
|
25
|
-
raise ArgumentError,
|
|
25
|
+
raise ArgumentError, I18n.t('spree.price_list_import_no_price_list') if price_list.nil?
|
|
26
26
|
|
|
27
27
|
variant = find_variant
|
|
28
28
|
currency = currency_for(attributes['currency'])
|
|
@@ -62,15 +62,15 @@ module Spree
|
|
|
62
62
|
# group is one SKU's rows and every rung would otherwise repeat the join.
|
|
63
63
|
def find_variant
|
|
64
64
|
sku = attributes['sku'].to_s.strip
|
|
65
|
-
raise ArgumentError,
|
|
65
|
+
raise ArgumentError, I18n.t('spree.price_list_import_sku_required') if sku.blank?
|
|
66
66
|
|
|
67
67
|
variants = cached_lookup(:price_list_import_variant, sku.downcase) do
|
|
68
68
|
# Case-insensitive, as the product import matches SKUs and as the
|
|
69
69
|
# uniqueness validation compares them.
|
|
70
70
|
store.variants.where(Spree::Variant.arel_table[:sku].lower.eq(sku.downcase)).limit(2).to_a
|
|
71
71
|
end
|
|
72
|
-
raise ArgumentError,
|
|
73
|
-
raise ArgumentError,
|
|
72
|
+
raise ArgumentError, I18n.t('spree.price_list_import_unknown_sku', sku: sku) if variants.empty?
|
|
73
|
+
raise ArgumentError, I18n.t('spree.price_list_import_ambiguous_sku', sku: sku) if variants.many?
|
|
74
74
|
|
|
75
75
|
variants.first
|
|
76
76
|
end
|
|
@@ -82,7 +82,7 @@ module Spree
|
|
|
82
82
|
currency = value.to_s.strip.upcase.presence || store.default_currency
|
|
83
83
|
return currency if supported_currencies.include?(currency)
|
|
84
84
|
|
|
85
|
-
raise ArgumentError,
|
|
85
|
+
raise ArgumentError, I18n.t('spree.price_list_import_unsupported_currency', currency: currency)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def supported_currencies
|
|
@@ -99,7 +99,7 @@ module Spree
|
|
|
99
99
|
def amount_for(value, column)
|
|
100
100
|
text = value.to_s.strip
|
|
101
101
|
return nil if text.blank?
|
|
102
|
-
raise ArgumentError,
|
|
102
|
+
raise ArgumentError, I18n.t('spree.price_list_import_invalid_amount', column: column, value: text) unless text.match?(AMOUNT_FORMAT)
|
|
103
103
|
|
|
104
104
|
BigDecimal(text)
|
|
105
105
|
end
|
|
@@ -151,17 +151,17 @@ module Spree
|
|
|
151
151
|
value = {} unless value.is_a?(Hash)
|
|
152
152
|
|
|
153
153
|
if value[:over_cap].present?
|
|
154
|
-
|
|
154
|
+
I18n.t('spree.price_list_import_too_many_breaks', count: Spree::Price::MAXIMUM_BREAKS_PER_VARIANT)
|
|
155
155
|
elsif value[:invalid_quantities].present?
|
|
156
|
-
|
|
156
|
+
I18n.t('spree.price_list_import_invalid_quantity', value: attributes['min_quantity'])
|
|
157
157
|
elsif (rising = value[:rising_ladders].presence)
|
|
158
158
|
# Named rung and floor, like the sibling branches: a file is written
|
|
159
159
|
# one rung per row, so the merchant needs to know which one and what
|
|
160
160
|
# it had to beat.
|
|
161
|
-
|
|
161
|
+
I18n.t('spree.price_list_import_price_rises_with_quantity',
|
|
162
162
|
quantity: rising.first[:min_quantity], floor: rising.first[:floor])
|
|
163
163
|
else
|
|
164
|
-
result.error.to_s.presence ||
|
|
164
|
+
result.error.to_s.presence || I18n.t('spree.price_list_import_failed')
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -3,15 +3,23 @@ module Spree
|
|
|
3
3
|
module RowProcessors
|
|
4
4
|
class ProductVariant < Base
|
|
5
5
|
OPTION_TYPES_COUNT = 3
|
|
6
|
+
CATEGORY_FIELDS = %w[category1 category2 category3].freeze
|
|
7
|
+
# Written as plain decimals ("16.50"): the models store text as sent,
|
|
8
|
+
# and "1,599.99" or "16,50" would otherwise be saved as 1 or 16.
|
|
9
|
+
DECIMAL_FIELDS = %w[price compare_at_price cost_price weight height width depth carton_weight].freeze
|
|
6
10
|
|
|
7
11
|
def initialize(row, **)
|
|
8
12
|
super
|
|
13
|
+
ensure_decimals!
|
|
9
14
|
@store = row.store
|
|
10
15
|
@seller = import.seller
|
|
16
|
+
# Before the product is saved: rows are not wrapped in a transaction,
|
|
17
|
+
# so an unknown location must fail the row with nothing written.
|
|
18
|
+
@stock_location = import_stock_location if attributes['inventory_count'].present?
|
|
11
19
|
@product = ensure_product_exists
|
|
12
20
|
end
|
|
13
21
|
|
|
14
|
-
attr_reader :product, :store, :seller
|
|
22
|
+
attr_reader :product, :store, :seller, :stock_location
|
|
15
23
|
|
|
16
24
|
def process!
|
|
17
25
|
variant = if attributes['sku'].present?
|
|
@@ -62,7 +70,7 @@ module Spree
|
|
|
62
70
|
variant.set_stock(
|
|
63
71
|
attributes['inventory_count'].to_i,
|
|
64
72
|
attributes['inventory_backorderable']&.to_b,
|
|
65
|
-
|
|
73
|
+
stock_location
|
|
66
74
|
)
|
|
67
75
|
end
|
|
68
76
|
|
|
@@ -79,6 +87,14 @@ module Spree
|
|
|
79
87
|
|
|
80
88
|
private
|
|
81
89
|
|
|
90
|
+
def ensure_decimals!
|
|
91
|
+
DECIMAL_FIELDS.each do |column|
|
|
92
|
+
Spree::Money::Rounding.parse_decimal(attributes[column])
|
|
93
|
+
rescue Spree::Money::InvalidFormat
|
|
94
|
+
raise ArgumentError, I18n.t('spree.import_invalid_number', column: column, value: attributes[column].to_s.strip)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
82
98
|
# A product-header (options-empty) row creates an option-less default
|
|
83
99
|
# variant to hold product-level attributes. Once an option-bearing
|
|
84
100
|
# variant exists, that placeholder is a leftover — remove it and
|
|
@@ -91,9 +107,17 @@ module Spree
|
|
|
91
107
|
product.update_column(:default_variant_id, product.variants.reload.first&.id)
|
|
92
108
|
end
|
|
93
109
|
|
|
110
|
+
# Whether this row carries the product itself, as opposed to only one of
|
|
111
|
+
# its variants. A row is a product row when it has no options at all, or
|
|
112
|
+
# when it names the product alongside them — an export (and Shopify's
|
|
113
|
+
# own CSV) writes the product's attributes on its first variant's row,
|
|
114
|
+
# so that row has to both create the product and create a variant.
|
|
115
|
+
def product_row?
|
|
116
|
+
options.empty? || has_product_attributes?
|
|
117
|
+
end
|
|
118
|
+
|
|
94
119
|
def ensure_product_exists
|
|
95
|
-
if
|
|
96
|
-
# For product/header rows (no options), create or update the product
|
|
120
|
+
if product_row?
|
|
97
121
|
product = Spree::Product.new
|
|
98
122
|
if attributes['slug'].present?
|
|
99
123
|
existing_product = product_scope.find_by(slug: attributes['slug'].strip.downcase)
|
|
@@ -167,9 +191,12 @@ module Spree
|
|
|
167
191
|
end
|
|
168
192
|
|
|
169
193
|
def assign_attributes_to_product(product)
|
|
170
|
-
# set the SKU on a product/header row so process! updates the default variant instead of creating a new one
|
|
171
194
|
if product.new_record?
|
|
172
195
|
product.slug = attributes['slug']
|
|
196
|
+
# Only an option-less row's SKU belongs to the product's own variant,
|
|
197
|
+
# so process! updates that variant instead of creating a second one.
|
|
198
|
+
# On a row that also carries options the SKU identifies that option
|
|
199
|
+
# variant, which process! creates for itself.
|
|
173
200
|
product.sku = attributes['sku'] if attributes['sku'].present? && options.empty?
|
|
174
201
|
product.store = store
|
|
175
202
|
# A seller's import creates the seller's products. Without this the
|
|
@@ -197,11 +224,9 @@ module Spree
|
|
|
197
224
|
product.status = to_spree_status(attributes['status'])
|
|
198
225
|
end
|
|
199
226
|
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
product.product_type = product_type if product_type.present?
|
|
204
|
-
end
|
|
227
|
+
if attributes['product_type'].present?
|
|
228
|
+
product_type = prepare_product_type
|
|
229
|
+
product.product_type = product_type if product_type.present?
|
|
205
230
|
end
|
|
206
231
|
|
|
207
232
|
product
|
|
@@ -243,23 +268,32 @@ module Spree
|
|
|
243
268
|
end
|
|
244
269
|
end
|
|
245
270
|
|
|
246
|
-
# Where an imported row's stock lands.
|
|
247
|
-
#
|
|
248
|
-
# Nil for an operator's import, which leaves `set_stock` to pick the
|
|
249
|
-
# store's default. A seller's stock belongs in their own warehouse:
|
|
250
|
-
# `Variant#default_stock_location` answers the *store's*, so without
|
|
251
|
-
# this a seller's `inventory_count` would deposit their goods in the
|
|
252
|
-
# marketplace's location — the same mistake the seller products
|
|
253
|
-
# endpoint narrows `stock_levels` to prevent.
|
|
254
|
-
#
|
|
255
|
-
# A seller with no location of their own falls back to the store's
|
|
256
|
-
# rather than dropping the count on the floor.
|
|
257
271
|
def import_stock_location
|
|
258
|
-
|
|
272
|
+
name = attributes['stock_location'].to_s.squish
|
|
259
273
|
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
if name.blank?
|
|
275
|
+
return cached_lookup(:default_stock_location) do
|
|
276
|
+
Spree::StockLocation.owned_by(store_id: store.id, seller_id: seller&.id).active.order_default.first ||
|
|
277
|
+
store.default_stock_location
|
|
278
|
+
end
|
|
262
279
|
end
|
|
280
|
+
|
|
281
|
+
owner_ids = stock_location_owner_ids
|
|
282
|
+
name_column = Spree::StockLocation.arel_table[:name]
|
|
283
|
+
cached_lookup(:stock_location, owner_ids, name.downcase) do
|
|
284
|
+
store.stock_locations.where(seller_id: owner_ids).
|
|
285
|
+
order(Arel.sql('CASE WHEN seller_id IS NULL THEN 1 ELSE 0 END')).
|
|
286
|
+
find_by(name_column.lower.eq(Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(name)])))
|
|
287
|
+
end || raise(ArgumentError, I18n.t('spree.product_import_unknown_stock_location', name: name))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# A seller reaches only their own locations — never the marketplace's
|
|
291
|
+
# shelf. An operator reaches the marketplace's and, for a seller's
|
|
292
|
+
# product, that seller's, whose own wins on a shared name as with cartons.
|
|
293
|
+
def stock_location_owner_ids
|
|
294
|
+
return [seller.id] if seller.present?
|
|
295
|
+
|
|
296
|
+
[nil, product_scope.where(slug: attributes['slug'].to_s.strip.downcase).pick(:seller_id)].uniq
|
|
263
297
|
end
|
|
264
298
|
|
|
265
299
|
def prepare_option_value_variants
|
|
@@ -304,11 +338,11 @@ module Spree
|
|
|
304
338
|
def find_or_create_option_type!(label)
|
|
305
339
|
cached_lookup(:option_type, label) do
|
|
306
340
|
begin
|
|
307
|
-
store_option_types.
|
|
341
|
+
store_option_types.with_name(label).first || store_option_types.create!(label: label)
|
|
308
342
|
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
309
343
|
raise unless uniqueness_conflict?(e, :name)
|
|
310
344
|
|
|
311
|
-
store_option_types.
|
|
345
|
+
store_option_types.with_name(label).first!
|
|
312
346
|
end
|
|
313
347
|
end
|
|
314
348
|
end
|
|
@@ -320,11 +354,11 @@ module Spree
|
|
|
320
354
|
def find_or_create_option_value!(option_type, label)
|
|
321
355
|
cached_lookup(:option_value, option_type.id, label) do
|
|
322
356
|
begin
|
|
323
|
-
option_type.option_values.
|
|
357
|
+
option_type.option_values.with_name(label).first || option_type.option_values.create!(label: label)
|
|
324
358
|
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
325
359
|
raise unless uniqueness_conflict?(e, :name)
|
|
326
360
|
|
|
327
|
-
option_type.option_values.
|
|
361
|
+
option_type.option_values.with_name(label).first!
|
|
328
362
|
end
|
|
329
363
|
end
|
|
330
364
|
end
|
|
@@ -375,7 +409,7 @@ module Spree
|
|
|
375
409
|
end
|
|
376
410
|
|
|
377
411
|
def has_product_attributes?
|
|
378
|
-
%w[name status description
|
|
412
|
+
(%w[name status description] + CATEGORY_FIELDS).any? { |key| attributes[key].present? }
|
|
379
413
|
end
|
|
380
414
|
|
|
381
415
|
def handle_custom_fields(product)
|
|
@@ -442,6 +476,7 @@ module Spree
|
|
|
442
476
|
# seller audience in the permission catalog at all.
|
|
443
477
|
def handle_categories(product)
|
|
444
478
|
return if seller.present?
|
|
479
|
+
return unless category_columns_mapped?
|
|
445
480
|
|
|
446
481
|
names = prepare_taxon_pretty_names
|
|
447
482
|
return Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, names) if import.preferred_inline
|
|
@@ -450,11 +485,19 @@ module Spree
|
|
|
450
485
|
end
|
|
451
486
|
|
|
452
487
|
def prepare_taxon_pretty_names
|
|
453
|
-
[
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
488
|
+
CATEGORY_FIELDS.map { |field| attributes[field] }.compact_blank.map(&:strip).uniq
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# A blank category cell means "file this product under nothing", and
|
|
492
|
+
# CreateCategoriesJob assigns the list wholesale to honour that. A file
|
|
493
|
+
# that carries no category columns at all means something different —
|
|
494
|
+
# it simply doesn't speak about categories — so without this a Shopify
|
|
495
|
+
# export, or any spreadsheet that omits them, would unfile every
|
|
496
|
+
# product it touched.
|
|
497
|
+
def category_columns_mapped?
|
|
498
|
+
cached_lookup(:category_columns_mapped) do
|
|
499
|
+
import.mappings.mapped.any? { |mapping| CATEGORY_FIELDS.include?(mapping.schema_field) }
|
|
500
|
+
end
|
|
458
501
|
end
|
|
459
502
|
|
|
460
503
|
def to_spree_status(status)
|