feat: add product provider mappings
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-09 12:58:02 -04:00
parent bab4efc6ad
commit 4d3af37cd7
17 changed files with 253 additions and 3 deletions
@@ -0,0 +1,42 @@
import { and, eq } from 'drizzle-orm';
import { pricingProviderMappings, products } from '../../../infrastructure/database/schema.js';
export function createProviderMappingRepository(db) {
return {
async createForSeller(mapping, sellerId) {
const [ownedProduct] = await db.select({ id: products.id }).from(products)
.where(and(eq(products.id, mapping.productId), eq(products.sellerId, sellerId)));
if (!ownedProduct) return { status: 'NOT_FOUND' };
const [created] = await db.insert(pricingProviderMappings).values(mapping).returning();
return { status: 'CREATED', mapping: created };
},
async listForSeller(productId, sellerId) {
return db.select({ mapping: pricingProviderMappings }).from(pricingProviderMappings)
.innerJoin(products, eq(products.id, pricingProviderMappings.productId))
.where(and(
eq(pricingProviderMappings.productId, productId),
eq(products.sellerId, sellerId),
eq(pricingProviderMappings.status, 'ACTIVE'),
))
.then((rows) => rows.map(({ mapping }) => mapping));
},
async disableForSeller(id, sellerId) {
const [mapping] = await db.select().from(pricingProviderMappings)
.innerJoin(products, eq(products.id, pricingProviderMappings.productId))
.where(and(
eq(pricingProviderMappings.id, id),
eq(products.sellerId, sellerId),
eq(pricingProviderMappings.status, 'ACTIVE'),
));
if (!mapping) return { status: 'NOT_FOUND' };
const [updated] = await db.update(pricingProviderMappings)
.set({ status: 'DISABLED', disabledAt: new Date(), updatedAt: new Date() })
.where(eq(pricingProviderMappings.id, id))
.returning();
return { status: 'DISABLED', mapping: updated };
},
};
}
@@ -0,0 +1,47 @@
import { toAdminProviderMappingDto } from '@pokemon-inventory/contracts/provider-mapping';
import { createProviderMappingSchema } from '@pokemon-inventory/validation/provider-mapping';
import { createApplicationError } from '../../../shared/errors/application-error.js';
function validationError(reply, result) {
return reply.code(400).send({
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed.',
details: result.error.issues.map(({ message, path }) => ({ message, path })),
},
});
}
export async function registerAdminProviderMappingRoutes(app, options) {
const {
authenticateAdmin = async () => { throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401); },
authorizeUnsafeRequest,
sessionCookieName = 'pokemon_inventory_session',
providerMappingService,
} = options;
app.addHook('preHandler', async (request) => {
const admin = await authenticateAdmin(request);
if (!admin?.sellerId) throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401);
request.admin = admin;
});
app.get('/products/:id/provider-mappings', async (request) => ({
data: (await providerMappingService.list({ productId: request.params.id, sellerId: request.admin.sellerId })).map(toAdminProviderMappingDto),
}));
app.post('/products/:id/provider-mappings', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
const result = createProviderMappingSchema.safeParse(request.body);
if (!result.success) return validationError(reply, result);
const mapping = await providerMappingService.create({ ...result.data, productId: request.params.id, sellerId: request.admin.sellerId });
return reply.code(201).send({ data: toAdminProviderMappingDto(mapping) });
});
app.post('/provider-mappings/:id/disable', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
const mapping = await providerMappingService.disable({ id: request.params.id, sellerId: request.admin.sellerId });
return reply.code(200).send({ data: toAdminProviderMappingDto(mapping) });
});
}
@@ -0,0 +1,19 @@
import { createApplicationError } from '../../../shared/errors/application-error.js';
export function createProviderMappingService({ providerMappingRepository }) {
return {
async create({ sellerId, ...input }) {
const result = await providerMappingRepository.createForSeller(input, sellerId);
if (result.status === 'NOT_FOUND') throw createApplicationError('PRODUCT_NOT_FOUND', 'Product was not found.', 404);
return result.mapping;
},
async list({ productId, sellerId }) {
return providerMappingRepository.listForSeller(productId, sellerId);
},
async disable({ id, sellerId }) {
const result = await providerMappingRepository.disableForSeller(id, sellerId);
if (result.status === 'NOT_FOUND') throw createApplicationError('PROVIDER_MAPPING_NOT_FOUND', 'Provider mapping was not found.', 404);
return result.mapping;
},
};
}