Workbook import phase 1: manifest-driven batch command
Implements the 18.09. strategy's phase 1 on top of the existing single importer (which stays the one write path): - new vitec:import-product-workbooks: runs vitec:import-product-workbook per manifest entry (in-process sub-command), aggregates a per-file report (writes/unchanged/na, URL-mismatch and error flags, details on -v or failure) plus a summary; without --apply every run is a dry run; --only filters - migrations/products_xlsx/ now holds working copies of all 100 agency workbooks in their category folders (git-ignored) plus the TRACKED manifest.json: workbook path -> product slug, family entries mark the K-level landing pages (never imported), product null = unresolved. Generated from the 18.09. analysis + sitemap column K: 68 product / 17 family / 15 open - --init-manifest resolves open entries against the full product table (hidden records included): URL segment/filename vs slug, normalized title, unique slug prefix; only unambiguous hits are written back - single command compare is now tolerant of the RTE storage transform (whitespace collapse + <p>-wrap simulation), fixing the known every-run-WRITE false positive on re-imports Post-dry-run hardening, applied the same day (69 products, 785 field writes across two runs, zero errors, verified against the live JSON): - PROTECTED_FIELDS title/urltitle in the single command: the stored module mapping would have overwritten six canonical sitemap-V2 product titles - the stored mapping predated the 18.09. defaults and silently dropped `highlights` on every product; the single command now merges DEFAULT_MAPPING under the stored mapping (defaults as base, stored as per-field override) - known limitation for phase 2: the g: composed fields report WRITE on every run (RTE transform alters ul/strong markup beyond the p-wrap normalisation) - the change watch must compare against stored imported values, not the database Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6811463343
commit
6961254e57
5 files changed
+661
-9
No files matched your search
@@ -42,6 +42,15 @@ final class ImportProductWorkbookCommand extends Command
|
||||
{
|
||||
private const TABLE = 'tx_vitec_domain_model_product';
|
||||
|
||||
/**
|
||||
* Never written by the workbook import, whatever the stored mapping
|
||||
* says: product titles are canonical per sitemap V2 (decision
|
||||
* 2026-09-04) - the 2026-09-24 batch dry run showed a stored module
|
||||
* mapping would have renamed six products. urltitle would only receive
|
||||
* a duplicate of the hero one-liner.
|
||||
*/
|
||||
private const PROTECTED_FIELDS = ['title', 'urltitle'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ProductXlsxReader $reader,
|
||||
private readonly MappingRepository $mappingRepository,
|
||||
@@ -89,20 +98,30 @@ final class ImportProductWorkbookCommand extends Command
|
||||
$output->writeln(sprintf('<comment>URL mismatch: /%s (workbook) vs /%s (record) - check the target!</comment>', $url, $slug));
|
||||
}
|
||||
|
||||
$mapping = array_filter($this->mappingRepository->load('product_xlsx')['mapping'] ?? []);
|
||||
if ($mapping === []) {
|
||||
$mapping = ProductTextImportController::DEFAULT_MAPPING;
|
||||
}
|
||||
// Defaults as the base, the stored module mapping as per-field
|
||||
// override - a field the module never saved (its dropdown was empty
|
||||
// on an old-format workbook, e.g. `highlights`) still imports with
|
||||
// its default selector. Found in the 2026-09-24 batch run: the
|
||||
// stored mapping predated the highlights default and silently
|
||||
// dropped the field on all 69 products.
|
||||
$mapping = array_merge(
|
||||
ProductTextImportController::DEFAULT_MAPPING,
|
||||
array_filter($this->mappingRepository->load('product_xlsx')['mapping'] ?? [])
|
||||
);
|
||||
|
||||
$data = [];
|
||||
foreach ($mapping as $field => $selector) {
|
||||
if (in_array($field, self::PROTECTED_FIELDS, true)) {
|
||||
$output->writeln(sprintf('%-20s %-34s -> protected (canonical, never written)', $field, $selector));
|
||||
continue;
|
||||
}
|
||||
$value = $this->reader->valueFor($parsed, $selector);
|
||||
if ($value === null) {
|
||||
$output->writeln(sprintf('%-20s %-34s -> not in this file', $field, $selector));
|
||||
continue;
|
||||
}
|
||||
$old = (string)($product[$field] ?? '');
|
||||
if (trim($old) === trim($value)) {
|
||||
if ($this->isUnchanged($old, $value)) {
|
||||
$output->writeln(sprintf('%-20s %-34s -> unchanged', $field, $selector));
|
||||
continue;
|
||||
}
|
||||
@@ -134,6 +153,22 @@ final class ImportProductWorkbookCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare tolerant of the RTE storage transform, which wraps bare
|
||||
* content in <p>...</p> and strips newlines on save - without this every
|
||||
* re-run reported already-imported richtext fields as WRITE (the known
|
||||
* false-positive from the module's preview).
|
||||
*/
|
||||
private function isUnchanged(string $old, string $new): bool
|
||||
{
|
||||
$normalize = static fn(string $s): string => trim((string)preg_replace('/\s+/u', ' ', $s));
|
||||
if ($normalize($old) === $normalize($new)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $normalize($old) === $normalize('<p>' . $new . '</p>');
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
private function loadProduct(string $slugOrUid): ?array
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user