362 lines
13 KiB
PHP
362 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use App\Models\ClaimFilesModel;
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
use Mpdf\Mpdf;
|
|
use Mpdf\Output\Destination;
|
|
|
|
/**
|
|
* Dedicated smoke test for claim PDF merge with S3-backed claim_files.
|
|
*
|
|
* Usage:
|
|
* php spark storage:smoke-test-merge-pdf
|
|
* php spark storage:smoke-test-merge-pdf --list
|
|
* php spark storage:smoke-test-merge-pdf --keep # leave S3/DB artifacts (debug)
|
|
*/
|
|
class StorageMergePdfSmokeTest extends BaseCommand
|
|
{
|
|
protected $group = 'Storage';
|
|
protected $name = 'storage:smoke-test-merge-pdf';
|
|
protected $description = 'Run dedicated merge_ticket_pdfs S3 scenarios (claim_files).';
|
|
protected $usage = 'storage:smoke-test-merge-pdf [--list] [--keep]';
|
|
protected $options = [
|
|
'--list' => 'List merge scenarios without executing.',
|
|
'--keep' => 'Skip cleanup of S3 objects and claim_files rows.',
|
|
];
|
|
|
|
private int $pass = 0;
|
|
private int $fail = 0;
|
|
private int $skip = 0;
|
|
|
|
/** @var list<array{id:string,title:string}> */
|
|
private array $catalog = [
|
|
['id' => 'MERGE-01', 'title' => 'Create & upload 2 source PDFs to S3 (claim_files, no permanent local)'],
|
|
['id' => 'MERGE-02', 'title' => 'Insert claim_files DB rows for synthetic ticket'],
|
|
['id' => 'MERGE-03', 'title' => 'merge_ticket_pdfs resolves sources from S3 (temp download)'],
|
|
['id' => 'MERGE-04', 'title' => 'Merge succeeds with pages >= 2'],
|
|
['id' => 'MERGE-05', 'title' => 'Merged PDF exists on S3'],
|
|
['id' => 'MERGE-06', 'title' => 'Merged permanent local path gone when S3 enabled'],
|
|
['id' => 'MERGE-07', 'title' => 'Merged claim_files row registered (file_type=4)'],
|
|
['id' => 'MERGE-08', 'title' => 'Claim runtime temps cleaned after merge'],
|
|
['id' => 'MERGE-09', 'title' => 'Cleanup removes S3 objects + soft-deletes DB rows'],
|
|
];
|
|
|
|
public function run(array $params)
|
|
{
|
|
helper(['utility', 'merge_pdf']);
|
|
|
|
if (CLI::getOption('list') !== null) {
|
|
CLI::write('========== MERGE PDF SCENARIOS ==========', 'cyan');
|
|
foreach ($this->catalog as $row) {
|
|
CLI::write(" {$row['id']} {$row['title']}");
|
|
}
|
|
CLI::newLine();
|
|
CLI::write('Run: php spark storage:smoke-test-merge-pdf');
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
$storage = \Config\Services::getFileStorageService(false);
|
|
$keep = CLI::getOption('keep') !== null;
|
|
|
|
CLI::write('========== CLAIM PDF MERGE SMOKE ==========', 'cyan');
|
|
CLI::write('usesS3: ' . ($storage->usesS3() ? 'YES' : 'NO'));
|
|
CLI::write('Bucket: ' . ($storage->getBucket() ?? 'N/A'));
|
|
CLI::write('RETAIN_LOCAL: ' . (storage_env_retain_local() ? 'true' : 'false'));
|
|
CLI::newLine();
|
|
|
|
if (! $storage->usesS3()) {
|
|
foreach ($this->catalog as $row) {
|
|
$this->skip($row['id'], 'FILE_STORAGE_DRIVER must be s3');
|
|
}
|
|
$this->printResult();
|
|
return EXIT_ERROR;
|
|
}
|
|
|
|
$uploadDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR
|
|
. 'uploads' . DIRECTORY_SEPARATOR
|
|
. 'claim_files' . DIRECTORY_SEPARATOR;
|
|
if (! is_dir($uploadDir)) {
|
|
@mkdir($uploadDir, 0775, true);
|
|
}
|
|
|
|
$ticketId = 990000000 + random_int(1000, 999999);
|
|
$stamp = date('Ymd_His') . '_' . bin2hex(random_bytes(3));
|
|
$srcNames = [
|
|
'smoke_merge_a_' . $stamp . '.pdf',
|
|
'smoke_merge_b_' . $stamp . '.pdf',
|
|
];
|
|
$sourceIds = [];
|
|
$mergedName = null;
|
|
$mergedId = null;
|
|
$claimFiles = new ClaimFilesModel();
|
|
|
|
try {
|
|
// MERGE-01
|
|
$uploaded = 0;
|
|
foreach ($srcNames as $i => $name) {
|
|
$local = $this->makeOnePagePdf($uploadDir, $name, 'Smoke source ' . ($i + 1));
|
|
if ($local === null || ! is_file($local)) {
|
|
continue;
|
|
}
|
|
$up = $storage->upload($local, rtrim($uploadDir, '/\\'), $name);
|
|
@unlink($local);
|
|
if (($up['success'] ?? false) && $storage->exists(rtrim($uploadDir, '/\\'), $name) && ! is_file($uploadDir . $name)) {
|
|
$uploaded++;
|
|
}
|
|
}
|
|
$this->step('MERGE-01', $uploaded === 2, "uploaded={$uploaded}/2 ticket_id={$ticketId}");
|
|
|
|
if ($uploaded !== 2) {
|
|
foreach (array_slice($this->catalog, 1) as $row) {
|
|
$this->skip($row['id'], 'source upload failed');
|
|
}
|
|
$this->printResult();
|
|
return EXIT_ERROR;
|
|
}
|
|
|
|
// MERGE-02
|
|
foreach ($srcNames as $idx => $name) {
|
|
$id = $claimFiles->insert([
|
|
'ticket_id' => $ticketId,
|
|
'file_type' => 2,
|
|
'doc_name' => 'SMOKE_MERGE_SRC_' . ($idx + 1),
|
|
'file_name' => $name,
|
|
'url' => $name,
|
|
'mime_type' => 'application/pdf',
|
|
'is_active' => 1,
|
|
'created_by' => 1,
|
|
]);
|
|
if ($id) {
|
|
$sourceIds[] = (int) $id;
|
|
}
|
|
}
|
|
$this->step('MERGE-02', count($sourceIds) === 2, 'ids=' . implode(',', $sourceIds));
|
|
|
|
if (count($sourceIds) !== 2) {
|
|
foreach (array_slice($this->catalog, 2) as $row) {
|
|
$this->skip($row['id'], 'DB insert failed');
|
|
}
|
|
$this->cleanup($storage, $uploadDir, $srcNames, null, $ticketId, $sourceIds, $keep);
|
|
$this->printResult();
|
|
return EXIT_ERROR;
|
|
}
|
|
|
|
// MERGE-03..07 via merge_ticket_pdfs
|
|
$result = merge_ticket_pdfs($ticketId, [
|
|
'replace' => true,
|
|
'created_by' => 1,
|
|
'include_file_types' => [1, 2],
|
|
'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'],
|
|
]);
|
|
|
|
$this->step(
|
|
'MERGE-03',
|
|
(bool) ($result['status'] ?? false) && (int) ($result['source_count'] ?? 0) >= 2,
|
|
(string) ($result['message'] ?? '') . ' sources=' . (int) ($result['source_count'] ?? 0)
|
|
);
|
|
|
|
$pages = (int) ($result['pages'] ?? 0);
|
|
$this->step(
|
|
'MERGE-04',
|
|
(bool) ($result['status'] ?? false) && $pages >= 2,
|
|
'pages=' . $pages
|
|
);
|
|
|
|
$mergedName = (string) ($result['file_name'] ?? '');
|
|
$mergedId = $result['merged_file_id'] ?? null;
|
|
$onS3 = $mergedName !== '' && $storage->exists(rtrim($uploadDir, '/\\'), $mergedName);
|
|
$this->step('MERGE-05', $onS3, $mergedName !== '' ? $mergedName : 'no merged name');
|
|
|
|
$localMergedGone = $mergedName === '' || ! is_file($uploadDir . $mergedName);
|
|
$this->step('MERGE-06', $localMergedGone, $mergedName !== '' ? basename($mergedName) : 'n/a');
|
|
|
|
$rowOk = false;
|
|
if ($mergedId) {
|
|
$row = $claimFiles->find((int) $mergedId);
|
|
$rowOk = is_array($row)
|
|
&& (int) ($row['file_type'] ?? 0) === MERGED_CLAIM_FILE_TYPE
|
|
&& (int) ($row['is_active'] ?? 0) === 1
|
|
&& (string) ($row['file_name'] ?? '') === $mergedName;
|
|
}
|
|
$this->step('MERGE-07', $rowOk, 'merged_file_id=' . ($mergedId ?? 'null'));
|
|
|
|
// MERGE-08: no leftover smoke temps for our source names in claim_files_runtime
|
|
$afterRuntime = $this->listClaimRuntimeFiles();
|
|
$leaked = [];
|
|
foreach ($afterRuntime as $f) {
|
|
foreach ($srcNames as $src) {
|
|
if (strpos($f, $src) !== false) {
|
|
$leaked[] = $f;
|
|
}
|
|
}
|
|
}
|
|
$this->step('MERGE-08', $leaked === [], $leaked === [] ? 'no leaks' : implode(',', $leaked));
|
|
|
|
// MERGE-09 cleanup
|
|
$cleanOk = $this->cleanup(
|
|
$storage,
|
|
$uploadDir,
|
|
$srcNames,
|
|
$mergedName !== '' ? $mergedName : null,
|
|
$ticketId,
|
|
$sourceIds,
|
|
$keep,
|
|
$mergedId ? [(int) $mergedId] : []
|
|
);
|
|
if ($keep) {
|
|
$this->skip('MERGE-09', '--keep set; left ticket_id=' . $ticketId);
|
|
} else {
|
|
$srcGone = true;
|
|
foreach ($srcNames as $name) {
|
|
if ($storage->exists(rtrim($uploadDir, '/\\'), $name)) {
|
|
$srcGone = false;
|
|
}
|
|
}
|
|
$mergedGone = $mergedName === '' || ! $storage->exists(rtrim($uploadDir, '/\\'), $mergedName);
|
|
$this->step('MERGE-09', $cleanOk && $srcGone && $mergedGone, 'ticket_id=' . $ticketId);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
CLI::error('Unhandled: ' . $e->getMessage());
|
|
$this->fail++;
|
|
$this->cleanup($storage, $uploadDir, $srcNames, $mergedName, $ticketId, $sourceIds, $keep, $mergedId ? [(int) $mergedId] : []);
|
|
}
|
|
|
|
$this->printResult();
|
|
return $this->fail > 0 ? EXIT_ERROR : EXIT_SUCCESS;
|
|
}
|
|
|
|
private function makeOnePagePdf(string $uploadDir, string $fileName, string $text): ?string
|
|
{
|
|
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
|
if (! is_dir($tempDir)) {
|
|
@mkdir($tempDir, 0775, true);
|
|
}
|
|
$path = $uploadDir . $fileName;
|
|
try {
|
|
$mpdf = new Mpdf(['tempDir' => $tempDir, 'mode' => 'utf-8']);
|
|
$mpdf->WriteHTML('<h1>' . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '</h1><p>storage merge smoke</p>');
|
|
$mpdf->Output($path, Destination::FILE);
|
|
return is_file($path) ? $path : null;
|
|
} catch (\Throwable $e) {
|
|
CLI::error('PDF create failed: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function listClaimRuntimeFiles(): array
|
|
{
|
|
$dir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR
|
|
. 'cache' . DIRECTORY_SEPARATOR
|
|
. 'claim_files_runtime' . DIRECTORY_SEPARATOR;
|
|
if (! is_dir($dir)) {
|
|
return [];
|
|
}
|
|
$out = [];
|
|
foreach (scandir($dir) ?: [] as $f) {
|
|
if ($f === '.' || $f === '..') {
|
|
continue;
|
|
}
|
|
if (is_file($dir . $f)) {
|
|
$out[] = $f;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $srcNames
|
|
* @param list<int> $sourceIds
|
|
* @param list<int> $extraIds
|
|
*/
|
|
private function cleanup(
|
|
$storage,
|
|
string $uploadDir,
|
|
array $srcNames,
|
|
?string $mergedName,
|
|
int $ticketId,
|
|
array $sourceIds,
|
|
bool $keep,
|
|
array $extraIds = []
|
|
): bool {
|
|
if ($keep) {
|
|
return true;
|
|
}
|
|
|
|
$ok = true;
|
|
$dir = rtrim($uploadDir, '/\\');
|
|
foreach ($srcNames as $name) {
|
|
if ($storage->exists($dir, $name)) {
|
|
$del = $storage->delete($dir, $name);
|
|
if (! ($del['success'] ?? false)) {
|
|
$ok = false;
|
|
}
|
|
}
|
|
@unlink($uploadDir . $name);
|
|
}
|
|
if ($mergedName) {
|
|
if ($storage->exists($dir, $mergedName)) {
|
|
$del = $storage->delete($dir, $mergedName);
|
|
if (! ($del['success'] ?? false)) {
|
|
$ok = false;
|
|
}
|
|
}
|
|
@unlink($uploadDir . $mergedName);
|
|
}
|
|
|
|
try {
|
|
$claimFiles = new ClaimFilesModel();
|
|
$ids = array_values(array_unique(array_merge($sourceIds, $extraIds)));
|
|
if ($ids !== []) {
|
|
$claimFiles->whereIn('id', $ids)->set(['is_active' => 0])->update();
|
|
}
|
|
$claimFiles->where('ticket_id', $ticketId)->set(['is_active' => 0])->update();
|
|
} catch (\Throwable $e) {
|
|
CLI::write('DB cleanup warning: ' . $e->getMessage(), 'yellow');
|
|
$ok = false;
|
|
}
|
|
|
|
return $ok;
|
|
}
|
|
|
|
private function step(string $id, bool $ok, string $detail = ''): void
|
|
{
|
|
$title = $this->titleFor($id);
|
|
if ($ok) {
|
|
$this->pass++;
|
|
CLI::write("PASS {$id} {$title}" . ($detail !== '' ? " — {$detail}" : ''), 'green');
|
|
} else {
|
|
$this->fail++;
|
|
CLI::write("FAIL {$id} {$title}" . ($detail !== '' ? " — {$detail}" : ''), 'red');
|
|
}
|
|
}
|
|
|
|
private function skip(string $id, string $reason): void
|
|
{
|
|
$this->skip++;
|
|
CLI::write("SKIP {$id} {$this->titleFor($id)} — {$reason}", 'yellow');
|
|
}
|
|
|
|
private function titleFor(string $id): string
|
|
{
|
|
foreach ($this->catalog as $row) {
|
|
if ($row['id'] === $id) {
|
|
return $row['title'];
|
|
}
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
private function printResult(): void
|
|
{
|
|
CLI::newLine();
|
|
CLI::write(
|
|
sprintf('MERGE RESULT: %d passed | %d failed | %d skipped', $this->pass, $this->fail, $this->skip),
|
|
$this->fail === 0 ? 'green' : 'red'
|
|
);
|
|
}
|
|
}
|