spree_core 6.0.0.beta3 → 6.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- 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/seller_payouts/sweep_seller_job.rb +2 -0
- data/app/mailers/spree/admin_user_mailer.rb +4 -19
- data/app/mailers/spree/base_mailer.rb +96 -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 +13 -1
- data/app/models/concerns/spree/api_typed.rb +91 -0
- data/app/models/concerns/spree/calculator/tiers.rb +76 -0
- data/app/models/concerns/spree/customer_methods.rb +15 -14
- 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.rb +4 -12
- data/app/models/concerns/spree/product_scopes.rb +11 -1
- 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 +7 -3
- data/app/models/concerns/spree/purchase/purchase_order.rb +2 -2
- data/app/models/concerns/spree/purchase/totals.rb +49 -1
- 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/single_store_resource.rb +1 -1
- data/app/models/concerns/spree/stores/email_branding.rb +31 -0
- data/app/models/concerns/spree/type_labels.rb +38 -0
- data/app/models/concerns/spree/typed_adjustment_line.rb +1 -1
- data/app/models/concerns/spree/typed_associations.rb +6 -0
- data/app/models/spree/address.rb +11 -1
- data/app/models/spree/adjusters/promotion.rb +1 -15
- data/app/models/spree/allowed_origin.rb +0 -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 +70 -22
- data/app/models/spree/calculator/flat_percent_item_total.rb +1 -3
- data/app/models/spree/calculator/flat_rate.rb +1 -3
- data/app/models/spree/calculator/flexi_rate.rb +1 -3
- data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
- data/app/models/spree/calculator/price_sack.rb +1 -3
- data/app/models/spree/calculator/shipping/digital_delivery.rb +1 -3
- data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +1 -3
- data/app/models/spree/calculator/shipping/flat_rate.rb +1 -3
- data/app/models/spree/calculator/shipping/flexi_rate.rb +1 -3
- data/app/models/spree/calculator/shipping/per_item.rb +1 -3
- data/app/models/spree/calculator/shipping/price_sack.rb +1 -3
- data/app/models/spree/calculator/tiered_flat_rate.rb +6 -23
- data/app/models/spree/calculator/tiered_percent.rb +7 -25
- data/app/models/spree/cart.rb +78 -18
- data/app/models/spree/channel/gating.rb +1 -1
- data/app/models/spree/channel.rb +4 -4
- data/app/models/spree/claim.rb +21 -1
- data/app/models/spree/claim_line_item.rb +41 -6
- data/app/models/spree/collection.rb +0 -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 +2 -3
- 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/product_rule.rb +1 -1
- 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 +9 -23
- data/app/models/spree/custom_fields/boolean.rb +1 -1
- data/app/models/spree/custom_fields/json.rb +1 -1
- data/app/models/spree/data_feed/google.rb +0 -2
- data/app/models/spree/data_request.rb +1 -1
- data/app/models/spree/delivery_method.rb +30 -13
- data/app/models/spree/delivery_method_rule.rb +4 -11
- data/app/models/spree/delivery_rate.rb +4 -5
- data/app/models/spree/delivery_rate_provider/base.rb +1 -0
- data/app/models/spree/delivery_zone.rb +0 -1
- 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 +15 -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 +1 -1
- data/app/models/spree/fee.rb +4 -2
- data/app/models/spree/fulfillment.rb +53 -27
- data/app/models/spree/fulfillment_changer.rb +2 -2
- 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 +4 -0
- data/app/models/spree/gateway/custom_payment_source_method.rb +5 -0
- data/app/models/spree/gateway.rb +5 -1
- data/app/models/spree/gift_card.rb +12 -1
- data/app/models/spree/gift_card_batch.rb +0 -1
- data/app/models/spree/import.rb +2 -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 +8 -18
- data/app/models/spree/invitation.rb +5 -5
- data/app/models/spree/line_item.rb +13 -2
- data/app/models/spree/locale.rb +11 -8
- data/app/models/spree/market.rb +1 -0
- data/app/models/spree/newsletter_subscriber.rb +3 -0
- data/app/models/spree/order.rb +56 -138
- data/app/models/spree/order_group.rb +11 -2
- 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 +1 -2
- data/app/models/spree/payment/gateway_options.rb +14 -52
- data/app/models/spree/payment/processing.rb +2 -4
- data/app/models/spree/payment.rb +20 -5
- data/app/models/spree/payment_method/session_instrumentation.rb +1 -0
- data/app/models/spree/payment_method/store_credit.rb +4 -4
- data/app/models/spree/payment_method.rb +36 -4
- 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/price_list.rb +1 -2
- data/app/models/spree/price_rules/channel_rule.rb +1 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +1 -1
- data/app/models/spree/product.rb +31 -7
- data/app/models/spree/product_submission.rb +0 -12
- data/app/models/spree/product_type.rb +0 -1
- data/app/models/spree/promotion/actions/create_line_items.rb +27 -17
- data/app/models/spree/promotion/rules/category.rb +0 -4
- data/app/models/spree/promotion/rules/channel.rb +0 -4
- data/app/models/spree/promotion/rules/country.rb +0 -4
- data/app/models/spree/promotion/rules/currency.rb +0 -4
- data/app/models/spree/promotion/rules/customer_group.rb +0 -4
- data/app/models/spree/promotion/rules/first_order.rb +0 -4
- data/app/models/spree/promotion/rules/item_total.rb +4 -8
- data/app/models/spree/promotion/rules/market.rb +0 -4
- data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -4
- data/app/models/spree/promotion/rules/option_value.rb +0 -3
- data/app/models/spree/promotion/rules/product.rb +0 -4
- 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 +35 -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 +1 -0
- data/app/models/spree/quantity_rule.rb +1 -1
- data/app/models/spree/refund.rb +28 -3
- data/app/models/spree/return.rb +30 -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 +1 -1
- data/app/models/spree/seller.rb +2 -1
- data/app/models/spree/seller_payout.rb +7 -0
- data/app/models/spree/seller_requirement.rb +4 -12
- data/app/models/spree/seller_requirement_custom_field.rb +1 -2
- data/app/models/spree/seller_requirement_submission.rb +2 -2
- data/app/models/spree/seller_requirements/accept_terms.rb +2 -3
- data/app/models/spree/seller_transfer.rb +5 -0
- data/app/models/spree/shipping_label.rb +1 -0
- data/app/models/spree/stock/availability_validator.rb +2 -2
- 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 +1 -1
- 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 +4 -2
- data/app/models/spree/store.rb +33 -7
- data/app/models/spree/store_credit.rb +8 -9
- data/app/models/spree/store_credit_event.rb +5 -5
- data/app/models/spree/tax_exemption_certificate.rb +2 -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 +59 -33
- data/app/models/spree/tax_provider/recorded_share.rb +114 -0
- data/app/models/spree/tax_rate.rb +1 -2
- data/app/models/spree/taxonomy.rb +0 -1
- data/app/models/spree/variant.rb +4 -3
- data/app/models/spree/variant_media.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +10 -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 +1 -1
- data/app/presenters/spree/csv/product_variant_presenter.rb +42 -12
- 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 +19 -5
- 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 +65 -34
- data/app/services/spree/imports/row_processors/purchase_order.rb +13 -13
- data/app/services/spree/locales.rb +4 -5
- 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 +2 -1
- data/app/services/spree/orders/discounts/create.rb +7 -4
- 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/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/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 +15 -14
- data/app/subscribers/spree/seller_onboarding_subscriber.rb +1 -1
- 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 +51 -47
- 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/process.rb +2 -2
- 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/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/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 +5 -5
- 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/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/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/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 +37 -503
- 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/lib/generators/spree/dummy/dummy_generator.rb +0 -9
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- data/lib/spree/checkout/default_requirements.rb +37 -13
- data/lib/spree/core/configuration.rb +1 -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 +48 -167
- data/lib/spree/core/permissions/default_catalog.rb +5 -0
- data/lib/spree/core/preferences/json_conversion.rb +182 -0
- data/lib/spree/core/preferences/masking.rb +4 -1
- data/lib/spree/core/preferences/preferable.rb +137 -83
- data/lib/spree/core/preferences/preferable_class_methods.rb +81 -146
- data/lib/spree/core/previews/import_preview.rb +10 -0
- data/lib/spree/core/tax/provider_error.rb +37 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +85 -370
- 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/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/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/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 +45 -0
- data/lib/tasks/categories_and_collections.rake +3 -3
- data/lib/tasks/core.rake +6 -4
- data/lib/tasks/media.rake +3 -3
- data/lib/tasks/package_types.rake +1 -1
- data/lib/tasks/preferences_json.rake +101 -0
- data/spec/fixtures/mailer_views/custom_views_probe_mailer/welcome_email.html.erb +4 -0
- metadata +188 -44
- 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
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
module Collections
|
|
3
3
|
class RemoveProducts
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include Spree::ProductMemberships
|
|
5
6
|
|
|
6
7
|
# Removes the given products from the given collections and re-packs positions.
|
|
7
8
|
#
|
|
@@ -11,54 +12,17 @@ module Spree
|
|
|
11
12
|
def call(collections:, products:)
|
|
12
13
|
return if collections.blank? || products.blank?
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
product_ids = products.pluck(:id)
|
|
16
|
-
|
|
17
|
-
ApplicationRecord.transaction do
|
|
18
|
-
collection_ids.each do |collection_id|
|
|
19
|
-
Spree::ProductCollection.where(collection_id: collection_id, product_id: product_ids).delete_all
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
product_collections_params = collection_ids.flat_map do |collection_id|
|
|
23
|
-
position = 0
|
|
24
|
-
existing_product_ids = Spree::ProductCollection.where(collection_id: collection_id).pluck(:product_id)
|
|
25
|
-
|
|
26
|
-
existing_product_ids.map do |product_id|
|
|
27
|
-
{
|
|
28
|
-
collection_id: collection_id,
|
|
29
|
-
product_id: product_id,
|
|
30
|
-
position: (position += 1),
|
|
31
|
-
created_at: Time.current,
|
|
32
|
-
updated_at: Time.current
|
|
33
|
-
}
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
if product_collections_params.any?
|
|
38
|
-
opts = {}
|
|
39
|
-
# Column list rather than the index name — the composite unique index is named
|
|
40
|
-
# index_product_collections_on_collection_and_product (shortened to fit the
|
|
41
|
-
# identifier limit), so match by columns to stay name-independent.
|
|
42
|
-
opts[:unique_by] = %i[collection_id product_id] unless mysql_adapter?
|
|
43
|
-
|
|
44
|
-
Spree::ProductCollection.upsert_all(product_collections_params, **opts)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
collection_ids.each { |id| Spree::Collection.reset_counters(id, :product_collections) }
|
|
49
|
-
product_ids.each { |id| Spree::Product.reset_counters(id, :product_collections) }
|
|
50
|
-
|
|
51
|
-
Spree::Product.where(id: product_ids).touch_all
|
|
52
|
-
products.each(&:enqueue_search_index)
|
|
53
|
-
Spree::Collection.where(id: collection_ids).touch_all
|
|
54
|
-
|
|
15
|
+
remove_memberships(collections, products)
|
|
55
16
|
success(true)
|
|
56
17
|
end
|
|
57
18
|
|
|
58
19
|
private
|
|
59
20
|
|
|
60
|
-
def
|
|
61
|
-
|
|
21
|
+
def membership_class = Spree::ProductCollection
|
|
22
|
+
def group_class = Spree::Collection
|
|
23
|
+
|
|
24
|
+
def refresh_group_counters(collection_ids)
|
|
25
|
+
collection_ids.each { |id| Spree::Collection.reset_counters(id, :product_collections) }
|
|
62
26
|
end
|
|
63
27
|
end
|
|
64
28
|
end
|
|
@@ -4,13 +4,15 @@ module Spree
|
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
6
|
def call(promotion:, quantity: 10)
|
|
7
|
-
coupon_codes = []
|
|
8
|
-
|
|
9
7
|
Spree::CouponCode.transaction do
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
Spree::CouponCode.insert_all(
|
|
9
|
+
Array.new(quantity) do
|
|
10
|
+
{
|
|
11
|
+
promotion_id: promotion.id, code: create_code(promotion.code_prefix),
|
|
12
|
+
created_at: Time.current, updated_at: Time.current
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
success(promotion.reload.coupon_codes)
|
|
@@ -24,14 +26,6 @@ module Spree
|
|
|
24
26
|
break code unless Spree::CouponCode.exists?(code: code)
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
|
-
|
|
28
|
-
def coupon_attributes(promotion)
|
|
29
|
-
{
|
|
30
|
-
promotion_id: promotion.id,
|
|
31
|
-
created_at: Time.current,
|
|
32
|
-
updated_at: Time.current
|
|
33
|
-
}
|
|
34
|
-
end
|
|
35
29
|
end
|
|
36
30
|
end
|
|
37
31
|
end
|
|
@@ -26,7 +26,7 @@ module Spree
|
|
|
26
26
|
# and a second erasure has nothing to erase.
|
|
27
27
|
if customer.anonymized?
|
|
28
28
|
data_request = Spree::DataRequest.new(customer: customer)
|
|
29
|
-
data_request.errors.add(:base,
|
|
29
|
+
data_request.errors.add(:base, I18n.t('spree.data_request_errors.already_anonymized'))
|
|
30
30
|
return failure(data_request)
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -15,12 +15,12 @@ module Spree
|
|
|
15
15
|
# @return [Spree::ServiceModule::Result]
|
|
16
16
|
def call(delivery:)
|
|
17
17
|
if delivery.shipping_label_id.present?
|
|
18
|
-
delivery.errors.add(:base, :has_label, message:
|
|
18
|
+
delivery.errors.add(:base, :has_label, message: I18n.t('spree.deliveries.errors.has_label'))
|
|
19
19
|
return failure(delivery)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
if delivery.delivered?
|
|
23
|
-
delivery.errors.add(:base, :delivered, message:
|
|
23
|
+
delivery.errors.add(:base, :delivered, message: I18n.t('spree.deliveries.errors.delivered'))
|
|
24
24
|
return failure(delivery)
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module EmailTemplates
|
|
3
|
+
# Renders a draft the way customers would receive it, with sample data
|
|
4
|
+
# and strict variables, and reports what stops it from rendering. A
|
|
5
|
+
# draft of the layout or a shared partial is checked inside every
|
|
6
|
+
# editable email, since all of them use it, and in every language the
|
|
7
|
+
# store has published its own version of an email in.
|
|
8
|
+
#
|
|
9
|
+
# An email the store has no record for cannot be rendered with data; its
|
|
10
|
+
# draft is still checked for Liquid syntax errors.
|
|
11
|
+
class Check
|
|
12
|
+
# @param store [Spree::Store]
|
|
13
|
+
# @param draft [Spree::EmailTemplateDraft]
|
|
14
|
+
def initialize(store:, draft:)
|
|
15
|
+
@store = store
|
|
16
|
+
@draft = draft
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<Hash>] one `{ email:, message:, line: }` per problem; empty when the draft renders
|
|
20
|
+
def call
|
|
21
|
+
locales.flat_map { |locale| email_keys.filter_map { |email_key| problem_in(email_key, locale) } }.uniq
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def email_keys
|
|
27
|
+
definition = @draft.definition
|
|
28
|
+
definition.email? ? [definition.key] : Spree.editable_email_templates.emails.map(&:key)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def locales
|
|
32
|
+
return [@draft.locale] if @draft.definition.email? || @draft.locale != Spree::EmailTemplate::ANY_LOCALE
|
|
33
|
+
|
|
34
|
+
[@draft.locale, *@store.email_templates.published.where.not(locale: Spree::EmailTemplate::ANY_LOCALE).distinct.pluck(:locale)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def problem_in(email_key, locale)
|
|
38
|
+
Preview.new(store: @store, key: @draft.key, locale: locale, subject: @draft.subject, body: @draft.body,
|
|
39
|
+
email_key: email_key, strict: true).call
|
|
40
|
+
nil
|
|
41
|
+
rescue Spree::EmailTemplates::NoSampleRecord
|
|
42
|
+
syntax_problem(email_key)&.merge(locale: locale)
|
|
43
|
+
rescue Liquid::Error => e
|
|
44
|
+
{ email: email_key, locale: locale, message: e.message, line: e.line_number }
|
|
45
|
+
rescue MRML::Error => e
|
|
46
|
+
{ email: email_key, locale: locale, message: e.message, line: nil }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def syntax_problem(email_key)
|
|
50
|
+
Liquid::Template.parse(@draft.body, environment: Spree::Emails::Renderer.environment, line_numbers: true)
|
|
51
|
+
Liquid::Template.parse(@draft.subject.to_s, environment: Spree::Emails::Renderer.environment)
|
|
52
|
+
nil
|
|
53
|
+
rescue Liquid::Error => e
|
|
54
|
+
{ email: email_key, message: e.message, line: e.line_number }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module EmailTemplates
|
|
3
|
+
# Renders an editable email template with sample data, through the same
|
|
4
|
+
# renderer customers' emails go through. An unsaved subject and body take
|
|
5
|
+
# the place of the store's published version; everything else the email
|
|
6
|
+
# uses comes from the store's published templates and the files.
|
|
7
|
+
#
|
|
8
|
+
# The layout and shared partials are previewed inside an email that uses
|
|
9
|
+
# them, the first editable email unless another is named.
|
|
10
|
+
class Preview
|
|
11
|
+
# @return [Hash] the variables the email was rendered with, once {#call} has run
|
|
12
|
+
attr_reader :variables
|
|
13
|
+
|
|
14
|
+
# @return [Spree::Emails::RenderedEmail, nil] the rendered email, once {#call} has run
|
|
15
|
+
attr_reader :rendered
|
|
16
|
+
|
|
17
|
+
# @param store [Spree::Store]
|
|
18
|
+
# @param key [String] the editable template being previewed
|
|
19
|
+
# @param locale [String] a language code, or "any"
|
|
20
|
+
# @param subject [String, nil] an unsaved subject; nil keeps the current one
|
|
21
|
+
# @param body [String, nil] an unsaved body; nil keeps the current one
|
|
22
|
+
# @param record_id [String, nil] the prefixed id of the record to build sample data from
|
|
23
|
+
# @param email_key [String, nil] for the layout or a partial, the email to show it in
|
|
24
|
+
# @param strict [Boolean] whether an unknown variable raises
|
|
25
|
+
# @param branding [Hash] unsaved branding values (see Spree::Emails::Branding)
|
|
26
|
+
def initialize(store:, key:, locale: Spree::EmailTemplate::ANY_LOCALE, subject: nil, body: nil,
|
|
27
|
+
record_id: nil, email_key: nil, strict: false, branding: {})
|
|
28
|
+
@store = store
|
|
29
|
+
@key = key
|
|
30
|
+
@locale = locale.presence || Spree::EmailTemplate::ANY_LOCALE
|
|
31
|
+
@subject = subject
|
|
32
|
+
@body = body
|
|
33
|
+
@record_id = record_id
|
|
34
|
+
@email_key = email_key
|
|
35
|
+
@strict = strict
|
|
36
|
+
@branding = branding
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Spree::Emails::RenderedEmail]
|
|
40
|
+
# @raise [Spree::EmailTemplates::NoSampleRecord] when the store has no record to build the sample from
|
|
41
|
+
def call
|
|
42
|
+
sample = email.sample_class.new(store: @store, record_id: @record_id)
|
|
43
|
+
|
|
44
|
+
in_locale do
|
|
45
|
+
renderer = Spree::Emails::Renderer.new(resolver: resolver, store: @store, currency: sample.currency, strict: @strict,
|
|
46
|
+
branding: @branding)
|
|
47
|
+
sample_variables = sample.variables
|
|
48
|
+
@variables = renderer.variables(sample_variables)
|
|
49
|
+
@rendered = renderer.render(resolver.find(email.key), sample_variables)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Spree::Emails::EditableTemplates::Definition] the email the preview renders
|
|
54
|
+
def email
|
|
55
|
+
@email ||= if definition.email?
|
|
56
|
+
definition
|
|
57
|
+
else
|
|
58
|
+
emails = Spree.editable_email_templates.emails
|
|
59
|
+
emails.find { |candidate| candidate.key == @email_key } || emails.first
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def definition
|
|
66
|
+
Spree.editable_email_templates[@key] || raise(ArgumentError, "#{@key} is not an editable email template")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolver
|
|
70
|
+
@resolver ||= Spree::Emails::TemplateResolver.for_mailers(store: @store, locale: language, drafts: drafts)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An unsaved subject or body sent alone is previewed with the other half
|
|
74
|
+
# customers currently get.
|
|
75
|
+
def drafts
|
|
76
|
+
return {} if @subject.nil? && @body.nil?
|
|
77
|
+
|
|
78
|
+
current = Spree::Emails::TemplateResolver.for_mailers(store: @store, locale: language).find(@key)
|
|
79
|
+
subject = @subject.nil? ? current&.subject : @subject
|
|
80
|
+
body = @body.nil? ? current&.body : @body
|
|
81
|
+
{ @key => Spree::Emails::Template.new(key: @key, subject: subject, body: body) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def language
|
|
85
|
+
@locale == Spree::EmailTemplate::ANY_LOCALE ? @store.default_locale : @locale
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Mirrors how mailers render: the email's language with the store's
|
|
89
|
+
# translation fallbacks, restored afterwards.
|
|
90
|
+
def in_locale(&block)
|
|
91
|
+
previous_fallbacks = Mobility.store_based_fallbacks
|
|
92
|
+
Spree::Locales::SetFallbackLocaleForStore.new.call(store: @store)
|
|
93
|
+
I18n.with_locale(language.presence || I18n.default_locale, &block)
|
|
94
|
+
ensure
|
|
95
|
+
Mobility.store_based_fallbacks = previous_fallbacks
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Loads the partials templates pull in with `{% render 'spree/shared/line_item' %}`
|
|
4
|
+
# from `spree/shared/_line_item.liquid`, following Rails' partial naming.
|
|
5
|
+
class FileSystem
|
|
6
|
+
# @param resolver [Spree::Emails::TemplateResolver]
|
|
7
|
+
def initialize(resolver)
|
|
8
|
+
@resolver = resolver
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param name [String]
|
|
12
|
+
# @return [String] the partial's source
|
|
13
|
+
def read_template_file(name)
|
|
14
|
+
partial = @resolver.find_partial(name.to_s)
|
|
15
|
+
raise Liquid::FileSystemError, "No such email partial: #{name}" unless partial
|
|
16
|
+
|
|
17
|
+
partial.body
|
|
18
|
+
rescue Spree::Emails::TemplateResolver::InvalidKey
|
|
19
|
+
raise Liquid::FileSystemError, 'Invalid email partial name'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# The filters email templates can use, in addition to Liquid's standard
|
|
4
|
+
# set. Names follow common Liquid conventions where the meaning is the same,
|
|
5
|
+
# is the Liquid most developers and agents already know.
|
|
6
|
+
module Filters
|
|
7
|
+
# @example {{ line_item.price | money }}
|
|
8
|
+
# @param amount [String, Numeric, nil]
|
|
9
|
+
# @param currency [String, nil] defaults to the email's currency
|
|
10
|
+
# @return [String]
|
|
11
|
+
def money(amount, currency = nil)
|
|
12
|
+
return '' if amount.blank?
|
|
13
|
+
|
|
14
|
+
Spree::Money.new(amount, currency: currency.presence || @context.registers[:currency]).to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @example {{ order.total | money_with_currency }} # => "$10.00 USD"
|
|
18
|
+
# @return [String]
|
|
19
|
+
def money_with_currency(amount, currency = nil)
|
|
20
|
+
return '' if amount.blank?
|
|
21
|
+
|
|
22
|
+
Spree::Money.new(amount, currency: currency.presence || @context.registers[:currency], with_currency: true).to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Formats a date in the store's time zone, never the server's. Takes a
|
|
26
|
+
# named format from the locale's `date.formats` (`long`, `short`,
|
|
27
|
+
# `default`), or a strftime pattern. A date with no time of day, such as
|
|
28
|
+
# "2026-12-31", is shown as that day in every zone.
|
|
29
|
+
#
|
|
30
|
+
# @example {{ order.completed_at | date: 'long' }}
|
|
31
|
+
# @example {{ 'now' | date: '%Y' }}
|
|
32
|
+
# @return [String]
|
|
33
|
+
def date(input, format = 'default')
|
|
34
|
+
value = parse_time(input)
|
|
35
|
+
return input.to_s if value.nil?
|
|
36
|
+
|
|
37
|
+
value = value.in_time_zone(time_zone) unless value.instance_of?(Date)
|
|
38
|
+
format = format.to_s
|
|
39
|
+
|
|
40
|
+
return value.strftime(format) if format.include?('%')
|
|
41
|
+
|
|
42
|
+
format = 'default' unless I18n.exists?("date.formats.#{format}")
|
|
43
|
+
I18n.l(value.to_date, format: format.to_sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Translates one of Spree's own keys, with interpolation.
|
|
47
|
+
#
|
|
48
|
+
# @example {{ 'order_mailer.confirm_email.dear_customer' | t: name: order.customer_name }}
|
|
49
|
+
# @return [String]
|
|
50
|
+
def t(key, options = {})
|
|
51
|
+
interpolations = options.to_h.except('scope', 'default', :scope, :default).transform_keys(&:to_sym)
|
|
52
|
+
|
|
53
|
+
I18n.t(key.to_s, **interpolations, scope: :spree).to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Marks a value as trusted HTML, so it is output without escaping. Only
|
|
57
|
+
# for HTML that was sanitized when it was written.
|
|
58
|
+
#
|
|
59
|
+
# @example {{ product.description_html | raw }}
|
|
60
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
61
|
+
def raw(input)
|
|
62
|
+
input.to_s.html_safe
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def parse_time(input)
|
|
68
|
+
case input
|
|
69
|
+
when Time, DateTime, ActiveSupport::TimeWithZone then input
|
|
70
|
+
when Date then input
|
|
71
|
+
when 'now', 'today' then Time.current
|
|
72
|
+
when /\A\d{4}-\d{2}-\d{2}\z/ then Date.iso8601(input)
|
|
73
|
+
when String then Time.iso8601(input)
|
|
74
|
+
end
|
|
75
|
+
rescue ArgumentError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def time_zone
|
|
80
|
+
Time.find_zone(@context.registers[:store]&.preferred_timezone) || Time.zone
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# `{% render %}` gives each partial a fresh, isolated context, and Liquid
|
|
4
|
+
# does not carry strict mode into it.
|
|
5
|
+
#
|
|
6
|
+
# Inside a partial an argument the caller left out reads as empty, so
|
|
7
|
+
# partials can take optional arguments; a misspelled field on an object
|
|
8
|
+
# still raises under strict variables.
|
|
9
|
+
class LiquidContext < Liquid::Context
|
|
10
|
+
attr_accessor :optional_arguments
|
|
11
|
+
|
|
12
|
+
def new_isolated_subcontext
|
|
13
|
+
super.tap do |subcontext|
|
|
14
|
+
subcontext.strict_variables = strict_variables
|
|
15
|
+
subcontext.strict_filters = strict_filters
|
|
16
|
+
subcontext.optional_arguments = true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_variable(key, raise_on_not_found: true)
|
|
21
|
+
super(key, raise_on_not_found: raise_on_not_found && !optional_arguments)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Filters that can only rearrange escaped text, never decode it into
|
|
25
|
+
# markup. `url_decode` and the like are left out on purpose: turning
|
|
26
|
+
# "%3Cb%3E" back into "<b>" must not produce trusted HTML.
|
|
27
|
+
MARKUP_PRESERVING_FILTERS = %w[
|
|
28
|
+
strip lstrip rstrip strip_newlines upcase downcase capitalize
|
|
29
|
+
append prepend replace replace_first replace_last remove remove_first remove_last
|
|
30
|
+
truncate truncatewords
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# Captured text is already-escaped HTML. A filter from the list above
|
|
34
|
+
# keeps it that way: plain string arguments are escaped going in, and the
|
|
35
|
+
# result stays marked safe, so `{{ greeting | upcase }}` is not escaped a
|
|
36
|
+
# second time when printed. Any other filter's result is escaped again.
|
|
37
|
+
def invoke(method, *args)
|
|
38
|
+
input = args.first
|
|
39
|
+
return super unless registers[:escape_output] && input.is_a?(ActiveSupport::SafeBuffer)
|
|
40
|
+
return super(method, input.to_str, *args.drop(1)) unless MARKUP_PRESERVING_FILTERS.include?(method.to_s)
|
|
41
|
+
|
|
42
|
+
escaped = args.drop(1).map { |arg| arg.is_a?(String) ? ERB::Util.html_escape(arg) : arg }
|
|
43
|
+
result = super(method, input, *escaped)
|
|
44
|
+
result.is_a?(String) ? result.html_safe : result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# The finished parts of one email, ready for Action Mailer.
|
|
4
|
+
class RenderedEmail
|
|
5
|
+
attr_reader :subject, :html, :text
|
|
6
|
+
|
|
7
|
+
def initialize(subject:, html:, text:)
|
|
8
|
+
@subject = subject
|
|
9
|
+
@html = html
|
|
10
|
+
@text = text
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Emails
|
|
3
|
+
# Renders an email template into its subject, HTML and plain-text parts.
|
|
4
|
+
#
|
|
5
|
+
# Liquid fills in the data first, into the template and then the layout
|
|
6
|
+
# wrapping it; MJML then compiles the result into email-safe HTML. Every
|
|
7
|
+
# output is HTML-escaped unless a template marks it `| raw`, and a runaway
|
|
8
|
+
# template fails its one email rather than stalling the job queue.
|
|
9
|
+
class Renderer
|
|
10
|
+
LAYOUT = 'layouts/spree/base_mailer'.freeze
|
|
11
|
+
|
|
12
|
+
RESOURCE_LIMITS = {
|
|
13
|
+
render_length_limit: 2_000_000,
|
|
14
|
+
render_score_limit: 200_000,
|
|
15
|
+
assign_score_limit: 50_000
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# The Liquid environment every email renders in: strict parsing and
|
|
20
|
+
# Spree's filters, isolated from any other Liquid the host app runs.
|
|
21
|
+
#
|
|
22
|
+
# @return [Liquid::Environment]
|
|
23
|
+
def environment
|
|
24
|
+
@environment ||= Liquid::Environment.build(error_mode: :strict) do |environment|
|
|
25
|
+
environment.register_filter(Spree::Emails::Filters)
|
|
26
|
+
# `include` takes a variable as the partial name, which would echo
|
|
27
|
+
# data back in its error message; `render` only takes a fixed name.
|
|
28
|
+
environment.tags.delete('include')
|
|
29
|
+
environment.register_tag('capture', Spree::Core::Emails::EscapedOutput::Capture)
|
|
30
|
+
environment.register_tag('cycle', Spree::Core::Emails::EscapedOutput::Cycle)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param resolver [Spree::Emails::TemplateResolver]
|
|
36
|
+
# @param store [Spree::Store]
|
|
37
|
+
# @param currency [String, nil] what `money` formats in, defaults to the store's
|
|
38
|
+
# @param strict [Boolean] whether an unknown variable raises; defaults to
|
|
39
|
+
# true in development and test, false in production
|
|
40
|
+
# @param branding [Hash] unsaved branding values over the store's, for previews
|
|
41
|
+
def initialize(resolver:, store:, currency: nil, strict: Rails.env.local?, branding: {})
|
|
42
|
+
@resolver = resolver
|
|
43
|
+
@store = store
|
|
44
|
+
@branding = branding
|
|
45
|
+
@currency = currency.presence || store.default_currency
|
|
46
|
+
@strict = strict
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param template [Spree::Emails::Template] a Liquid template
|
|
50
|
+
# @param assigns [Hash] the template's variables
|
|
51
|
+
# @return [Spree::Emails::RenderedEmail]
|
|
52
|
+
def render(template, assigns = {})
|
|
53
|
+
assigns = variables(assigns)
|
|
54
|
+
subject = subject_for(template, assigns)
|
|
55
|
+
body = render_liquid(template.body, assigns)
|
|
56
|
+
html = render_layout(body, assigns.merge('subject' => subject))
|
|
57
|
+
|
|
58
|
+
Spree::Emails::RenderedEmail.new(subject: subject, html: html, text: render_text(template, assigns, html))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Wraps HTML a mailer rendered some other way — its own ERB views — in
|
|
62
|
+
# the email layout, so it carries the store's logo, header and footer.
|
|
63
|
+
#
|
|
64
|
+
# @param html [String] the email's body, trusted as rendered
|
|
65
|
+
# @param subject [String, nil]
|
|
66
|
+
# @return [String] the finished email HTML
|
|
67
|
+
def wrap(html, subject: nil)
|
|
68
|
+
body = %(<mj-section><mj-column><mj-text align="left">#{html}</mj-text></mj-column></mj-section>)
|
|
69
|
+
|
|
70
|
+
render_layout(body, variables('subject' => subject.to_s))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Everything a template receives: the given variables plus `store` and
|
|
74
|
+
# `locale`, with string keys.
|
|
75
|
+
#
|
|
76
|
+
# @param assigns [Hash]
|
|
77
|
+
# @return [Hash]
|
|
78
|
+
def variables(assigns = {})
|
|
79
|
+
base_assigns.merge(assigns.deep_stringify_keys)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def render_layout(body, assigns)
|
|
85
|
+
layout = @resolver.find(LAYOUT) || raise(ArgumentError, "Missing email layout #{LAYOUT}.liquid")
|
|
86
|
+
|
|
87
|
+
MRML.to_html(render_liquid(layout.body, assigns.merge('content_for_layout' => body.html_safe)))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The subject is plain text for a mail header, so it is not HTML-escaped.
|
|
91
|
+
def subject_for(template, assigns)
|
|
92
|
+
render_liquid(template.subject.to_s, assigns, escape: false).squish
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_text(template, assigns, html)
|
|
96
|
+
text_template = @resolver.find_text(template.key)
|
|
97
|
+
return Spree::Emails::TextConverter.call(html) unless text_template
|
|
98
|
+
|
|
99
|
+
render_liquid(text_template.body, assigns, escape: false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_liquid(source, assigns, escape: true)
|
|
103
|
+
context = Spree::Emails::LiquidContext.build(
|
|
104
|
+
environment: self.class.environment,
|
|
105
|
+
environments: [assigns],
|
|
106
|
+
registers: { store: @store, currency: @currency, escape_output: escape,
|
|
107
|
+
file_system: Spree::Emails::FileSystem.new(@resolver) },
|
|
108
|
+
resource_limits: Liquid::ResourceLimits.new(RESOURCE_LIMITS),
|
|
109
|
+
rethrow_errors: true
|
|
110
|
+
)
|
|
111
|
+
context.strict_variables = @strict
|
|
112
|
+
context.strict_filters = true
|
|
113
|
+
|
|
114
|
+
Liquid::Template.parse(source, environment: self.class.environment, line_numbers: true).render!(context)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def base_assigns
|
|
118
|
+
@base_assigns ||= {
|
|
119
|
+
'store' => JSON.parse(Spree::Emails::StoreSerializer.new(@store, params: { branding: @branding }).serialize),
|
|
120
|
+
'locale' => I18n.locale.to_s
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -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
|