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 { registerAdminAcquisitionRoutes } from '../modules/acquisitions/routes/admin-acquisition-routes.js';
|
||||||
import { registerAdminDashboardRoutes } from '../modules/dashboard/routes/admin-dashboard-routes.js';
|
import { registerAdminDashboardRoutes } from '../modules/dashboard/routes/admin-dashboard-routes.js';
|
||||||
import { registerAdminPricingRoutes } from '../modules/pricing/routes/admin-pricing-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) {
|
export function buildApp(dependencies) {
|
||||||
const app = Fastify();
|
const app = Fastify();
|
||||||
@@ -82,6 +83,10 @@ export function buildApp(dependencies) {
|
|||||||
...dependencies,
|
...dependencies,
|
||||||
prefix: '/api/v1/admin',
|
prefix: '/api/v1/admin',
|
||||||
});
|
});
|
||||||
|
app.register(registerAdminProviderMappingRoutes, {
|
||||||
|
...dependencies,
|
||||||
|
prefix: '/api/v1/admin',
|
||||||
|
});
|
||||||
app.register(registerPublicSearchRoutes, {
|
app.register(registerPublicSearchRoutes, {
|
||||||
...dependencies,
|
...dependencies,
|
||||||
prefix: '/api/v1',
|
prefix: '/api/v1',
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import { createDashboardService } from '../modules/dashboard/services/dashboard-
|
|||||||
import { createPricingRepository } from '../modules/pricing/repositories/pricing-repository.js';
|
import { createPricingRepository } from '../modules/pricing/repositories/pricing-repository.js';
|
||||||
import { createPricingService } from '../modules/pricing/services/pricing-service.js';
|
import { createPricingService } from '../modules/pricing/services/pricing-service.js';
|
||||||
import { createPricingRefreshJobRepository } from '../modules/pricing/repositories/pricing-refresh-job-repository.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) {
|
function parsePort(value) {
|
||||||
const port = Number.parseInt(value ?? '3000', 10);
|
const port = Number.parseInt(value ?? '3000', 10);
|
||||||
@@ -72,6 +74,7 @@ async function startServer() {
|
|||||||
const acquisitionRepository = createAcquisitionRepository(database.db);
|
const acquisitionRepository = createAcquisitionRepository(database.db);
|
||||||
const pricingRepository = createPricingRepository(database.db);
|
const pricingRepository = createPricingRepository(database.db);
|
||||||
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
|
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
|
||||||
|
const providerMappingRepository = createProviderMappingRepository(database.db);
|
||||||
const userRepository = createUserRepository(database.db);
|
const userRepository = createUserRepository(database.db);
|
||||||
const sessionRepository = createSessionRepository(database.db);
|
const sessionRepository = createSessionRepository(database.db);
|
||||||
const passwordResetRepository = createPasswordResetRepository(database.db);
|
const passwordResetRepository = createPasswordResetRepository(database.db);
|
||||||
@@ -137,6 +140,7 @@ async function startServer() {
|
|||||||
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
||||||
}),
|
}),
|
||||||
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository }),
|
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository }),
|
||||||
|
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
||||||
publicListingService: createPublicListingService({ listingRepository, publicImageService }),
|
publicListingService: createPublicListingService({ listingRepository, publicImageService }),
|
||||||
productService: createProductService({ productRepository }),
|
productService: createProductService({ productRepository }),
|
||||||
publicProductService: createPublicProductService({ productRepository }),
|
publicProductService: createPublicProductService({ productRepository }),
|
||||||
|
|||||||
@@ -299,6 +299,20 @@ export const marketValueOverrides = pgTable('market_value_overrides', {
|
|||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
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', {
|
export const pricingRefreshJobs = pgTable('pricing_refresh_jobs', {
|
||||||
id: uuid('id').defaultRandom().primaryKey(),
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
sellerId: uuid('seller_id').notNull().references(() => sellers.id),
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { createProviderMappingService } from '../../src/modules/pricing/services/provider-mapping-service.js';
|
||||||
|
|
||||||
|
test('creates and disables seller-owned provider mappings', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const service = createProviderMappingService({
|
||||||
|
providerMappingRepository: {
|
||||||
|
async createForSeller(input, sellerId) { calls.push(['create', input, sellerId]); return { status: 'CREATED', mapping: { id: 'mapping-1', ...input } }; },
|
||||||
|
async listForSeller() { return [{ id: 'mapping-1', status: 'ACTIVE' }]; },
|
||||||
|
async disableForSeller() { return { status: 'DISABLED', mapping: { id: 'mapping-1', status: 'DISABLED' } }; },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await service.create({ sellerId: 'seller-1', productId: 'product-1', providerId: 'fixture', mappingType: 'PRODUCT', externalIdentifier: 'ext-1' });
|
||||||
|
assert.equal(created.id, 'mapping-1');
|
||||||
|
assert.equal(calls[0][2], 'seller-1');
|
||||||
|
assert.equal((await service.list({ sellerId: 'seller-1', productId: 'product-1' })).length, 1);
|
||||||
|
assert.equal((await service.disable({ sellerId: 'seller-1', id: 'mapping-1' })).status, 'DISABLED');
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { buildApp } from '../../src/app/build-app.js';
|
||||||
|
|
||||||
|
test('protects provider mapping writes and scopes mappings to the seller', async (testContext) => {
|
||||||
|
let received;
|
||||||
|
const app = buildApp({
|
||||||
|
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||||
|
authorizeUnsafeRequest: async () => {},
|
||||||
|
providerMappingService: {
|
||||||
|
async create(input) { received = input; return { id: 'mapping-1', ...input, status: 'ACTIVE' }; },
|
||||||
|
async list() { return []; },
|
||||||
|
async disable() { return { id: 'mapping-1', status: 'DISABLED' }; },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
testContext.after(() => app.close());
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/admin/products/product-1/provider-mappings',
|
||||||
|
headers: { 'x-csrf-token': 'csrf' },
|
||||||
|
payload: { providerId: 'fixture', mappingType: 'PRODUCT', externalIdentifier: 'ext-1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 201);
|
||||||
|
assert.equal(received.sellerId, 'seller-1');
|
||||||
|
assert.equal(response.json().data.providerId, 'fixture');
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { createAcquisitionRepository } from '../../src/modules/acquisitions/repositories/acquisition-repository.js';
|
import { createAcquisitionRepository } from '../../src/modules/acquisitions/repositories/acquisition-repository.js';
|
||||||
import { createPricingRepository } from '../../src/modules/pricing/repositories/pricing-repository.js';
|
import { createPricingRepository } from '../../src/modules/pricing/repositories/pricing-repository.js';
|
||||||
import { createPricingRefreshJobRepository } from '../../src/modules/pricing/repositories/pricing-refresh-job-repository.js';
|
import { createPricingRefreshJobRepository } from '../../src/modules/pricing/repositories/pricing-refresh-job-repository.js';
|
||||||
|
import { createProviderMappingRepository } from '../../src/modules/pricing/repositories/provider-mapping-repository.js';
|
||||||
import { createSaleRepository } from '../../src/modules/sales/repositories/sale-repository.js';
|
import { createSaleRepository } from '../../src/modules/sales/repositories/sale-repository.js';
|
||||||
|
|
||||||
const connectionString = process.env.TEST_DATABASE_URL;
|
const connectionString = process.env.TEST_DATABASE_URL;
|
||||||
@@ -33,6 +34,7 @@ const database = createDatabase(connectionString);
|
|||||||
const acquisitionRepository = createAcquisitionRepository(database.db);
|
const acquisitionRepository = createAcquisitionRepository(database.db);
|
||||||
const pricingRepository = createPricingRepository(database.db);
|
const pricingRepository = createPricingRepository(database.db);
|
||||||
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
|
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
|
||||||
|
const providerMappingRepository = createProviderMappingRepository(database.db);
|
||||||
const saleRepository = createSaleRepository(database.db);
|
const saleRepository = createSaleRepository(database.db);
|
||||||
|
|
||||||
async function createFixture() {
|
async function createFixture() {
|
||||||
@@ -73,7 +75,7 @@ async function createFixture() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await database.db.execute(sql`TRUNCATE pricing_refresh_jobs, market_value_overrides, market_valuations, pricing_observations, sale_items, sales, acquisitions, security_audit_events, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`);
|
await database.db.execute(sql`TRUNCATE pricing_provider_mappings, pricing_refresh_jobs, market_value_overrides, market_valuations, pricing_observations, sale_items, sales, acquisitions, security_audit_events, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
@@ -176,3 +178,18 @@ test('claims and completes a durable pricing refresh job', async () => {
|
|||||||
assert.deepEqual(completed.valuation, { valueMinor: 2500 });
|
assert.deepEqual(completed.valuation, { valueMinor: 2500 });
|
||||||
assert.equal(user.email, 'transactions@example.test');
|
assert.equal(user.email, 'transactions@example.test');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('persists and disables seller-owned provider mappings', async () => {
|
||||||
|
const { seller, product } = await createFixture();
|
||||||
|
const created = await providerMappingRepository.createForSeller({
|
||||||
|
productId: product.id,
|
||||||
|
providerId: 'integration',
|
||||||
|
mappingType: 'PRODUCT',
|
||||||
|
externalIdentifier: 'catalog-1',
|
||||||
|
}, seller.id);
|
||||||
|
assert.equal(created.status, 'CREATED');
|
||||||
|
assert.equal((await providerMappingRepository.listForSeller(product.id, seller.id)).length, 1);
|
||||||
|
const disabled = await providerMappingRepository.disableForSeller(created.mapping.id, seller.id);
|
||||||
|
assert.equal(disabled.status, 'DISABLED');
|
||||||
|
assert.equal((await providerMappingRepository.listForSeller(product.id, seller.id)).length, 0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE "pricing_provider_mappings" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"product_id" uuid NOT NULL REFERENCES "products"("id"),
|
||||||
|
"provider_id" text NOT NULL,
|
||||||
|
"mapping_type" text NOT NULL,
|
||||||
|
"external_identifier" text,
|
||||||
|
"search_query" text,
|
||||||
|
"source_url" text,
|
||||||
|
"status" text DEFAULT 'ACTIVE' NOT NULL,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"disabled_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
@@ -127,6 +127,13 @@
|
|||||||
"when": 1790064000000,
|
"when": 1790064000000,
|
||||||
"tag": "0017_pricing_refresh_jobs",
|
"tag": "0017_pricing_refresh_jobs",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790150400000,
|
||||||
|
"tag": "0018_provider_mappings",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,9 @@ POST /api/v1/admin/inventory/:id/archive
|
|||||||
POST /api/v1/admin/inventory/:id/relist
|
POST /api/v1/admin/inventory/:id/relist
|
||||||
POST /api/v1/admin/inventory/:id/pricing/refresh
|
POST /api/v1/admin/inventory/:id/pricing/refresh
|
||||||
GET /api/v1/admin/jobs/:id
|
GET /api/v1/admin/jobs/:id
|
||||||
|
GET /api/v1/admin/products/:id/provider-mappings
|
||||||
|
POST /api/v1/admin/products/:id/provider-mappings
|
||||||
|
POST /api/v1/admin/provider-mappings/:id/disable
|
||||||
|
|
||||||
GET /api/v1/admin/listings
|
GET /api/v1/admin/listings
|
||||||
GET /api/v1/admin/listings/:id
|
GET /api/v1/admin/listings/:id
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ Work through one bounded vertical slice at a time. The linked documentation rema
|
|||||||
- Concrete provider adapters, asynchronous pricing refresh transport, richer exports/settings, full integration CI services, and deployment/backup automation remain incomplete to varying degrees.
|
- Concrete provider adapters, asynchronous pricing refresh transport, richer exports/settings, full integration CI services, and deployment/backup automation remain incomplete to varying degrees.
|
||||||
- The pricing refresh worker now claims durable PostgreSQL jobs with row locking, persists completed/failed outcomes, and routes provider observations through the pricing service; configured external providers remain deployment work.
|
- The pricing refresh worker now claims durable PostgreSQL jobs with row locking, persists completed/failed outcomes, and routes provider observations through the pricing service; configured external providers remain deployment work.
|
||||||
- Pricing refresh requests now enqueue seller-scoped jobs and expose a safe `GET /api/v1/admin/jobs/:id` polling contract instead of doing provider work inline.
|
- Pricing refresh requests now enqueue seller-scoped jobs and expose a safe `GET /api/v1/admin/jobs/:id` polling contract instead of doing provider work inline.
|
||||||
|
- Product-level provider mappings now persist seller-scoped `PRODUCT`/`SEARCH` relationships and support active listing and controlled disabling.
|
||||||
- `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes.
|
- `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes.
|
||||||
|
|
||||||
## Local Commands
|
## Local Commands
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"./public-inventory": "./public-inventory.js",
|
"./public-inventory": "./public-inventory.js",
|
||||||
"./public-product": "./public-product.js",
|
"./public-product": "./public-product.js",
|
||||||
"./public-seller": "./public-seller.js"
|
"./public-seller": "./public-seller.js"
|
||||||
,"./pricing": "./pricing.js"
|
,"./pricing": "./pricing.js",
|
||||||
|
"./provider-mapping": "./provider-mapping.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function toAdminProviderMappingDto(mapping) {
|
||||||
|
return {
|
||||||
|
id: mapping.id,
|
||||||
|
productId: mapping.productId,
|
||||||
|
providerId: mapping.providerId,
|
||||||
|
mappingType: mapping.mappingType,
|
||||||
|
externalIdentifier: mapping.externalIdentifier ?? null,
|
||||||
|
searchQuery: mapping.searchQuery ?? null,
|
||||||
|
sourceUrl: mapping.sourceUrl ?? null,
|
||||||
|
status: mapping.status,
|
||||||
|
metadata: mapping.metadata ?? {},
|
||||||
|
createdAt: mapping.createdAt instanceof Date ? mapping.createdAt.toISOString() : mapping.createdAt,
|
||||||
|
updatedAt: mapping.updatedAt instanceof Date ? mapping.updatedAt.toISOString() : mapping.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"./listing": "./listing.js",
|
"./listing": "./listing.js",
|
||||||
"./sale": "./sale.js",
|
"./sale": "./sale.js",
|
||||||
"./product": "./product.js"
|
"./product": "./product.js"
|
||||||
,"./pricing": "./pricing.js"
|
,"./pricing": "./pricing.js",
|
||||||
|
"./provider-mapping": "./provider-mapping.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.8"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const createProviderMappingSchema = z.object({
|
||||||
|
providerId: z.string().trim().min(1).max(100),
|
||||||
|
mappingType: z.enum(['PRODUCT', 'SEARCH']),
|
||||||
|
externalIdentifier: z.string().trim().max(255).optional(),
|
||||||
|
searchQuery: z.string().trim().max(1000).optional(),
|
||||||
|
sourceUrl: z.string().trim().url().optional(),
|
||||||
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}).strict();
|
||||||
Reference in New Issue
Block a user