Add logic for booked department 75 redistribution with fixed pricing and subscription weights. Update OpenAPI spec, unit tests, and service integration.

This commit is contained in:
Jeppe Bundgaard
2026-03-16 13:40:03 +01:00
parent 425c2c9142
commit 6aa04eb951
6 changed files with 1116 additions and 4 deletions
@@ -0,0 +1,374 @@
<?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('FakeEconomicV2BookedDepartment75VersioningService')) {
class FakeEconomicV2BookedDepartment75VersioningService extends economic_v2_versioning_service
{
public ?array $fixedVersion = null;
public array $subscriptionVersions = [];
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): ?array
{
return null;
}
public function runBestEffortBackfill(): array
{
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('TestableEconomicV2BookedDepartment75DistributionService')) {
class TestableEconomicV2BookedDepartment75DistributionService extends economic_v2_distribution_service
{
public array $stubOrders = [];
public array $stubOrderItems = [];
public array $stubVersionRows = [];
public array $subscriptionPrices = [];
public array $stubBookedInvoices = [];
public array $stubBookedInvoiceLines = [];
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.0) * (float)($item['quantity'] ?? 0.0);
}
return $total;
}
protected function getSubscriptionMonthlyPrice(int $vehicle_type): float
{
return (float)($this->subscriptionPrices[$vehicle_type] ?? 0.0);
}
protected function fetchBookedInvoicesInDateRange(string $date_from, string $date_to, array &$warnings): array
{
return $this->stubBookedInvoices;
}
protected function fetchBookedInvoiceLines(array $invoice_ids, array &$warnings): array
{
return $this->stubBookedInvoiceLines;
}
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): array
{
return [
'id' => $order_id,
'date' => $created_at,
'amount' => round((float)($amount ?? 0.0), 5),
'booked' => true,
'department_id' => $department_id,
'excluded' => !$this->isDepartmentEligible($department_id),
];
}
protected function isDepartmentEligible(int $department_id): bool
{
return $department_id > 0 && $department_id !== 10;
}
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
{
return true;
}
}
}
it('redistributes booked department 75 net amounts using fixed pricing and wash subscription weights', function (): void {
$versioning = new FakeEconomicV2BookedDepartment75VersioningService();
$versioning->fixedVersion = [
'id' => 91,
'price' => 500.0,
'description' => 'Fixed pricing agreement',
'source' => 'test.fixed',
'confidence' => 1.0,
'inferred' => false,
'effective_from' => '2026-01-01 00:00:00',
'effective_to' => null,
];
$versioning->subscriptionVersions = [[
'id' => 42,
'reg' => 'AB12345',
'vehicle_type' => 77,
'source' => 'test.subscription',
'confidence' => 1.0,
'inferred' => false,
]];
$service = new TestableEconomicV2BookedDepartment75DistributionService($versioning);
$service->subscriptionPrices = [
77 => 120.0,
];
$service->stubOrders = [
[
'id' => 101,
'customer_id' => 12345,
'department_id' => 1,
'created_at' => '2026-01-10 10:00:00',
'reg_1' => '',
'reference' => 'Internal fixed pricing basis',
],
[
'id' => 102,
'customer_id' => 12345,
'department_id' => 2,
'created_at' => '2026-01-11 10:00:00',
'reg_1' => 'AB12345',
'reference' => '',
],
];
$service->stubOrderItems = [
101 => [[
'product_id' => 61,
'price' => 400.0,
'quantity' => 1,
'reference' => '',
]],
102 => [[
'product_id' => 77,
'price' => 60.0,
'quantity' => 2,
'reference' => 'AB12345',
]],
];
$service->stubBookedInvoices = [[
'bookedInvoiceNumber' => 7001,
'date' => '2026-01-31',
'customer' => [
'customerNumber' => 12345,
],
]];
$service->stubBookedInvoiceLines = [
7001 => [
['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #999 ]'],
['lineNumber' => 2, 'description' => 'Reference:'],
['lineNumber' => 3, 'description' => '# Fast pris aftale'],
[
'lineNumber' => 4,
'description' => 'Fixed price',
'quantity' => 1,
'unitNetPrice' => 200,
'totalNetAmount' => 200,
'product' => ['productNumber' => 61],
'departmentalDistribution' => [
'distributions' => [
['percentage' => 50, 'department' => ['departmentNumber' => 75]],
['percentage' => 50, 'department' => ['departmentNumber' => 1]],
],
],
],
[
'lineNumber' => 5,
'description' => 'Rabat',
'quantity' => 1,
'unitNetPrice' => -40,
'totalNetAmount' => -40,
'product' => ['productNumber' => 'TotDiscount'],
'departmentalDistribution' => [
'distributions' => [
['percentage' => 50, 'department' => ['departmentNumber' => 75]],
['percentage' => 50, 'department' => ['departmentNumber' => 1]],
],
],
],
['lineNumber' => 6, 'description' => '[ 01/01/2026 00:00 Auto #1000 ]'],
['lineNumber' => 7, 'description' => 'Reference:'],
['lineNumber' => 8, 'description' => '# Vaskeabonnementer'],
[
'lineNumber' => 9,
'description' => 'Subscription',
'quantity' => 1,
'unitNetPrice' => 120,
'totalNetAmount' => 120,
'product' => ['productNumber' => 77],
'departmentalDistribution' => [
'distributions' => [
['percentage' => 100, 'department' => ['departmentNumber' => 75]],
],
],
],
],
];
$result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31');
expect($result['warnings'])->toBe([]);
expect($result['customers'])->toHaveCount(1);
$meta = $result['customers'][0]['meta']['booked_department_75'];
expect($meta['booked_net_amount'])->toBe(200.0);
expect($meta['distributed_net_amount'])->toBe(200.0);
expect($meta['undistributed_net_amount'])->toBe(0.0);
expect($meta['department_distribution']['1'])->toBe(61.53846);
expect($meta['department_distribution']['2'])->toBe(138.46154);
$groups = [];
foreach ($meta['booked_groups'] as $group) {
$groups[$group['source_category']] = $group;
}
expect($groups['fixed_pricing']['booked_net_amount'])->toBe(80.0);
expect($groups['fixed_pricing']['department_distribution']['1'])->toBe(61.53846);
expect($groups['fixed_pricing']['department_distribution']['2'])->toBe(18.46154);
expect($groups['fixed_pricing']['undistributed_net_amount'])->toBe(0.0);
expect($groups['wash_subscriptions']['booked_net_amount'])->toBe(120.0);
expect($groups['wash_subscriptions']['department_distribution']['2'])->toBe(120.0);
expect($groups['wash_subscriptions']['undistributed_net_amount'])->toBe(0.0);
expect($result['collective_results']['booked_net_amount'])->toBe(200.0);
expect($result['collective_results']['distributed_net_amount'])->toBe(200.0);
expect($result['collective_results']['undistributed_net_amount'])->toBe(0.0);
expect($result['collective_results']['department_distribution']['1'])->toBe(61.53846);
expect($result['collective_results']['department_distribution']['2'])->toBe(138.46154);
});
it('keeps classified booked department 75 amounts undistributed when no monthly basis exists', function (): void {
$service = new TestableEconomicV2BookedDepartment75DistributionService(new FakeEconomicV2BookedDepartment75VersioningService());
$service->stubBookedInvoices = [[
'bookedInvoiceNumber' => 7002,
'date' => '2026-01-31',
'customer' => [
'customerNumber' => 67890,
],
]];
$service->stubBookedInvoiceLines = [
7002 => [
['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #1001 ]'],
['lineNumber' => 2, 'description' => 'Reference:'],
['lineNumber' => 3, 'description' => '# Fast pris aftale'],
[
'lineNumber' => 4,
'description' => 'Fixed price',
'quantity' => 1,
'unitNetPrice' => 60,
'totalNetAmount' => 60,
'product' => ['productNumber' => 61],
'departmentalDistribution' => [
'distributions' => [
['percentage' => 100, 'department' => ['departmentNumber' => 75]],
],
],
],
],
];
$result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31');
expect($result['customers'])->toHaveCount(1);
expect($result['customers'][0]['meta']['booked_department_75']['booked_groups'][0]['source_category'])->toBe('fixed_pricing');
expect($result['customers'][0]['meta']['booked_department_75']['distributed_net_amount'])->toBe(0.0);
expect($result['customers'][0]['meta']['booked_department_75']['undistributed_net_amount'])->toBe(60.0);
expect($result['collective_results']['undistributed_net_amount'])->toBe(60.0);
expect(implode("\n", $result['warnings']))->toContain('has no redistribution basis');
});
it('keeps unclassified booked department 75 lines undistributed with warnings', function (): void {
$service = new TestableEconomicV2BookedDepartment75DistributionService(new FakeEconomicV2BookedDepartment75VersioningService());
$service->stubBookedInvoices = [[
'bookedInvoiceNumber' => 7003,
'date' => '2026-01-31',
'customer' => [
'customerNumber' => 77777,
],
]];
$service->stubBookedInvoiceLines = [
7003 => [
['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #1002 ]'],
[
'lineNumber' => 2,
'description' => 'Unknown booked line',
'quantity' => 1,
'unitNetPrice' => 25,
'totalNetAmount' => 25,
'product' => ['productNumber' => 99],
'departmentalDistribution' => [
'distributions' => [
['percentage' => 100, 'department' => ['departmentNumber' => 75]],
],
],
],
],
];
$result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31');
expect($result['customers'])->toHaveCount(1);
expect($result['customers'][0]['meta']['booked_department_75']['booked_groups'][0]['source_category'])->toBe('unclassified');
expect($result['customers'][0]['meta']['booked_department_75']['distributed_net_amount'])->toBe(0.0);
expect($result['customers'][0]['meta']['booked_department_75']['undistributed_net_amount'])->toBe(25.0);
expect($result['collective_results']['undistributed_net_amount'])->toBe(25.0);
$warning_text = implode("\n", $result['warnings']);
expect($warning_text)->toContain('Unable to classify booked department 75 line');
expect($warning_text)->toContain('could not be classified and remains undistributed');
});