Files
api/services/nginx/app/tests/Unit/Invoicing/InvoicingPeriodDraftOverlayTest.php
T
Jeppe Bundgaard 6d4066be1c Add unit tests for InvoicingPeriodDraftOverlay and reference suggestion logic, including fake DB integration and aggregation methods
- Implemented `InvoicingPeriodDraftOverlayTest` with coverage for blocking and permitting invoicing actions based on draft states, transactions, and metadata.
- Created `ReferenceSuggestionsApiTest` to validate ranked and filtered suggestions across bookings, orders, and vehicles with varied match relevance, context, and frequency.
- Added `order_reference_suggestions_service` class, including query methods, normalization utilities, and aggregation logic for reference suggestions.
- Enhanced query handling in `InvoicingPeriodDraftOverlayFakeDb` to validate SQL constraints and column cache resets in overlapping invoicing contexts.
2026-05-11 18:18:08 +02:00

297 lines
9.3 KiB
PHP

<?php
app_require('routes/InvoicingPeriodRoute.php');
use routes\InvoicingPeriodRoute;
function invoicing_period_draft_overlay_invoke(string $method, array $args = []): mixed
{
$reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$target = $reflection->getMethod($method);
$target->setAccessible(true);
return $target->invokeArgs(null, $args);
}
function invoicing_period_draft_overlay_draft(
int $invoice_collection_id,
int $customer_number,
bool $is_period_relevant = true
): array {
return [
'invoice_collection_id' => $invoice_collection_id,
'customer_number' => $customer_number,
'created_at' => '2026-04-01 00:00:01',
'closed_at' => '2026-04-01 00:00:01',
'is_period_relevant' => $is_period_relevant,
];
}
function invoicing_period_draft_overlay_transaction(
int $id,
?int $invoice_collection_id,
bool $booked = false,
bool $excluded = false,
?string $queue_status = null
): array {
return [
'id' => $id,
'booked' => $booked,
'excluded' => $excluded,
'invoice_collection_id' => $invoice_collection_id,
'queue_status' => $queue_status,
'queue_job_id' => $queue_status === null ? null : 99,
];
}
function invoicing_period_draft_overlay_reset_deleted_at_column_cache(): void
{
$reflection = new ReflectionClass(InvoicingPeriodRoute::class);
$property = $reflection->getProperty('collectedOrderInvoicesHasDeletedAtColumn');
$property->setAccessible(true);
$property->setValue(null, null);
}
class InvoicingPeriodDraftOverlayFakeDbResult
{
public int $num_rows;
public function __construct(private array $rows = [])
{
$this->num_rows = count($rows);
}
public function fetch_assoc(): ?array
{
return array_shift($this->rows);
}
}
class InvoicingPeriodDraftOverlayFakeDb
{
public string $selectSql = '';
public function __construct(private bool $hasDeletedAtColumn)
{
}
public function escape_string(string $value): string
{
return addslashes($value);
}
public function query(string $sql): InvoicingPeriodDraftOverlayFakeDbResult|bool
{
if (str_starts_with($sql, 'SHOW COLUMNS')) {
return new InvoicingPeriodDraftOverlayFakeDbResult(
$this->hasDeletedAtColumn ? [['Field' => 'deleted_at']] : []
);
}
$this->selectSql = $sql;
return false;
}
}
it('blocks invoicing when all actionable transactions are backed by valid e-conomic drafts', function (): void {
$draft = invoicing_period_draft_overlay_draft(14578, 42424242);
$customer = [
'customer_number' => 42424242,
'customer_name' => 'Draft Customer',
'requires_action' => true,
'transactions' => [
invoicing_period_draft_overlay_transaction(501, 14578),
],
];
$result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
$customer,
[14578 => $draft],
[42424242 => [$draft]],
]);
expect($result['requires_action'])->toBeFalse();
expect($result['queue'])->toBe([
'has_active_job' => false,
'statuses' => [],
'invoice_collection_ids' => [],
'is_action_blocked' => false,
]);
expect($result['draft'])->toBe([
'has_valid_draft' => true,
'invoice_collection_ids' => [14578],
'is_action_blocked' => true,
]);
});
it('keeps invoicing available when valid drafts only cover part of the actionable work', function (): void {
$draft = invoicing_period_draft_overlay_draft(2001, 43434343);
$customer = [
'customer_number' => 43434343,
'customer_name' => 'Mixed Draft Customer',
'requires_action' => true,
'transactions' => [
invoicing_period_draft_overlay_transaction(601, 2001),
invoicing_period_draft_overlay_transaction(602, null),
],
];
$result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
$customer,
[2001 => $draft],
[43434343 => [$draft]],
]);
expect($result['requires_action'])->toBeTrue();
expect($result['draft'])->toBe([
'has_valid_draft' => true,
'invoice_collection_ids' => [2001],
'is_action_blocked' => false,
]);
});
it('excludes errored, booked, deleted, and missing-external-id invoice collections at query time', function (): void {
$hadDb = array_key_exists('db', $GLOBALS);
$originalDb = $GLOBALS['db'] ?? null;
$fakeDb = new InvoicingPeriodDraftOverlayFakeDb(true);
invoicing_period_draft_overlay_reset_deleted_at_column_cache();
$GLOBALS['db'] = $fakeDb;
try {
invoicing_period_draft_overlay_invoke('getValidCollectedInvoiceDraftOverlay', [
[
'all' => [
[
'customer_number' => 45454545,
'transactions' => [
invoicing_period_draft_overlay_transaction(701, 3001),
],
'meta' => [
'fixed_pricing' => [
'price' => 1200,
],
],
],
],
],
'2026-04-01',
'2026-04-30',
]);
} finally {
if ($hadDb) {
$GLOBALS['db'] = $originalDb;
} else {
unset($GLOBALS['db']);
}
invoicing_period_draft_overlay_reset_deleted_at_column_cache();
}
expect($fakeDb->selectSql)->toContain('deleted_at IS NULL');
expect($fakeDb->selectSql)->toContain('processor = 1');
expect($fakeDb->selectSql)->toContain('external_id IS NOT NULL');
expect($fakeDb->selectSql)->toContain("external_id <> ''");
expect($fakeDb->selectSql)->toContain('booked_invoice_id IS NULL');
expect($fakeDb->selectSql)->toContain('error_message IS NULL');
});
it('still checks valid drafts when the collection table has no deleted marker column', function (): void {
$hadDb = array_key_exists('db', $GLOBALS);
$originalDb = $GLOBALS['db'] ?? null;
$fakeDb = new InvoicingPeriodDraftOverlayFakeDb(false);
invoicing_period_draft_overlay_reset_deleted_at_column_cache();
$GLOBALS['db'] = $fakeDb;
try {
invoicing_period_draft_overlay_invoke('getValidCollectedInvoiceDraftOverlay', [
[
'all' => [
[
'customer_number' => 12345679,
'transactions' => [
invoicing_period_draft_overlay_transaction(61415, 17389),
],
],
],
],
'2026-05-11',
'2026-05-11',
]);
} finally {
if ($hadDb) {
$GLOBALS['db'] = $originalDb;
} else {
unset($GLOBALS['db']);
}
invoicing_period_draft_overlay_reset_deleted_at_column_cache();
}
expect($fakeDb->selectSql)->not->toContain('deleted_at IS NULL');
expect($fakeDb->selectSql)->toContain('id IN (17389)');
expect($fakeDb->selectSql)->toContain('processor = 1');
expect($fakeDb->selectSql)->toContain('error_message IS NULL');
});
it('blocks fixed-pricing and subscription customer-level work when a relevant valid draft exists', function (): void {
$draft = invoicing_period_draft_overlay_draft(3001, 45454545, true);
$customer = [
'customer_number' => 45454545,
'customer_name' => 'Subscription Draft Customer',
'requires_action' => true,
'transactions' => [],
'meta' => [
'fixed_pricing' => [
'price' => 1200,
],
],
];
$result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
$customer,
[],
[45454545 => [$draft]],
]);
expect($result['requires_action'])->toBeFalse();
expect($result['draft'])->toBe([
'has_valid_draft' => true,
'invoice_collection_ids' => [3001],
'is_action_blocked' => true,
]);
});
it('keeps queue blocking ahead of the draft label when all work is covered by queue or draft state', function (): void {
$draft = invoicing_period_draft_overlay_draft(5002, 46464646, true);
$customer = [
'customer_number' => 46464646,
'customer_name' => 'Queue And Draft Customer',
'requires_action' => true,
'transactions' => [
invoicing_period_draft_overlay_transaction(801, 5001, false, false, 'QUEUED'),
invoicing_period_draft_overlay_transaction(802, 5002),
],
'queue' => [
'has_active_job' => true,
'statuses' => ['QUEUED'],
'invoice_collection_ids' => [5001],
'is_action_blocked' => false,
],
];
$result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
$customer,
[5002 => $draft],
[46464646 => [$draft]],
]);
expect($result['requires_action'])->toBeFalse();
expect($result['queue']['is_action_blocked'])->toBeTrue();
expect($result['draft'])->toBe([
'has_valid_draft' => true,
'invoice_collection_ids' => [5002],
'is_action_blocked' => false,
]);
});