Skip to content

Sealed UPC Ingest (PR 1) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ship real manufacturer UPCs on sealed products by reading the extUPC column the sealed price importer already downloads, and stop sending synthetic LGSF… barcodes for sealed entirely.

Architecture: A new global reference collection sealed_product_barcodes is populated by the existing daily sealed price importer, which already resolves every TCGCSV row to an MTGJSON sealed product uuid. buildSealedSyncPayload reads that collection; when no UPC is known the barcode field is omitted from the Shopify payload rather than filled with a generated value. Singles are untouched.

Tech Stack: Node.js (CommonJS), Mongoose, Vitest (ESM test files), Express.

Spec: docs/superpowers/specs/2026-07-30-sealed-upc-from-tcgcsv-design.md

Global Constraints

  • Server code is CommonJS (require/module.exports); test files are ESM (import). Test files are co-located as x.test.js next to x.js.
  • Never introduce a fallback default for an identity parameter — game, shop, uuid (CLAUDE.md §5.5). Absent → throw or reject. Grep the diff for || 'mtg', ?? 'mtg', = 'mtg': zero matches.
  • Every new persisted field is declared field-by-field in the Mongoose schema and ships with a schema-shape round-trip test (CLAUDE.md §5.2).
  • Data-matching literals must cite the real document or importer they came from (CLAUDE.md §5.4). The extUPC column name is verified at https://tcgcsv.com/tcgplayer/1/23874/ProductsAndPrices.csv.
  • Regex character classes use [0-9], never \d — inline \d is mangled when commands are pasted through Git Bash, and a silently non-matching pattern is the failure mode.
  • Husky pre-commit runs ESLint. Fix lint; never use --no-verify.
  • All Shopify API calls go through server/services/shopifyAPI.js (not applicable in this PR — no Shopify calls are added).
  • Commit messages: one imperative sentence describing the merchant-visible outcome, sentence case, no trailing period.

Parity note (CLAUDE.md §5.1)

  • mtg — implemented here.
  • pokemon — exempt: no sealed catalog source. transformSealedToProduct exists but nothing enumerates sealed products for it.
  • riftbound — exempt: same reason.

Verified at server/plugins/mtg/index.js:417, the only plugin reading SetModel.data.sealedProduct. State this in the PR description.

Deviation from the spec (intentional)

The spec's Section 2 says to source game from getPlugin('mtg').gameId. On reading the importer, server/scripts/data-loading/updateSealedProductPrices.js:48 already defines const IMPORTER_GAME = 'mtg'; and buildSealedProductMap already stamps game: IMPORTER_GAME onto every productMap entry (:189). This plan therefore reads product.game off the map entry — same value, no new literal, no new import. The spec's intent (no bare 'mtg' literal in the diff) is satisfied more directly.

File Structure

FileResponsibility
server/models/SealedProductBarcode.jsCreate. Global reference collection + getBarcode(game, uuid) static.
server/models/SealedProductBarcode.test.jsCreate. Schema-shape round-trip guard (§5.2) + static behavior.
server/scripts/data-loading/updateSealedProductPrices.jsModify. Add extractUpc(); ingest UPCs inside processPriceRows before the no-price early-continue.
server/scripts/data-loading/updateSealedProductPrices.test.jsModify. Add extractUpc unit tests + the ordering-trap test.
server/utils/barcodeGenerator.jsModify. resolveSealedBarcode drops the generated fallback.
server/utils/barcodeGenerator.test.jsModify. Replace the two fallback tests with omission tests.
server/routes/sealedProducts.jsModify. buildSealedSyncPayload reads the reference collection; export it for tests; fix the stale comment.
server/routes/sealedProducts.buildSyncPayload.test.jsCreate. Payload resolution tests.

Task 1: SealedProductBarcode model

Files:

  • Create: server/models/SealedProductBarcode.js
  • Test: server/models/SealedProductBarcode.test.js

Interfaces:

  • Consumes: nothing.

  • Produces: SealedProductBarcode Mongoose model with fields { game: String, uuid: String, barcode: String, source: String, tcgplayerProductId: Number } plus timestamps: true, and static getBarcode(game: string, uuid: string): Promise<string|null> which throws if either argument is falsy.

  • [ ] Step 1: Write the failing test

Create server/models/SealedProductBarcode.test.js:

