Add unit and integration tests for collected invoice queue handling, route hardening, lifecycle validation, and manual batch processing logic.

This commit is contained in:
Jeppe Bundgaard
2026-04-08 15:53:22 +02:00
parent c5cd42be7f
commit 653680376a
26 changed files with 2974 additions and 428 deletions
@@ -1,6 +1,6 @@
<?php
it('returns the queue job id in POST /collected-invoices/economic enqueue response', function (): void {
it('documents both synchronous fallback and queued response contracts for POST /collected-invoices/economic', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
@@ -15,15 +15,18 @@ it('returns the queue job id in POST /collected-invoices/economic enqueue respon
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($endpointBlock)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($endpointBlock)->toContain("'message' => 'Collected invoice export processed synchronously'");
expect($endpointBlock)->toContain("'mode' => 'synchronous_fallback'");
expect($endpointBlock)->toContain("'result' => \$result");
expect($endpointBlock)->toContain("\$job_id = (int)(\$job['id'] ?? 0);");
expect($endpointBlock)->toContain("'job_id' => \$job_id");
expect($endpointBlock)->toContain("'job' => \$job");
expect($endpointBlock)->toContain("], 202);");
expect($endpointBlock)->not->toContain('exportCollectedInvoiceSynchronously');
expect($endpointBlock)->toContain('exportCollectedInvoiceSynchronously');
});
it('returns queued contract for POST /collected-invoices/stripe/book', function (): void {
it('documents both synchronous fallback and queued response contracts for POST /collected-invoices/stripe/book', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
@@ -38,7 +41,10 @@ it('returns queued contract for POST /collected-invoices/stripe/book', function
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($endpointBlock)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($endpointBlock)->toContain("'message' => 'Stripe collected invoice export processed synchronously'");
expect($endpointBlock)->toContain("'mode' => 'synchronous_fallback'");
expect($endpointBlock)->toContain("'result' => \$result");
expect($endpointBlock)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
expect($endpointBlock)->toContain("\$job_id = (int)(\$job['id'] ?? 0);");
expect($endpointBlock)->toContain("'job_id' => \$job_id");
@@ -0,0 +1,147 @@
<?php
use classes\economic_transfer_queue;
use classes\economic_transfer_queue_details_summary;
it('builds a completed collected-invoice queue summary from payload and result', function (): void {
$summary = economic_transfer_queue_details_summary::buildCollectedInvoiceSummary([
'status' => economic_transfer_queue::STATUS_COMPLETED,
'payload' => [
'collected_invoice_id' => '14578',
'send_as_is' => '1',
'requested_by' => '18',
],
'result' => [
'message' => 'Invoice transferred',
'customer_number' => '43425425',
'customer_name' => 'North Freight',
'economic_invoice_draft_id' => '2001',
'economic_invoice_booked_id' => '3001',
'external_id' => 'abc-123',
'total_net_amount' => '670',
'orders' => [
['id' => 1],
['id' => 2],
],
],
]);
expect($summary['message'])->toBe('Invoice transferred');
expect($summary['target'])->toBe([
'collected_invoice_id' => 14578,
'send_as_is' => true,
'requested_by' => 18,
]);
expect($summary['customer'])->toBe([
'customer_number' => 43425425,
'name' => 'North Freight',
]);
expect($summary['outcome'])->toBe([
'economic_invoice_draft_id' => 2001,
'economic_invoice_booked_id' => 3001,
'external_id' => 'abc-123',
'total_net_amount' => 670.0,
'order_count' => 2,
]);
expect($summary['raw_available'])->toBe([
'payload' => true,
'result' => true,
]);
});
it('prefers queue error messages for failed jobs and keeps null-safe outcome fields', function (): void {
$summary = economic_transfer_queue_details_summary::buildCollectedInvoiceSummary([
'status' => economic_transfer_queue::STATUS_FAILED,
'collected_invoice_id' => 9001,
'error_message' => 'No billable order items',
'payload' => [
'collected_invoice_id' => 9001,
],
'result' => null,
]);
expect($summary['message'])->toBe('No billable order items');
expect($summary['target']['collected_invoice_id'])->toBe(9001);
expect($summary['target']['send_as_is'])->toBeNull();
expect($summary['target']['requested_by'])->toBeNull();
expect($summary['customer'])->toBe([
'customer_number' => null,
'name' => null,
]);
expect($summary['outcome'])->toBe([
'economic_invoice_draft_id' => null,
'economic_invoice_booked_id' => null,
'external_id' => null,
'total_net_amount' => null,
'order_count' => null,
]);
expect($summary['raw_available'])->toBe([
'payload' => true,
'result' => false,
]);
});
it('uses deterministic status message fallback when no explicit message exists', function (): void {
$expectations = [
[economic_transfer_queue::STATUS_QUEUED, 'Queued'],
[economic_transfer_queue::STATUS_PROCESSING, 'Processing'],
[economic_transfer_queue::STATUS_COMPLETED, 'Completed'],
[economic_transfer_queue::STATUS_FAILED, 'Failed'],
['UNKNOWN', null],
];
foreach ($expectations as [$status, $expected]) {
$summary = economic_transfer_queue_details_summary::buildCollectedInvoiceSummary([
'status' => $status,
'payload' => null,
'result' => null,
'error_message' => null,
'progress_message' => null,
]);
expect($summary['message'])->toBe($expected);
}
});
it('handles object payload/result values and malformed fields without throwing', function (): void {
$summary = economic_transfer_queue_details_summary::buildCollectedInvoiceSummary([
'status' => economic_transfer_queue::STATUS_COMPLETED,
'payload' => (object)[
'collected_invoice_id' => '501',
'send_as_is' => 'invalid',
'requested_by' => '-1',
],
'result' => (object)[
'draft_invoice_id' => '44',
'booked_invoice_id' => '55',
'total_net_amount' => 'invalid',
'user' => (object)[
'customer_number' => '9345',
'company_name' => 'Acme Carrier',
],
],
'error_message' => null,
]);
expect($summary['target'])->toBe([
'collected_invoice_id' => 501,
'send_as_is' => null,
'requested_by' => null,
]);
expect($summary['customer'])->toBe([
'customer_number' => 9345,
'name' => 'Acme Carrier',
]);
expect($summary['outcome'])->toBe([
'economic_invoice_draft_id' => 44,
'economic_invoice_booked_id' => 55,
'external_id' => null,
'total_net_amount' => null,
'order_count' => null,
]);
expect($summary['raw_available'])->toBe([
'payload' => true,
'result' => true,
]);
});
@@ -0,0 +1,101 @@
<?php
it('returns pagination metadata and strict status handling for collected-invoice queue list', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
$content = (string)$content;
$start = strpos($content, "\$this->get('/collected-invoices/economic/queue'");
$end = strpos($content, "\$this->get('/collected-invoices/economic/queue/status'");
expect($start)->not->toBeFalse();
expect($end)->not->toBeFalse();
expect($end)->toBeGreaterThan($start);
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain('$statuses = $this->parseCollectedInvoiceQueueStatuses();');
expect($endpointBlock)->toContain("['limit' => \$limit, 'offset' => \$offset] = \$this->parseCollectedInvoiceQueuePagination();");
expect($endpointBlock)->toContain('$jobs = $queue->listJobs(');
expect($endpointBlock)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
expect($endpointBlock)->toContain('$total_jobs = $this->countCollectedInvoiceQueueJobs($queue, $statuses);');
expect($endpointBlock)->toContain("'items' => \$this->withCollectedInvoiceQueueDetailsSummaryList(\$jobs)");
expect($endpointBlock)->toContain("'total' => \$total_jobs");
expect($endpointBlock)->toContain("'limit' => \$limit");
expect($endpointBlock)->toContain("'offset' => \$offset");
expect($endpointBlock)->toContain("'has_more' => \$has_more");
expect($content)->toContain('private function parseCollectedInvoiceQueueStatuses(): array');
expect($content)->toContain('status must contain only: ');
expect($content)->toContain('private function parseCollectedInvoiceQueuePagination(): array');
expect($content)->toContain('limit must be between 1 and 500');
expect($content)->toContain('offset must be at least 0');
expect($content)->toContain('private function countCollectedInvoiceQueueJobs(economic_transfer_queue $queue, array $statuses): int');
expect($content)->toContain("method_exists(\$queue, 'countJobs')");
expect($content)->toContain("SELECT COUNT(*) AS total FROM economic_transfer_queue_jobs WHERE ");
});
it('enforces retry constraints for collected-invoice queue jobs before retry execution', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
$content = (string)$content;
$start = strpos($content, "\$this->post('/collected-invoices/economic/queue/retry'");
$end = strpos($content, "\$this->post('/collected-invoices/move-multiple/registration-numbers'");
expect($start)->not->toBeFalse();
expect($end)->not->toBeFalse();
expect($end)->toBeGreaterThan($start);
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain('$job_id = $this->requireCollectedInvoiceQueueJobId();');
expect($endpointBlock)->toContain('$job = $this->requireCollectedInvoiceQueueJobById($job_id, true);');
expect($endpointBlock)->toContain("Collected invoice queue job reached max retry attempts', 409");
expect($endpointBlock)->toContain('Failed to retry collected invoice queue job: ');
expect($endpointBlock)->toContain("'job' => \$this->withCollectedInvoiceQueueDetailsSummary(\$retried)");
expect($content)->toContain("\$response->success(\$this->withCollectedInvoiceQueueDetailsSummary(\$job));");
expect($content)->toContain('private function requireCollectedInvoiceQueueJobId(): int');
expect($content)->toContain('private function requireCollectedInvoiceQueueJobById(int $job_id, bool $mustBeFailed = false): array');
expect($content)->toContain("Collected invoice queue job can only be retried when status is FAILED', 409");
expect($content)->toContain('private function withCollectedInvoiceQueueDetailsSummary(array $job): array');
expect($content)->toContain('private function withCollectedInvoiceQueueDetailsSummaryList(array $jobs): array');
expect($content)->toContain('private function resolveCollectedInvoiceQueueRetryErrorStatus(string $message): int');
expect($content)->toContain('only failed jobs can be retried');
expect($content)->toContain('max retry attempts');
});
it('runs collected-invoice queue batches through an explicit manual endpoint', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
$content = (string)$content;
$start = strpos($content, "\$this->post('/collected-invoices/economic/queue/run'");
$end = strpos($content, "\$this->post('/collected-invoices/move-multiple/registration-numbers'");
expect($start)->not->toBeFalse();
expect($end)->not->toBeFalse();
expect($end)->toBeGreaterThan($start);
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($endpointBlock)->toContain("\$limit = 10;");
expect($endpointBlock)->toContain("limit must be a positive integer");
expect($endpointBlock)->toContain("\$limit = max(1, min(10, \$limit));");
expect($endpointBlock)->toContain("\$batch = \$this->runCollectedInvoiceQueueBatch(\$queue, \$limit);");
expect($endpointBlock)->toContain("\$result = (array)(\$batch['result'] ?? []);");
expect($endpointBlock)->toContain("economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT");
expect($endpointBlock)->toContain("'message' => (bool)(\$batch['fallback'] ?? false)");
expect($endpointBlock)->toContain("'processed' => (int)(\$result['processed'] ?? 0)");
expect($endpointBlock)->toContain("'completed' => (int)(\$result['completed'] ?? 0)");
expect($endpointBlock)->toContain("'failed' => (int)(\$result['failed'] ?? 0)");
expect($endpointBlock)->toContain("'jobs' => array_values(array_map('intval', (array)(\$result['jobs'] ?? [])))");
expect($endpointBlock)->toContain("'limit' => \$limit");
expect($endpointBlock)->toContain("'transfer_type' => economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT");
expect($endpointBlock)->toContain("'fallback' => (bool)(\$batch['fallback'] ?? false)");
expect($content)->toContain('private function runCollectedInvoiceQueueBatch(economic_transfer_queue $queue, int $limit): array');
expect($content)->toContain("method_exists(\$queue, 'processPendingByTransferType')");
expect($content)->toContain("\$queue->processPendingByTransferType(");
expect($content)->toContain("\$queue->processPending(\$limit)");
});
@@ -1,6 +1,6 @@
<?php
it('keeps order draft export route queue-only', function (): void {
it('supports synchronous fallback and queued processing for order draft export route', function (): void {
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
expect($content)->not->toBeFalse();
@@ -13,13 +13,15 @@ it('keeps order draft export route queue-only', function (): void {
expect($end)->toBeGreaterThan($start);
$endpointBlock = substr($content, (int)$start, (int)$end - (int)$start);
expect($endpointBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($endpointBlock)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($endpointBlock)->toContain("'mode' => 'synchronous_fallback'");
expect($endpointBlock)->toContain("'result' => \$result");
expect($endpointBlock)->toContain('economic_transfer_queue::TYPE_ORDER_DRAFT_EXPORT');
expect($endpointBlock)->toContain("'job_id' => \$job_id");
expect($endpointBlock)->toContain("], 202);");
});
it('keeps collected invoice draft-producing routes queue-only in orderInvoicesRoute', function (): void {
it('supports synchronous fallback and queued processing for collected invoice draft-producing routes', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
@@ -32,12 +34,13 @@ it('keeps collected invoice draft-producing routes queue-only in orderInvoicesRo
expect($collectedEnd)->toBeGreaterThan($collectedStart);
$collectedBlock = substr($content, (int)$collectedStart, (int)$collectedEnd - (int)$collectedStart);
expect($collectedBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($collectedBlock)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($collectedBlock)->toContain('exportCollectedInvoiceSynchronously');
expect($collectedBlock)->toContain("'mode' => 'synchronous_fallback'");
expect($collectedBlock)->toContain("'result' => \$result");
expect($collectedBlock)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
expect($collectedBlock)->toContain("'job_id' => \$job_id");
expect($collectedBlock)->toContain("], 202);");
expect($collectedBlock)->not->toContain('addToEconomic(');
expect($collectedBlock)->not->toContain('exportCollectedInvoiceSynchronously');
$stripeStart = strpos($content, "\$this->post('/collected-invoices/stripe/book'");
$stripeEnd = strpos($content, "\$this->post('/collected-invoices/vehicle-subscriptions'");
@@ -46,12 +49,14 @@ it('keeps collected invoice draft-producing routes queue-only in orderInvoicesRo
expect($stripeEnd)->toBeGreaterThan($stripeStart);
$stripeBlock = substr($content, (int)$stripeStart, (int)$stripeEnd - (int)$stripeStart);
expect($stripeBlock)->toContain("\$this->ensureEconomicTransferQueueIsAvailable();");
expect($stripeBlock)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($stripeBlock)->toContain('exportCollectedInvoiceSynchronously');
expect($stripeBlock)->toContain("'mode' => 'synchronous_fallback'");
expect($stripeBlock)->toContain("'result' => \$result");
expect($stripeBlock)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
expect($stripeBlock)->toContain("'message' => 'Stripe collected invoice export queued'");
expect($stripeBlock)->toContain("'job_id' => \$job_id");
expect($stripeBlock)->toContain("], 202);");
expect($stripeBlock)->not->toContain('addToEconomic(');
expect($content)->not->toContain('exportCollectedInvoiceSynchronously');
expect($content)->toContain('private function exportCollectedInvoiceSynchronously');
});
@@ -1,83 +1,123 @@
<?php
it('guards all economic invoice queue endpoints before constructing queue service', function (): void {
it('keeps queue guards on economic invoice queue status and retry routes', function (): void {
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
expect($content)->not->toBeFalse();
$guardCount = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
$queueInitCount = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
expect($guardCount)->toBe(6);
expect($queueInitCount)->toBe(6);
$guard_count = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
$queue_init_count = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
expect($guard_count)->toBe(4);
expect($queue_init_count)->toBe(6);
$endpointPatterns = [
"/\\\$this->post\\('\\/economic\\/invoice\\/draft\\/export'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/economic\\/invoice\\/export'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
$queue_endpoint_patterns = [
"/\\\$this->get\\('\\/economic\\/invoice\\/draft\\/export\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/economic\\/invoice\\/draft\\/export\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->get\\('\\/economic\\/invoice\\/export\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/economic\\/invoice\\/export\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
];
foreach ($endpointPatterns as $pattern) {
foreach ($queue_endpoint_patterns as $pattern) {
expect(preg_match($pattern, (string)$content))->toBe(1);
}
});
it('guards collected-invoice and stripe draft-producing endpoints before constructing queue service', function (): void {
it('supports synchronous fallback branches before queue enqueue on economic invoice export routes', function (): void {
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
expect($content)->not->toBeFalse();
expect($content)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($content)->toContain("'mode' => 'synchronous_fallback'");
expect($content)->toContain('ECONOMIC_INVOICE_DRAFT_EXPORT_FALLBACK');
expect($content)->toContain('ECONOMIC_INVOICE_EXPORT_FALLBACK');
$draft_start = strpos((string)$content, "\$this->post('/economic/invoice/draft/export'");
$draft_end = strpos((string)$content, "\$this->delete('/economic/invoice/draft/delete'");
expect($draft_start)->not->toBeFalse();
expect($draft_end)->not->toBeFalse();
expect($draft_end)->toBeGreaterThan($draft_start);
$draft_block = substr((string)$content, (int)$draft_start, (int)$draft_end - (int)$draft_start);
expect($draft_block)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($draft_block)->toContain('exportOrderDraftSynchronously');
expect($draft_block)->toContain("'mode' => 'synchronous_fallback'");
expect($draft_block)->toContain('new economic_transfer_queue();');
$invoice_start = strpos((string)$content, "\$this->post('/economic/invoice/export'");
$invoice_end = strpos((string)$content, "\$this->get('/economic/invoice/draft/export/status'");
expect($invoice_start)->not->toBeFalse();
expect($invoice_end)->not->toBeFalse();
expect($invoice_end)->toBeGreaterThan($invoice_start);
$invoice_block = substr((string)$content, (int)$invoice_start, (int)$invoice_end - (int)$invoice_start);
expect($invoice_block)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($invoice_block)->toContain('exportOrderInvoiceSynchronously');
expect($invoice_block)->toContain("'mode' => 'synchronous_fallback'");
expect($invoice_block)->toContain('new economic_transfer_queue();');
});
it('keeps queue status endpoints guarded while collected invoice export routes support synchronous fallback', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
$guardCount = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
$queueInitCount = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
expect($guardCount)->toBe(5);
expect($queueInitCount)->toBe(5);
$guard_count = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
$queue_init_count = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
expect($guard_count)->toBe(4);
expect($queue_init_count)->toBe(6);
$endpointPatterns = [
"/\\\$this->post\\('\\/collected-invoices\\/economic'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
$queue_endpoint_patterns = [
"/\\\$this->get\\('\\/collected-invoices\\/economic\\/queue'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->get\\('\\/collected-invoices\\/economic\\/queue\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/collected-invoices\\/economic\\/queue\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/collected-invoices\\/stripe\\/book'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
"/\\\$this->post\\('\\/collected-invoices\\/economic\\/queue\\/run'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
];
foreach ($endpointPatterns as $pattern) {
foreach ($queue_endpoint_patterns as $pattern) {
expect(preg_match($pattern, (string)$content))->toBe(1);
}
expect($content)->not->toContain('exportCollectedInvoiceSynchronously');
expect($content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
$collected_start = strpos((string)$content, "\$this->post('/collected-invoices/economic'");
$collected_end = strpos((string)$content, "\$this->get('/collected-invoices/economic/queue'");
expect($collected_start)->not->toBeFalse();
expect($collected_end)->not->toBeFalse();
expect($collected_end)->toBeGreaterThan($collected_start);
$collected_block = substr((string)$content, (int)$collected_start, (int)$collected_end - (int)$collected_start);
expect($collected_block)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($collected_block)->toContain('exportCollectedInvoiceSynchronously');
expect($collected_block)->toContain("'mode' => 'synchronous_fallback'");
expect($collected_block)->toContain('new economic_transfer_queue();');
$stripe_start = strpos((string)$content, "\$this->post('/collected-invoices/stripe/book'");
$stripe_end = strpos((string)$content, "\$this->post('/collected-invoices/vehicle-subscriptions'");
expect($stripe_start)->not->toBeFalse();
expect($stripe_end)->not->toBeFalse();
expect($stripe_end)->toBeGreaterThan($stripe_start);
$stripe_block = substr((string)$content, (int)$stripe_start, (int)$stripe_end - (int)$stripe_start);
expect($stripe_block)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($stripe_block)->toContain('exportCollectedInvoiceSynchronously');
expect($stripe_block)->toContain("'mode' => 'synchronous_fallback'");
expect($stripe_block)->toContain('new economic_transfer_queue();');
expect($content)->toContain('private function exportCollectedInvoiceSynchronously');
expect($content)->toContain('ADD_COLLECTED_INVOICE_ECONOMIC_FALLBACK');
expect($content)->toContain('ADD_COLLECTED_INVOICE_STRIPE_FALLBACK');
});
it('uses a consistent unavailable-service contract for missing queue dependencies', function (): void {
$invoiceRouteContent = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
$orderInvoicesRouteContent = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
it('uses a consistent unavailable-service contract for queue status lifecycle endpoints', function (): void {
$invoice_route_content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
$order_invoices_route_content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($invoiceRouteContent)->not->toBeFalse();
expect($orderInvoicesRouteContent)->not->toBeFalse();
expect($invoice_route_content)->not->toBeFalse();
expect($order_invoices_route_content)->not->toBeFalse();
expect($invoiceRouteContent)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
expect($invoiceRouteContent)->toContain("!class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($invoiceRouteContent)->toContain("!class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($invoiceRouteContent)->toContain('!class_exists(economic_transfer_queue::class)');
expect($invoice_route_content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
expect($invoice_route_content)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($invoice_route_content)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($invoice_route_content)->toContain('class_exists(economic_transfer_queue::class)');
expect($invoice_route_content)->toContain("Economic transfer queue is unavailable in this deployment', 503");
expect($orderInvoicesRouteContent)->toContain('private function isEconomicTransferQueueAvailable(): bool');
expect($orderInvoicesRouteContent)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($orderInvoicesRouteContent)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($orderInvoicesRouteContent)->toContain('class_exists(economic_transfer_queue::class)');
expect(preg_match_all('/if \\(!\\$this->isEconomicTransferQueueAvailable\\(\\)\\) \\{/', (string)$orderInvoicesRouteContent))->toBe(1);
expect($invoiceRouteContent)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
expect($invoiceRouteContent)->toContain("Economic transfer queue is unavailable in this deployment', 503");
expect($invoiceRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
expect($invoiceRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';");
expect($invoiceRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue.php';");
expect($orderInvoicesRouteContent)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
expect($orderInvoicesRouteContent)->toContain("Economic transfer queue is unavailable in this deployment', 503");
expect($orderInvoicesRouteContent)->not->toContain('exportCollectedInvoiceSynchronously');
expect($orderInvoicesRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
expect($orderInvoicesRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';");
expect($orderInvoicesRouteContent)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue.php';");
expect($order_invoices_route_content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
expect($order_invoices_route_content)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($order_invoices_route_content)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($order_invoices_route_content)->toContain('class_exists(economic_transfer_queue::class)');
expect($order_invoices_route_content)->toContain("Economic transfer queue is unavailable in this deployment', 503");
});
@@ -0,0 +1,11 @@
<?php
it('wires root cron entrypoint to the full cron scheduler with queue worker tasks', function (): void {
$content = file_get_contents(app_path('cron.php'));
expect($content)->not->toBeFalse();
expect($content)->toContain("require_once __DIR__ . '/vendor/autoload.php';");
expect($content)->toContain("require_once __DIR__ . '/config.php';");
expect($content)->toContain("require_once __DIR__ . '/cron/Cron.php';");
expect($content)->toContain("Cron scheduler failed:");
});
@@ -16,5 +16,13 @@ it('hardens transfer queue with type validation retry caps and stale lock recove
expect($content)->toContain('ECONOMIC_TRANSFER_JOB_VALIDATION_REJECTED');
expect($content)->toContain('Queue job reached max retry attempts');
expect($content)->toContain('AND attempts < max_attempts');
expect($content)->toContain('public function listJobs(array $statuses = [], int $limit = 50, int $offset = 0, ?string $transfer_type = null): array');
expect($content)->toContain('public function countJobs(array $statuses = [], ?string $transfer_type = null): int');
expect($content)->toContain('public function processPendingByTransferType(string $transfer_type, int $limit = 10): array');
expect($content)->toContain('private function processPendingInternal(int $limit = 5, ?string $transfer_type = null): array');
expect($content)->toContain('private function claimNextJob(?string $transfer_type = null): ?array');
expect($content)->toContain("transfer_type = '");
expect($content)->toContain('private function sanitizeStatuses(array $statuses): array');
expect($content)->toContain('private function buildListJobsWhereClause(array $statuses = [], ?string $transfer_type = null): string');
expect($content)->toContain('private function releaseStaleProcessingLocks(): void');
});
@@ -26,15 +26,30 @@ function economic_transfer_queue_openapi_content_or_skip(): string
test()->markTestSkipped('openapi.yaml is not available in this runtime environment.');
}
function economic_transfer_queue_openapi_block(string $content, string $start, string $end): string
{
$start_pos = strpos($content, $start);
$end_pos = strpos($content, $end);
expect($start_pos)->not->toBeFalse();
expect($end_pos)->not->toBeFalse();
expect($end_pos)->toBeGreaterThan($start_pos);
return substr($content, (int)$start_pos, (int)$end_pos - (int)$start_pos);
}
it('documents economic transfer queue paths in openapi', function (): void {
$content = economic_transfer_queue_openapi_content_or_skip();
expect($content)->toContain('/collected-invoices/economic:');
expect($content)->toContain('/collected-invoices/stripe/book:');
expect($content)->toContain('/collected-invoices/economic/queue:');
expect($content)->toContain('/collected-invoices/economic/queue/status:');
expect($content)->toContain('/collected-invoices/economic/queue/retry:');
expect($content)->toContain('/collected-invoices/economic/queue/run:');
expect($content)->toContain('/economic/invoice/draft/export:');
expect($content)->toContain('/economic/invoice/draft/export/status:');
expect($content)->toContain('/economic/invoice/draft/export/retry:');
expect($content)->toContain('/economic/invoice/export:');
expect($content)->toContain('/economic/invoice/export/status:');
expect($content)->toContain('/economic/invoice/export/retry:');
});
@@ -45,14 +60,91 @@ it('documents economic transfer queue schemas in openapi', function (): void {
expect($content)->toContain('EconomicTransferQueueStatus:');
expect($content)->toContain('EconomicTransferQueueJob:');
expect($content)->toContain('EconomicTransferQueueEnqueueResponse:');
expect($content)->toContain('EconomicTransferSynchronousFallbackResponse:');
expect($content)->toContain('EconomicTransferQueueStatusResponse:');
expect($content)->toContain('EconomicTransferQueueRetryResponse:');
expect($content)->toContain('EconomicTransferQueueRunResponse:');
expect($content)->toContain('EconomicTransferQueueListResponse:');
});
it('documents unavailable queue dependency responses for async economic transfer endpoints', function (): void {
it('documents 200 fallback plus 202 queue contracts for export endpoints and keeps 503 on queue lifecycle endpoints', function (): void {
$content = economic_transfer_queue_openapi_content_or_skip();
expect($content)->toContain('ServiceUnavailable:');
expect($content)->toContain("'503': { \$ref: '#/components/responses/ServiceUnavailable' }");
expect($content)->toContain("#/components/schemas/EconomicTransferSynchronousFallbackResponse");
$collected_export = economic_transfer_queue_openapi_block(
$content,
'/collected-invoices/economic:',
'/collected-invoices/stripe/book:'
);
$stripe_export = economic_transfer_queue_openapi_block(
$content,
'/collected-invoices/stripe/book:',
'/collected-invoices/economic/queue:'
);
$draft_export = economic_transfer_queue_openapi_block(
$content,
'/economic/invoice/draft/export:',
'/economic/invoice/draft/export/status:'
);
$invoice_export = economic_transfer_queue_openapi_block(
$content,
'/economic/invoice/export:',
'/economic/invoice/export/status:'
);
foreach ([$collected_export, $stripe_export, $draft_export, $invoice_export] as $block) {
expect($block)->toContain("'200':");
expect($block)->toContain("'202':");
expect($block)->not->toContain("'503': { \$ref: '#/components/responses/ServiceUnavailable' }");
}
$queue_blocks = [
economic_transfer_queue_openapi_block($content, '/collected-invoices/economic/queue:', '/collected-invoices/economic/queue/status:'),
economic_transfer_queue_openapi_block($content, '/collected-invoices/economic/queue/status:', '/collected-invoices/economic/queue/retry:'),
economic_transfer_queue_openapi_block($content, '/collected-invoices/economic/queue/retry:', '/collected-invoices/economic/queue/run:'),
economic_transfer_queue_openapi_block($content, '/collected-invoices/economic/queue/run:', '/collected-invoices/economic/compare:'),
economic_transfer_queue_openapi_block($content, '/economic/invoice/draft/export/status:', '/economic/invoice/draft/export/retry:'),
economic_transfer_queue_openapi_block($content, '/economic/invoice/export/status:', '/economic/invoice/export/retry:'),
];
foreach ($queue_blocks as $block) {
expect($block)->toContain("'503': { \$ref: '#/components/responses/ServiceUnavailable' }");
}
});
it('documents additive collected-invoice queue pagination metadata and retry conflict semantics', function (): void {
$content = economic_transfer_queue_openapi_content_or_skip();
$queue_list_block = economic_transfer_queue_openapi_block(
$content,
'/collected-invoices/economic/queue:',
'/collected-invoices/economic/queue/status:'
);
expect($queue_list_block)->toContain('style: form');
expect($queue_list_block)->toContain('explode: false');
expect($queue_list_block)->toContain("type: array");
expect($queue_list_block)->toContain("\$ref: '#/components/schemas/EconomicTransferQueueStatus'");
$retry_block = economic_transfer_queue_openapi_block(
$content,
'/collected-invoices/economic/queue/retry:',
'/collected-invoices/economic/queue/run:'
);
expect($retry_block)->toContain("'409': { \$ref: '#/components/responses/Conflict' }");
$list_schema_block = economic_transfer_queue_openapi_block(
$content,
'EconomicTransferQueueListResponse:',
'CollectedInvoiceEconomicCompareResponse:'
);
expect($list_schema_block)->toContain('total:');
expect($list_schema_block)->toContain('limit:');
expect($list_schema_block)->toContain('offset:');
expect($list_schema_block)->toContain('has_more:');
expect($list_schema_block)->toContain('- total');
expect($list_schema_block)->toContain('- limit');
expect($list_schema_block)->toContain('- offset');
expect($list_schema_block)->toContain('- has_more');
});
@@ -5,8 +5,10 @@ it('registers queued economic invoice endpoints in economicInvoiceRoute', functi
expect($content)->not->toBeFalse();
expect($content)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
expect($content)->toContain("!class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($content)->toContain("!class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($content)->toContain("if (!\$this->isEconomicTransferQueueAvailable()) {");
expect($content)->toContain('$this->ensureEconomicTransferQueueIsAvailable();');
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
expect($content)->toContain('/economic/invoice/draft/export');
@@ -19,6 +21,11 @@ it('registers queued economic invoice endpoints in economicInvoiceRoute', functi
expect($content)->toContain('economic_transfer_queue::TYPE_ORDER_INVOICE_EXPORT');
expect($content)->toContain("'job_id' => \$job_id");
expect($content)->toContain("missing queue job id in enqueue response");
expect($content)->toContain("'mode' => 'synchronous_fallback'");
expect($content)->toContain('ECONOMIC_INVOICE_DRAFT_EXPORT_FALLBACK');
expect($content)->toContain('ECONOMIC_INVOICE_EXPORT_FALLBACK');
expect($content)->toContain('private function exportOrderDraftSynchronously');
expect($content)->toContain('private function exportOrderInvoiceSynchronously');
});
it('registers collected-invoice queue endpoints in orderInvoicesRoute', function (): void {
@@ -30,19 +37,24 @@ it('registers collected-invoice queue endpoints in orderInvoicesRoute', function
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
expect($content)->toContain('$this->ensureEconomicTransferQueueIsAvailable();');
expect($content)->not->toContain('exportCollectedInvoiceSynchronously');
expect($content)->toContain('private function exportCollectedInvoiceSynchronously');
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
expect($content)->toContain('/collected-invoices/economic');
expect($content)->toContain('/collected-invoices/economic/queue');
expect($content)->toContain('/collected-invoices/economic/queue/status');
expect($content)->toContain('/collected-invoices/economic/queue/retry');
expect($content)->toContain('/collected-invoices/economic/queue/run');
expect($content)->toContain('/collected-invoices/stripe/book');
expect($content)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
expect($content)->toContain('processPendingByTransferType(');
expect($content)->toContain("'job_id' => \$job_id");
expect($content)->toContain("missing queue job id in enqueue response");
expect($content)->toContain("requirePermission('add_collected_invoice_economic')");
expect($content)->toContain("requirePermission('add_collected_invoice_stripe')");
expect($content)->toContain('send_as_is must be a boolean');
expect($content)->toContain("in_array(\$normalized_send_as_is, ['true', '1'], true)");
expect($content)->toContain("'mode' => 'synchronous_fallback'");
expect($content)->toContain('ADD_COLLECTED_INVOICE_ECONOMIC_FALLBACK');
expect($content)->toContain('ADD_COLLECTED_INVOICE_STRIPE_FALLBACK');
expect($content)->toContain("'message' => 'Stripe collected invoice export queued'");
});
@@ -0,0 +1,23 @@
<?php
it('exposes an explicit collected-invoice transfer queue run route instead of ticking read endpoints', function (): void {
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
expect($content)->not->toBeFalse();
expect($content)->toContain("\$this->get('/collected-invoices/economic/queue'");
expect($content)->toContain("\$this->get('/collected-invoices/economic/queue/status'");
expect($content)->toContain("\$this->post('/collected-invoices/economic/queue/run'");
expect($content)->toContain('$queue->processPendingByTransferType(');
expect($content)->not->toContain("\$this->processEconomicTransferQueueTick(5);");
expect($content)->not->toContain("\$this->processEconomicTransferQueueTick(1);");
});
it('keeps order transfer queue status routes read-only', function (): void {
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
expect($content)->not->toBeFalse();
expect($content)->toContain("\$this->get('/economic/invoice/draft/export/status'");
expect($content)->toContain("\$this->get('/economic/invoice/export/status'");
expect($content)->not->toContain("\$this->processEconomicTransferQueueTick(1);");
expect($content)->not->toContain('private function processEconomicTransferQueueTick(int $limit = 1): void');
});
@@ -0,0 +1,141 @@
<?php
app_require('routes/InvoicingPeriodRoute.php');
use routes\InvoicingPeriodRoute;
function invoicing_period_queue_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_queue_overlay_job(
int $queue_job_id,
string $queue_status,
int $invoice_collection_id,
int $customer_number,
bool $is_period_relevant = true
): array {
return [
'queue_job_id' => $queue_job_id,
'queue_status' => $queue_status,
'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,
];
}
it('marks queued transactions and clears requires_action when all actionable work is already queued', function (): void {
$queueJob = invoicing_period_queue_overlay_job(93, 'QUEUED', 14578, 42424242);
$customer = [
'customer_number' => 42424242,
'customer_name' => 'Queue Customer',
'requires_action' => true,
'transactions' => [
[
'id' => 501,
'booked' => false,
'excluded' => false,
'invoice_collection_id' => 14578,
'queue_status' => null,
'queue_job_id' => null,
],
],
];
$result = invoicing_period_queue_overlay_invoke('applyCollectedInvoiceQueueOverlayToCustomer', [
$customer,
[14578 => $queueJob],
[42424242 => [$queueJob]],
]);
expect($result['requires_action'])->toBeFalse();
expect($result['transactions'][0]['queue_status'])->toBe('QUEUED');
expect($result['transactions'][0]['queue_job_id'])->toBe(93);
expect($result['queue'])->toBe([
'has_active_job' => true,
'statuses' => ['QUEUED'],
'invoice_collection_ids' => [14578],
'is_action_blocked' => true,
]);
});
it('keeps requires_action true when a customer still has unqueued actionable transactions', function (): void {
$queueJob = invoicing_period_queue_overlay_job(94, 'PROCESSING', 2001, 43434343);
$customer = [
'customer_number' => 43434343,
'customer_name' => 'Mixed Customer',
'requires_action' => true,
'transactions' => [
[
'id' => 601,
'booked' => false,
'excluded' => false,
'invoice_collection_id' => 2001,
'queue_status' => null,
'queue_job_id' => null,
],
[
'id' => 602,
'booked' => false,
'excluded' => false,
'invoice_collection_id' => null,
'queue_status' => null,
'queue_job_id' => null,
],
],
];
$result = invoicing_period_queue_overlay_invoke('applyCollectedInvoiceQueueOverlayToCustomer', [
$customer,
[2001 => $queueJob],
[43434343 => [$queueJob]],
]);
expect($result['requires_action'])->toBeTrue();
expect($result['transactions'][0]['queue_status'])->toBe('PROCESSING');
expect($result['transactions'][1]['queue_status'])->toBeNull();
expect($result['queue'])->toBe([
'has_active_job' => true,
'statuses' => ['PROCESSING'],
'invoice_collection_ids' => [2001],
'is_action_blocked' => false,
]);
});
it('blocks fixed-pricing or subscription customers with no transactions when a relevant queue job exists', function (): void {
$queueJob = invoicing_period_queue_overlay_job(95, 'QUEUED', 3001, 45454545, true);
$customer = [
'customer_number' => 45454545,
'customer_name' => 'Subscription Customer',
'requires_action' => true,
'transactions' => [],
'meta' => [
'fixed_pricing' => [
'price' => 1200,
],
],
];
$result = invoicing_period_queue_overlay_invoke('applyCollectedInvoiceQueueOverlayToCustomer', [
$customer,
[],
[45454545 => [$queueJob]],
]);
expect($result['requires_action'])->toBeFalse();
expect($result['queue'])->toBe([
'has_active_job' => true,
'statuses' => ['QUEUED'],
'invoice_collection_ids' => [3001],
'is_action_blocked' => true,
]);
});
@@ -0,0 +1,27 @@
<?php
it('wires mobile stripe payment intent routes to normalized lifecycle handling', function (): void {
$routeFile = app_path('routes/ordersRoute.php');
$content = file_get_contents($routeFile);
expect($content)->not->toBeFalse();
$start = strpos($content, "\$this->post('/orders/module/stripe/payment_intent'");
$end = strpos($content, "\$this->post('/orders/module/stripe/debug/simulate_payment'");
expect($start)->not->toBeFalse();
expect($end)->not->toBeFalse();
$stripeSection = substr($content, (int)$start, (int)$end - (int)$start);
expect($stripeSection)->toContain('buildStripePaymentIntentResponse(');
expect($stripeSection)->toContain('isStripePaymentIntentReusable($storedPaymentIntent)');
expect($stripeSection)->toContain("'reused' => true");
expect($stripeSection)->toContain("'message' => 'No active payment intent for this order.'");
expect($stripeSection)->toContain("'message' => 'Payment intent cleared successfully.'");
expect($stripeSection)->toContain("Stored payment intent is stale. Start the payment again.");
expect($stripeSection)->toContain("Payment intent has already been captured.");
expect($stripeSection)->toContain("Payment intent was cancelled. Start the payment again.");
expect($stripeSection)->toContain("Payment intent is not ready to capture.");
expect($stripeSection)->toContain("paidWithStripe(\$paymentIntent->id);");
expect($stripeSection)->not->toContain("Order does not have a payment intent");
});
@@ -0,0 +1,19 @@
<?php
it('wires stripe payment intent persistence to prune duplicates and clear reader state safely', function (): void {
$objectFile = app_path('objects/stripe_payment_intents_o.php');
$content = file_get_contents($objectFile);
expect($content)->not->toBeFalse();
expect($content)->toContain("new object_property(\$this->table, \$this->id, 'reader_id', 'string', false);");
expect($content)->toContain('clearOrderPaymentIntents($order_id);');
expect($content)->toContain('public function getOrderPaymentIntentRows(int $order_id): array');
expect($content)->toContain('usort($rows');
expect($content)->toContain('public function clearOrderPaymentIntents(int $order_id, ?int $keepId = null): void');
expect($content)->toContain("self::delete_object(\$this->getTable(), \$rowId);");
expect($content)->toContain('public function updateStoredPaymentIntent(mixed $paymentIntent): void');
expect($content)->toContain('public function setReaderId(?string $readerId): void');
expect($content)->toContain('sendCancelPaymentIntent($readerId);');
expect($content)->toContain('payment_intents->cancel($paymentIntentId);');
expect($content)->toContain('deleteDuplicateOrderPaymentIntents(int $order_id, int $keepId): void');
});
@@ -222,6 +222,14 @@ afterEach(function (): void {
}
});
it('builds department weather preload targets in cli without a request uri', function (): void {
unset($_SERVER['REQUEST_URI']);
$targets = moduleWeatherAPIRoute::getDepartmentWeatherHotPreloadTargets(5, 900);
expect($targets)->toBe([]);
});
it('serves fresh cached payloads without invoking the resolver', function (): void {
$route = new moduleWeatherAPIRoute();
putenv('DEPARTMENTS_WEATHER_CACHE_TTL=60');