Skip to content

Platform Export - Pricing Engine

Overview

The pricing engine calculates export prices for each platform using a formula:

Export Price = AK + Margin + Transport Surcharge

Price Calculation

class PricingEngine
{
    public function calculatePrice(
        Article $article,
        PlatformConfig $platform,
        float $basePrice // AK from Webshop
    ): Money {
        // 1. Get applicable margin
        $marginRule = $this->findApplicableMarginRule($article, $platform);
        $marginAmount = $this->calculateMargin($basePrice, $marginRule);

        // 2. Calculate transport surcharge
        $transportSurcharge = $this->calculateTransportSurcharge($article, $platform);

        // 3. Sum up
        $totalPrice = $basePrice + $marginAmount + $transportSurcharge;

        return Money::EUR($totalPrice);
    }

    private function findApplicableMarginRule(Article $article, PlatformConfig $platform): ?PlatformMarginRule
    {
        $rules = $platform->getMarginRules();

        // Sort by specificity (most specific first)
        $sortedRules = $this->sortBySpecificity($rules);

        foreach ($sortedRules as $rule) {
            if ($this->ruleApplies($rule, $article)) {
                return $rule;
            }
        }

        return null;
    }

    private function ruleApplies(PlatformMarginRule $rule, Article $article): bool
    {
        // Article-specific rule
        if ($rule->getArticleNumber() === $article->getArticleNumber()) {
            return true;
        }

        // Check all criteria must match
        $matches = true;

        if ($rule->getCategoryId() && $rule->getCategoryId() !== $article->getCategory()->getId()) {
            $matches = false;
        }

        if ($rule->getBrandId() && $rule->getBrandId() !== $article->getBrand()->getId()) {
            $matches = false;
        }

        if ($rule->getProductType() && $rule->getProductType() !== $article->getType()) {
            $matches = false;
        }

        if ($rule->getDiameter() && $article instanceof Wheel && $rule->getDiameter() !== $article->getDiameter()) {
            $matches = false;
        }

        if ($rule->getTyreSize() && $article instanceof Tyre && $rule->getTyreSize() !== $article->getSize()) {
            $matches = false;
        }

        return $matches;
    }

    private function calculateMargin(float $basePrice, ?PlatformMarginRule $rule): float
    {
        if (!$rule) {
            return 0;
        }

        return match ($rule->getMarginType()) {
            'percentage' => $basePrice * ($rule->getMarginValue() / 100),
            'fixed' => $rule->getMarginValue(),
            default => 0,
        };
    }

    private function calculateTransportSurcharge(Article $article, PlatformConfig $platform): float
    {
        $weight = $article->getWeight();
        $tiers = $platform->getTransportTiers();

        foreach ($tiers as $tier) {
            if ($weight >= $tier->getMinWeight() && $weight < $tier->getMaxWeight()) {
                return $tier->getSurcharge();
            }
        }

        return 0;
    }
}

Margin Rules

Entity

#[ORM\Entity]
class PlatformMarginRule
{
    #[ORM\ManyToOne(targetEntity: PlatformConfig::class)]
    private PlatformConfig $platform;

    #[ORM\Column(length: 20)]
    private string $marginType; // 'percentage' or 'fixed'

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    private string $marginValue;

    // Specificity criteria (null = any)
    #[ORM\Column(nullable: true)]
    private ?int $categoryId;

    #[ORM\Column(nullable: true)]
    private ?int $brandId;

    #[ORM\Column(length: 20, nullable: true)]
    private ?string $productType; // 'tyre', 'wheel', 'accessory'

    #[ORM\Column(type: 'decimal', precision: 4, scale: 1, nullable: true)]
    private ?string $diameter; // For wheels

    #[ORM\Column(length: 20, nullable: true)]
    private ?string $tyreSize; // e.g., "225/45R17"

    #[ORM\Column(length: 50, nullable: true)]
    private ?string $articleNumber; // Most specific

    #[ORM\Column]
    private int $priority = 0; // Higher = more specific
}

Margin Hierarchy (Most to Least Specific)

Level Example Priority
Article Article "TYRE-001" +€5 100
Tyre Size Size 225/45R17 +8% 90
Diameter 19" wheels +10% 80
Brand + Category Michelin tyres +12% 70
Brand Michelin +15% 60
Category Tyres +20% 50
Product Type All wheels +18% 40
Default Everything +25% 0

Transport Staffel (Weight Tiers)

#[ORM\Entity]
class PlatformTransportTier
{
    #[ORM\ManyToOne(targetEntity: PlatformConfig::class)]
    private PlatformConfig $platform;

    #[ORM\Column(type: 'decimal', precision: 8, scale: 2)]
    private string $minWeight; // kg

    #[ORM\Column(type: 'decimal', precision: 8, scale: 2)]
    private string $maxWeight; // kg

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    private string $surcharge; // €
}