javascript
/**
 * Unit tests for the SealedProductBarcode reference model.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createRequire } from 'node:module';

// The model is CommonJS. Load it through the CJS registry so spies land on the
// same model instance production code requires (an `await import()` compiles a
// second copy and trips Mongoose's OverwriteModelError).
const require = createRequire(import.meta.url);
const SealedProductBarcode = require('./SealedProductBarcode.js');

const validData = {
    game: 'mtg',
    uuid: 'a1b2c3d4-0000-0000-0000-000000000001',
    barcode: '195166278636',
    source: 'tcgcsv',
    tcgplayerProductId: 610553,
};

afterEach(() => {
    vi.restoreAllMocks();
});

describe('SealedProductBarcode schema', () => {
    // Schema-shape round-trip guard (CLAUDE.md §5.2): every field here is
    // written by the sealed price importer. An undeclared field is silently
    // dropped by strict mode, which would blank the barcode with no error.
    it('declares and retains every persisted field', () => {
        const doc = new SealedProductBarcode(validData);
        expect(doc.validateSync()).toBeUndefined();
        expect(doc.game).toBe('mtg');
        expect(doc.uuid).toBe('a1b2c3d4-0000-0000-0000-000000000001');
        expect(doc.barcode).toBe('195166278636');
        expect(doc.source).toBe('tcgcsv');
        expect(doc.tcgplayerProductId).toBe(610553);
    });

    it('requires game, uuid, and barcode', () => {
        for (const field of ['game', 'uuid', 'barcode']) {
            const data = { ...validData };
            delete data[field];
            const error = new SealedProductBarcode(data).validateSync();
            expect(error?.errors?.[field]).toBeDefined();
        }
    });

    it('defaults source to tcgcsv', () => {
        const data = { ...validData };
        delete data.source;
        expect(new SealedProductBarcode(data).source).toBe('tcgcsv');
    });

    it('declares a unique compound index on game and uuid', () => {
        const indexes = SealedProductBarcode.schema.indexes();
        const compound = indexes.find(([keys]) => keys.game === 1 && keys.uuid === 1);
        expect(compound).toBeDefined();
        expect(compound[1].unique).toBe(true);
    });
});

describe('SealedProductBarcode.getBarcode', () => {
    it('returns the stored barcode', async () => {
        vi.spyOn(SealedProductBarcode, 'findOne').mockReturnValue({
            lean: () => Promise.resolve({ barcode: '195166278636' }),
        });
        await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBe('195166278636');
    });

    it('returns null when no row exists', async () => {
        vi.spyOn(SealedProductBarcode, 'findOne').mockReturnValue({
            lean: () => Promise.resolve(null),
        });
        await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBeNull();
    });

    it('scopes the lookup by game and uuid', async () => {
        const findOne = vi.spyOn(SealedProductBarcode, 'findOne').mockReturnValue({
            lean: () => Promise.resolve(null),
        });
        await SealedProductBarcode.getBarcode('mtg', 'uuid-1');
        expect(findOne).toHaveBeenCalledWith({ game: 'mtg', uuid: 'uuid-1' });
    });

    // CLAUDE.md §5.5: identity parameters are never defaulted. A missing game
    // must surface as an error, not silently resolve another game's barcode.
    it('throws when game is missing', async () => {
        await expect(SealedProductBarcode.getBarcode(null, 'uuid-1')).rejects.toThrow(/game is required/);
    });

    it('throws when uuid is missing', async () => {
        await expect(SealedProductBarcode.getBarcode('mtg', null)).rejects.toThrow(/uuid is required/);
    });
});
  • [ ] Step 2: Run the test to verify it fails
bash
npx vitest run server/models/SealedProductBarcode.test.js

Expected: FAIL — Cannot find module './SealedProductBarcode.js'.

  • [ ] Step 3: Write the implementation

Create server/models/SealedProductBarcode.js:

javascript
/**
 * Sealed Product Barcode Model
 *
 * Global reference table mapping a sealed product's catalog uuid to the real
 * manufacturer UPC printed on its packaging.
 *
 * Source: the `extUPC` column of TCGCSV's ProductsAndPrices.csv, which
 * server/scripts/data-loading/updateSealedProductPrices.js already downloads
 * (verified against https://tcgcsv.com/tcgplayer/1/23874/ProductsAndPrices.csv
 * — CLAUDE.md §5.4). Coverage is narrow: roughly the booster pack, display and
 * display case per set. Everything else has no UPC and ships no barcode.
 *
 * Global, not per-store: the data is upstream and authoritative, so no store
 * can corrupt it. Same trust model as SealedProductMSRP.
 */

const mongoose = require('mongoose');

const sealedProductBarcodeSchema = new mongoose.Schema({
    // Game identifier ('mtg' today). On the key from the start so Pokemon and
    // Riftbound need no migration when sealed support lands for them — both
    // pull the same TCGCSV columns from the same endpoint.
    game: {
        type: String,
        required: true
    },

    // Catalog uuid of the sealed product. For MTG this is MTGJSON's
    // set.data.sealedProduct[].uuid — the same key buildSealedSyncPayload
    // resolves against, so the join needs no translation.
    uuid: {
        type: String,
        required: true
    },

    // The manufacturer barcode itself: 12 digits (UPC-A) or 13 (EAN-13).
    // Validated at ingest by extractUpc() in the importer.
    barcode: {
        type: String,
        required: true
    },

    // Provenance, so a future non-TCGCSV source is distinguishable.
    source: {
        type: String,
        default: 'tcgcsv'
    },

    // The TCGCSV row this came from, for tracing a wrong value back to source.
    tcgplayerProductId: Number
}, {
    timestamps: true
});

