This commit is contained in:
@@ -19,6 +19,7 @@ import { registerAdminSaleRoutes } from '../modules/sales/routes/admin-sale-rout
|
||||
import { registerAdminAcquisitionRoutes } from '../modules/acquisitions/routes/admin-acquisition-routes.js';
|
||||
import { registerAdminDashboardRoutes } from '../modules/dashboard/routes/admin-dashboard-routes.js';
|
||||
import { registerAdminPricingRoutes } from '../modules/pricing/routes/admin-pricing-routes.js';
|
||||
import { registerAdminProviderMappingRoutes } from '../modules/pricing/routes/admin-provider-mapping-routes.js';
|
||||
|
||||
export function buildApp(dependencies) {
|
||||
const app = Fastify();
|
||||
@@ -82,6 +83,10 @@ export function buildApp(dependencies) {
|
||||
...dependencies,
|
||||
prefix: '/api/v1/admin',
|
||||
});
|
||||
app.register(registerAdminProviderMappingRoutes, {
|
||||
...dependencies,
|
||||
prefix: '/api/v1/admin',
|
||||
});
|
||||
app.register(registerPublicSearchRoutes, {
|
||||
...dependencies,
|
||||
prefix: '/api/v1',
|
||||
|
||||
@@ -45,6 +45,8 @@ import { createDashboardService } from '../modules/dashboard/services/dashboard-
|
||||
import { createPricingRepository } from '../modules/pricing/repositories/pricing-repository.js';
|
||||
import { createPricingService } from '../modules/pricing/services/pricing-service.js';
|
||||
import { createPricingRefreshJobRepository } from '../modules/pricing/repositories/pricing-refresh-job-repository.js';
|
||||
import { createProviderMappingRepository } from '../modules/pricing/repositories/provider-mapping-repository.js';
|
||||
import { createProviderMappingService } from '../modules/pricing/services/provider-mapping-service.js';
|
||||
|
||||
function parsePort(value) {
|
||||
const port = Number.parseInt(value ?? '3000', 10);
|
||||
@@ -72,6 +74,7 @@ async function startServer() {
|
||||
const acquisitionRepository = createAcquisitionRepository(database.db);
|
||||
const pricingRepository = createPricingRepository(database.db);
|
||||
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
|
||||
const providerMappingRepository = createProviderMappingRepository(database.db);
|
||||
const userRepository = createUserRepository(database.db);
|
||||
const sessionRepository = createSessionRepository(database.db);
|
||||
const passwordResetRepository = createPasswordResetRepository(database.db);
|
||||
@@ -137,6 +140,7 @@ async function startServer() {
|
||||
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
||||
}),
|
||||
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository }),
|
||||
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
||||
publicListingService: createPublicListingService({ listingRepository, publicImageService }),
|
||||
productService: createProductService({ productRepository }),
|
||||
publicProductService: createPublicProductService({ productRepository }),
|
||||
|
||||
@@ -299,6 +299,20 @@ export const marketValueOverrides = pgTable('market_value_overrides', {
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const pricingProviderMappings = pgTable('pricing_provider_mappings', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
productId: uuid('product_id').notNull().references(() => products.id),
|
||||
providerId: text('provider_id').notNull(),
|
||||
mappingType: text('mapping_type').notNull(),
|
||||
externalIdentifier: text('external_identifier'),
|
||||
searchQuery: text('search_query'),
|
||||
sourceUrl: text('source_url'),
|
||||
status: text('status').default('ACTIVE').notNull(),
|
||||
metadata: jsonb('metadata').default({}).notNull(),
|
||||
disabledAt: timestamp('disabled_at', { withTimezone: true }),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const pricingRefreshJobs = pgTable('pricing_refresh_jobs', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
sellerId: uuid('seller_id').notNull().references(() => sellers.id),
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user