442 lines
16 KiB
PHP
442 lines
16 KiB
PHP
<?php
|
|
|
|
app_require('classes/economic_v2_versioning_service.php');
|
|
app_require('classes/economic_v2_distribution_service.php');
|
|
|
|
use classes\economic_v2_distribution_service;
|
|
use classes\economic_v2_versioning_service;
|
|
|
|
if (!class_exists('FakeEconomicV2DistributionVersioningService')) {
|
|
class FakeEconomicV2DistributionVersioningService extends economic_v2_versioning_service
|
|
{
|
|
public ?array $fixedVersion = null;
|
|
public array $subscriptionVersions = [];
|
|
public int $backfillCalls = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
public function resolveFixedPricingVersionAt(int $customer_number, string $timestamp): ?array
|
|
{
|
|
return $this->fixedVersion;
|
|
}
|
|
|
|
public function resolveVehicleSubscriptionVersionsAt(int $customer_number, string $timestamp): array
|
|
{
|
|
return $this->subscriptionVersions;
|
|
}
|
|
|
|
public function resolveDiscountOverrideAt(int $customer_number, bool $is_category, string|int $object_id, string $timestamp, ?int $department_id = null): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function runBestEffortBackfill(): array
|
|
{
|
|
$this->backfillCalls++;
|
|
return [
|
|
'fixed_pricing' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
|
|
'vehicle_subscriptions' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
|
|
'discount_overrides' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
|
|
'inferred' => ['fixed_pricing' => 0, 'vehicle_subscriptions' => 0],
|
|
'warnings' => [],
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!class_exists('TestableEconomicV2DistributionService')) {
|
|
class TestableEconomicV2DistributionService extends economic_v2_distribution_service
|
|
{
|
|
public array $stubOrders = [];
|
|
public array $stubOrderItems = [];
|
|
public array $stubVersionRows = [];
|
|
public array $subscriptionPrices = [];
|
|
public array $versionTableState = [];
|
|
public int $fallbackDepartmentId = 8;
|
|
public array $customerDefaultDepartments = [];
|
|
public ?array $includedCustomers = null;
|
|
|
|
public function __construct(?economic_v2_versioning_service $versioning = null)
|
|
{
|
|
parent::__construct($versioning);
|
|
}
|
|
|
|
protected function fetchOrdersInRange(string $from_ts, string $to_ts): array
|
|
{
|
|
return $this->stubOrders;
|
|
}
|
|
|
|
protected function fetchOrderItemsByOrderIds(array $order_ids): array
|
|
{
|
|
return $this->stubOrderItems;
|
|
}
|
|
|
|
protected function fetchVehicleSubscriptionVersionRows(string $from_ts, string $to_ts): array
|
|
{
|
|
return $this->stubVersionRows;
|
|
}
|
|
|
|
protected function calculateOrderOriginalPrice(array $order_items, int $customer_number, int $department_id, string $timestamp): float
|
|
{
|
|
$total = 0.0;
|
|
foreach ($order_items as $item) {
|
|
$total += (float)($item['price'] ?? 0) * (float)($item['quantity'] ?? 0);
|
|
}
|
|
return $total;
|
|
}
|
|
|
|
protected function getSubscriptionMonthlyPrice(int $vehicle_type): float
|
|
{
|
|
return (float)($this->subscriptionPrices[$vehicle_type] ?? 0.0);
|
|
}
|
|
|
|
protected function getProductDepartmentPrice(int $product_id, int $department_id): float
|
|
{
|
|
return 100.0;
|
|
}
|
|
|
|
protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp, ?int $department_id = null): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
protected function buildCustomerEnvelope(int $customer_number, array $transaction_map): array
|
|
{
|
|
return [
|
|
'id' => $customer_number,
|
|
'customer_number' => $customer_number,
|
|
'customer_name' => 'Customer ' . $customer_number,
|
|
'transactions' => array_values($transaction_map),
|
|
'requires_action' => false,
|
|
'meta' => [],
|
|
];
|
|
}
|
|
|
|
protected function buildTransactionObject(int $order_id, string $created_at, int $department_id, ?float $amount = null, ?bool $included = null): array
|
|
{
|
|
return [
|
|
'id' => $order_id,
|
|
'date' => $created_at,
|
|
'amount' => round((float)($amount ?? 0.0), 5),
|
|
'booked' => true,
|
|
'department_id' => $department_id,
|
|
'excluded' => !($included ?? $this->isDepartmentEligible($department_id)),
|
|
];
|
|
}
|
|
|
|
protected function isDepartmentEligible(int $department_id): bool
|
|
{
|
|
return $department_id > 0 && $department_id !== 10;
|
|
}
|
|
|
|
protected function shouldIncludeCustomerNumber(int $customer_number): bool
|
|
{
|
|
if ($this->includedCustomers === null) {
|
|
return true;
|
|
}
|
|
|
|
return in_array($customer_number, $this->includedCustomers, true);
|
|
}
|
|
|
|
protected function getCustomerDefaultDepartmentId(int $customer_number): ?int
|
|
{
|
|
return $this->customerDefaultDepartments[$customer_number] ?? null;
|
|
}
|
|
|
|
protected function getFallbackDistributionDepartmentId(): int
|
|
{
|
|
return $this->fallbackDepartmentId;
|
|
}
|
|
|
|
protected function parseDepartmentMap(array $department_map): array
|
|
{
|
|
$parsed = [];
|
|
foreach ($department_map as $department_id => $amount) {
|
|
$parsed['Department ' . $department_id] = round((float)$amount, 5);
|
|
}
|
|
return $parsed;
|
|
}
|
|
|
|
protected function versionTableHasRows(string $table): bool
|
|
{
|
|
if (array_key_exists($table, $this->versionTableState)) {
|
|
return (bool)$this->versionTableState[$table];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
it('runs best-effort backfill when fixed pricing version history is empty', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
$versioning->fixedVersion = [
|
|
'id' => 91,
|
|
'price' => 499.95,
|
|
'description' => 'Fixed pricing system order',
|
|
'source' => 'backfill.inferred_fixed_pricing_order',
|
|
'confidence' => 0.55,
|
|
'inferred' => true,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
];
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->versionTableState = [
|
|
'customer_fixed_pricing_versions' => false,
|
|
];
|
|
$service->stubOrders = [[
|
|
'id' => 501,
|
|
'customer_id' => 12345,
|
|
'department_id' => 10,
|
|
'created_at' => '2026-01-15 10:00:00',
|
|
'reg_1' => '',
|
|
'reference' => 'Fast pris aftale',
|
|
]];
|
|
$service->stubOrderItems = [
|
|
501 => [[
|
|
'product_id' => 61,
|
|
'price' => 499.95,
|
|
'quantity' => 1,
|
|
'reference' => '',
|
|
]],
|
|
];
|
|
|
|
$result = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect($versioning->backfillCalls)->toBe(1);
|
|
expect($result['customers'])->toHaveCount(1);
|
|
});
|
|
|
|
it('falls back to system orders for fixed pricing using the configured fallback department', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
$versioning->fixedVersion = [
|
|
'id' => 91,
|
|
'price' => 499.95,
|
|
'description' => 'Fixed pricing system order',
|
|
'source' => 'backfill.inferred_fixed_pricing_order',
|
|
'confidence' => 0.55,
|
|
'inferred' => true,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
];
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->stubOrders = [[
|
|
'id' => 501,
|
|
'customer_id' => 12345,
|
|
'department_id' => 10,
|
|
'created_at' => '2026-01-15 10:00:00',
|
|
'reg_1' => '',
|
|
'reference' => 'Fast pris aftale',
|
|
]];
|
|
$service->stubOrderItems = [
|
|
501 => [[
|
|
'product_id' => 61,
|
|
'price' => 499.95,
|
|
'quantity' => 1,
|
|
'reference' => '',
|
|
]],
|
|
];
|
|
|
|
$result = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect($result['customers'])->toHaveCount(1);
|
|
expect($result['customers'][0]['customer_number'])->toBe(12345);
|
|
expect($result['customers'][0]['meta']['fixed_pricing']['price'])->toBe(499.95);
|
|
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['order_ids'])->toBe([501]);
|
|
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals']['8'])->toBe(499.95);
|
|
expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals'])->not->toHaveKey('10');
|
|
expect($result['collective_results']['total_department_totals']['8'])->toBe(499.95);
|
|
expect($result['collective_results']['total_fixed_price'])->toBe(499.95);
|
|
expect($result['warnings'])->toContain('System order fallback used for fixed pricing (department 10).');
|
|
});
|
|
|
|
it('falls back to system orders for wash subscriptions using the configured fallback department', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
$versioning->subscriptionVersions = [[
|
|
'id' => 42,
|
|
'reg' => 'AB12345',
|
|
'vehicle_type' => 77,
|
|
'source' => 'backfill.inferred_subscription_order',
|
|
'confidence' => 0.5,
|
|
'inferred' => true,
|
|
]];
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->subscriptionPrices = [
|
|
77 => 299.0,
|
|
];
|
|
$service->stubOrders = [[
|
|
'id' => 601,
|
|
'customer_id' => 12345,
|
|
'department_id' => 10,
|
|
'created_at' => '2026-01-15 10:00:00',
|
|
'reg_1' => '',
|
|
'reference' => 'Vaskeabonnementer',
|
|
]];
|
|
$service->stubOrderItems = [
|
|
601 => [[
|
|
'product_id' => 77,
|
|
'price' => 299.0,
|
|
'quantity' => 1,
|
|
'reference' => 'AB12345',
|
|
]],
|
|
];
|
|
|
|
$result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect($result['customers'])->toHaveCount(1);
|
|
expect($result['customers'][0]['customer_number'])->toBe(12345);
|
|
expect($result['customers'][0]['meta']['subscription']['subscription_total'])->toBe(299.0);
|
|
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['reg'])->toBe('AB12345');
|
|
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0);
|
|
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('10');
|
|
expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0);
|
|
expect($result['collective_results']['total_subscription_price'])->toBe(299.0);
|
|
expect($result['warnings'])->toContain('System order fallback used for wash subscriptions (department 10).');
|
|
});
|
|
|
|
it('uses the configured fallback department when a subscription has no customer department basis', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->subscriptionPrices = [
|
|
77 => 299.0,
|
|
];
|
|
$service->stubVersionRows = [[
|
|
'id' => 42,
|
|
'customer_number' => 12345,
|
|
'reg' => 'AB12345',
|
|
'vehicle_type' => 77,
|
|
'wash_subscription' => 1,
|
|
'source' => 'test.version',
|
|
'confidence' => 1.0,
|
|
'inferred' => false,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
]];
|
|
|
|
$result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect($result['customers'])->toHaveCount(1);
|
|
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0);
|
|
expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('1');
|
|
expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0);
|
|
});
|
|
|
|
it('excludes orphaned customer traces from fixed pricing and customer price distributions', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
$versioning->fixedVersion = [
|
|
'id' => 91,
|
|
'price' => 499.95,
|
|
'description' => 'Fixed pricing agreement',
|
|
'source' => 'test.fixed',
|
|
'confidence' => 1.0,
|
|
'inferred' => false,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
];
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->includedCustomers = [12345];
|
|
$service->stubOrders = [
|
|
[
|
|
'id' => 501,
|
|
'customer_id' => 80000038,
|
|
'department_id' => 1,
|
|
'created_at' => '2026-01-15 10:00:00',
|
|
'include_in_invoice' => 1,
|
|
'reg_1' => '',
|
|
'reference' => '',
|
|
],
|
|
[
|
|
'id' => 502,
|
|
'customer_id' => 12345,
|
|
'department_id' => 1,
|
|
'created_at' => '2026-01-16 10:00:00',
|
|
'include_in_invoice' => 1,
|
|
'reg_1' => '',
|
|
'reference' => '',
|
|
],
|
|
];
|
|
$service->stubOrderItems = [
|
|
501 => [[
|
|
'product_id' => 61,
|
|
'price' => 250.0,
|
|
'quantity' => 1,
|
|
'reference' => '',
|
|
]],
|
|
502 => [[
|
|
'product_id' => 61,
|
|
'price' => 300.0,
|
|
'quantity' => 1,
|
|
'reference' => '',
|
|
]],
|
|
];
|
|
|
|
$fixedPricing = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31');
|
|
$customerPrices = $service->getCustomerPricesDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect(array_column($fixedPricing['customers'], 'customer_number'))->toBe([12345]);
|
|
expect($fixedPricing['collective_results']['total_fixed_price'])->toBe(499.95);
|
|
expect($fixedPricing['collective_results']['total_original_price'])->toBe(300.0);
|
|
|
|
expect(array_column($customerPrices['customers'], 'customer_number'))->toBe([12345]);
|
|
expect($customerPrices['collective_results']['total_discount_amount'])->toBe(0.0);
|
|
});
|
|
|
|
it('excludes orphaned customers from subscription fallback versions', function (): void {
|
|
$versioning = new FakeEconomicV2DistributionVersioningService();
|
|
|
|
$service = new TestableEconomicV2DistributionService($versioning);
|
|
$service->includedCustomers = [12345];
|
|
$service->stubOrders = [[
|
|
'id' => 701,
|
|
'customer_id' => 12345,
|
|
'department_id' => 4,
|
|
'created_at' => '2026-01-15 10:00:00',
|
|
'include_in_invoice' => 1,
|
|
'reg_1' => 'AB12345',
|
|
'reference' => '',
|
|
]];
|
|
$service->subscriptionPrices = [
|
|
77 => 299.0,
|
|
];
|
|
$service->stubVersionRows = [
|
|
[
|
|
'id' => 41,
|
|
'customer_number' => 80000038,
|
|
'reg' => 'ZZ99999',
|
|
'vehicle_type' => 77,
|
|
'wash_subscription' => 1,
|
|
'source' => 'test.version',
|
|
'confidence' => 1.0,
|
|
'inferred' => false,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
],
|
|
[
|
|
'id' => 42,
|
|
'customer_number' => 12345,
|
|
'reg' => 'AB12345',
|
|
'vehicle_type' => 77,
|
|
'wash_subscription' => 1,
|
|
'source' => 'test.version',
|
|
'confidence' => 1.0,
|
|
'inferred' => false,
|
|
'effective_from' => '2026-01-01 00:00:00',
|
|
'effective_to' => null,
|
|
],
|
|
];
|
|
|
|
$result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31');
|
|
|
|
expect(array_column($result['customers'], 'customer_number'))->toBe([12345]);
|
|
expect($result['warnings'])->toBe([
|
|
'Fallback allocation used for subscription AB12345 customer 12345 in 2026-01',
|
|
]);
|
|
});
|