Appearance
Sealed Barcode Availability 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: Tell a merchant, while they are adding a sealed product, whether it will get a scannable barcode โ and when it won't, why.
Architecture: SealedProductBarcode gains a batched getBarcodeStatuses that becomes the single implementation of the "usable only if exactly one product holds it" rule; getBarcode is refactored into a thin caller so the rule is never written twice. setDetails annotates each MTG sealed product with a three-state barcodeStatus, and a small SealedBarcodeNote component renders the explanation in both the grid and table views.
Tech Stack: Node.js (CommonJS), Mongoose, React 18, Tailwind 4, retroui, Vitest (ESM tests), @testing-library/react.
Spec: docs/superpowers/specs/2026-07-31-sealed-barcode-availability-design.md
Global Constraints โ
- Server code is CommonJS; test files are ESM. Tests are co-located as
x.test.js/x.test.jsx. - Never default an identity parameter โ
game,shop,uuid(CLAUDE.md ยง5.5). Absent โ throw or Zod reject. - Client components come from the retroui barrel (
import { Button } from '../components/retroui'), withcn()fromlib/utils.js. retroui variant vocabularies diverge between components โ Badge, Button and Alert have different variant maps, and a wrong name renders unstyled with no error. Check the target component's own variant map; never copy a variant name from a sibling component. - Aggregations put
$matchbefore$group; no$sort.sealedproductbarcodesis a plain collection, not time-series, so ยง5.6's carve-out does not apply. - Husky pre-commit runs ESLint. Fix lint; never
--no-verify. - Commit messages: one imperative sentence stating the merchant-visible outcome, sentence case, no trailing period.
Environment note โ read before Task 3 โ
client/node_modules is absent in a fresh worktree. Client tests will fail with a module-resolution error until you run:
bash
npm --prefix client install1
Do this once before Task 3. Server tests need no such step.
Per-game parity (CLAUDE.md ยง5.1) โ
- mtg โ implemented.
setDetails.js:262is the only branch that returns a non-emptysealedProducts. - pokemon โ exempt: its branch returns
sealedProducts: [](setDetails.js:378-379). Nothing to annotate. - riftbound โ exempt: same, no sealed listing in this endpoint.
getBarcodeStatuses is game-scoped, so whenever sealed support lands for either game the annotation comes with it. State this in the PR.
File Structure โ
| File | Responsibility |
|---|---|
server/models/SealedProductBarcode.js | Modify. Add getBarcodeStatuses; refactor getBarcode to call it. |
server/models/SealedProductBarcode.test.js | Modify. Cover the new static; rewrite the getBarcode tests against the new internals so they still guard sync behavior. |
server/routes/setDetails.js | Modify. Add + export annotateSealedBarcodeStatus; wire it into the MTG sealed branch. |
server/routes/setDetails.sealedBarcode.test.js | Create. Unit-tests the pure annotator โ no Mongo, no router introspection. |
client/src/components/SealedBarcodeNote.jsx | Create. Renders the three states. Used by both sealed views. |
client/src/components/SealedBarcodeNote.test.jsx | Create. Three render states. |
client/src/components/CatalogSetBrowser.jsx | Modify. Render the note in the grid card and the table row. |
Why a separate SealedBarcodeNote component: it is needed in two places (grid card and table row), and CatalogSetBrowser.jsx is already over 1,100 lines with no test file. Extracting a small component gives a testable unit without standing up a test harness for the whole browser, and avoids duplicating the copy in two render paths (ยง5.8).
Why the annotator is a separate exported function: server/routes/api.catalogSealed.test.js tests routes by introspecting router.stack against a real local MongoDB. That is heavy and needs docker compose up -d. A pure annotator can be unit-tested with zero I/O, so the new logic gets fast, dependency-free coverage.
Task 1: getBarcodeStatuses, with getBarcode as a thin caller โ
Files:
- Modify:
server/models/SealedProductBarcode.js - Test:
server/models/SealedProductBarcode.test.js
Interfaces:
Consumes: nothing.
Produces:
getBarcodeStatuses(game: string, uuids: string[]): Promise<Map<string, {status: 'available'|'shared', barcode: string, sharedBy: number}>>โ a uuid absent from the Map has no row (the'unpublished'state). Throws ifgameis falsy. Returns an empty Map without querying whenuuidsis empty.getBarcode(game, uuid)โ unchanged signature and behavior.
[ ] Step 1: Rewrite the getBarcode tests against the new internals
In server/models/SealedProductBarcode.test.js, replace the whole describe('SealedProductBarcode.getBarcode', ...) block with the version below. It mocks find and aggregate (the real I/O after the refactor) rather than findOne/countDocuments, so it still exercises the full path through getBarcodeStatuses and remains a genuine guard on sync behavior.
javascript
describe('SealedProductBarcode.getBarcode', () => {
/** Mock a lookup that finds `barcode`, held by `sharedCount` products. */
function mockLookup(barcode, sharedCount = 1) {
vi.spyOn(SealedProductBarcode, 'find').mockReturnValue({
select: () => ({
lean: () => Promise.resolve(barcode === null ? [] : [{ uuid: 'uuid-1', barcode }]),
}),
});
vi.spyOn(SealedProductBarcode, 'aggregate').mockResolvedValue(
barcode === null ? [] : [{ _id: barcode, n: sharedCount }]
);
}
it('returns the stored barcode when it identifies exactly one product', async () => {
mockLookup('195166278636', 1);
await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBe('195166278636');
});
it('returns null when no row exists', async () => {
mockLookup(null);
await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBeNull();
});
// Regression guard on the Task 1 refactor: suppression must survive
// getBarcode being reimplemented on top of getBarcodeStatuses.
it('suppresses a barcode shared by more than one product', async () => {
mockLookup('195166121468', 10);
await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBeNull();
});
it('suppresses a barcode shared by exactly two products', async () => {
mockLookup('630509997053', 2);
await expect(SealedProductBarcode.getBarcode('mtg', 'uuid-1')).resolves.toBeNull();
});
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/);
});
});
describe('SealedProductBarcode.getBarcodeStatuses', () => {
function mockRows(rows, counts) {
vi.spyOn(SealedProductBarcode, 'find').mockReturnValue({
select: () => ({ lean: () => Promise.resolve(rows) }),
});
vi.spyOn(SealedProductBarcode, 'aggregate').mockResolvedValue(counts);
}
it('reports a solely-held barcode as available', async () => {
mockRows([{ uuid: 'u1', barcode: '195166326399' }], [{ _id: '195166326399', n: 1 }]);
const statuses = await SealedProductBarcode.getBarcodeStatuses('mtg', ['u1']);
expect(statuses.get('u1')).toEqual({ status: 'available', barcode: '195166326399', sharedBy: 1 });
});
it('reports a multi-holder barcode as shared, with the count', async () => {
mockRows([{ uuid: 'u1', barcode: '195166313085' }], [{ _id: '195166313085', n: 4 }]);
const statuses = await SealedProductBarcode.getBarcodeStatuses('mtg', ['u1']);
expect(statuses.get('u1')).toEqual({ status: 'shared', barcode: '195166313085', sharedBy: 4 });
});
// Absence, not a sentinel value: callers must not confuse "no data" with
// "bad data". A uuid with no row simply is not in the Map.
it('omits uuids that have no row', async () => {
mockRows([{ uuid: 'u1', barcode: '195166326399' }], [{ _id: '195166326399', n: 1 }]);
const statuses = await SealedProductBarcode.getBarcodeStatuses('mtg', ['u1', 'u2']);
expect(statuses.has('u2')).toBe(false);
expect(statuses.size).toBe(1);
});
it('classifies a mixed batch in one call', async () => {
mockRows(
[{ uuid: 'u1', barcode: 'AAA' }, { uuid: 'u2', barcode: 'BBB' }],
[{ _id: 'AAA', n: 1 }, { _id: 'BBB', n: 3 }]
);
const statuses = await SealedProductBarcode.getBarcodeStatuses('mtg', ['u1', 'u2']);
expect(statuses.get('u1').status).toBe('available');
expect(statuses.get('u2').status).toBe('shared');
expect(statuses.get('u2').sharedBy).toBe(3);
});
it('returns an empty Map without querying when given no uuids', async () => {
const find = vi.spyOn(SealedProductBarcode, 'find');
const statuses = await SealedProductBarcode.getBarcodeStatuses('mtg', []);
expect(statuses.size).toBe(0);
expect(find).not.toHaveBeenCalled();
});
it('throws when game is missing', async () => {
await expect(SealedProductBarcode.getBarcodeStatuses(null, ['u1'])).rejects.toThrow(/game is required/);
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
- [ ] Step 2: Run to verify they fail
bash
npx vitest run server/models/SealedProductBarcode.test.js1
Expected: FAIL โ SealedProductBarcode.getBarcodeStatuses is not a function, plus the rewritten getBarcode tests failing because the old implementation calls findOne, which is no longer mocked.
- [ ] Step 3: Write the implementation
In server/models/SealedProductBarcode.js, replace the entire existing getBarcode static with the two statics below. Keep the existing block comment above it โ it documents the suppression rule and stays accurate; move it so it sits above getBarcodeStatuses.
javascript
/**
* Resolve barcode status for many sealed products at once.
*
* This is the single implementation of the suppression rule described above;
* getBarcode is a thin caller so the rule is never written twice (ยง5.8).
*
* A uuid absent from the returned Map has no row at all โ the "unpublished"
* state. Absence rather than a sentinel entry, so a caller cannot confuse
* "we have no data" with "we have data and it is unusable".
*
* Two queries regardless of batch size: one find for the rows, one aggregation
* counting holders of each distinct barcode found. Both are served by the
* existing {game, uuid} and {game, barcode} indexes.
*
* @param {string} game - Game identifier, never defaulted (CLAUDE.md ยง5.5)
* @param {string[]} uuids - Sealed product catalog uuids
* @returns {Promise<Map<string, {status: string, barcode: string, sharedBy: number}>>}
*/
sealedProductBarcodeSchema.statics.getBarcodeStatuses = async function(game, uuids) {
if (!game) {
throw new Error('game is required to resolve a sealed barcode');
}
const wanted = Array.isArray(uuids) ? uuids.filter(Boolean) : [];
const statuses = new Map();
if (wanted.length === 0) return statuses;
const rows = await this.find({ game, uuid: { $in: wanted } }).select('uuid barcode').lean();
if (rows.length === 0) return statuses;
const barcodes = [...new Set(rows.map(r => r.barcode))];
const counts = await this.aggregate([
{ $match: { game, barcode: { $in: barcodes } } },
{ $group: { _id: '$barcode', n: { $sum: 1 } } }
]);
const holders = new Map(counts.map(c => [c._id, c.n]));
for (const row of rows) {
const sharedBy = holders.get(row.barcode) || 1;
statuses.set(row.uuid, {
status: sharedBy > 1 ? 'shared' : 'available',
barcode: row.barcode,
sharedBy
});
}
return statuses;
};
/**
* 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 unknown or ambiguous
*/
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 statuses = await this.getBarcodeStatuses(game, [uuid]);
const entry = statuses.get(uuid);
return entry && entry.status === 'available' ? entry.barcode : null;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
- [ ] Step 4: Run to verify they pass
bash
npx vitest run server/models/SealedProductBarcode.test.js1
Expected: PASS โ 7 getBarcodeStatuses tests + 6 getBarcode tests + the pre-existing schema tests.
- [ ] Step 5: Verify the sync path still works end to end
buildSealedSyncPayload calls getBarcode. Confirm its tests still pass unchanged โ they must, since the signature and behavior did not change:
bash
npx vitest run server/routes/sealedProducts.buildSyncPayload.test.js1
Expected: PASS, with no edits to that file.
- [ ] Step 6: Commit
bash
git add server/models/SealedProductBarcode.js server/models/SealedProductBarcode.test.js
git commit -m "Report why a sealed barcode is unavailable, not just that it is"1
2
2
Task 2: Annotate sealed products in setDetails โ
Files:
- Modify:
server/routes/setDetails.js(MTG sealed branch at:259-270; export at the file end) - Test:
server/routes/setDetails.sealedBarcode.test.js(create)
Interfaces:
- Consumes:
SealedProductBarcode.getBarcodeStatuses(game, uuids)from Task 1. - Produces:
annotateSealedBarcodeStatus(products, statuses)exported fromsetDetails.js, returning a new array where each product gainsbarcodeStatus: 'available'|'shared'|'unpublished'and, only when shared,barcodeSharedBy: number.
Note the existing export is module.exports = { handleGetSetDetails }; โ add to that object, do not replace it.
- [ ] Step 1: Write the failing test
Create server/routes/setDetails.sealedBarcode.test.js:
javascript
/**
* Unit tests for the sealed barcode annotation in setDetails.
*
* Deliberately tests the pure annotator rather than the route: the route tests
* in api.catalogSealed.test.js introspect router.stack against a real local
* MongoDB, which is slow and needs docker compose. The mapping logic needs
* neither.
*/
import { describe, it, expect } from 'vitest';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { annotateSealedBarcodeStatus } = require('./setDetails.js');
const products = [
{ uuid: 'u1', name: 'The Hobbit Play Booster Box' },
{ uuid: 'u2', name: 'Wakanda Forever (CE)' },
{ uuid: 'u3', name: 'Some Bundle' },
];
describe('annotateSealedBarcodeStatus', () => {
it('marks a solely-held barcode available', () => {
const statuses = new Map([['u1', { status: 'available', barcode: '195166326399', sharedBy: 1 }]]);
const [first] = annotateSealedBarcodeStatus([products[0]], statuses);
expect(first.barcodeStatus).toBe('available');
expect(first.barcodeSharedBy).toBeUndefined();
});
it('marks a shared barcode and carries the count', () => {
const statuses = new Map([['u2', { status: 'shared', barcode: '195166313085', sharedBy: 4 }]]);
const [first] = annotateSealedBarcodeStatus([products[1]], statuses);
expect(first.barcodeStatus).toBe('shared');
expect(first.barcodeSharedBy).toBe(4);
});
it('marks a uuid absent from the Map as unpublished', () => {
const [first] = annotateSealedBarcodeStatus([products[2]], new Map());
expect(first.barcodeStatus).toBe('unpublished');
expect(first.barcodeSharedBy).toBeUndefined();
});
// The shared UPC is deliberately not exposed: showing it invites a merchant
// to paste it into Shopify, giving several products one barcode and
// breaking the POS lookup this whole feature protects.
it('never leaks the barcode value itself', () => {
const statuses = new Map([['u2', { status: 'shared', barcode: '195166313085', sharedBy: 4 }]]);
const [first] = annotateSealedBarcodeStatus([products[1]], statuses);
expect(JSON.stringify(first)).not.toContain('195166313085');
});
it('preserves the original product fields', () => {
const statuses = new Map([['u1', { status: 'available', barcode: 'X', sharedBy: 1 }]]);
const [first] = annotateSealedBarcodeStatus([products[0]], statuses);
expect(first.name).toBe('The Hobbit Play Booster Box');
expect(first.uuid).toBe('u1');
});
it('does not mutate its input (ยง5.11)', () => {
const input = [{ uuid: 'u1', name: 'x' }];
annotateSealedBarcodeStatus(input, new Map());
expect(input[0].barcodeStatus).toBeUndefined();
});
it('handles an empty product list', () => {
expect(annotateSealedBarcodeStatus([], new Map())).toEqual([]);
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
- [ ] Step 2: Run to verify it fails
bash
npx vitest run server/routes/setDetails.sealedBarcode.test.js1
Expected: FAIL โ annotateSealedBarcodeStatus is not a function.
- [ ] Step 3: Write the annotator
Add to server/routes/setDetails.js, above handleGetSetDetails:
javascript
/**
* Attach barcode availability to sealed products for the catalog UI.
*
* Three states, because "never will have one" and "might get one later" call
* for different merchant action โ relabel now, versus check back. The barcode
* VALUE is deliberately never included: surfacing a shared UPC invites a
* merchant to enter it manually, which would give several products the same
* barcode and break the POS lookup the suppression exists to protect.
*
* @param {Array<object>} products - Mapped sealed products
* @param {Map<string, {status: string, sharedBy: number}>} statuses - from
* SealedProductBarcode.getBarcodeStatuses; a missing uuid means no row
* @returns {Array<object>} New array; inputs are not mutated (ยง5.11)
*/
function annotateSealedBarcodeStatus(products, statuses) {
return products.map(product => {
const entry = statuses.get(product.uuid);
if (!entry) {
return { ...product, barcodeStatus: 'unpublished' };
}
if (entry.status === 'shared') {
return { ...product, barcodeStatus: 'shared', barcodeSharedBy: entry.sharedBy };
}
return { ...product, barcodeStatus: 'available' };
});
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Change the export at the end of the file from:
javascript
module.exports = { handleGetSetDetails };1
to:
javascript
module.exports = { handleGetSetDetails, annotateSealedBarcodeStatus };1
- [ ] Step 4: Run to verify it passes
bash
npx vitest run server/routes/setDetails.sealedBarcode.test.js1
Expected: PASS โ 7 tests.
- [ ] Step 5: Wire it into the MTG sealed branch
Add the model require alongside the others near the top of server/routes/setDetails.js:
javascript
const SealedProductBarcode = require('../models/SealedProductBarcode');1
Replace the sealed block at :259-270:
javascript
// Include sealed products (usually small list, no pagination needed)
if (include.includes('sealed')) {
const sealedProducts = data.sealedProduct || [];
response.sealedProducts = sealedProducts.map(product => ({
uuid: product.uuid,
name: product.name,
category: product.category,
subtype: product.subtype,
releaseDate: product.releaseDate,
purchaseUrls: product.purchaseUrls,
identifiers: product.identifiers
}));
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
with:
javascript
// Include sealed products (usually small list, no pagination needed)
if (include.includes('sealed')) {
const sealedProducts = data.sealedProduct || [];
const mapped = sealedProducts.map(product => ({
uuid: product.uuid,
name: product.name,
category: product.category,
subtype: product.subtype,
releaseDate: product.releaseDate,
purchaseUrls: product.purchaseUrls,
identifiers: product.identifiers
}));
// One batched lookup for the whole page โ a set can list ~30
// sealed products, and per-product resolution would be ~60
// queries per page load.
const barcodeStatuses = await SealedProductBarcode.getBarcodeStatuses(
game,
mapped.map(p => p.uuid)
);
response.sealedProducts = annotateSealedBarcodeStatus(mapped, barcodeStatuses);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
game is already in scope in this branch โ it is the value the surrounding if (game === 'mtg') tested. Do not introduce a literal.
- [ ] Step 6: Verify nothing regressed
bash
npx vitest run server/routes/1
Expected: PASS. If api.catalogSealed.test.js errors on a MongoDB connection, that is the pre-existing local-Mongo dependency, not this change โ start it with docker compose up -d and re-run to confirm.
- [ ] Step 7: Commit
bash
git add server/routes/setDetails.js server/routes/setDetails.sealedBarcode.test.js
git commit -m "Tell the catalog whether a sealed product has a scannable barcode"1
2
2
Task 3: SealedBarcodeNote component โ
Files:
- Create:
client/src/components/SealedBarcodeNote.jsx - Test:
client/src/components/SealedBarcodeNote.test.jsx
Interfaces:
Consumes: the
barcodeStatus/barcodeSharedByfields produced in Task 2.Produces: default-exported
SealedBarcodeNote({ status, sharedBy }). Rendersnullfor'available',undefined, or any unrecognised value.[ ] Step 1: Install client dependencies
A fresh worktree has no client/node_modules, and every client test fails without it.
bash
npm --prefix client install1
- [ ] Step 2: Write the failing test
Create client/src/components/SealedBarcodeNote.test.jsx:
jsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import SealedBarcodeNote from './SealedBarcodeNote';
describe('SealedBarcodeNote', () => {
// The happy path is the majority of rows โ a badge on every one would be
// noise, so silence is the correct render.
it('renders nothing when a barcode is available', () => {
const { container } = render(<SealedBarcodeNote status="available" />);
expect(container).toBeEmptyDOMElement();
});
it('renders nothing when status is missing', () => {
const { container } = render(<SealedBarcodeNote />);
expect(container).toBeEmptyDOMElement();
});
it('explains a shared manufacturer code and how many products share it', () => {
render(<SealedBarcodeNote status="shared" sharedBy={4} />);
expect(screen.getByText(/No scannable barcode/i)).toBeInTheDocument();
expect(screen.getByText(/all 4 products in this line/i)).toBeInTheDocument();
});
it('says a barcode may arrive later when none is published', () => {
render(<SealedBarcodeNote status="unpublished" />);
expect(screen.getByText(/No barcode published yet/i)).toBeInTheDocument();
expect(screen.getByText(/future data update/i)).toBeInTheDocument();
});
// The two messages must stay distinguishable: one means "print your own
// labels", the other means "check back".
it('uses different wording for shared versus unpublished', () => {
const shared = render(<SealedBarcodeNote status="shared" sharedBy={2} />).container.textContent;
const unpublished = render(<SealedBarcodeNote status="unpublished" />).container.textContent;
expect(shared).not.toBe(unpublished);
});
it('never displays a raw barcode value', () => {
const { container } = render(<SealedBarcodeNote status="shared" sharedBy={4} />);
expect(container.textContent).not.toMatch(/[0-9]{12,13}/);
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- [ ] Step 3: Run to verify it fails
bash
npm run test:client -- SealedBarcodeNote1
Expected: FAIL โ cannot resolve ./SealedBarcodeNote.
- [ ] Step 4: Write the component
Create client/src/components/SealedBarcodeNote.jsx:
jsx
import { Info } from 'lucide-react';
/**
* Explains why a sealed product will not get a scannable barcode.
*
* Renders nothing in the common case. Shared and unpublished are kept distinct
* because they call for different merchant action: a shared manufacturer code
* is permanent (print your own labels), while an unpublished one may arrive in
* a later data update (check back).
*
* The barcode value is never shown โ see server/routes/setDetails.js.
*/
export default function SealedBarcodeNote({ status, sharedBy }) {
if (status !== 'shared' && status !== 'unpublished') return null;
const count = Number(sharedBy) || 2;
const message = status === 'shared'
? `No scannable barcode โ the manufacturer publishes one code for all ${count} product${count === 1 ? '' : 's'} in this line.`
: 'No barcode published yet โ may appear in a future data update.';
return (
<p className="flex items-start gap-1 text-xs text-muted-foreground">
<Info className="h-3 w-3 mt-0.5 shrink-0" aria-hidden="true" />
<span>{message}</span>
</p>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
No retroui component is used here, so the diverging-variant hazard does not apply. lucide-react is already a client dependency โ CatalogSetBrowser.jsx:6 imports TrendingUp and Package from it.
- [ ] Step 5: Run to verify it passes
bash
npm run test:client -- SealedBarcodeNote1
Expected: PASS โ 6 tests.
- [ ] Step 6: Commit
bash
git add client/src/components/SealedBarcodeNote.jsx client/src/components/SealedBarcodeNote.test.jsx
git commit -m "Explain in plain words why a sealed product has no barcode"1
2
2
Task 4: Render the note in both sealed views โ
Files:
- Modify:
client/src/components/CatalogSetBrowser.jsx(import; table row at:1082-1104;SealedProductCardinfo section)
Interfaces:
- Consumes:
SealedBarcodeNote(Task 3) and thebarcodeStatus/barcodeSharedByfields (Task 2). - Produces: no new exports.
Both views must show it. A merchant who prefers table view would otherwise never see the explanation.
- [ ] Step 1: Add the import
At the top of client/src/components/CatalogSetBrowser.jsx, alongside the other local component imports:
javascript
import SealedBarcodeNote from './SealedBarcodeNote';1
- [ ] Step 2: Render it in the grid card
In SealedProductCard, inside the {/* Product Info */} block, immediately after the release-date paragraph:
jsx
<p className="text-xs text-muted-foreground">
{formatShortDate(product.releaseDate)}
</p>
<SealedBarcodeNote status={product.barcodeStatus} sharedBy={product.barcodeSharedBy} />1
2
3
4
5
2
3
4
5
- [ ] Step 3: Render it in the table row
In the table body, replace the name cell:
jsx
<td className="px-2 sm:px-4 py-2 sm:py-3">{product.name}</td>1
with:
jsx
<td className="px-2 sm:px-4 py-2 sm:py-3">
<div className="space-y-1">
<span>{product.name}</span>
<SealedBarcodeNote status={product.barcodeStatus} sharedBy={product.barcodeSharedBy} />
</div>
</td>1
2
3
4
5
6
2
3
4
5
6
It goes in the name cell rather than a new column so the table keeps its current column count and responsive hidden sm:table-cell breakpoints.
- [ ] Step 4: Verify the client builds and tests pass
bash
npm run test:client1
Expected: PASS, no regressions.
bash
npm run build1
Expected: exit 0.
- [ ] Step 5: Commit
bash
git add client/src/components/CatalogSetBrowser.jsx
git commit -m "Show barcode availability on sealed products in both catalog views"1
2
2
Task 5: Full verification โ
- [ ] Step 1: Server suite
bash
npm test1
Expected: exit 0, zero failures.
- [ ] Step 2: Client suite
bash
npm run test:client1
Expected: exit 0, zero failures.
- [ ] Step 3: Lint
bash
npm run lint1
Expected: exit 0. Compare the warning count against the pre-change baseline; it must be unchanged. To confirm the diff itself is clean:
bash
npx eslint server/models/SealedProductBarcode.js server/routes/setDetails.js client/src/components/SealedBarcodeNote.jsx client/src/components/CatalogSetBrowser.jsx -f json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const r=JSON.parse(s);let w=0,e=0;for(const f of r){w+=f.warningCount;e+=f.errorCount;}console.log('errors:',e,'warnings:',w);})"1
Expected: errors: 0 warnings: 0.
- [ ] Step 4: Build
bash
npm run build1
Expected: exit 0.
- [ ] Step 5: Per-file coverage
bash
npm run test:coverage1
Read the per-file table by hand for SealedProductBarcode.js and setDetails.js; each must be โฅ70% on statements, branches, functions and lines. The configured thresholds are a global ratchet set just under measured totals โ they fail on regression but do not enforce the per-file bar, so the printed numbers are the only signal.
- [ ] Step 6: Confirm the barcode value never reaches the client
bash
git diff main... -- server/routes/setDetails.js | grep -n "barcode:" || echo "clean โ no barcode value in the response shape"1
Expected: no barcode: field added to the response. Only barcodeStatus and barcodeSharedBy.
- [ ] Step 7: Confirm no identity defaults (ยง5.5)
bash
git diff main... -- server/ | grep -nE "\|\| 'mtg'|\?\? 'mtg'|= 'mtg'" || echo "NONE (clean)"1
Expected: no output.
PR description checklist โ
- Parity (ยง5.1): mtg implemented; pokemon and riftbound exempt โ both return
sealedProducts: []from this endpoint, so there is nothing to annotate.getBarcodeStatusesis game-scoped for when that changes. getBarcodebehavior is unchanged; the refactor is guarded by tests that mock the real I/O (find/aggregate) rather than the new method, so they still exercise the full path.- No new persisted fields, so ยง5.2 does not apply. No new endpoint, so no Zod schema is needed โ the change is additive to an existing response.
- The shared UPC value is deliberately not returned to the client, asserted by a test.
- Server and client ship together (ยง5.9); the client renders nothing when
barcodeStatusis absent, so deploy ordering does not matter.
Follow-on work (not this PR) โ
- A Shopify admin block on the product page, if merchants still ask after seeing this at add time.
- A per-SKU barcode source for the suppressed 36% โ GCI-DB was investigated and needs a commercial conversation, not an integration.