Example Transport Tiers

Min Weight Max Weight Surcharge
0 kg 5 kg €5.00
5 kg 10 kg €8.00
10 kg 20 kg €12.00
20 kg 50 kg €18.00
50 kg 100 kg €25.00

Gherkin Scenarios

Feature: Platform Pricing
  As the platform export system
  I want to calculate accurate export prices
  Based on configurable margin rules

  Scenario: Apply category margin
    Given platform "Tyre24" has 20% margin for category "Tyres"
    And tyre "MICH-001" costs €100 (AK)
    When I calculate the export price
    Then the margin should be €20

  Scenario: Brand margin overrides category
    Given platform has 20% margin for tyres (category)
    And platform has 12% margin for Michelin (brand)
    And Michelin tyre costs €100
    When I calculate the export price
    Then the margin should be €12 (brand is more specific)

  Scenario: Article-specific margin is highest priority
    Given platform has 20% margin for tyres
    And platform has €5 fixed margin for article "TYRE-001"
    When I calculate price for "TYRE-001" at €100 AK
    Then the margin should be €5

  Scenario: Transport surcharge by weight
    Given platform has €8 transport for 5-10kg
    And article weighs 7kg
    When I calculate the export price
    Then transport surcharge should be €8

  Scenario: Complete price calculation
    Given article costs €100 (AK)
    And applicable margin is 15% (€15)
    And transport surcharge is €8
    When I calculate the export price
    Then total should be €123

Admin Configuration

class PlatformMarginController
{
    #[Route('/admin/platforms/{id}/margins', name: 'admin_platform_margins')]
    public function margins(int $id): Response
    {
        $platform = $this->platformRepository->find($id);
        $rules = $platform->getMarginRules();

        // Group by type for display
        $groupedRules = [
            'article' => [],
            'tyre_size' => [],
            'diameter' => [],
            'brand_category' => [],
            'brand' => [],
            'category' => [],
            'type' => [],
            'default' => [],
        ];

        foreach ($rules as $rule) {
            $group = $this->determineRuleGroup($rule);
            $groupedRules[$group][] = $rule;
        }

        return $this->render('admin/platforms/margins.html.twig', [
            'platform' => $platform,
            'groupedRules' => $groupedRules,
        ]);
    }

    #[Route('/admin/platforms/{id}/margins/create', name: 'admin_platform_margin_create')]
    public function createMargin(int $id, Request $request): Response
    {
        $platform = $this->platformRepository->find($id);

        $rule = new PlatformMarginRule();
        $rule->setPlatform($platform);

        $form = $this->createForm(PlatformMarginRuleType::class, $rule);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // Auto-calculate priority based on specificity
            $rule->setPriority($this->calculatePriority($rule));

            $this->entityManager->persist($rule);
            $this->entityManager->flush();

            return $this->redirectToRoute('admin_platform_margins', ['id' => $id]);
        }

        return $this->render('admin/platforms/margin_form.html.twig', [
            'platform' => $platform,
            'form' => $form,
        ]);
    }

    private function calculatePriority(PlatformMarginRule $rule): int
    {
        $priority = 0;

        if ($rule->getArticleNumber()) $priority += 100;
        if ($rule->getTyreSize()) $priority += 90;
        if ($rule->getDiameter()) $priority += 80;
        if ($rule->getBrandId() && $rule->getCategoryId()) $priority += 70;
        if ($rule->getBrandId()) $priority += 60;
        if ($rule->getCategoryId()) $priority += 50;
        if ($rule->getProductType()) $priority += 40;

        return $priority;
    }
}

Database Schema

-- Platform configurations
CREATE TABLE platform_configs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    connection_type VARCHAR(20) NOT NULL,
    connection_config JSON NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    article_filters JSON,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

-- Margin rules
CREATE TABLE platform_margin_rules (
    id INT PRIMARY KEY AUTO_INCREMENT,
    platform_id INT NOT NULL,
    margin_type VARCHAR(20) NOT NULL,
    margin_value DECIMAL(10,2) NOT NULL,
    category_id INT,
    brand_id INT,
    product_type VARCHAR(20),
    diameter DECIMAL(4,1),
    tyre_size VARCHAR(20),
    article_number VARCHAR(50),
    priority INT NOT NULL DEFAULT 0,
    FOREIGN KEY (platform_id) REFERENCES platform_configs(id),
    INDEX idx_platform_priority (platform_id, priority DESC)
);

-- Transport tiers
CREATE TABLE platform_transport_tiers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    platform_id INT NOT NULL,
    min_weight DECIMAL(8,2) NOT NULL,
    max_weight DECIMAL(8,2) NOT NULL,
    surcharge DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (platform_id) REFERENCES platform_configs(id),
    INDEX idx_platform_weight (platform_id, min_weight)
);