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)
);