// One barcode per sealed product per game. Also the lookup index for
// getBarcode's exact-match query.
sealedProductBarcodeSchema.index({ game: 1, uuid: 1 }, { unique: true });

/**
 * Resolve the manufacturer barcode for one sealed product.
 *
 * @param {string} game - Game identifier, never defaulted (CLAUDE.md §5.5)
 * @param {string} uuid - Sealed product catalog uuid
 * @returns {Promise<string|null>} The UPC, or null when none is known
 */
sealedProductBarcodeSchema.statics.getBarcode = async function(game, uuid) {
    if (!game) {
        throw new Error('game is required to resolve a sealed barcode');
    }
    if (!uuid) {
        throw new Error('uuid is required to resolve a sealed barcode');
    }

    const doc = await this.findOne({ game, uuid }).lean();
    return doc ? doc.barcode : null;
};

module.exports = mongoose.model('SealedProductBarcode', sealedProductBarcodeSchema);
  • [ ] Step 4: Run the test to verify it passes
bash
npx vitest run server/models/SealedProductBarcode.test.js

Expected: PASS — 9 tests.

  • [ ] Step 5: Lint
bash
npm run lint

Expected: exit 0, no new warnings.

  • [ ] Step 6: Commit
bash
git add server/models/SealedProductBarcode.js server/models/SealedProductBarcode.test.js
git commit -m "Add a reference table for real sealed-product barcodes"

Task 2: extractUpc validation helper

Files:

  • Modify: server/scripts/data-loading/updateSealedProductPrices.js (add helper + named export near module.exports at :399-402)
  • Test: server/scripts/data-loading/updateSealedProductPrices.test.js (append a new describe)

Interfaces:

  • Consumes: nothing from Task 1.

  • Produces: extractUpc(row: object): string|null — exported as module.exports.extractUpc. Returns the trimmed value when it is 12 or 13 digits, otherwise null. Task 3 consumes this.

  • [ ] Step 1: Write the failing test

Append to server/scripts/data-loading/updateSealedProductPrices.test.js:

javascript
describe('extractUpc', () => {
    // Column name and sample values copied from the real CSV (CLAUDE.md §5.4):
    // https://tcgcsv.com/tcgplayer/1/23874/ProductsAndPrices.csv
    //   Aetherdrift - Play Booster Display -> extUPC 195166278636
    it('returns a valid 12-digit UPC-A', () => {
        expect(extractUpc({ extUPC: '195166278636' })).toBe('195166278636');
    });

    it('returns a valid 13-digit EAN-13', () => {
        expect(extractUpc({ extUPC: '0195166278636' })).toBe('0195166278636');
    });

    it('trims surrounding whitespace', () => {
        expect(extractUpc({ extUPC: '  195166278636  ' })).toBe('195166278636');
    });

    it('returns null when the column is absent', () => {
        expect(extractUpc({})).toBeNull();
    });

    it('returns null for an empty or whitespace-only value', () => {
        expect(extractUpc({ extUPC: '' })).toBeNull();
        expect(extractUpc({ extUPC: '   ' })).toBeNull();
    });

    it('rejects non-digit characters rather than passing them through', () => {
        expect(extractUpc({ extUPC: '19516-278636' })).toBeNull();
        expect(extractUpc({ extUPC: 'N/A' })).toBeNull();
    });

    it('rejects wrong-length digit strings', () => {
        expect(extractUpc({ extUPC: '12345' })).toBeNull();
        expect(extractUpc({ extUPC: '12345678901234' })).toBeNull();
    });

    it('accepts a numeric value from the CSV parser', () => {
        expect(extractUpc({ extUPC: 195166278636 })).toBe('195166278636');
    });
});

Add extractUpc to the existing destructured require near the top of the file (currently line 20):

javascript
const { getMSRPFallback, buildSealedProductMap, extractUpc } = require('./updateSealedProductPrices.js');
  • [ ] Step 2: Run the test to verify it fails
bash
npx vitest run server/scripts/data-loading/updateSealedProductPrices.test.js

Expected: FAIL — extractUpc is not a function.

  • [ ] Step 3: Write the implementation

In server/scripts/data-loading/updateSealedProductPrices.js, add above processPriceRows (which begins at :243):

javascript
// UPC-A is 12 digits, EAN-13 is 13. Character class is [0-9] rather than \d
// deliberately: an escaped \d is mangled when this file's patterns are pasted
// through Git Bash, and a silently non-matching regex would blank every UPC.
const UPC_PATTERN = /^[0-9]{12,13}$/;

