refactor: make inventory images the publication source

Remove the temporary listing-level image count from the schema, validation, DTO, and draft contract. Publication now requires the persisted inventory image repository count. Add migration 0009 and update listing tests and implementation status. Verification: npm test passed 62 tests and integration passed 14 tests. No build script is configured in this repository.
This commit is contained in:
2026-09-08 17:34:59 -04:00
parent c728016dec
commit 31f11f6d56
12 changed files with 28 additions and 35 deletions
@@ -275,7 +275,6 @@ export const listings = pgTable('listings', {
.notNull()
.references(() => conditions.id),
askingPriceMinor: integer('asking_price_minor').notNull(),
imageCount: integer('image_count').notNull(),
status: listingStatus('status').default('DRAFT').notNull(),
notes: text('notes'),
...timestamps,
@@ -41,7 +41,6 @@ export function createListing({
categoryId,
conditionId,
askingPriceMinor,
imageCount,
notes,
}) {
return {
@@ -50,7 +49,6 @@ export function createListing({
categoryId: requiredText(categoryId, 'Category ID'),
conditionId: requiredText(conditionId, 'Condition ID'),
askingPriceMinor: requirePositiveMinorUnit(askingPriceMinor, 'Asking price'),
imageCount: requirePositiveInteger(imageCount, 'Image count'),
status: LISTING_STATUS.DRAFT,
...(typeof notes === 'string' && notes.trim().length > 0
? { notes: notes.trim() }
@@ -122,10 +120,3 @@ function requirePositiveMinorUnit(value, fieldName) {
return value;
}
function requirePositiveInteger(value, fieldName) {
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${fieldName} must be a positive integer.`);
}
return value;
}
@@ -8,7 +8,6 @@ import { createApplicationError } from '../../../shared/errors/application-error
export function createListingService({ listingRepository, inventoryImageRepository }) {
return {
async create(input) {
canPublishListing(input);
const listing = createListing(input);
return listingRepository.create(listing);
},
@@ -23,9 +22,7 @@ export function createListingService({ listingRepository, inventoryImageReposito
const status = transitionListingStatus(listing.status, nextStatus);
if (status === 'AVAILABLE') {
const imageCount = inventoryImageRepository
? await inventoryImageRepository.countByInventoryItemId(listing.inventoryItemId)
: listing.imageCount;
const imageCount = await inventoryImageRepository.countByInventoryItemId(listing.inventoryItemId);
canPublishListing({ ...listing, imageCount });
}
@@ -26,7 +26,6 @@ test('creates a draft listing only after the listing publish requirements are me
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
notes: ' Near mint ',
});
@@ -37,7 +36,6 @@ test('creates a draft listing only after the listing publish requirements are me
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
notes: 'Near mint',
});
@@ -50,9 +48,8 @@ test('creates a draft listing only after the listing publish requirements are me
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 0,
imageCount: 2,
}),
(error) => error.code === 'LISTING_REQUIREMENTS_NOT_MET',
/Asking price must be a positive integer minor unit/,
);
assert.equal(created.length, 1);
@@ -70,7 +67,6 @@ test('publishes an owned draft only when its persisted requirements are met', as
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
};
},
@@ -79,6 +75,11 @@ test('publishes an owned draft only when its persisted requirements are met', as
return { id, status };
},
},
inventoryImageRepository: {
async countByInventoryItemId() {
return 2;
},
},
});
assert.deepEqual(
@@ -119,7 +120,6 @@ test('does not publish a listing for sold or archived inventory', async () => {
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
inventoryStatus,
status: 'DRAFT',
};
@@ -128,6 +128,11 @@ test('does not publish a listing for sold or archived inventory', async () => {
throw new Error('A blocked listing must not be updated.');
},
},
inventoryImageRepository: {
async countByInventoryItemId() {
return 2;
},
},
});
await assert.rejects(
-2
View File
@@ -15,7 +15,6 @@ test('creates a draft listing with the required public offer fields', () => {
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
notes: ' Near mint ',
});
@@ -25,7 +24,6 @@ test('creates a draft listing with the required public offer fields', () => {
categoryId: 'category-id',
conditionId: 'condition-id',
askingPriceMinor: 2499,
imageCount: 2,
status: LISTING_STATUS.DRAFT,
notes: 'Near mint',
});
+5 -5
View File
@@ -26,7 +26,6 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest, listing } =
categoryId: 'category-1',
conditionId: 'condition-1',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
};
},
@@ -38,7 +37,6 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest, listing } =
categoryId: 'category-1',
conditionId: 'condition-1',
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
}];
},
@@ -51,13 +49,17 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest, listing } =
categoryId: 'category-1',
conditionId: 'condition-1',
askingPriceMinor: 2499,
imageCount: 2,
}),
id,
status,
};
},
},
inventoryImageRepository: {
async countByInventoryItemId() {
return 2;
},
},
}),
inventoryImageService: {
async create(input) {
@@ -90,7 +92,6 @@ test('creates a draft listing for the authenticated seller', async (testContext)
categoryId: 'ab8150c7-96c1-4b55-8d69-7d0b77d48958',
conditionId: '21eb218b-7a95-4f80-a9e3-e306f7ca1f2e',
askingPriceMinor: 2499,
imageCount: 2,
notes: ' Near mint ',
},
});
@@ -122,7 +123,6 @@ test('rejects a listing write without CSRF validation', async (testContext) => {
categoryId: 'ab8150c7-96c1-4b55-8d69-7d0b77d48958',
conditionId: '21eb218b-7a95-4f80-a9e3-e306f7ca1f2e',
askingPriceMinor: 2499,
imageCount: 2,
},
});
@@ -97,7 +97,6 @@ test('persists a draft listing for a physical inventory item', async () => {
categoryId: category.id,
conditionId: condition.id,
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
notes: 'Near mint',
});
@@ -117,7 +116,6 @@ test('lists the most recent draft entries for an inventory item', async () => {
categoryId: category.id,
conditionId: condition.id,
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
});
@@ -167,7 +165,6 @@ test('reads and updates listings only through the owning seller scope', async ()
categoryId: category.id,
conditionId: condition.id,
askingPriceMinor: 2499,
imageCount: 2,
status: 'DRAFT',
});