Resolve recommended-profile Critical and High findings, retain narrow analyzer exceptions, and update the edge-broker WebSocket dependency to a non-vulnerable release.
295 lines
9.2 KiB
PHP
295 lines
9.2 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);
|
|
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->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,
|
|
]);
|
|
});
|