/**
 * Pull the manufacturer barcode out of one TCGCSV row.
 *
 * The `extUPC` column is present on every ProductsAndPrices.csv but populated
 * only for sealed products — roughly the booster pack, display and display case
 * per set. Verified against the live CSV (CLAUDE.md §5.4):
 * https://tcgcsv.com/tcgplayer/1/23874/ProductsAndPrices.csv
 *
 * @param {Object} row - One parsed CSV row
 * @returns {string|null} The validated barcode, or null if absent/malformed
 */
function extractUpc(row) {
    if (row.extUPC === undefined || row.extUPC === null) return null;

    const trimmed = String(row.extUPC).trim();
    if (!trimmed) return null;

    return UPC_PATTERN.test(trimmed) ? trimmed : null;
}

Then add the named export alongside the existing ones at the bottom of the file (after :402):

javascript
module.exports.extractUpc = extractUpc;
  • [ ] Step 4: Run the test to verify it passes
bash
npx vitest run server/scripts/data-loading/updateSealedProductPrices.test.js

Expected: PASS — the 8 new extractUpc tests plus all pre-existing tests in the file.

  • [ ] Step 5: Commit
bash
git add server/scripts/data-loading/updateSealedProductPrices.js server/scripts/data-loading/updateSealedProductPrices.test.js
git commit -m "Validate manufacturer barcodes read from the TCGCSV feed"

Task 3: Ingest UPCs in processPriceRows

Files:

  • Modify: server/scripts/data-loading/updateSealedProductPrices.js (processPriceRows, :243-334)
  • Test: server/scripts/data-loading/updateSealedProductPrices.test.js

Interfaces:

  • Consumes: SealedProductBarcode (Task 1), extractUpc (Task 2).
  • Produces: processPriceRows(rows, productMap) exported as module.exports.processPriceRows; its returned summary object gains totalUpcs and totalMalformedUpcs alongside the existing totalProcessed, totalMatched, totalInserted, totalWithMSRP, totalSkipped.

The trap this task exists to avoid: processPriceRows contains if (!finalPrice) continue; at :304. The UPC write must be placed before it. A sealed product with no market price and no MSRP fallback still has a real barcode on its box; putting the write after that line means those products silently never get one.

  • [ ] Step 1: Write the failing test

Append to server/scripts/data-loading/updateSealedProductPrices.test.js. Add processPriceRows to the destructured require, and SealedProductBarcode to the model requires at the top:

javascript
const { getMSRPFallback, buildSealedProductMap, extractUpc, processPriceRows } = require('./updateSealedProductPrices.js');
const SealedProductBarcode = require('../../models/SealedProductBarcode.js');
const Price = require('../../models/Price.js');

Then append:

javascript
describe('processPriceRows — UPC ingest', () => {
    const UUID = 'a1b2c3d4-0000-0000-0000-000000000001';

    // buildSealedProductMap stamps `game` onto every entry (see :189), so the
    // fixture carries it exactly as the real map would.
    function productMapWith(overrides = {}) {
        return new Map([['610553', {
            uuid: UUID,
            name: 'Aetherdrift - Play Booster Display',
            category: 'booster_box',
            game: 'mtg',
            setCode: 'DFT',
            setName: 'Aetherdrift',
            releaseDate: new Date('2025-02-14'),
            ...overrides,
        }]]);
    }

    let bulkWrite;

    beforeEach(() => {
        bulkWrite = vi.spyOn(SealedProductBarcode, 'bulkWrite').mockResolvedValue({});
        vi.spyOn(Price, 'insertMany').mockResolvedValue([]);
    });

    it('upserts the barcode for a row with a valid extUPC', async () => {
        await processPriceRows(
            [{ productId: '610553', marketPrice: '129.99', extUPC: '195166278636' }],
            productMapWith()
        );

        expect(bulkWrite).toHaveBeenCalledTimes(1);
        const [ops] = bulkWrite.mock.calls[0];
        expect(ops).toHaveLength(1);
        expect(ops[0].updateOne.filter).toEqual({ game: 'mtg', uuid: UUID });
        expect(ops[0].updateOne.update.$set).toMatchObject({
            barcode: '195166278636',
            source: 'tcgcsv',
            tcgplayerProductId: 610553,
        });
        expect(ops[0].updateOne.upsert).toBe(true);
    });

    // THE ORDERING TRAP. processPriceRows early-continues when no price can be
    // resolved. If the UPC write sits after that line, every sealed product
    // without a market price or MSRP fallback silently loses its barcode — and
    // nothing else in the system would ever write it.
    it('upserts the barcode even when the row has no usable price', async () => {
        vi.spyOn(SealedProductMSRP, 'getMSRP').mockResolvedValue(null);

        const result = await processPriceRows(
            [{ productId: '610553', marketPrice: '', extUPC: '195166278636' }],
            productMapWith()
        );

        expect(result.totalInserted).toBe(0);
        expect(bulkWrite).toHaveBeenCalledTimes(1);
        expect(bulkWrite.mock.calls[0][0][0].updateOne.update.$set.barcode).toBe('195166278636');
    });

    it('does not upsert when extUPC is absent', async () => {
        await processPriceRows(
            [{ productId: '610553', marketPrice: '129.99' }],
            productMapWith()
        );
        expect(bulkWrite).not.toHaveBeenCalled();
    });

    it('counts a malformed extUPC without writing or throwing', async () => {
        const result = await processPriceRows(
            [{ productId: '610553', marketPrice: '129.99', extUPC: 'N/A' }],
            productMapWith()
        );

        expect(result.totalMalformedUpcs).toBe(1);
        expect(result.totalUpcs).toBe(0);
        expect(bulkWrite).not.toHaveBeenCalled();
    });

    it('ignores rows whose product is not in the catalog map', async () => {
        await processPriceRows(
            [{ productId: '999999', marketPrice: '10.00', extUPC: '195166278636' }],
            productMapWith()
        );
        expect(bulkWrite).not.toHaveBeenCalled();
    });

    it('reports how many barcodes were written', async () => {
        const map = productMapWith();
        map.set('610554', { ...map.get('610553'), uuid: 'uuid-2' });

        const result = await processPriceRows(
            [
                { productId: '610553', marketPrice: '129.99', extUPC: '195166278636' },
                { productId: '610554', marketPrice: '9.99', extUPC: '195166278629' },
            ],
            map
        );

        expect(result.totalUpcs).toBe(2);
    });
});
  • [ ] Step 2: Run the test to verify it fails
