diff --git a/.wordlist.txt b/.wordlist.txt index 53ba974349..5bf2651e44 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -72,6 +72,8 @@ ArrayFacade AssociationField AsyncPaymentTransactionStruct AsynchronousPaymentHandlerInterface +AudienceContext +AudienceContextResolver AuditLogValueEntity AuthenticationIdentityLoader AutoIncrementField @@ -453,6 +455,15 @@ Iframe ImportTranslationsTrait Inclusivity IndexerService +IndividualPricingApplyExtension +IndividualPricingBuildCacheSingleRuleMessage +IndividualPricingCacheEntryUpdaterMessage +IndividualPricingComputedCacheEntity +IndividualPricingIndexerEvent +IndividualPricingIndexingMessage +IndividualPricingLookupBatchCriteriaEvent +IndividualPricingLookupCriteriaEvent +IndividualPricingProductSubscriber Init Initialisms IntField @@ -738,6 +749,7 @@ ProductController ProductCountRouteResponse ProductDataSelection ProductDataSet +ProductEntity ProductListRoute ProductManufacturerDefinition ProductMedia @@ -1074,6 +1086,8 @@ YamlFileLoader ZSH accel acl +actionAmount +actionType actionability activateShopwareTheme adr @@ -1089,6 +1103,7 @@ api apiKey apk appVersion +applyToAllProducts args arrayfacade async @@ -1238,6 +1253,7 @@ customerAware customerGroup customerGroupAware customerHasFeature +customerId customerRecovery customizability customizable @@ -1454,6 +1470,7 @@ inclusivity incrementer incrementing indexActions +individualPricingId infoIt ini init @@ -1616,6 +1633,7 @@ orderIds orderTransaction orderTransactionCaptureRefund org's +organizationUnitIds otel otlp overrideComponentSetup @@ -1682,6 +1700,7 @@ pricefactory productCountRoute productId productNumber +productStreamId productproxy productsfacade profiler @@ -1696,6 +1715,8 @@ pyspelling qa qa@shopware.com qty's +qtyFrom +qtyTo quantityBefore quantityDelta quickstart @@ -1801,6 +1822,7 @@ shopware's shopwarelabs shopwarepartners shorthands +showStrikeThrough simples simplexml skipOnFeature @@ -1929,11 +1951,14 @@ upsert uri url urls +useValidityRange userAware userRecovery userland utils uuid +validFrom +validUntil validator validators varchar diff --git a/products/extensions/b2b-components/index.md b/products/extensions/b2b-components/index.md index 6663e3f8e4..57670af639 100644 --- a/products/extensions/b2b-components/index.md +++ b/products/extensions/b2b-components/index.md @@ -17,6 +17,8 @@ In the world of digital B2B commerce, where businesses engage with other compani * **Order Approval** allows for a more controlled buying process by introducing an approval workflow. +* **Individual Pricing** enables merchants to define catalog-wide discounts and special pricing based on flexible conditions for B2B scenarios, including volume pricing and company-specific pricing agreements. + * **Quick Order and Shopping List** takes care of distinctive B2B buying behaviors. * **Organization Unit** allows for the configuration of more differentiated and specific access rights to meet the needs of businesses with complex structures. @@ -84,7 +86,7 @@ We will place this check before every route, controller or API as follows: ```php use Shopware\Commercial\B2B\QuickOrder\Domain\CustomerSpecificFeature\CustomerSpecificFeatureService; - + class ApiController { public function __construct(private readonly CustomerSpecificFeatureService $customerSpecificFeatureService) @@ -131,11 +133,11 @@ class CustomerSpecificFeatureTwigExtension extends AbstractExtension if (\array_key_exists('context', $twigContext) && $twigContext['context'] instanceof SalesChannelContext) { $customerId = $twigContext['context']->getCustomerId(); } - + if (!$customerId) { return false; } - + return $this->customerSpecificFeatureService->isAllowed($customerId, $feature); } } diff --git a/products/extensions/b2b-components/individual-pricing/concepts/01-entities-and-workflow.md b/products/extensions/b2b-components/individual-pricing/concepts/01-entities-and-workflow.md new file mode 100644 index 0000000000..fe3c7588c9 --- /dev/null +++ b/products/extensions/b2b-components/individual-pricing/concepts/01-entities-and-workflow.md @@ -0,0 +1,200 @@ +--- +nav: + title: Entities and workflow + position: 10 + +--- + +# Entities and workflow + +## Entities + +### Individual Pricing + +The Individual Pricing entity is the main configuration entity that defines a pricing rule. It contains all the settings needed to determine which products get special pricing, who receives it, and how the pricing is calculated. + +**Key properties:** + +- **name**: Human-readable name for the pricing rule +- **description**: Optional description of the pricing rule +- **active**: Boolean flag to enable/disable the rule +- **priority**: Integer value determining evaluation order (higher values = higher priority) +- **target**: Type of audience (companies, tags) +- **actionType**: Type of price modification (by_percent, by_fixed, to_fixed, volume_pricing) +- **actionAmount**: Amount for the pricing action (percentage or fixed value) +- **applyToAllProducts**: If true, applies to all products in the catalog +- **productStreamId**: Reference to a Shopware product stream for filtering specific products +- **useValidityRange**: Boolean indicating if time-based validity is used +- **validFrom**: Start date/time for the pricing rule +- **validUntil**: End date/time for the pricing rule +- **showStrikeThrough**: Whether to show original price with strike-through + +**Action Types:** + +- `by_percent`: Reduce price by a percentage (e.g., 10% off) +- `by_fixed`: Reduce price by a fixed amount (e.g., 5$ off) +- `to_fixed`: Set price to a specific amount (e.g., 99.99$) +- `volume_pricing`: Use tiered pricing based on quantity + +### Individual Pricing Tier + +The tier entity defines volume-based pricing tiers for quantity discounts. Multiple tiers can be associated with a single Individual Pricing rule when the action type is set to `volume_pricing`. + +**Key properties:** + +- **individualPricingId**: Reference to the parent pricing rule +- **qtyFrom**: Minimum quantity for this tier (inclusive) +- **qtyTo**: Maximum quantity for this tier (inclusive, null for unlimited) +- **price**: Price collection containing prices for different currencies + +**Example tiers:** + +- Tier 1: 1-9 units @ 10$ each +- Tier 2: 10-49 units @ 9$ each +- Tier 3: 50+ units @ 8$ each + +### Individual Pricing Company Assignment + +This entity links pricing rules to specific business partner companies or organization units. It determines which companies or parts of companies receive the special pricing. + +**Key properties:** + +- **individualPricingId**: Reference to the pricing rule +- **customerId**: Reference to the business partner customer +- **scope**: Defines assignment scope (whole_company, all_org_units, specific_units) +- **organizationUnitIds**: JSON array of specific organization unit IDs (when scope is specific_units) + +**Scopes:** + +- `whole_company`: Applies to all employees of the business partner +- `all_org_units`: Applies to all organization units within the company +- `specific_units`: Applies only to specific organization units (requires organizationUnitIds) + +### Individual Pricing Computed Cache + +The computed cache entity pre-calculates which products are affected by which pricing rules. This significantly improves performance by avoiding repeated filter evaluations. + +**Key properties:** + +- **individualPricingId**: Reference to the pricing rule +- **productId**: Reference to the affected product (NULL if rule applies to all products) + +This cache uses a hybrid approach: specific cache entries for targeted rules, and NULL entries for catalog-wide rules. + +## Schema + +```mermaid +erDiagram + b2b_components_individual_pricing { + uuid id PK + boolean active + boolean show_strike_through + string name + string target + int priority + boolean apply_to_all_products + uuid product_stream_id FK + boolean use_validity_range + datetime valid_from + datetime valid_until + string description + string action_type + float action_amount + uuid created_by_id FK + uuid updated_by_id FK + json custom_fields + } + b2b_components_individual_pricing_tier { + uuid id PK + uuid individual_pricing_id FK + int qty_from + int qty_to + json price + } + b2b_components_individual_pricing_company_assignment { + uuid id PK + uuid individual_pricing_id FK + uuid customer_id FK + string scope + json organization_unit_ids + } + b2b_components_individual_pricing_computed_cache { + uuid id PK + uuid individual_pricing_id FK + uuid product_id FK + } + b2b_components_individual_pricing_tag { + uuid individual_pricing_id FK + uuid tag_id FK + } + product_stream { + uuid id PK + string name + } + + b2b_components_individual_pricing ||--o{ b2b_components_individual_pricing_tier : "has volume tiers" + b2b_components_individual_pricing ||--o{ b2b_components_individual_pricing_company_assignment : "assigned to companies" + b2b_components_individual_pricing ||--o{ b2b_components_individual_pricing_computed_cache : "cached for products" + b2b_components_individual_pricing ||--o{ b2b_components_individual_pricing_tag : "targets tags" + b2b_components_individual_pricing }o--|| product_stream : "uses" + b2b_components_individual_pricing_company_assignment }o--|| customer : "belongs to" + b2b_components_individual_pricing_computed_cache }o--|| product : "references" + b2b_components_individual_pricing_tag }o--|| tag : "references" +``` + +## Workflow + +The following diagram shows how individual pricing is applied to a product: + +```mermaid +flowchart TD + A[Customer views product] --> B{Is logged-in?} + B -->|No| C[Show standard catalog price] + B -->|Yes| D[Load customer context] + D --> E[Query active pricing rules] + E --> F{Any rules found?} + F -->|No| C + F -->|Yes| G[Get all rules at highest priority] + G --> H[Evaluate all rules at this priority] + H --> I{Any rules match product streams?} + I -->|No| C + I -->|Yes| J{Multiple rules match?} + J -->|No| K[Use single matching rule] + J -->|Yes| L[Calculate price for each rule] + L --> M[Select rule with lowest price] + K --> N{Volume pricing?} + M --> N + N -->|Yes| O[Select appropriate tier] + N -->|No| P[Apply action type] + O --> Q[Calculate final price] + P --> Q + Q --> R{Show strike-through?} + R -->|Yes| S[Display with original price] + R -->|No| T[Display discounted price] +``` + +## Target type evaluation + +Depending on the target type, different evaluation logic applies: + +### Companies target + +- Customer must be a business partner or employee +- Company assignment must exist linking the pricing rule to the customer's company +- Scope is checked (whole company, all units, or specific units) +- If specific units, customer must belong to one of the specified organization units + +### Tags target + +- Customer must have at least one of the tags specified in the pricing rule + +## Priority and rule selection + +When multiple pricing rules could apply to the same product: + +1. Only rules at the highest priority level are considered +2. All rules at this priority level are evaluated +3. If multiple rules match, the one resulting in the lowest price is selected +4. If no rules match at the highest priority, standard catalog pricing is used + +**Note:** Lower priority rules are never evaluated. This ensures the most important pricing takes precedence, and when multiple rules compete at the same level, customers always get the best price. diff --git a/products/extensions/b2b-components/individual-pricing/concepts/02-pricing-workflow.md b/products/extensions/b2b-components/individual-pricing/concepts/02-pricing-workflow.md new file mode 100644 index 0000000000..1cee760d2d --- /dev/null +++ b/products/extensions/b2b-components/individual-pricing/concepts/02-pricing-workflow.md @@ -0,0 +1,311 @@ +--- +nav: + title: Pricing workflow + position: 20 + +--- + +# Pricing workflow + +This document describes how Individual Pricing integrates with Shopware's pricing system and how prices are calculated at runtime. + +## Price Override Flow + +Individual prices are applied to products at runtime during the sales channel product loading phase. + +### Phase 1: Context Creation + +The system identifies the customer type (business partner, employee, or tag-based customer) and prepares the audience context for pricing resolution. This is handled by `AudienceContextResolver` during request initialization. + +### Phase 2: Product Loading + +When products are loaded via the Store API, `IndividualPricingProductSubscriber` listens to the product loaded event and triggers the pricing application process. + +### Phase 3: Price Resolution + +The system queries the computed cache to find applicable pricing rules for each product based on the customer's audience context. If multiple rules match at the highest priority level, the one with the lowest price is selected. + +### Phase 4: Price Application + +Prices are applied differently based on the pricing type: + +- **Single pricing**: The product gets one price with optional strike-through of the original price +- **Volume pricing**: Multiple tier prices are assigned with quantity ranges, allowing different prices based on order quantity + +If strike-through is enabled, the original price is preserved for display alongside the discounted price. + +### High-level sequence + +```mermaid +sequenceDiagram + participant Customer + participant Storefront + participant CartProcessor + participant PricingResolver + participant Cache + participant PriceCalculator + + Customer->>Storefront: Add product to cart + Storefront->>CartProcessor: Process cart + CartProcessor->>PricingResolver: Resolve pricing for line items + PricingResolver->>Cache: Query computed cache + Cache-->>PricingResolver: Return applicable pricing rules + PricingResolver->>PricingResolver: Filter by priority + PricingResolver->>PricingResolver: Evaluate target conditions + PricingResolver->>PriceCalculator: Calculate price for matching rule + + alt Volume pricing + PriceCalculator->>PriceCalculator: Select appropriate tier + PriceCalculator->>PriceCalculator: Apply tier price + else Percentage discount + PriceCalculator->>PriceCalculator: Calculate percentage + PriceCalculator->>PriceCalculator: Apply discount + else Fixed discount + PriceCalculator->>PriceCalculator: Subtract fixed amount + else Fixed price + PriceCalculator->>PriceCalculator: Set fixed price + end + + PriceCalculator-->>CartProcessor: Return calculated price + CartProcessor-->>Storefront: Updated cart + Storefront-->>Customer: Display cart with pricing +``` + +## Pricing resolution process + +The pricing resolution follows these steps: + +1. **Context identification**: Determine the customer's company, groups, tags, and organization units +2. **Cache lookup**: Query the computed cache for applicable pricing rules +3. **Rule filtering**: Filter rules by: + - Active status + - Validity period (if configured) + - Target type matching (company, group, or tag) + - Product stream matching +4. **Priority sorting**: Sort matching rules by priority (highest first) +5. **Rule evaluation and selection**: For all rules at the highest priority, calculate their resulting prices and select the rule that yields the lowest price +6. **Price calculation**: Apply the selected pricing rule's action to calculate the final price + +### Priority-based selection + +```mermaid +flowchart TD + A[Multiple rules exist] --> B[Sort by priority DESC] + B --> C[Get all rules with highest priority] + C --> D[Evaluate all rules at this priority] + D --> E{Any rules match?} + E -->|No| F[Use standard price] + E -->|Yes| G{Multiple rules match?} + G -->|No| H[Use single matching rule] + G -->|Yes| I[Calculate price for each rule] + I --> J[Select rule with lowest price] + H --> K[Apply selected rule] + J --> K + K --> L[Calculate final price] +``` + +## Volume pricing calculation + +When the action type is `volume_pricing`, the system evaluates tiers based on quantity: + +```mermaid +flowchart TD + A[Volume pricing rule selected] --> B[Get line item quantity] + B --> C[Load pricing tiers] + C --> D[Sort tiers by qtyFrom] + D --> E[Find matching tier] + E --> F{Quantity >= qtyFrom?} + F -->|No| G[Check next tier] + F -->|Yes| H{Quantity <= qtyTo or qtyTo is null?} + H -->|No| G + H -->|Yes| I[Select this tier] + I --> J[Apply tier price] + G --> K{More tiers?} + K -->|Yes| F + K -->|No| L[Use base price] +``` + +## Caching and indexing + +Individual Pricing uses a hybrid caching strategy combining pre-computed entries with runtime-evaluated rules. This balances query performance with storage efficiency. + +**Two approaches:** + +- **Specific products**: Pre-computed cache entries for each product-rule pair (instant lookup) +- **Apply to all products**: Single NULL entry per rule (fast lookup + runtime calculation) + +### Cache workflow + +The system maintains cache entries automatically: + +```mermaid +flowchart LR + A[Pricing rule created] --> B[Indexer triggered] + B --> C{Apply to all products?} + C -->|Yes| D[Create single cache entry with product_id=NULL] + C -->|No| E[Match via product stream] + E --> F[Create cache entries for matching products] + D --> G[Cache ready for lookup] + F --> G +``` + +Cache entries are regenerated when rules are updated and removed when rules are deleted. + +## Interaction with standard pricing + +Individual Pricing works alongside Shopware's standard pricing system: + +1. **Standard price as base**: The product's standard price serves as the starting point +2. **Individual pricing override**: When applicable, individual pricing replaces or modifies the standard price +3. **Advanced prices respected**: Product-level advanced prices (graduated prices) are considered if no individual pricing applies +4. **Rule-based prices**: Shopware's rule-based prices have lower priority than individual pricing + +**Priority evaluation:** Only the highest priority level is considered. If multiple rules match at that level, the one producing the lowest price is selected, ensuring customers always get the best available price. + +## Custom Pricing and Individual Pricing + +Individual Pricing takes precedence over Shopware's custom pricing feature. When an Individual Pricing rule applies to a customer and product combination, any existing custom pricing is bypassed. + +**Priority hierarchy:** + +1. Individual Pricing (highest priority when applicable) +2. Shopware custom pricing +3. Product advanced/graduated prices +4. Rule-based prices +5. Standard list price (lowest priority) + +This ensures that B2B customers receive their negotiated Individual Pricing rates rather than generic custom pricing. Custom pricing will only apply when no Individual Pricing rules match the customer and product. + +## Future Development and Migration Plans + +### Migration from Custom Pricing + +We plan to provide a migration path from Shopware's custom pricing to Individual Pricing. This migration will help consolidate pricing management into a single, B2B-optimized system. + +**Benefits of migration:** + +- Unified pricing management interface +- Better performance through pre-computed caching +- Priority-based rule evaluation +- Target-based pricing (companies, tags) +- Volume pricing support +- Time-based validity periods + +### Mass Upsert API + +A mass upsert API endpoint is planned for Individual Pricing to support: + +- Bulk import of pricing rules from external systems +- Migration of existing custom pricing data +- Efficient creation/update of large pricing rule sets +- Integration with ERP and pricing management systems + +This API will enable merchants to programmatically manage thousands of pricing rules efficiently, supporting large-scale B2B pricing scenarios. + +## Strike-through pricing + +When `showStrikeThrough` is enabled: + +1. The original standard price is preserved +2. The calculated individual price is set as the current price +3. The storefront can display both prices with the original price struck through +4. This visually communicates the discount to the customer + +## Limitations and Workarounds + +### Price Sorting and Filtering Limitations + +Individual Pricing modifies product prices **at runtime after products are loaded** via the Store API. This architectural approach creates limitations with database-level and search index operations. + +#### The Issue + +**Database queries and Elasticsearch:** + +- Store original product prices in indexes (MySQL/MariaDB and Elasticsearch) +- Price sorting uses these indexed original prices +- Price range filtering uses these indexed original prices +- Individual pricing is applied AFTER query execution + +**Impact on customers:** + +- **Price sorting**: Products may appear in incorrect order based on individual prices + - Example: A product with 100$ original price but 80$ individual price sorts by 100$ +- **Price filtering**: Products may be incorrectly included or excluded from results + - Example: Filtering 50$-75$ might miss a 100$ product that costs 60$ with individual pricing + - Example: Filtering 50$-75$ might include a 50$ product that costs 85$ with individual pricing + +#### Why This Happens + +The runtime modification approach of Individual Pricing provides flexibility and performance benefits but is incompatible with persistent storage operations: + +- **Runtime modification**: Allows dynamic pricing based on customer context +- **Pre-computed caching**: Ensures fast price lookups without database queries +- **Audience-based resolution**: Different customers see different prices for the same product + +However, database indexes and Elasticsearch contain only one price per product (the original price), not the customer-specific individual price. + +#### Workaround + +To prevent displaying incorrect results to customers: + +**Price sorting and price range filtering features are automatically disabled in the storefront when Individual Pricing applies to the logged-in customer.** + +This ensures customers don't see misleading or incorrect product ordering/filtering based on prices they won't actually pay. + +## Performance considerations + +### Lookup optimization + +The hybrid caching approach ensures optimal performance: + +**Pre-computed cache (specific products):** + +- No runtime calculation needed +- Direct cache hit returns the rule immediately +- Best for frequently accessed products with complex filters + +**Runtime-evaluated cache (all products):** + +- Cache lookup is still indexed and fast +- Calculation happens once per request + +### HTTP Cache Strategy + +The HTTP caching behavior of Individual Pricing depends on the customer type: + +| Customer Type | Cacheable | Reason | +|-----------------------------|--------------|----------------------------------------------------| +| Tag-based customers | Yes (shared) | Responses shared by customers with same tags | +| Organization unit employees | Yes (shared) | Responses shared within departments/teams | +| Business partner accounts | No | Customer-specific pricing, ensures confidentiality | +| Employees without org unit | No | Individual pricing cannot be shared | + +The system prioritizes cacheability by checking for tags first, then organization units, falling back to non-cacheable for customer-specific pricing. This balances performance with price accuracy and confidentiality. + +### Indexing strategy + +> **Notice:** Individual Pricing uses queue-based indexing. After creating or updating pricing rules, it may take some time for the new prices to be visible to customers. The actual time depends on: +> +> - Number of products affected by the rule +> - Queue worker configuration and processing speed +> - Current queue load +> +> For rules with "Apply to all products" enabled, prices are available immediately as they use runtime calculation. For rules with specific products, wait for the queue to finish processing before the computed prices become available. + +Individual Pricing uses an asynchronous indexing system that pre-computes cache entries via Shopware's message queue. This ensures optimal runtime performance while handling large product catalogs efficiently. + +#### Indexing flows + +Individual Pricing maintains the computed cache through 5 indexing flows: + +**1. Product Indexing** - When products change, cache entries for those specific products are rebuilt incrementally. + +**2. Rule Changes** - When pricing rules are created or updated, `IndividualPricingCacheEntryUpdaterMessage` is dispatched to rebuild cache for all matching products. + +**3. Product Stream Filters** - When stream conditions change (e.g., changing product tags), affected pricing rules are re-indexed to reflect new product matches. + +**4. Tier Changes** - When volume pricing tiers are modified, only the affected rule's cache is updated via `IndividualPricingBuildCacheSingleRuleMessage`. + +**5. Full Re-index** - Manual command to rebuild the entire cache, coordinated by `IndividualPricingIndexingMessage`. + +All indexing happens asynchronously via Shopware's message queue in batches of 1,000 products, ensuring that pricing rule updates don't block other operations. diff --git a/products/extensions/b2b-components/individual-pricing/guides/01-extensibility-events-messages.md b/products/extensions/b2b-components/individual-pricing/guides/01-extensibility-events-messages.md new file mode 100644 index 0000000000..8c9d6c6291 --- /dev/null +++ b/products/extensions/b2b-components/individual-pricing/guides/01-extensibility-events-messages.md @@ -0,0 +1,211 @@ +--- +nav: + title: Extensibility - Events, Messages, and Extensions + position: 10 + +--- + +# Extensibility - Events, Messages, and Extensions + +This guide explains how external developers can extend Individual Pricing functionality by subscribing to events, handling messages, and using extension points. + +## Overview + +Individual Pricing provides three main extensibility mechanisms: + +1. **Extensions** - Hook into specific moments during pricing application to add custom logic +2. **Events** - Subscribe to events for custom validation, filtering, or side effects +3. **Messages** - Handle asynchronous indexing messages for custom cache management + +## Extensions + +Extensions allow you to hook into specific moments during the pricing workflow. They use Shopware's Extension API. + +### Available Extensions + +| Name | Description | Purpose | Available Data | +|-----------------------------------|------------------------------------------------------------|-----------------------------------------------------------------------------|----------------------------------------------------------------| +| `IndividualPricingApplyExtension` | Intercepts when individual pricing is applied to a product | Validation, conditional prevention, modifications, logging, or side effects | Product entity, computed pricing entity, sales channel context | + +### Extension Details + +#### IndividualPricingApplyExtension + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Extension\IndividualPricingApplyExtension` + +**Extension Name**: `individual_pricing.apply` + +**Properties**: + +- `product` (ProductEntity) - The product to which pricing is being applied +- `individualPricing` (IndividualPricingComputedCacheEntity) - The resolved individual pricing being applied +- `context` (SalesChannelContext) - Current customer/sales-channel context + +**Use Cases**: + +- Log pricing changes for auditing +- Validate pricing before application +- Trigger external systems when prices change +- Add custom data to products based on pricing + +## Events + +Events allow you to subscribe to specific moments in the pricing workflow and modify behavior. + +### Available Events + +| Name | Description | Purpose | When Dispatched | +|---------------------------------------------|----------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------| +| `IndividualPricingIndexerEvent` | Dispatched with individual pricing IDs that need to be indexed | React to indexing requests, invalidate related caches | When individual pricing rules need indexing | +| `IndividualPricingLookupCriteriaEvent` | Modify criteria for single product pricing lookup | Add custom filters/conditions to pricing resolution | Before querying cache for single product | +| `IndividualPricingLookupBatchCriteriaEvent` | Modify criteria for batch product pricing lookup | Add custom filters/conditions for multiple products | Before querying cache for product batch | + +### Event Details + +#### IndividualPricingIndexerEvent + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Event\IndividualPricingIndexerEvent` + +**Properties**: + +- `ids` Individual pricing IDs that need to be indexed +- `context` (Context) - Current context +- `skip` List of actions to be skipped during indexing + +**Use Cases**: + +- React to indexing requests for specific pricing rules +- Invalidate custom caches before/during pricing indexing +- Trigger external API updates +- Log indexing activities + +#### IndividualPricingLookupCriteriaEvent + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Event\IndividualPricingLookupCriteriaEvent` + +**Properties**: + +- `criteria` (Criteria) - Mutable criteria for cache lookup +- `productId` (string) - Product ID being looked up +- `audience` (AudienceContext) - Customer's audience context +- `applicableRuleIds` Rules that could apply +- `context` (Context) - Current context + +**Use Cases**: + +- Add custom filters to pricing resolution +- Modify sorting or limits +- Add associations for additional data + +#### IndividualPricingLookupBatchCriteriaEvent + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Event\IndividualPricingLookupBatchCriteriaEvent` + +**Properties**: + +- `criteria` (Criteria) - Mutable criteria for cache lookup +- `productIds` Product IDs being looked up +- `audience` (AudienceContext) - Customer's audience context +- `applicableRuleIds` Rules that could apply +- `context` (Context) - Current context + +**Use Cases**: + +- Apply batch-specific filtering logic +- Optimize queries for bulk operations +- Add custom aggregations + +## Messages + +Messages are dispatched to the message bus for asynchronous processing. They handle indexing operations. + +### Available Messages + +| Name | Description | Purpose | When Dispatched | +|------------------------------------------------|------------------------------------------------------|-------------------------------------------|--------------------------------------| +| `IndividualPricingCacheEntryUpdaterMessage` | Handles rule-level cache rebuilding | Process custom logic when rules change | Rule create/update/delete operations | +| `IndividualPricingBuildCacheSingleRuleMessage` | Handles cache building when indexing a specific rule | Process custom logic during rule indexing | When indexing a single pricing rule | + +### Message Details + +#### IndividualPricingCacheEntryUpdaterMessage + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Domain\Indexer\IndividualPricingCacheEntryUpdaterMessage` + +**Properties** (inherits from `EntityIndexingMessage`): + +- `data` Individual pricing IDs to be indexed +- `offset` Pagination offset for batch processing +- `context` (Context) - Current context +- `skip` List of indexers to skip during processing +- `forceQueue` (bool) - Whether to force using the message queue +- `isFullIndexing` (bool) - Whether this is full indexing + +**Purpose**: Handles cache rebuilding when specific pricing rules are created, updated, or deleted + +**Use Cases**: + +- Invalidate downstream caches when rules change +- Trigger partial updates in external systems +- Track rule-level changes for auditing +- Synchronize external pricing systems with rule modifications + +#### IndividualPricingBuildCacheSingleRuleMessage + +**Namespace**: `Shopware\Commercial\B2B\IndividualPricing\Domain\Indexer\IndividualPricingBuildCacheSingleRuleMessage` + +**Properties**: + +- `productIds` Product IDs to build cache for +- `ruleId` (string) - The pricing rule ID being indexed +- `context` (Context) - Current context + +**Purpose**: Handles cache building when indexing a single pricing rule + +**Use Cases**: + +- React to individual rule indexing operations +- Fine-grained cache control per rule +- Detailed logging and monitoring of rule indexing +- Trigger external integrations when specific rules are indexed + +## Examples + +### Example 1: Using IndividualPricingApplyExtension + +Hook into the extension to log pricing changes: + +```php + 'onPricingApply', + ]; + } + + public function onPricingApply(IndividualPricingApplyExtension $extension): void + { + $this->logger->info('Individual pricing applied', [ + 'product_id' => $extension->product->getId(), + 'product_number' => $extension->product->getProductNumber(), + 'rule_id' => $extension->individualPricing->getIndividualPricingId(), + 'customer_id' => $extension->context->getCustomer()?->getId(), + ]); + } +} +``` diff --git a/products/extensions/b2b-components/individual-pricing/index.md b/products/extensions/b2b-components/individual-pricing/index.md new file mode 100644 index 0000000000..5c8be3ffab --- /dev/null +++ b/products/extensions/b2b-components/individual-pricing/index.md @@ -0,0 +1,90 @@ +--- +nav: + title: Individual Pricing + position: 40 + +--- + +# Individual Pricing + +Individual Pricing is a B2B component that enables merchants to define catalog-wide discounts and special pricing based on flexible conditions, specifically tailored for B2B scenarios. + +::: info +This feature is available since Shopware 6.7.8.0 +::: + +## Basic idea + +Individual Pricing allows merchants to create sophisticated pricing strategies for their B2B customers. Instead of manually managing prices for each product and customer combination, merchants can define pricing rules that automatically apply discounts or custom prices based on various conditions (the same as for product streaming rules). + +This component is particularly powerful in B2B contexts where: + +- Different companies negotiate different pricing agreements +- Volume-based pricing (tiered pricing) is common +- Specific customer segments receive special pricing +- Seasonal or time-limited promotions need to be managed centrally + +## Key features + +### Target-based pricing + +Individual Pricing supports two target types: + +- **Companies**: Apply pricing to specific companies or organization units or employees +- **Tags**: Apply pricing to customers with specific tags + +### Volume pricing (Tiers) + +Define quantity-based pricing tiers where prices decrease as purchase quantities increase. Each tier can have different prices across multiple currencies. + +### Product filtering + +Control which products the pricing applies to: + +- Apply to all products in the catalog +- Use condition components to target specific product sets based on properties, categories, manufacturers, etc. + +### Priority-based rule evaluation + +When multiple pricing rules could apply, only rules at the highest priority level are evaluated. If multiple rules match at that level, the one that results in the lowest price for the customer is automatically selected. Lower priority rules are never considered. + +### Validity periods + +Set time-based validity for pricing rules with optional start and end dates, perfect for seasonal promotions or temporary pricing agreements. + +### Strike-through pricing + +Optionally display the original price with a strike-through effect to highlight the discount being applied to the customer. + +## Requirements + +- Employee Management component and Organization Unit component must be installed and activated (see [Employee Management](../employee-management/) and [Organization Unit](../organization-unit/)) + +## How it works + +When a customer browses products or adds items to their cart: + +1. The system identifies all active individual pricing rules that could apply based on the customer's context (company, tags) +2. Only rules at the highest priority level are considered +3. All rules at this priority level are evaluated together +4. Products that qualify for each rule are determined +5. If multiple rules match, the one producing the lowest price is selected +6. If no rules match at the highest priority, standard catalog pricing is used +7. If volume pricing is configured, the appropriate tier is selected based on quantity +8. The calculated price is applied to the product, overriding the standard catalog price +9. If strike-through is enabled, the original price is preserved for display purposes + +## Performance optimization + +Individual Pricing uses a hybrid caching strategy: pre-computed cache entries for specific products (instant lookup) and runtime-evaluated entries if the rule is applied to all products. The cache is automatically maintained through background indexing and incremental updates. +This approach ensures fast pricing lookups even with thousands of products while keeping storage requirements minimal. + +## Extensibility + +Individual Pricing provides comprehensive extensibility through: + +- **Extensions**: Hook into pricing application moments (e.g., `IndividualPricingApplyExtension`) +- **Events**: Subscribe to events for custom validation and filtering (e.g., `IndividualPricingLookupCriteriaEvent`) +- **Messages**: Handle asynchronous indexing operations (e.g., `IndividualPricingIndexingMessage`) + +For detailed information on all available extensibility points, see [Extensibility - Events, Messages, and Extensions](guides/01-extensibility-events-messages.md).