Add unit and integration tests for collected invoice queue handling, route hardening, lifecycle validation, and manual batch processing logic.
This commit is contained in:
+11
-5
@@ -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");
|
||||
|
||||
+147
@@ -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)");
|
||||
});
|
||||
+14
-9
@@ -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');
|
||||
});
|
||||
|
||||
+87
-47
@@ -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");
|
||||
});
|
||||
|
||||
+11
@@ -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');
|
||||
});
|
||||
|
||||
+15
-3
@@ -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,
|
||||
]);
|
||||
});
|
||||
+27
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user