bash
npx vitest run server/scripts/data-loading/updateSealedProductPrices.test.js -t "UPC ingest"

Expected: FAIL — processPriceRows is not a function.

  • [ ] Step 3: Write the implementation

3a. Add the model require alongside the existing ones (near :29-31):

javascript
const SealedProductBarcode = require('../../models/SealedProductBarcode');

3b. Inside processPriceRows, add to the counter declarations (after let loggedColumns = false;):

javascript
    let barcodeOps = [];
    let totalUpcs = 0;
    let totalMalformedUpcs = 0;
    const seenUpcs = new Map();

    async function flushBarcodeOps() {
        if (barcodeOps.length === 0) return;
        await SealedProductBarcode.bulkWrite(barcodeOps, { ordered: false });
        barcodeOps = [];
    }

3c. Insert this block immediately after totalMatched++; and before the let finalPrice = null; line:

javascript
        // Ingest the manufacturer barcode BEFORE the `if (!finalPrice) continue`
        // below. A sealed product with no market price and no MSRP fallback
        // still has a real UPC on the box, and nothing else in the system ever
        // writes one — placing this after that line loses them silently.
        const rawUpc = String(row.extUPC ?? '').trim();
        const upc = extractUpc(row);

        if (rawUpc && !upc) {
            totalMalformedUpcs++;
        } else if (upc) {
            // Two uuids can legitimately share a UPC (reprints, repackaging) and
            // Shopify permits duplicate barcodes — surface it, don't block it.
            const previousUuid = seenUpcs.get(upc);
            if (previousUuid && previousUuid !== product.uuid) {
                log(`   ⚠️  Duplicate UPC ${upc}: ${previousUuid} and ${product.uuid}`);
            }
            seenUpcs.set(upc, product.uuid);

            totalUpcs++;
            barcodeOps.push({
                updateOne: {
                    filter: { game: product.game, uuid: product.uuid },
                    update: {
                        $set: {
                            barcode: upc,
                            source: 'tcgcsv',
                            tcgplayerProductId: Number(tcgplayerProductId)
                        }
                    },
                    upsert: true
                }
            });

            if (barcodeOps.length >= BATCH_SIZE) {
                await flushBarcodeOps();
            }
        }

3d. After the existing trailing if (batchBuffer.length > 0) { ... } flush, add:

javascript
    await flushBarcodeOps();

3e. Extend the summary log and return value:

javascript
    log(`   Barcodes recorded: ${totalUpcs}`);
    log(`   Malformed UPCs skipped: ${totalMalformedUpcs}`);

    return { totalProcessed, totalMatched, totalInserted, totalWithMSRP, totalSkipped, totalUpcs, totalMalformedUpcs };

3f. Export it for tests, alongside the existing named exports at the bottom:

javascript
module.exports.processPriceRows = processPriceRows;

Note on absence-is-not-deletion: the write is $set inside an upsert, never a delete. A row whose extUPC goes blank upstream leaves the stored value untouched, which is required — nothing would ever re-push it.

  • [ ] Step 4: Run the tests to verify they pass
bash
npx vitest run server/scripts/data-loading/updateSealedProductPrices.test.js

Expected: PASS — the 6 new ingest tests plus all pre-existing tests.

  • [ ] Step 5: Commit
bash
git add server/scripts/data-loading/updateSealedProductPrices.js server/scripts/data-loading/updateSealedProductPrices.test.js
git commit -m "Record real sealed-product barcodes from the daily TCGCSV import"

