196 lines
8.2 KiB
PHP
196 lines
8.2 KiB
PHP
<?php
|
|
|
|
use classes\xlvask_automation_service;
|
|
|
|
require_once WD . '/classes/xlvask_automation_service.php';
|
|
|
|
it('normalizes registrations for XL Vask automation signatures', function (): void {
|
|
expect(xlvask_automation_service::normalizeRegistrationForAutomation(' ec 21-233 '))
|
|
->toBe('EC21233');
|
|
});
|
|
|
|
it('builds stable XL Vask automation item signatures', function (): void {
|
|
$items = [
|
|
['product_id' => 20, 'quantity' => 1, 'price' => 275],
|
|
['product_id' => 10, 'quantity' => 2, 'price' => 649],
|
|
['product_id' => 20, 'quantity' => 1, 'price' => 0],
|
|
];
|
|
|
|
expect(xlvask_automation_service::itemSignaturePartsForAutomation($items))
|
|
->toBe([
|
|
'10:2:649',
|
|
'20:1:0',
|
|
'20:1:275',
|
|
]);
|
|
});
|
|
|
|
it('identifies strict price agreement matches by product, quantity, and total', function (): void {
|
|
$usageItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
|
['product_id' => 21, 'quantity' => 1, 'price' => 79],
|
|
];
|
|
$orderItems = [
|
|
['product_id' => 21, 'quantity' => 1, 'price' => 79],
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
|
];
|
|
|
|
expect(xlvask_automation_service::isExactItemMatchForAutomation($usageItems, $orderItems))
|
|
->toBeTrue();
|
|
});
|
|
|
|
it('rejects price agreement automation when product lines differ despite equal total', function (): void {
|
|
$usageItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
|
];
|
|
$orderItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
|
['product_id' => 50, 'quantity' => 1, 'price' => 39],
|
|
];
|
|
|
|
expect(xlvask_automation_service::itemsTotalForAutomation($usageItems))
|
|
->toBe(xlvask_automation_service::itemsTotalForAutomation($orderItems))
|
|
->and(xlvask_automation_service::isExactItemMatchForAutomation($usageItems, $orderItems))
|
|
->toBeFalse();
|
|
});
|
|
|
|
it('normalizes persisted XL Vask usage-log rows before helper hydration', function (): void {
|
|
$row = xlvask_automation_service::normalizeUsageLogRowForAutomation([
|
|
'id' => 47086,
|
|
'WashId' => 'cc1eabc1-b4e1-425b-ad7c-dc68f8c97ceb',
|
|
'WashItems' => '[{"OriginalProductName":"Bus","Count":1}]',
|
|
]);
|
|
|
|
expect($row)
|
|
->not->toHaveKey('id')
|
|
->and($row['WashItems'])->toBe([
|
|
[
|
|
'OriginalProductName' => 'Bus',
|
|
'Count' => 1,
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('builds stable OpenAI cache keys for identical automation input', function (): void {
|
|
$prompt = 'Prompt';
|
|
$schemaName = 'xlvask_automation';
|
|
$schema = [
|
|
'required' => ['action'],
|
|
'properties' => [
|
|
'confidence' => ['type' => 'number'],
|
|
'action' => ['type' => 'string'],
|
|
],
|
|
];
|
|
$schemaWithDifferentKeyOrder = [
|
|
'properties' => [
|
|
'action' => ['type' => 'string'],
|
|
'confidence' => ['type' => 'number'],
|
|
],
|
|
'required' => ['action'],
|
|
];
|
|
$payloadA = [
|
|
'usage_log' => [
|
|
'registration' => 'AB12345',
|
|
'creation_allowed' => true,
|
|
],
|
|
'candidate_orders' => [
|
|
['id' => 10, 'items' => [['product_id' => 1, 'quantity' => 1, 'price' => 100]]],
|
|
],
|
|
];
|
|
$payloadB = [
|
|
'candidate_orders' => [
|
|
['items' => [['price' => 100, 'quantity' => 1, 'product_id' => 1]], 'id' => 10],
|
|
],
|
|
'usage_log' => [
|
|
'creation_allowed' => true,
|
|
'registration' => 'AB12345',
|
|
],
|
|
];
|
|
|
|
expect(xlvask_automation_service::openAiCacheKeyForAutomation($schemaName, $prompt, $payloadA, $schema, 0.1))
|
|
->toBe(xlvask_automation_service::openAiCacheKeyForAutomation($schemaName, $prompt, $payloadB, $schemaWithDifferentKeyOrder, 0.1));
|
|
});
|
|
|
|
it('changes OpenAI cache keys when automation eligibility input changes', function (): void {
|
|
$schema = ['type' => 'object'];
|
|
$newerWashPayload = ['usage_log' => ['creation_allowed' => false, 'age_bucket' => 'newer_than_6_hours']];
|
|
$olderWashPayload = ['usage_log' => ['creation_allowed' => true, 'age_bucket' => 'older_than_6_hours']];
|
|
|
|
expect(xlvask_automation_service::openAiCacheKeyForAutomation('xlvask_automation', 'Prompt', $newerWashPayload, $schema, 0.1))
|
|
->not->toBe(xlvask_automation_service::openAiCacheKeyForAutomation('xlvask_automation', 'Prompt', $olderWashPayload, $schema, 0.1));
|
|
});
|
|
|
|
it('declares a persistent OpenAI cache table for XL Vask automation', function (): void {
|
|
$bootstrapContent = file_get_contents(WD . '/classes/xlvask_usage_logs_schema_bootstrap.php');
|
|
|
|
expect($bootstrapContent)
|
|
->toContain('xlvask_automation_openai_cache')
|
|
->toContain('UNIQUE KEY `uniq_xlvask_openai_cache_key` (`cache_key`)');
|
|
});
|
|
|
|
it('declares cached amount summary columns for XL Vask usage logs', function (): void {
|
|
$bootstrapContent = file_get_contents(WD . '/classes/xlvask_usage_logs_schema_bootstrap.php');
|
|
|
|
expect($bootstrapContent)
|
|
->toContain('cached_total_net_amount')
|
|
->toContain('cached_primary_product_name')
|
|
->toContain('cached_amount_at');
|
|
});
|
|
|
|
it('keeps automatic XL Vask execution scoped to exact attachments', function (): void {
|
|
$serviceContent = file_get_contents(WD . '/classes/xlvask_automation_service.php');
|
|
|
|
expect($serviceContent)
|
|
->toContain('&& $this->isExactAttachSuggestionForContext($suggestion, $context)')
|
|
->toContain("\$candidateOrderJson = \$suggestion['candidate_order_json'] ?? null;")
|
|
->toContain("return 'Automatisk accepteret: Prisoverensstemmelse.';");
|
|
});
|
|
|
|
it('does not automatically create XL Vask orders', function (): void {
|
|
$serviceContent = file_get_contents(WD . '/classes/xlvask_automation_service.php');
|
|
|
|
expect($serviceContent)
|
|
->toContain('if ($action === self::ACTION_CREATE) {')
|
|
->toContain('return false;');
|
|
});
|
|
|
|
it('scores same-day orders with matching XL Vask products and extra add-ons as attach suggestions', function (): void {
|
|
$usageItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
|
|
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
|
|
['product_id' => 99, 'quantity' => 8, 'price' => 0, 'product' => ['name' => 'Halleje']],
|
|
];
|
|
$orderItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
|
|
['product_id' => 50, 'quantity' => 1, 'price' => 319, 'product' => ['name' => 'Indvendig vask Forvogn']],
|
|
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
|
|
];
|
|
|
|
$score = xlvask_automation_service::scoreItemMatchForAutomation($usageItems, $orderItems);
|
|
|
|
expect($score['source'])
|
|
->toBe('fuzzy')
|
|
->and($score['confidence'])->toBeGreaterThanOrEqual(0.70)
|
|
->and($score['confidence'])->toBeLessThan(0.92)
|
|
->and($score['reason'])->toContain('ekstra ydelser');
|
|
});
|
|
|
|
it('does not score an order with only the primary product as a matching add-on attachment', function (): void {
|
|
$usageItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
|
|
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
|
|
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
|
|
];
|
|
$orderItems = [
|
|
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
|
|
['product_id' => 50, 'quantity' => 1, 'price' => 319, 'product' => ['name' => 'Indvendig vask Forvogn']],
|
|
];
|
|
|
|
expect(xlvask_automation_service::scoreItemMatchForAutomation($usageItems, $orderItems)['confidence'])
|
|
->toBe(0.0);
|
|
});
|