This commit is contained in:
@@ -51,7 +51,10 @@ import { createProviderMappingRepository } from "../modules/pricing/repositories
|
|||||||
import { createProviderMappingService } from "../modules/pricing/services/provider-mapping-service.js";
|
import { createProviderMappingService } from "../modules/pricing/services/provider-mapping-service.js";
|
||||||
import { createPricingProviderRegistry } from "../modules/pricing/providers/pricing-provider.js";
|
import { createPricingProviderRegistry } from "../modules/pricing/providers/pricing-provider.js";
|
||||||
import { createFixturePricingProvider } from "../modules/pricing/providers/fixture-pricing-provider.js";
|
import { createFixturePricingProvider } from "../modules/pricing/providers/fixture-pricing-provider.js";
|
||||||
import { createHttpPricingProvider } from "../modules/pricing/providers/http-pricing-provider.js";
|
import {
|
||||||
|
assertProductionPricingProviderConfig,
|
||||||
|
createHttpPricingProvider,
|
||||||
|
} from "../modules/pricing/providers/http-pricing-provider.js";
|
||||||
import { createAdminExportService } from "../modules/exports/services/admin-export-service.js";
|
import { createAdminExportService } from "../modules/exports/services/admin-export-service.js";
|
||||||
import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js";
|
import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js";
|
||||||
import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js";
|
import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js";
|
||||||
@@ -70,6 +73,12 @@ function parsePort(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
assertProductionPricingProviderConfig({
|
||||||
|
fixtureEnabled: process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true",
|
||||||
|
endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT,
|
||||||
|
});
|
||||||
|
}
|
||||||
const imageStorageRoot = resolveProjectImageStorageRoot(
|
const imageStorageRoot = resolveProjectImageStorageRoot(
|
||||||
process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage",
|
process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
import { createPricingProvider } from './pricing-provider.js';
|
import { createPricingProvider } from './pricing-provider.js';
|
||||||
|
|
||||||
|
export function assertProductionPricingProviderConfig({ fixtureEnabled, endpoint }) {
|
||||||
|
if (fixtureEnabled) {
|
||||||
|
throw new Error('The fixture pricing provider must be disabled in production.');
|
||||||
|
}
|
||||||
|
if (typeof endpoint !== 'string' || endpoint.trim().length === 0) {
|
||||||
|
throw new Error('A production pricing provider endpoint is required.');
|
||||||
|
}
|
||||||
|
let parsedUrl;
|
||||||
|
try {
|
||||||
|
parsedUrl = new URL(endpoint);
|
||||||
|
} catch {
|
||||||
|
throw new Error('The production pricing provider endpoint must be a valid HTTPS URL.');
|
||||||
|
}
|
||||||
|
if (parsedUrl.protocol !== 'https:') {
|
||||||
|
throw new Error('The production pricing provider endpoint must use HTTPS.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createHttpPricingProvider({
|
export function createHttpPricingProvider({
|
||||||
id,
|
id,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
createPricingProviderRegistry,
|
createPricingProviderRegistry,
|
||||||
} from "../../src/modules/pricing/providers/pricing-provider.js";
|
} from "../../src/modules/pricing/providers/pricing-provider.js";
|
||||||
import { createFixturePricingProvider } from "../../src/modules/pricing/providers/fixture-pricing-provider.js";
|
import { createFixturePricingProvider } from "../../src/modules/pricing/providers/fixture-pricing-provider.js";
|
||||||
import { createHttpPricingProvider } from "../../src/modules/pricing/providers/http-pricing-provider.js";
|
import {
|
||||||
|
assertProductionPricingProviderConfig,
|
||||||
|
createHttpPricingProvider,
|
||||||
|
} from "../../src/modules/pricing/providers/http-pricing-provider.js";
|
||||||
|
|
||||||
test("registers pricing providers behind a validated adapter contract", async () => {
|
test("registers pricing providers behind a validated adapter contract", async () => {
|
||||||
const provider = createPricingProvider({
|
const provider = createPricingProvider({
|
||||||
@@ -78,6 +81,25 @@ test("http provider rejects insecure or malformed external provider URLs", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("requires a secure external provider in production", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertProductionPricingProviderConfig({ fixtureEnabled: true, endpoint: "https://provider.example/prices" }),
|
||||||
|
/fixture pricing provider must be disabled/i,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertProductionPricingProviderConfig({ fixtureEnabled: false, endpoint: "" }),
|
||||||
|
/endpoint is required/i,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertProductionPricingProviderConfig({ fixtureEnabled: false, endpoint: "http://provider.example/prices" }),
|
||||||
|
/must use HTTPS/i,
|
||||||
|
);
|
||||||
|
assert.doesNotThrow(() => assertProductionPricingProviderConfig({
|
||||||
|
fixtureEnabled: false,
|
||||||
|
endpoint: "https://provider.example/prices",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
test("http provider sends mapping context and normalizes its response", async () => {
|
test("http provider sends mapping context and normalizes its response", async () => {
|
||||||
let request;
|
let request;
|
||||||
const provider = createHttpPricingProvider({
|
const provider = createHttpPricingProvider({
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import { createPricingRefreshJobRepository } from "../../api/src/modules/pricing
|
|||||||
import { createPricingService } from "../../api/src/modules/pricing/services/pricing-service.js";
|
import { createPricingService } from "../../api/src/modules/pricing/services/pricing-service.js";
|
||||||
import { createPricingProviderRegistry } from "../../api/src/modules/pricing/providers/pricing-provider.js";
|
import { createPricingProviderRegistry } from "../../api/src/modules/pricing/providers/pricing-provider.js";
|
||||||
import { createFixturePricingProvider } from "../../api/src/modules/pricing/providers/fixture-pricing-provider.js";
|
import { createFixturePricingProvider } from "../../api/src/modules/pricing/providers/fixture-pricing-provider.js";
|
||||||
import { createHttpPricingProvider } from "../../api/src/modules/pricing/providers/http-pricing-provider.js";
|
import {
|
||||||
|
assertProductionPricingProviderConfig,
|
||||||
|
createHttpPricingProvider,
|
||||||
|
} from "../../api/src/modules/pricing/providers/http-pricing-provider.js";
|
||||||
import { createInventoryItemRepository } from "../../api/src/modules/inventory/repositories/inventory-item-repository.js";
|
import { createInventoryItemRepository } from "../../api/src/modules/inventory/repositories/inventory-item-repository.js";
|
||||||
import { createInventoryItemService } from "../../api/src/modules/inventory/services/inventory-item-service.js";
|
import { createInventoryItemService } from "../../api/src/modules/inventory/services/inventory-item-service.js";
|
||||||
import { createInventoryExportJobRepository } from "../../api/src/modules/exports/repositories/inventory-export-job-repository.js";
|
import { createInventoryExportJobRepository } from "../../api/src/modules/exports/repositories/inventory-export-job-repository.js";
|
||||||
@@ -45,6 +48,12 @@ const pricingJobCleanupIntervalMs = Number.parseInt(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function runWorker() {
|
async function runWorker() {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
assertProductionPricingProviderConfig({
|
||||||
|
fixtureEnabled: process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true",
|
||||||
|
endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT,
|
||||||
|
});
|
||||||
|
}
|
||||||
const imageStorageRoot = resolveProjectImageStorageRoot(
|
const imageStorageRoot = resolveProjectImageStorageRoot(
|
||||||
process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage",
|
process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
|
|||||||
+4
-1
@@ -30,7 +30,10 @@ keeps database and Redis ports internal to the Compose network.
|
|||||||
Before starting, create a production `.env` containing real values for
|
Before starting, create a production `.env` containing real values for
|
||||||
`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL`,
|
`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL`,
|
||||||
`REDIS_URL`, `SMTP_URL`, `SMTP_FROM`, and a stable base64-encoded 32-byte
|
`REDIS_URL`, `SMTP_URL`, `SMTP_FROM`, and a stable base64-encoded 32-byte
|
||||||
`MFA_ENCRYPTION_KEY`. Do not use the fixture pricing provider in production.
|
`MFA_ENCRYPTION_KEY`. Set `PRICING_HTTP_PROVIDER_ENDPOINT` to the approved
|
||||||
|
provider's HTTPS endpoint and provide its token through
|
||||||
|
`PRICING_HTTP_PROVIDER_TOKEN` when required. Production startup rejects the
|
||||||
|
fixture provider, missing endpoints, and non-HTTPS endpoints.
|
||||||
|
|
||||||
Start the stack and apply migrations with:
|
Start the stack and apply migrations with:
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
|
|||||||
- No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables.
|
- No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables.
|
||||||
- Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains.
|
- Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains.
|
||||||
- The provider abstraction, fixture provider, HTTP provider adapter, Product mapping workflow, durable refresh queue, worker execution, polling contract, retry policy, and completed-job purge operation are implemented. Production readiness still requires approved external provider credentials and provider-specific mapping data.
|
- The provider abstraction, fixture provider, HTTP provider adapter, Product mapping workflow, durable refresh queue, worker execution, polling contract, retry policy, and completed-job purge operation are implemented. Production readiness still requires approved external provider credentials and provider-specific mapping data.
|
||||||
|
- API and worker production startup now reject fixture pricing, missing provider endpoints, and non-HTTPS provider endpoints before opening runtime connections.
|
||||||
- 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.
|
- Product-level provider mappings now persist seller-scoped `PRODUCT`/`SEARCH` relationships and support active listing and controlled disabling.
|
||||||
- Pricing refresh enqueue now resolves its provider from an active Product mapping and rejects unmapped Products explicitly.
|
- Pricing refresh enqueue now resolves its provider from an active Product mapping and rejects unmapped Products explicitly.
|
||||||
|
|||||||
Reference in New Issue
Block a user