Task 4: resolveSealedBarcode stops generating

Files:

  • Modify: server/utils/barcodeGenerator.js:32-50
  • Test: server/utils/barcodeGenerator.test.js:70-99

Interfaces:

  • Consumes: nothing.

  • Produces: resolveSealedBarcode({ officialBarcode }): string|undefined. The gameCode and uuid parameters are removed. Returns undefined when no official barcode is supplied. generateBarcode is unchanged and still used by singles.

  • [ ] Step 1: Rewrite the failing tests

In server/utils/barcodeGenerator.test.js, replace the entire describe('resolveSealedBarcode', ...) block (lines 70-99) with:

javascript
    describe('resolveSealedBarcode', () => {
        it('returns the official barcode when one exists', () => {
            expect(resolveSealedBarcode({ officialBarcode: '195166278636' })).toBe('195166278636');
        });

        it('trims a padded official barcode', () => {
            expect(resolveSealedBarcode({ officialBarcode: '  195166278636  ' })).toBe('195166278636');
        });

        // A synthetic barcode is worse than none on sealed: it occupies
        // Shopify's barcode field, so scanning the real UPC printed on the box
        // silently misses. Omitting the field lets the merchant scan it into
        // Shopify directly, and nothing re-pushes a sealed barcode afterward.
        it('returns undefined when no official barcode is known', () => {
            expect(resolveSealedBarcode({})).toBeUndefined();
        });

        it('treats a blank official barcode as absent', () => {
            expect(resolveSealedBarcode({ officialBarcode: '   ' })).toBeUndefined();
        });

        it('never produces a generated LGSF value', () => {
            for (const input of [{}, { officialBarcode: '' }, { officialBarcode: null }]) {
                expect(resolveSealedBarcode(input)).not.toMatch(/^LGSF/);
            }
        });
    });
  • [ ] Step 2: Run the tests to verify they fail
bash
npx vitest run server/utils/barcodeGenerator.test.js

Expected: FAIL — resolveSealedBarcode({}) throws generateBarcode requires a gameCode.

  • [ ] Step 3: Write the implementation

Replace resolveSealedBarcode in server/utils/barcodeGenerator.js (lines 32-50) with:

javascript
/**
 * Resolve the barcode to ship for a sealed product.
 *
 * Sealed products carry a real manufacturer UPC printed on the packaging, so
 * there is deliberately NO generated fallback here: a synthetic LGSF… value
 * would occupy Shopify's barcode field and make a POS scan of the real code
 * silently miss. When the UPC is unknown we send no barcode at all, leaving
 * the merchant free to scan it into Shopify directly — nothing re-pushes a
 * sealed barcode afterward, so that edit persists.
 *
 * Singles are the opposite case and still use generateBarcode: a card has no
 * manufacturer barcode, so a generated one is the only option.
 *
 * @param {Object} args
 * @param {string} [args.officialBarcode] - Real UPC/EAN if known
 * @returns {string|undefined} The barcode to send, or undefined to omit the field
 */
function resolveSealedBarcode({ officialBarcode }) {
    const trimmed = typeof officialBarcode === 'string' ? officialBarcode.trim() : officialBarcode;
    if (trimmed) {
        return String(trimmed);
    }
    return undefined;
}
  • [ ] Step 4: Run the tests to verify they pass
bash
npx vitest run server/utils/barcodeGenerator.test.js

Expected: PASS — all tests including the 5 rewritten ones.

  • [ ] Step 5: Confirm no caller still passes the removed arguments
bash
grep -rn "resolveSealedBarcode" server/ --include=*.js

Expected: three sites — the definition and export in server/utils/barcodeGenerator.js, the require and call in server/routes/sealedProducts.js (still passing gameCode/uuid; Task 5 fixes it), and the test file. Extra arguments are harmless in JS, so this does not fail the build — Task 5 removes them.

  • [ ] Step 6: Commit
bash
git add server/utils/barcodeGenerator.js server/utils/barcodeGenerator.test.js
git commit -m "Stop putting synthetic barcodes on products that have a real one"

Task 5: buildSealedSyncPayload reads the reference table

Files:

  • Modify: server/routes/sealedProducts.js (require block near :17, buildSealedSyncPayload at :267-297, stale comment at :415-417, module.exports at :1140)
  • Test: server/routes/sealedProducts.buildSyncPayload.test.js (create)

Interfaces:

  • Consumes: SealedProductBarcode.getBarcode (Task 1), resolveSealedBarcode (Task 4).

  • Produces: module.exports.buildSealedSyncPayload = buildSealedSyncPayload — signature unchanged: (plugin, transformed, { existing } = {}) => Promise<{ metafields, barcode, msrp, weight, weightUnit }>. barcode is now string|undefined rather than always a string.

  • [ ] Step 1: Write the failing test

Create server/routes/sealedProducts.buildSyncPayload.test.js:

javascript
/**
 * Unit tests for buildSealedSyncPayload's barcode resolution.
 *
 * Sealed products get the real manufacturer UPC when TCGCSV supplied one and
 * no barcode at all otherwise — never a generated LGSF… value.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const { buildSealedSyncPayload } = require('./sealedProducts.js');
const SealedProductBarcode = require('../models/SealedProductBarcode.js');
const SealedProductMSRP = require('../models/SealedProductMSRP.js');

const plugin = { gameId: 'mtg', displayName: 'Magic: The Gathering', gameCode: 'MTG' };

const transformed = {
    uuid: 'a1b2c3d4-0000-0000-0000-000000000001',
    category: 'booster_box',
    setCode: 'DFT',
    metafields: { card_name: 'Aetherdrift - Play Booster Display' },
};

beforeEach(() => {
    vi.spyOn(SealedProductMSRP, 'getMSRP').mockResolvedValue(null);
});

afterEach(() => {
    vi.restoreAllMocks();
});

describe('buildSealedSyncPayload — barcode', () => {
    it('ships the real UPC when the reference table has one', async () => {
        vi.spyOn(SealedProductBarcode, 'getBarcode').mockResolvedValue('195166278636');

        const payload = await buildSealedSyncPayload(plugin, transformed);

        expect(payload.barcode).toBe('195166278636');
    });

    it('looks the barcode up scoped to the plugin game and product uuid', async () => {
        const getBarcode = vi.spyOn(SealedProductBarcode, 'getBarcode').mockResolvedValue(null);

        await buildSealedSyncPayload(plugin, transformed);

        expect(getBarcode).toHaveBeenCalledWith('mtg', transformed.uuid);
    });

    it('omits the barcode when no UPC is known', async () => {
        vi.spyOn(SealedProductBarcode, 'getBarcode').mockResolvedValue(null);

        const payload = await buildSealedSyncPayload(plugin, transformed);

        expect(payload.barcode).toBeUndefined();
    });

    // The whole point of the change: a synthetic code would occupy Shopify's
    // barcode field and make a scan of the real UPC on the box miss.
    it('never falls back to a generated LGSF value', async () => {
        vi.spyOn(SealedProductBarcode, 'getBarcode').mockResolvedValue(null);

        const payload = await buildSealedSyncPayload(plugin, transformed);

        expect(payload.barcode ?? '').not.toMatch(/^LGSF/);
    });

    it('prefers a value already stored on the product over the reference table', async () => {
        vi.spyOn(SealedProductBarcode, 'getBarcode').mockResolvedValue('195166278636');

        const payload = await buildSealedSyncPayload(plugin, transformed, {
            existing: { barcode: '850001234567' },
        });

        expect(payload.barcode).toBe('850001234567');
    });
});
  • [ ] Step 2: Run the test to verify it fails
bash
npx vitest run server/routes/sealedProducts.buildSyncPayload.test.js

Expected: FAIL — buildSealedSyncPayload is not a function (the module currently exports only the router).

  • [ ] Step 3: Write the implementation

3a. Add the model require alongside the existing ones (after :18):

javascript
const SealedProductBarcode = require('../models/SealedProductBarcode');

3b. In buildSealedSyncPayload, replace the const barcode = resolveSealedBarcode({...}) call at :286-290 with:

javascript
    // Real manufacturer UPC only — resolveSealedBarcode has no generated
    // fallback (see server/utils/barcodeGenerator.js). Unknown means the
    // barcode field is omitted from the Shopify payload entirely.
    const barcode = resolveSealedBarcode({
        officialBarcode: existing?.barcode
            || await SealedProductBarcode.getBarcode(plugin.gameId, transformed.uuid),
    });

3c. Fix the inaccurate comment at :415-417. Replace:

javascript
        // Only the resolved MSRP is real at create time. barcode/weight stay
        // unset (no catalog source) so the schema field means "official value",
        // not the generated barcode fallback — that is recomputed each sync.

with:

javascript
        // Only the resolved MSRP is real at create time. barcode/weight stay
        // unset here: the barcode's source of truth is the global
        // SealedProductBarcode reference table, resolved at sync time. Note
        // nothing re-pushes a sealed barcode after creation — createProduct is
        // the only writer — so a merchant's own edit in Shopify persists.

3d. Add the named export at the bottom of the file, after module.exports = router;:

javascript
// Named export for tests; the default export stays the mounted router.
module.exports.buildSealedSyncPayload = buildSealedSyncPayload;
  • [ ] Step 4: Run the test to verify it passes
bash
npx vitest run server/routes/sealedProducts.buildSyncPayload.test.js

Expected: PASS — 5 tests.

On the spec's "ships regardless of barcodesEnabled" test: the spec's Section 5 lists one, but it is not constructible as written — buildSealedSyncPayload receives no store and no sync config, so there is no toggle to vary and any such test would be vacuous. The guarantee is structural rather than test-enforced: grep -n barcodesEnabled server/routes/sealedProducts.js returns nothing, and applyBarcodePolicy in server/services/syncService.js:138 only ever touches buildVariantProducts output, which sealed does not use. Note this in the PR description, and treat a future diff that introduces barcodesEnabled into this file as the regression to catch in review — that is exactly what the c644924 cherry-pick would do.

  • [ ] Step 5: Verify the payload omits the field end-to-end

server/services/shopifyAPI.js:1837 already guards with ...(mongoProduct.variantbarcode ? { barcode: mongoProduct.variantbarcode } : {}), so an undefined barcode drops out of the GraphQL variables. Confirm that line is unchanged:

bash
grep -n "variantbarcode ? { barcode" server/services/shopifyAPI.js

Expected: one match at :1837. No edit needed.

  • [ ] Step 6: Commit
bash
git add server/routes/sealedProducts.js server/routes/sealedProducts.buildSyncPayload.test.js
git commit -m "Send the manufacturer barcode on sealed products when we know it"

Task 6: Full verification

Files: none modified — this task only runs and reads.

  • [ ] Step 1: Refresh the misleading sealed fixture

server/services/shopifyAPI.test.js:445 uses variantbarcode: 'LGSFMTGABCDEF012345' in its sealed fixture. That test exercises createProduct's pass-through and will still pass unchanged — but the fixture now documents behavior this PR removes, so a future reader would take it as evidence sealed products carry synthetic codes.

Update the fixture value at :445 and the matching assertion at :452 to a real UPC:

javascript
        variantbarcode: '195166278636',
javascript
        expect(updates.barcode).toBe('195166278636');

Leave the second test (omits barcode and measurement when not provided) exactly as-is — it already covers the unknown-UPC path this PR makes the common case.

  • [ ] Step 2: Full server test suite
bash
npm test

Expected: exit 0, zero failures.

  • [ ] Step 3: Lint
bash
npm run lint

Expected: exit 0, no new warnings versus pre-change output.

  • [ ] Step 4: Per-file coverage
bash
npm run test:coverage

Read the per-file table by hand for the four modified/created files. Every one must be ≥70% on statements, branches, functions and lines. The configured threshold gate is silently inoperative under Vitest 4 (flat threshold keys are ignored), so this command exits 0 regardless of coverage — the number on screen is the only signal.

  • [ ] Step 5: Confirm no identity defaults were introduced (§5.5)
bash
git diff main...HEAD -- server/ | grep -nE "\|\| 'mtg'|\?\? 'mtg'|= 'mtg'"

Expected: no output. IMPORTER_GAME is a pre-existing named constant, not a fallback, and is not re-introduced by this diff.

  • [ ] Step 6: Confirm no synthetic barcode can reach a sealed product
bash
grep -n "generateBarcode" server/routes/sealedProducts.js server/utils/barcodeGenerator.js

Expected: matches only inside server/utils/barcodeGenerator.js (the definition, and its use by the singles path). Zero matches in server/routes/sealedProducts.js.

  • [ ] Step 7: Commit the fixture refresh
bash
git add server/services/shopifyAPI.test.js
git commit -m "Use a real UPC in the sealed product test fixture"

PR description checklist

  • Parity (§5.1): mtg implemented; pokemon and riftbound exempt — neither has a sealed catalog source (server/plugins/mtg/index.js:417 is the only reader of SetModel.data.sealedProduct).
  • New persisted fields (§5.2): all five declared in SealedProductBarcode.js with a round-trip test that fails if any schema line is deleted.
  • Data literals (§5.4): extUPC column name and sample values cited from https://tcgcsv.com/tcgplayer/1/23874/ProductsAndPrices.csv.
  • No identity defaults (§5.5): verified by Task 6 Step 4.
  • No new aggregations, so §5.6 does not apply.
  • No Shopify calls added, so rule 6 is unaffected.
  • Note that syncConfig.barcodesEnabled intentionally does not gate sealed barcodes: a real UPC is the product's factual identity, not a synthetic label the merchant opted into. The toggle now governs generated barcodes on singles only.
  • Note that UPCs appear only after the next update-sealed-product-prices workflow run (5:00 UTC), and that already-synced sealed products are not repaired by this PR — that is PR 2.

Follow-on work (not this PR)

  • PR 2server/scripts/migrations/repairSyntheticSealedBarcodes.js. Must run only after this PR's first importer run has populated SealedProductBarcode; against an empty table it would clear every LGSF… code with nothing to refill them.
  • Open item from the spec — deleting the now-redundant SealedProduct.barcode field, contingent on confirming it is empty in production. This PR deliberately keeps the existing?.barcode precedence so the field stays harmless if that check has not run.
  • Cherry-picking c644924, whose sealed barcodesEnabled gate conflicts with this design and must be dropped on the way in.