feat: reconcile application vertical slices
Backfill the completed acquisitions, sales, dashboard, pricing, search, public detail, worker, migration, contracts, validation, tests, and implementation-status changes into one verified application slice.
This commit is contained in:
@@ -1,23 +1,26 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createAcquisitionService } from '../../src/modules/acquisitions/services/acquisition-service.js';
|
||||
import { createAcquisitionService } from "../../src/modules/acquisitions/services/acquisition-service.js";
|
||||
|
||||
test('creates an owned acquisition and calculates cost basis', async () => {
|
||||
test("creates an owned acquisition and calculates cost basis", async () => {
|
||||
let received;
|
||||
const service = createAcquisitionService({
|
||||
acquisitionRepository: {
|
||||
async createForSeller(input) {
|
||||
received = input;
|
||||
return { status: 'CREATED', acquisition: { id: 'acquisition-1', ...input.acquisition } };
|
||||
return {
|
||||
status: "CREATED",
|
||||
acquisition: { id: "acquisition-1", ...input.acquisition },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.create({
|
||||
sellerId: 'seller-1',
|
||||
inventoryItemId: 'inventory-1',
|
||||
method: 'PURCHASE',
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
method: "PURCHASE",
|
||||
costMinor: 1000,
|
||||
shippingMinor: 150,
|
||||
taxMinor: 75,
|
||||
@@ -25,17 +28,27 @@ test('creates an owned acquisition and calculates cost basis', async () => {
|
||||
estimatedTradeValueMinor: 200,
|
||||
});
|
||||
|
||||
assert.equal(received.sellerId, 'seller-1');
|
||||
assert.equal(received.sellerId, "seller-1");
|
||||
assert.equal(result.costBasisMinor, 1075);
|
||||
});
|
||||
|
||||
test('rejects acquisition for an inventory item outside the seller account', async () => {
|
||||
test("rejects acquisition for an inventory item outside the seller account", async () => {
|
||||
const service = createAcquisitionService({
|
||||
acquisitionRepository: { async createForSeller() { return { status: 'NOT_FOUND' }; } },
|
||||
acquisitionRepository: {
|
||||
async createForSeller() {
|
||||
return { status: "NOT_FOUND" };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.create({ sellerId: 'seller-1', inventoryItemId: 'inventory-1', method: 'PURCHASE', costMinor: 100 }),
|
||||
(error) => error.code === 'INVENTORY_NOT_FOUND' && error.statusCode === 404,
|
||||
() =>
|
||||
service.create({
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
method: "PURCHASE",
|
||||
costMinor: 100,
|
||||
}),
|
||||
(error) => error.code === "INVENTORY_NOT_FOUND" && error.statusCode === 404,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createDashboardService } from '../../src/modules/dashboard/services/dashboard-service.js';
|
||||
import { createDashboardService } from "../../src/modules/dashboard/services/dashboard-service.js";
|
||||
|
||||
test('aggregates seller dashboard counts and financial totals', async () => {
|
||||
test("aggregates seller dashboard counts and financial totals", async () => {
|
||||
const service = createDashboardService({
|
||||
productService: { async listBySellerId() { return [{ id: 'product-1' }]; } },
|
||||
inventoryItemService: { async listBySellerId() { return [{ id: 'item-1', status: 'ACTIVE' }, { id: 'item-2', status: 'SOLD' }]; } },
|
||||
listingService: { async list() { return [{ status: 'AVAILABLE', askingPriceMinor: 2500, imageCount: 1 }]; } },
|
||||
saleService: { async list() { return [{ status: 'COMPLETED', netProceedsMinor: 2200, profitMinor: 700 }]; } },
|
||||
acquisitionService: { async list() { return [{ costBasisMinor: 1500 }]; } },
|
||||
pricingRepository: { async listLatestValuationsForSeller() { return [{ inventoryItemId: 'item-1', valueMinor: 2800, calculatedAt: new Date() }]; } },
|
||||
pricingRefreshJobRepository: { async countFailedForSeller() { return 2; } },
|
||||
productService: {
|
||||
async listBySellerId() {
|
||||
return [{ id: "product-1" }];
|
||||
},
|
||||
},
|
||||
inventoryItemService: {
|
||||
async listBySellerId() {
|
||||
return [
|
||||
{ id: "item-1", status: "ACTIVE" },
|
||||
{ id: "item-2", status: "SOLD" },
|
||||
];
|
||||
},
|
||||
},
|
||||
listingService: {
|
||||
async list() {
|
||||
return [{ status: "AVAILABLE", askingPriceMinor: 2500, imageCount: 1 }];
|
||||
},
|
||||
},
|
||||
saleService: {
|
||||
async list() {
|
||||
return [
|
||||
{ status: "COMPLETED", netProceedsMinor: 2200, profitMinor: 700 },
|
||||
];
|
||||
},
|
||||
},
|
||||
acquisitionService: {
|
||||
async list() {
|
||||
return [{ costBasisMinor: 1500 }];
|
||||
},
|
||||
},
|
||||
pricingRepository: {
|
||||
async listLatestValuationsForSeller() {
|
||||
return [
|
||||
{
|
||||
inventoryItemId: "item-1",
|
||||
valueMinor: 2800,
|
||||
calculatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
pricingRefreshJobRepository: {
|
||||
async countFailedForSeller() {
|
||||
return 2;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dashboard = await service.get({ sellerId: 'seller-1' });
|
||||
const dashboard = await service.get({ sellerId: "seller-1" });
|
||||
assert.deepEqual(dashboard.counts, {
|
||||
products: 1,
|
||||
inventory: 2,
|
||||
@@ -35,4 +74,4 @@ test('aggregates seller dashboard counts and financial totals', async () => {
|
||||
totalProfitMinor: 700,
|
||||
totalMarketValueMinor: 2800,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,87 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createPricingProvider, createPricingProviderRegistry } from '../../src/modules/pricing/providers/pricing-provider.js';
|
||||
import { createFixturePricingProvider } from '../../src/modules/pricing/providers/fixture-pricing-provider.js';
|
||||
import {
|
||||
createPricingProvider,
|
||||
createPricingProviderRegistry,
|
||||
} from "../../src/modules/pricing/providers/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";
|
||||
|
||||
test('registers pricing providers behind a validated adapter contract', async () => {
|
||||
test("registers pricing providers behind a validated adapter contract", async () => {
|
||||
const provider = createPricingProvider({
|
||||
id: 'example',
|
||||
async observe() { return [{ priceMinor: 2500, sourceIdentifier: 'example-1' }]; },
|
||||
id: "example",
|
||||
async observe() {
|
||||
return [{ priceMinor: 2500, sourceIdentifier: "example-1" }];
|
||||
},
|
||||
});
|
||||
const registry = createPricingProviderRegistry([provider]);
|
||||
|
||||
assert.equal(registry.get('example'), provider);
|
||||
assert.deepEqual(await registry.get('example').observe({}), [{ priceMinor: 2500, sourceIdentifier: 'example-1' }]);
|
||||
assert.equal(registry.get("example"), provider);
|
||||
assert.deepEqual(await registry.get("example").observe({}), [
|
||||
{ priceMinor: 2500, sourceIdentifier: "example-1" },
|
||||
]);
|
||||
assert.deepEqual(registry.list(), [provider]);
|
||||
});
|
||||
|
||||
test('rejects malformed provider adapters and responses', async () => {
|
||||
assert.throws(() => createPricingProvider({ id: 'broken' }), /observe function is required/);
|
||||
const provider = createPricingProvider({ id: 'broken', async observe() { return null; } });
|
||||
await assert.rejects(() => provider.observe({}), /must return an observation array/);
|
||||
test("rejects malformed provider adapters and responses", async () => {
|
||||
assert.throws(
|
||||
() => createPricingProvider({ id: "broken" }),
|
||||
/observe function is required/,
|
||||
);
|
||||
const provider = createPricingProvider({
|
||||
id: "broken",
|
||||
async observe() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
() => provider.observe({}),
|
||||
/must return an observation array/,
|
||||
);
|
||||
});
|
||||
|
||||
test('fixture provider returns deterministic normalized observations', async () => {
|
||||
test("fixture provider returns deterministic normalized observations", async () => {
|
||||
const [observation] = await createFixturePricingProvider().observe({
|
||||
productId: 'product-1', inventoryItemId: 'item-1', conditionId: 'condition-1',
|
||||
productId: "product-1",
|
||||
inventoryItemId: "item-1",
|
||||
conditionId: "condition-1",
|
||||
});
|
||||
assert.deepEqual(observation, {
|
||||
productId: 'product-1',
|
||||
inventoryItemId: 'item-1',
|
||||
conditionId: 'condition-1',
|
||||
productId: "product-1",
|
||||
inventoryItemId: "item-1",
|
||||
conditionId: "condition-1",
|
||||
priceMinor: 2500,
|
||||
currency: 'USD',
|
||||
sourceIdentifier: 'fixture-item-1',
|
||||
currency: "USD",
|
||||
sourceIdentifier: "fixture-item-1",
|
||||
qualityScore: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("http provider sends mapping context and normalizes its response", async () => {
|
||||
let request;
|
||||
const provider = createHttpPricingProvider({
|
||||
id: "http-provider",
|
||||
endpoint: "https://provider.example/prices",
|
||||
token: "test-token",
|
||||
async fetchImplementation(url, options) {
|
||||
request = { url: String(url), options };
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return { observations: [{ priceMinor: 2500 }] };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
await provider.observe({
|
||||
externalIdentifier: "catalog-1",
|
||||
searchQuery: "Charizard",
|
||||
}),
|
||||
[{ priceMinor: 2500 }],
|
||||
);
|
||||
assert.match(request.url, /externalIdentifier=catalog-1/);
|
||||
assert.equal(request.options.headers.authorization, "Bearer test-token");
|
||||
});
|
||||
|
||||
@@ -1,77 +1,137 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createPricingService } from '../../src/modules/pricing/services/pricing-service.js';
|
||||
import { createPricingService } from "../../src/modules/pricing/services/pricing-service.js";
|
||||
|
||||
test('records an owned pricing observation and calculates a valuation', async () => {
|
||||
test("records an owned pricing observation and calculates a valuation", async () => {
|
||||
const service = createPricingService({
|
||||
pricingRepository: {
|
||||
async addObservationForSeller({ observation, sellerId }) {
|
||||
assert.equal(sellerId, 'seller-1');
|
||||
return { status: 'CREATED', observation };
|
||||
assert.equal(sellerId, "seller-1");
|
||||
return { status: "CREATED", observation };
|
||||
},
|
||||
async markOutliersForSeller() {
|
||||
return { status: 'UPDATED' };
|
||||
return { status: "UPDATED" };
|
||||
},
|
||||
async calculateForSeller() {
|
||||
return { status: 'CALCULATED', valuation: { valueMinor: 2500 }, observations: [] };
|
||||
return {
|
||||
status: "CALCULATED",
|
||||
valuation: { valueMinor: 2500 },
|
||||
observations: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const observation = await service.addObservation({
|
||||
sellerId: 'seller-1', productId: 'product-1', inventoryItemId: 'inventory-1',
|
||||
providerId: 'manual', priceMinor: 2500, sourceIdentifier: 'source-1', conditionId: 'condition-1',
|
||||
sellerId: "seller-1",
|
||||
productId: "product-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
providerId: "manual",
|
||||
priceMinor: 2500,
|
||||
sourceIdentifier: "source-1",
|
||||
conditionId: "condition-1",
|
||||
});
|
||||
assert.equal(observation.priceMinor, 2500);
|
||||
const valuation = await service.calculate({ sellerId: 'seller-1', inventoryItemId: 'inventory-1' });
|
||||
const valuation = await service.calculate({
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
});
|
||||
assert.equal(valuation.valuation.valueMinor, 2500);
|
||||
});
|
||||
|
||||
test('requires a reason for an audited market-value override', async () => {
|
||||
const service = createPricingService({ pricingRepository: { async createOverride() { throw new Error('should not run'); } } });
|
||||
test("requires a reason for an audited market-value override", async () => {
|
||||
const service = createPricingService({
|
||||
pricingRepository: {
|
||||
async createOverride() {
|
||||
throw new Error("should not run");
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
() => service.override({ sellerId: 'seller-1', actorUserId: 'user-1', inventoryItemId: 'inventory-1', valueMinor: 2000, reason: 'x' }),
|
||||
(error) => error.code === 'OVERRIDE_REASON_REQUIRED' && error.statusCode === 400,
|
||||
() =>
|
||||
service.override({
|
||||
sellerId: "seller-1",
|
||||
actorUserId: "user-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
valueMinor: 2000,
|
||||
reason: "x",
|
||||
}),
|
||||
(error) =>
|
||||
error.code === "OVERRIDE_REASON_REQUIRED" && error.statusCode === 400,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails clearly when a requested pricing provider is unavailable', async () => {
|
||||
test("fails clearly when a requested pricing provider is unavailable", async () => {
|
||||
const service = createPricingService({
|
||||
pricingRepository: {},
|
||||
pricingProviderRegistry: { get() { return null; } },
|
||||
pricingProviderRegistry: {
|
||||
get() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.refresh({ sellerId: 'seller-1', inventoryItemId: 'inventory-1', providerId: 'missing' }),
|
||||
(error) => error.code === 'PRICING_PROVIDER_UNAVAILABLE' && error.statusCode === 503,
|
||||
() =>
|
||||
service.refresh({
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
providerId: "missing",
|
||||
}),
|
||||
(error) =>
|
||||
error.code === "PRICING_PROVIDER_UNAVAILABLE" && error.statusCode === 503,
|
||||
);
|
||||
});
|
||||
|
||||
test('enqueues a seller-scoped pricing refresh and reads its status', async () => {
|
||||
test("enqueues a seller-scoped pricing refresh and reads its status", async () => {
|
||||
const service = createPricingService({
|
||||
pricingRepository: {},
|
||||
pricingRefreshJobRepository: {
|
||||
async create(input) { return { id: 'job-1', status: 'PENDING', ...input }; },
|
||||
async findByIdForSeller(id, sellerId) { return { id, sellerId, status: 'COMPLETED' }; },
|
||||
async create(input) {
|
||||
return { id: "job-1", status: "PENDING", ...input };
|
||||
},
|
||||
async findByIdForSeller(id, sellerId) {
|
||||
return { id, sellerId, status: "COMPLETED" };
|
||||
},
|
||||
},
|
||||
providerMappingRepository: {
|
||||
async findActiveForSeller() {
|
||||
return { providerId: "mapped-provider" };
|
||||
},
|
||||
},
|
||||
providerMappingRepository: { async findActiveForSeller() { return { providerId: 'mapped-provider' }; } },
|
||||
});
|
||||
|
||||
const job = await service.enqueueRefresh({ sellerId: 'seller-1', inventoryItemId: 'item-1', providerId: 'provider-1', productId: 'product-1', conditionId: 'condition-1' });
|
||||
assert.equal(job.status, 'PENDING');
|
||||
assert.equal(job.providerId, 'mapped-provider');
|
||||
assert.equal((await service.getJob({ sellerId: 'seller-1', jobId: job.id })).status, 'COMPLETED');
|
||||
const job = await service.enqueueRefresh({
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "item-1",
|
||||
providerId: "provider-1",
|
||||
productId: "product-1",
|
||||
conditionId: "condition-1",
|
||||
});
|
||||
assert.equal(job.status, "PENDING");
|
||||
assert.equal(job.providerId, "mapped-provider");
|
||||
assert.equal(
|
||||
(await service.getJob({ sellerId: "seller-1", jobId: job.id })).status,
|
||||
"COMPLETED",
|
||||
);
|
||||
});
|
||||
|
||||
test('lists and retries seller-scoped failed pricing jobs', async () => {
|
||||
test("lists and retries seller-scoped failed pricing jobs", async () => {
|
||||
const service = createPricingService({
|
||||
pricingRepository: {},
|
||||
pricingRefreshJobRepository: {
|
||||
async listForSeller() { return [{ id: 'job-1', status: 'FAILED' }]; },
|
||||
async retryForSeller() { return { id: 'job-1', status: 'PENDING' }; },
|
||||
async listForSeller() {
|
||||
return [{ id: "job-1", status: "FAILED" }];
|
||||
},
|
||||
async retryForSeller() {
|
||||
return { id: "job-1", status: "PENDING" };
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal((await service.listJobs({ sellerId: 'seller-1' })).length, 1);
|
||||
assert.equal((await service.retryJob({ sellerId: 'seller-1', jobId: 'job-1' })).status, 'PENDING');
|
||||
});
|
||||
assert.equal((await service.listJobs({ sellerId: "seller-1" })).length, 1);
|
||||
assert.equal(
|
||||
(await service.retryJob({ sellerId: "seller-1", jobId: "job-1" })).status,
|
||||
"PENDING",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createProviderMappingService } from '../../src/modules/pricing/services/provider-mapping-service.js';
|
||||
import { createProviderMappingService } from "../../src/modules/pricing/services/provider-mapping-service.js";
|
||||
|
||||
test('creates and disables seller-owned provider mappings', async () => {
|
||||
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' } }; },
|
||||
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');
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createSaleService } from '../../src/modules/sales/services/sale-service.js';
|
||||
import { createSaleService } from "../../src/modules/sales/services/sale-service.js";
|
||||
|
||||
function validInput(overrides = {}) {
|
||||
return {
|
||||
sellerId: 'seller-1',
|
||||
inventoryItemId: 'inventory-1',
|
||||
saleDate: '2026-09-09',
|
||||
sellerId: "seller-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
saleDate: "2026-09-09",
|
||||
soldPriceMinor: 2499,
|
||||
costBasisMinor: 1200,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('creates a sale through the repository with validated money values', async () => {
|
||||
test("creates a sale through the repository with validated money values", async () => {
|
||||
let received;
|
||||
const service = createSaleService({
|
||||
saleRepository: {
|
||||
async createForInventoryItem(input) {
|
||||
received = input;
|
||||
return {
|
||||
status: 'CREATED',
|
||||
status: "CREATED",
|
||||
sale: {
|
||||
id: 'sale-1',
|
||||
id: "sale-1",
|
||||
soldPriceMinor: 2499,
|
||||
platformFeesMinor: 100,
|
||||
paymentProcessingFeesMinor: 0,
|
||||
@@ -38,9 +38,9 @@ test('creates a sale through the repository with validated money values', async
|
||||
});
|
||||
|
||||
assert.deepEqual(await service.create(validInput()), {
|
||||
status: 'CREATED',
|
||||
status: "CREATED",
|
||||
sale: {
|
||||
id: 'sale-1',
|
||||
id: "sale-1",
|
||||
soldPriceMinor: 2499,
|
||||
platformFeesMinor: 100,
|
||||
paymentProcessingFeesMinor: 0,
|
||||
@@ -52,85 +52,105 @@ test('creates a sale through the repository with validated money values', async
|
||||
netProceedsMinor: 2399,
|
||||
profitMinor: 1199,
|
||||
});
|
||||
assert.equal(received.sale.sellerId, 'seller-1');
|
||||
assert.equal(received.sale.sellerId, "seller-1");
|
||||
assert.equal(received.sale.soldPriceMinor, 2499);
|
||||
assert.equal(received.costBasisMinor, 1200);
|
||||
});
|
||||
|
||||
test('lists seller sales with derived net proceeds and profit', async () => {
|
||||
test("lists seller sales with derived net proceeds and profit", async () => {
|
||||
const service = createSaleService({
|
||||
saleRepository: {
|
||||
async listBySellerId(sellerId) {
|
||||
assert.equal(sellerId, 'seller-1');
|
||||
return [{
|
||||
sale: { soldPriceMinor: 3000, shippingCostMinor: 200 },
|
||||
saleItem: { costBasisMinor: 1500 },
|
||||
inventoryItem: { id: 'inventory-1', status: 'SOLD' },
|
||||
}];
|
||||
assert.equal(sellerId, "seller-1");
|
||||
return [
|
||||
{
|
||||
sale: { soldPriceMinor: 3000, shippingCostMinor: 200 },
|
||||
saleItem: { costBasisMinor: 1500 },
|
||||
inventoryItem: { id: "inventory-1", status: "SOLD" },
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [sale] = await service.list({ sellerId: 'seller-1' });
|
||||
const [sale] = await service.list({ sellerId: "seller-1" });
|
||||
assert.equal(sale.netProceedsMinor, 2800);
|
||||
assert.equal(sale.profitMinor, 1300);
|
||||
});
|
||||
|
||||
test('voids an owned sale with a required reason', async () => {
|
||||
test("voids an owned sale with a required reason", async () => {
|
||||
let received;
|
||||
const service = createSaleService({
|
||||
saleRepository: {
|
||||
async voidForSeller(input) {
|
||||
received = input;
|
||||
return {
|
||||
status: 'VOIDED',
|
||||
sale: { id: 'sale-1', soldPriceMinor: 3000 },
|
||||
status: "VOIDED",
|
||||
sale: { id: "sale-1", soldPriceMinor: 3000 },
|
||||
saleItem: { costBasisMinor: 1500 },
|
||||
inventoryItem: { id: 'inventory-1', status: 'ACTIVE' },
|
||||
inventoryItem: { id: "inventory-1", status: "ACTIVE" },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.void({
|
||||
saleId: 'sale-1',
|
||||
sellerId: 'seller-1',
|
||||
actorUserId: 'user-1',
|
||||
reason: ' Duplicate sale ',
|
||||
saleId: "sale-1",
|
||||
sellerId: "seller-1",
|
||||
actorUserId: "user-1",
|
||||
reason: " Duplicate sale ",
|
||||
});
|
||||
|
||||
assert.equal(received.reason, 'Duplicate sale');
|
||||
assert.equal(result.inventoryItem.status, 'ACTIVE');
|
||||
assert.equal(received.reason, "Duplicate sale");
|
||||
assert.equal(result.inventoryItem.status, "ACTIVE");
|
||||
await assert.rejects(
|
||||
() => service.void({ saleId: 'sale-1', sellerId: 'seller-1', actorUserId: 'user-1', reason: 'x' }),
|
||||
(error) => error.code === 'SALE_VOID_REASON_REQUIRED' && error.statusCode === 400,
|
||||
() =>
|
||||
service.void({
|
||||
saleId: "sale-1",
|
||||
sellerId: "seller-1",
|
||||
actorUserId: "user-1",
|
||||
reason: "x",
|
||||
}),
|
||||
(error) =>
|
||||
error.code === "SALE_VOID_REASON_REQUIRED" && error.statusCode === 400,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates one sale containing multiple inventory items', async () => {
|
||||
test("creates one sale containing multiple inventory items", async () => {
|
||||
let received;
|
||||
const service = createSaleService({
|
||||
saleRepository: {
|
||||
async createForInventoryItems(input) {
|
||||
received = input;
|
||||
return {
|
||||
status: 'CREATED',
|
||||
sale: { id: 'sale-2', soldPriceMinor: 3000 },
|
||||
status: "CREATED",
|
||||
sale: { id: "sale-2", soldPriceMinor: 3000 },
|
||||
saleItems: input.items,
|
||||
saleItem: input.items[0],
|
||||
inventoryItems: input.items.map(({ inventoryItemId }) => ({ id: inventoryItemId, status: 'SOLD' })),
|
||||
inventoryItem: { id: input.items[0].inventoryItemId, status: 'SOLD' },
|
||||
inventoryItems: input.items.map(({ inventoryItemId }) => ({
|
||||
id: inventoryItemId,
|
||||
status: "SOLD",
|
||||
})),
|
||||
inventoryItem: { id: input.items[0].inventoryItemId, status: "SOLD" },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.create({
|
||||
sellerId: 'seller-1',
|
||||
saleDate: '2026-09-09',
|
||||
sellerId: "seller-1",
|
||||
saleDate: "2026-09-09",
|
||||
items: [
|
||||
{ inventoryItemId: 'inventory-1', soldPriceMinor: 1200, costBasisMinor: 500 },
|
||||
{ inventoryItemId: 'inventory-2', soldPriceMinor: 1800, costBasisMinor: 700 },
|
||||
{
|
||||
inventoryItemId: "inventory-1",
|
||||
soldPriceMinor: 1200,
|
||||
costBasisMinor: 500,
|
||||
},
|
||||
{
|
||||
inventoryItemId: "inventory-2",
|
||||
soldPriceMinor: 1800,
|
||||
costBasisMinor: 700,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -139,18 +159,27 @@ test('creates one sale containing multiple inventory items', async () => {
|
||||
assert.equal(result.profitMinor, 1800);
|
||||
});
|
||||
|
||||
test('maps missing and already-sold inventory to application errors', async () => {
|
||||
test("maps missing and already-sold inventory to application errors", async () => {
|
||||
for (const result of [
|
||||
{ status: 'NOT_FOUND', code: 'INVENTORY_NOT_FOUND', statusCode: 404 },
|
||||
{ status: 'NOT_ACTIVE', code: 'INVENTORY_NOT_AVAILABLE_FOR_SALE', statusCode: 409 },
|
||||
{ status: "NOT_FOUND", code: "INVENTORY_NOT_FOUND", statusCode: 404 },
|
||||
{
|
||||
status: "NOT_ACTIVE",
|
||||
code: "INVENTORY_NOT_AVAILABLE_FOR_SALE",
|
||||
statusCode: 409,
|
||||
},
|
||||
]) {
|
||||
const service = createSaleService({
|
||||
saleRepository: { async createForInventoryItem() { return result; } },
|
||||
saleRepository: {
|
||||
async createForInventoryItem() {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.create(validInput()),
|
||||
(error) => error.code === result.code && error.statusCode === result.statusCode,
|
||||
(error) =>
|
||||
error.code === result.code && error.statusCode === result.statusCode,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
calculateAcquisitionCostBasis,
|
||||
createAcquisition,
|
||||
} from '../../src/modules/acquisitions/domain/acquisition.js';
|
||||
} from "../../src/modules/acquisitions/domain/acquisition.js";
|
||||
import {
|
||||
calculateMedianPriceMinor,
|
||||
createPricingObservation,
|
||||
identifyOutlierPrices,
|
||||
} from '../../src/modules/pricing/domain/pricing-observation.js';
|
||||
} from "../../src/modules/pricing/domain/pricing-observation.js";
|
||||
|
||||
test('calculates acquisition cost basis from all cost components', () => {
|
||||
test("calculates acquisition cost basis from all cost components", () => {
|
||||
const total = calculateAcquisitionCostBasis({
|
||||
costMinor: 1000,
|
||||
shippingMinor: 150,
|
||||
@@ -23,43 +23,48 @@ test('calculates acquisition cost basis from all cost components', () => {
|
||||
assert.equal(total, 1075);
|
||||
});
|
||||
|
||||
test('rejects negative acquisition values and missing identity fields', () => {
|
||||
test("rejects negative acquisition values and missing identity fields", () => {
|
||||
assert.throws(
|
||||
() => createAcquisition({
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
method: 'PURCHASE',
|
||||
costMinor: -1,
|
||||
}),
|
||||
() =>
|
||||
createAcquisition({
|
||||
inventoryItemId: "inventory-item-id",
|
||||
method: "PURCHASE",
|
||||
costMinor: -1,
|
||||
}),
|
||||
/must be a non-negative integer minor unit/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => createAcquisition({
|
||||
inventoryItemId: '',
|
||||
method: 'PURCHASE',
|
||||
costMinor: 1000,
|
||||
}),
|
||||
() =>
|
||||
createAcquisition({
|
||||
inventoryItemId: "",
|
||||
method: "PURCHASE",
|
||||
costMinor: 1000,
|
||||
}),
|
||||
/Inventory item ID is required/,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates pricing observations and calculates a median observed price', () => {
|
||||
test("creates pricing observations and calculates a median observed price", () => {
|
||||
const observation = createPricingObservation({
|
||||
productId: 'product-id',
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
providerId: 'ebay',
|
||||
productId: "product-id",
|
||||
inventoryItemId: "inventory-item-id",
|
||||
providerId: "ebay",
|
||||
priceMinor: 2499,
|
||||
currency: 'USD',
|
||||
sourceIdentifier: 'listing-1',
|
||||
conditionId: 'condition-id',
|
||||
currency: "USD",
|
||||
sourceIdentifier: "listing-1",
|
||||
conditionId: "condition-id",
|
||||
});
|
||||
|
||||
assert.equal(observation.currency, 'USD');
|
||||
assert.equal(observation.currency, "USD");
|
||||
assert.equal(observation.priceMinor, 2499);
|
||||
assert.equal(calculateMedianPriceMinor([1199, 2499, 2000]), 2000);
|
||||
});
|
||||
|
||||
test('identifies extreme pricing observations without discarding normal values', () => {
|
||||
assert.deepEqual([...identifyOutlierPrices([1000, 1050, 1100, 5000])], [5000]);
|
||||
test("identifies extreme pricing observations without discarding normal values", () => {
|
||||
assert.deepEqual(
|
||||
[...identifyOutlierPrices([1000, 1050, 1100, 5000])],
|
||||
[5000],
|
||||
);
|
||||
assert.equal(identifyOutlierPrices([1000, 1050, 1100]).size, 0);
|
||||
});
|
||||
|
||||
@@ -1,76 +1,83 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
calculateNetSaleProceeds,
|
||||
createSale,
|
||||
createSaleItem,
|
||||
} from '../../src/modules/sales/domain/sale.js';
|
||||
} from "../../src/modules/sales/domain/sale.js";
|
||||
|
||||
test('creates a valid sale record and calculates net proceeds after fees', () => {
|
||||
test("creates a valid sale record and calculates net proceeds after fees", () => {
|
||||
const sale = createSale({
|
||||
sellerId: 'seller-id',
|
||||
saleDate: '2026-09-08',
|
||||
saleLocation: 'eBay',
|
||||
sellerId: "seller-id",
|
||||
saleDate: "2026-09-08",
|
||||
saleLocation: "eBay",
|
||||
soldPriceMinor: 4999,
|
||||
currency: 'USD',
|
||||
currency: "USD",
|
||||
platformFeesMinor: 300,
|
||||
paymentProcessingFeesMinor: 120,
|
||||
shippingCostMinor: 50,
|
||||
taxesMinor: 90,
|
||||
otherFeesMinor: 40,
|
||||
buyerName: 'Alex',
|
||||
buyerContact: 'alex@example.com',
|
||||
buyerName: "Alex",
|
||||
buyerContact: "alex@example.com",
|
||||
});
|
||||
|
||||
assert.equal(sale.soldPriceMinor, 4999);
|
||||
assert.equal(sale.currency, 'USD');
|
||||
assert.equal(calculateNetSaleProceeds({
|
||||
soldPriceMinor: 4999,
|
||||
platformFeesMinor: 300,
|
||||
paymentProcessingFeesMinor: 120,
|
||||
shippingCostMinor: 50,
|
||||
taxesMinor: 90,
|
||||
otherFeesMinor: 40,
|
||||
}), 4399);
|
||||
assert.equal(sale.currency, "USD");
|
||||
assert.equal(
|
||||
calculateNetSaleProceeds({
|
||||
soldPriceMinor: 4999,
|
||||
platformFeesMinor: 300,
|
||||
paymentProcessingFeesMinor: 120,
|
||||
shippingCostMinor: 50,
|
||||
taxesMinor: 90,
|
||||
otherFeesMinor: 40,
|
||||
}),
|
||||
4399,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects invalid sales and negative fee values', () => {
|
||||
test("rejects invalid sales and negative fee values", () => {
|
||||
assert.throws(
|
||||
() => createSale({
|
||||
sellerId: 'seller-id',
|
||||
saleDate: '2026-09-08',
|
||||
soldPriceMinor: 0,
|
||||
}),
|
||||
() =>
|
||||
createSale({
|
||||
sellerId: "seller-id",
|
||||
saleDate: "2026-09-08",
|
||||
soldPriceMinor: 0,
|
||||
}),
|
||||
/must be a positive integer minor unit/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => createSaleItem({
|
||||
saleId: 'sale-id',
|
||||
inventoryItemId: 'inventory-item-id',
|
||||
soldPriceMinor: 2000,
|
||||
costBasisMinor: -1,
|
||||
}),
|
||||
() =>
|
||||
createSaleItem({
|
||||
saleId: "sale-id",
|
||||
inventoryItemId: "inventory-item-id",
|
||||
soldPriceMinor: 2000,
|
||||
costBasisMinor: -1,
|
||||
}),
|
||||
/must be a non-negative integer minor unit/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => createSale({
|
||||
sellerId: 'seller-id',
|
||||
saleDate: '2026-02-30',
|
||||
soldPriceMinor: 2000,
|
||||
}),
|
||||
() =>
|
||||
createSale({
|
||||
sellerId: "seller-id",
|
||||
saleDate: "2026-02-30",
|
||||
soldPriceMinor: 2000,
|
||||
}),
|
||||
/must be a valid ISO date/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => createSale({
|
||||
sellerId: 'seller-id',
|
||||
saleDate: '2026-09-08',
|
||||
soldPriceMinor: 2000,
|
||||
currency: 'US dollars',
|
||||
}),
|
||||
() =>
|
||||
createSale({
|
||||
sellerId: "seller-id",
|
||||
saleDate: "2026-09-08",
|
||||
soldPriceMinor: 2000,
|
||||
currency: "US dollars",
|
||||
}),
|
||||
/must be a three-letter ISO currency code/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildSearchFilters,
|
||||
createSearchQuery,
|
||||
normalizeSearchQuery,
|
||||
} from '../../src/modules/search/domain/search-query.js';
|
||||
} from "../../src/modules/search/domain/search-query.js";
|
||||
|
||||
test('normalizes a public search query and keeps filter values within bounds', () => {
|
||||
test("normalizes a public search query and keeps filter values within bounds", () => {
|
||||
const query = createSearchQuery({
|
||||
query: ' Charizard ex ',
|
||||
category: 'Cards',
|
||||
query: " Charizard ex ",
|
||||
category: "Cards",
|
||||
minPriceMinor: 1000,
|
||||
maxPriceMinor: 5000,
|
||||
page: 1,
|
||||
@@ -18,8 +18,8 @@ test('normalizes a public search query and keeps filter values within bounds', (
|
||||
includeSold: false,
|
||||
});
|
||||
|
||||
assert.equal(query.text, 'charizard ex');
|
||||
assert.equal(query.category, 'Cards');
|
||||
assert.equal(query.text, "charizard ex");
|
||||
assert.equal(query.category, "Cards");
|
||||
assert.equal(query.minPriceMinor, 1000);
|
||||
assert.equal(query.maxPriceMinor, 5000);
|
||||
assert.equal(query.page, 1);
|
||||
@@ -27,25 +27,45 @@ test('normalizes a public search query and keeps filter values within bounds', (
|
||||
assert.equal(query.includeSold, false);
|
||||
});
|
||||
|
||||
test('rejects invalid pagination and price boundaries', () => {
|
||||
assert.throws(() => createSearchQuery({ query: 'abc', page: 0, limit: 20 }), /page must be >= 1/);
|
||||
assert.throws(() => createSearchQuery({ query: 'abc', minPriceMinor: 5000, maxPriceMinor: 1000 }), /minPriceMinor must be <= maxPriceMinor/);
|
||||
assert.throws(() => createSearchQuery({ query: 'abc', limit: 0 }), /limit must be between 1 and 100/);
|
||||
test("rejects invalid pagination and price boundaries", () => {
|
||||
assert.throws(
|
||||
() => createSearchQuery({ query: "abc", page: 0, limit: 20 }),
|
||||
/page must be >= 1/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createSearchQuery({
|
||||
query: "abc",
|
||||
minPriceMinor: 5000,
|
||||
maxPriceMinor: 1000,
|
||||
}),
|
||||
/minPriceMinor must be <= maxPriceMinor/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createSearchQuery({ query: "abc", limit: 0 }),
|
||||
/limit must be between 1 and 100/,
|
||||
);
|
||||
});
|
||||
|
||||
test('builds a public query filter set that hides sold items by default', () => {
|
||||
test("builds a public query filter set that hides sold items by default", () => {
|
||||
const filters = buildSearchFilters({ includeSold: false });
|
||||
|
||||
assert.equal(filters.status, 'AVAILABLE');
|
||||
assert.equal(filters.status, "AVAILABLE");
|
||||
assert.equal(filters.includeSold, false);
|
||||
assert.deepEqual(filters.sort, ['recently_added']);
|
||||
assert.deepEqual(filters.sort, ["recently_added"]);
|
||||
});
|
||||
|
||||
test('accepts only the supported public search sort values', () => {
|
||||
assert.equal(createSearchQuery({ sort: 'price_low' }).sort, 'price_low');
|
||||
assert.throws(() => createSearchQuery({ sort: 'random' }), /sort must be recent/);
|
||||
test("accepts only the supported public search sort values", () => {
|
||||
assert.equal(createSearchQuery({ sort: "price_low" }).sort, "price_low");
|
||||
assert.throws(
|
||||
() => createSearchQuery({ sort: "random" }),
|
||||
/sort must be recent/,
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizes whitespace and punctuation in free-text search terms', () => {
|
||||
assert.equal(normalizeSearchQuery(' Pikachu #199 / holo '), 'pikachu 199 holo');
|
||||
test("normalizes whitespace and punctuation in free-text search terms", () => {
|
||||
assert.equal(
|
||||
normalizeSearchQuery(" Pikachu #199 / holo "),
|
||||
"pikachu 199 holo",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
function createTestApp({ authenticateAdmin, authorizeUnsafeRequest } = {}) {
|
||||
return buildApp({
|
||||
@@ -9,7 +9,7 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest } = {}) {
|
||||
authorizeUnsafeRequest,
|
||||
acquisitionService: {
|
||||
async create(input) {
|
||||
return { id: 'acquisition-1', ...input, costBasisMinor: 1075 };
|
||||
return { id: "acquisition-1", ...input, costBasisMinor: 1075 };
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
@@ -18,21 +18,23 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
test('creates an acquisition for the authenticated seller', async (testContext) => {
|
||||
test("creates an acquisition for the authenticated seller", async (testContext) => {
|
||||
let csrfChecked = false;
|
||||
const app = createTestApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
authorizeUnsafeRequest: async () => { csrfChecked = true; },
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {
|
||||
csrfChecked = true;
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/acquisitions',
|
||||
headers: { 'x-csrf-token': 'csrf-token' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/acquisitions",
|
||||
headers: { "x-csrf-token": "csrf-token" },
|
||||
payload: {
|
||||
inventoryItemId: 'd2f67d74-9647-48d5-b3a4-74d3b16ea9f8',
|
||||
method: 'PURCHASE',
|
||||
inventoryItemId: "d2f67d74-9647-48d5-b3a4-74d3b16ea9f8",
|
||||
method: "PURCHASE",
|
||||
costMinor: 1000,
|
||||
},
|
||||
});
|
||||
@@ -42,16 +44,18 @@ test('creates an acquisition for the authenticated seller', async (testContext)
|
||||
assert.equal(csrfChecked, true);
|
||||
});
|
||||
|
||||
test('rejects invalid acquisition requests', async (testContext) => {
|
||||
const app = createTestApp({ authenticateAdmin: async () => ({ sellerId: 'seller-1' }) });
|
||||
test("rejects invalid acquisition requests", async (testContext) => {
|
||||
const app = createTestApp({
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/acquisitions',
|
||||
payload: { inventoryItemId: 'not-a-uuid', method: '', costMinor: -1 },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/acquisitions",
|
||||
payload: { inventoryItemId: "not-a-uuid", method: "", costMinor: -1 },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 400);
|
||||
assert.equal(response.json().error.code, 'VALIDATION_ERROR');
|
||||
});
|
||||
assert.equal(response.json().error.code, "VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
test('returns a seller-scoped dashboard', async (testContext) => {
|
||||
test("returns a seller-scoped dashboard", async (testContext) => {
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
dashboardService: { async get({ sellerId }) { assert.equal(sellerId, 'seller-1'); return { counts: {}, financials: {}, recentSales: [], recentAcquisitions: [] }; } },
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
dashboardService: {
|
||||
async get({ sellerId }) {
|
||||
assert.equal(sellerId, "seller-1");
|
||||
return {
|
||||
counts: {},
|
||||
financials: {},
|
||||
recentSales: [],
|
||||
recentAcquisitions: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/api/v1/admin/dashboard' });
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/admin/dashboard",
|
||||
});
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.json().data, { counts: {}, financials: {}, recentSales: [], recentAcquisitions: [] });
|
||||
});
|
||||
assert.deepEqual(response.json().data, {
|
||||
counts: {},
|
||||
financials: {},
|
||||
recentSales: [],
|
||||
recentAcquisitions: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,54 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
test('accepts a protected pricing observation and valuation request', async (testContext) => {
|
||||
test("accepts a protected pricing observation and valuation request", async (testContext) => {
|
||||
let csrfChecks = 0;
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
authorizeUnsafeRequest: async () => { csrfChecks += 1; },
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {
|
||||
csrfChecks += 1;
|
||||
},
|
||||
pricingService: {
|
||||
async addObservation(input) { return { id: 'observation-1', ...input }; },
|
||||
async calculate() { return { valuation: { id: 'valuation-1', inventoryItemId: 'inventory-1', valueMinor: 2500, currency: 'USD', strategy: 'MEDIAN', confidence: 'LOW', sourceCount: 1 }, observations: [] }; },
|
||||
async addObservation(input) {
|
||||
return { id: "observation-1", ...input };
|
||||
},
|
||||
async calculate() {
|
||||
return {
|
||||
valuation: {
|
||||
id: "valuation-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
valueMinor: 2500,
|
||||
currency: "USD",
|
||||
strategy: "MEDIAN",
|
||||
confidence: "LOW",
|
||||
sourceCount: 1,
|
||||
},
|
||||
observations: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const observationResponse = await app.inject({
|
||||
method: 'POST', url: '/api/v1/admin/pricing/observations', headers: { 'x-csrf-token': 'csrf' },
|
||||
payload: { productId: 'd2f67d74-9647-48d5-b3a4-74d3b16ea9f8', inventoryItemId: '21eb218b-7a95-4f80-a9e3-e306f7ca1f2e', providerId: 'manual', priceMinor: 2500, sourceIdentifier: 'source-1', conditionId: 'ab8150c7-96c1-4b55-8d69-7d0b77d48958' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/pricing/observations",
|
||||
headers: { "x-csrf-token": "csrf" },
|
||||
payload: {
|
||||
productId: "d2f67d74-9647-48d5-b3a4-74d3b16ea9f8",
|
||||
inventoryItemId: "21eb218b-7a95-4f80-a9e3-e306f7ca1f2e",
|
||||
providerId: "manual",
|
||||
priceMinor: 2500,
|
||||
sourceIdentifier: "source-1",
|
||||
conditionId: "ab8150c7-96c1-4b55-8d69-7d0b77d48958",
|
||||
},
|
||||
});
|
||||
const valuationResponse = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation",
|
||||
});
|
||||
const valuationResponse = await app.inject({ method: 'POST', url: '/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation' });
|
||||
|
||||
assert.equal(observationResponse.statusCode, 201);
|
||||
assert.equal(valuationResponse.statusCode, 200);
|
||||
@@ -27,59 +56,74 @@ test('accepts a protected pricing observation and valuation request', async (tes
|
||||
assert.equal(csrfChecks, 2);
|
||||
});
|
||||
|
||||
test('accepts a CSRF-protected market-value override', async (testContext) => {
|
||||
test("accepts a CSRF-protected market-value override", async (testContext) => {
|
||||
let received;
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ userId: 'user-1', sellerId: 'seller-1' }),
|
||||
authenticateAdmin: async () => ({ userId: "user-1", sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {},
|
||||
pricingService: { async override(input) { received = input; return { id: 'override-1', ...input }; } },
|
||||
pricingService: {
|
||||
async override(input) {
|
||||
received = input;
|
||||
return { id: "override-1", ...input };
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation/override',
|
||||
headers: { 'x-csrf-token': 'csrf' },
|
||||
payload: { valueMinor: 2600, reason: 'Verified recent sale' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation/override",
|
||||
headers: { "x-csrf-token": "csrf" },
|
||||
payload: { valueMinor: 2600, reason: "Verified recent sale" },
|
||||
});
|
||||
assert.equal(response.statusCode, 201);
|
||||
assert.equal(received.actorUserId, 'user-1');
|
||||
assert.equal(received.actorUserId, "user-1");
|
||||
assert.equal(response.json().data.valueMinor, 2600);
|
||||
});
|
||||
|
||||
test('enqueues a pricing refresh and exposes seller-scoped job status', async (testContext) => {
|
||||
test("enqueues a pricing refresh and exposes seller-scoped job status", async (testContext) => {
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ userId: 'user-1', sellerId: 'seller-1' }),
|
||||
authenticateAdmin: async () => ({ userId: "user-1", sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {},
|
||||
pricingService: {
|
||||
async enqueueRefresh(input) { return { id: 'job-1', status: 'PENDING', createdAt: input.sellerId }; },
|
||||
async getJob(input) { assert.equal(input.sellerId, 'seller-1'); return { id: input.jobId, status: 'COMPLETED', error: null }; },
|
||||
async enqueueRefresh(input) {
|
||||
return { id: "job-1", status: "PENDING", createdAt: input.sellerId };
|
||||
},
|
||||
async getJob(input) {
|
||||
assert.equal(input.sellerId, "seller-1");
|
||||
return { id: input.jobId, status: "COMPLETED", error: null };
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/inventory/inventory-1/pricing/refresh',
|
||||
headers: { 'x-csrf-token': 'csrf' },
|
||||
payload: { providerId: 'missing' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/inventory/inventory-1/pricing/refresh",
|
||||
headers: { "x-csrf-token": "csrf" },
|
||||
payload: { providerId: "missing" },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 202);
|
||||
assert.equal(response.json().data.status, 'PENDING');
|
||||
assert.equal(response.json().data.status, "PENDING");
|
||||
|
||||
const jobResponse = await app.inject({ method: 'GET', url: '/api/v1/admin/jobs/job-1' });
|
||||
const jobResponse = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/admin/jobs/job-1",
|
||||
});
|
||||
assert.equal(jobResponse.statusCode, 200);
|
||||
assert.equal(jobResponse.json().data.status, 'COMPLETED');
|
||||
assert.equal(jobResponse.json().data.status, "COMPLETED");
|
||||
});
|
||||
|
||||
test('returns a conflict when refresh has no active product mapping', async (testContext) => {
|
||||
test("returns a conflict when refresh has no active product mapping", async (testContext) => {
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {},
|
||||
pricingService: {
|
||||
async enqueueRefresh() {
|
||||
const error = new Error('No active pricing provider mapping exists for this product.');
|
||||
error.code = 'PRICING_PROVIDER_MAPPING_NOT_FOUND';
|
||||
const error = new Error(
|
||||
"No active pricing provider mapping exists for this product.",
|
||||
);
|
||||
error.code = "PRICING_PROVIDER_MAPPING_NOT_FOUND";
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
},
|
||||
@@ -88,30 +132,53 @@ test('returns a conflict when refresh has no active product mapping', async (tes
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/inventory/item-1/pricing/refresh',
|
||||
headers: { 'x-csrf-token': 'csrf' },
|
||||
payload: { productId: 'product-1', conditionId: 'condition-1' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/inventory/item-1/pricing/refresh",
|
||||
headers: { "x-csrf-token": "csrf" },
|
||||
payload: { productId: "product-1", conditionId: "condition-1" },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 409);
|
||||
assert.equal(response.json().error.code, 'PRICING_PROVIDER_MAPPING_NOT_FOUND');
|
||||
assert.equal(
|
||||
response.json().error.code,
|
||||
"PRICING_PROVIDER_MAPPING_NOT_FOUND",
|
||||
);
|
||||
});
|
||||
|
||||
test('lists jobs and retries a failed job with CSRF protection', async (testContext) => {
|
||||
test("lists jobs and retries a failed job with CSRF protection", async (testContext) => {
|
||||
let csrfChecks = 0;
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
authorizeUnsafeRequest: async () => { csrfChecks += 1; },
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {
|
||||
csrfChecks += 1;
|
||||
},
|
||||
pricingService: {
|
||||
async listJobs() { return [{ id: 'job-1', status: 'FAILED', providerId: 'fixture', inventoryItemId: 'item-1' }]; },
|
||||
async retryJob() { return { id: 'job-1', status: 'PENDING' }; },
|
||||
async listJobs() {
|
||||
return [
|
||||
{
|
||||
id: "job-1",
|
||||
status: "FAILED",
|
||||
providerId: "fixture",
|
||||
inventoryItemId: "item-1",
|
||||
},
|
||||
];
|
||||
},
|
||||
async retryJob() {
|
||||
return { id: "job-1", status: "PENDING" };
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
const listResponse = await app.inject({ method: 'GET', url: '/api/v1/admin/jobs' });
|
||||
const retryResponse = await app.inject({ method: 'POST', url: '/api/v1/admin/jobs/job-1/retry', headers: { 'x-csrf-token': 'csrf' } });
|
||||
const listResponse = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/admin/jobs",
|
||||
});
|
||||
const retryResponse = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/jobs/job-1/retry",
|
||||
headers: { "x-csrf-token": "csrf" },
|
||||
});
|
||||
assert.equal(listResponse.statusCode, 200);
|
||||
assert.equal(retryResponse.statusCode, 202);
|
||||
assert.equal(csrfChecks, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
test('protects provider mapping writes and scopes mappings to the seller', async (testContext) => {
|
||||
test("protects provider mapping writes and scopes mappings to the seller", async (testContext) => {
|
||||
let received;
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
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' }; },
|
||||
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' },
|
||||
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');
|
||||
assert.equal(received.sellerId, "seller-1");
|
||||
assert.equal(response.json().data.providerId, "fixture");
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
test('returns public search results with pagination metadata', async (testContext) => {
|
||||
test("returns public search results with pagination metadata", async (testContext) => {
|
||||
const app = buildApp({
|
||||
searchService: {
|
||||
async searchPublic(filters) {
|
||||
assert.equal(filters.page, 2);
|
||||
assert.equal(filters.limit, 5);
|
||||
assert.equal(filters.includeSold, true);
|
||||
assert.equal(filters.sort, 'price_low');
|
||||
assert.equal(filters.conditionId, 'condition-1');
|
||||
assert.equal(filters.sort, "price_low");
|
||||
assert.equal(filters.conditionId, "condition-1");
|
||||
return {
|
||||
items: [{ id: 'listing-1', status: 'CLOSED' }],
|
||||
items: [{ id: "listing-1", status: "CLOSED" }],
|
||||
pagination: { page: 2, limit: 5, total: 6, totalPages: 2 },
|
||||
};
|
||||
},
|
||||
@@ -22,8 +22,8 @@ test('returns public search results with pagination metadata', async (testContex
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/v1/public/search?query=charizard&page=2&limit=5&includeSold=true&sort=price_low&conditionId=condition-1',
|
||||
method: "GET",
|
||||
url: "/api/v1/public/search?query=charizard&page=2&limit=5&includeSold=true&sort=price_low&conditionId=condition-1",
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
@@ -33,44 +33,71 @@ test('returns public search results with pagination metadata', async (testContex
|
||||
total: 6,
|
||||
totalPages: 2,
|
||||
});
|
||||
assert.equal(response.json().data.items[0].id, 'listing-1');
|
||||
assert.equal(response.json().data.items[0].id, "listing-1");
|
||||
});
|
||||
|
||||
test('rejects malformed numeric search filters', async (testContext) => {
|
||||
const app = buildApp({ searchService: { async searchPublic() { throw new Error('should not run'); } } });
|
||||
test("rejects malformed numeric search filters", async (testContext) => {
|
||||
const app = buildApp({
|
||||
searchService: {
|
||||
async searchPublic() {
|
||||
throw new Error("should not run");
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/v1/public/search?minPriceMinor=12.5',
|
||||
method: "GET",
|
||||
url: "/api/v1/public/search?minPriceMinor=12.5",
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 400);
|
||||
assert.equal(response.json().error.code, 'INVALID_SEARCH_FILTER');
|
||||
assert.equal(response.json().error.code, "INVALID_SEARCH_FILTER");
|
||||
});
|
||||
|
||||
test('returns active public category options', async (testContext) => {
|
||||
test("returns active public category options", async (testContext) => {
|
||||
const app = buildApp({
|
||||
catalogLookupRepository: {
|
||||
async listCategories() { return [{ id: 'category-1', name: 'Trading Cards' }]; },
|
||||
async listCategories() {
|
||||
return [{ id: "category-1", name: "Trading Cards" }];
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/api/v1/public/lookups/categories' });
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/public/lookups/categories",
|
||||
});
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.json().data, [{ id: 'category-1', name: 'Trading Cards' }]);
|
||||
assert.deepEqual(response.json().data, [
|
||||
{ id: "category-1", name: "Trading Cards" },
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns public condition options without category internals', async (testContext) => {
|
||||
test("returns public condition options without category internals", async (testContext) => {
|
||||
const app = buildApp({
|
||||
catalogLookupRepository: {
|
||||
async listConditions() { return [{ id: 'condition-1', name: 'Near Mint', code: 'NM', categoryId: 'private-category' }]; },
|
||||
async listConditions() {
|
||||
return [
|
||||
{
|
||||
id: "condition-1",
|
||||
name: "Near Mint",
|
||||
code: "NM",
|
||||
categoryId: "private-category",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/api/v1/public/lookups/conditions' });
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/public/lookups/conditions",
|
||||
});
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.json().data, [{ id: 'condition-1', name: 'Near Mint', code: 'NM' }]);
|
||||
});
|
||||
assert.deepEqual(response.json().data, [
|
||||
{ id: "condition-1", name: "Near Mint", code: "NM" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
import { buildApp } from "../../src/app/build-app.js";
|
||||
|
||||
const saleResult = {
|
||||
sale: {
|
||||
id: 'sale-1',
|
||||
sellerId: 'seller-1',
|
||||
saleDate: '2026-09-09',
|
||||
id: "sale-1",
|
||||
sellerId: "seller-1",
|
||||
saleDate: "2026-09-09",
|
||||
soldPriceMinor: 2499,
|
||||
currency: 'USD',
|
||||
currency: "USD",
|
||||
platformFeesMinor: 0,
|
||||
paymentProcessingFeesMinor: 0,
|
||||
shippingCostMinor: 0,
|
||||
@@ -17,12 +17,12 @@ const saleResult = {
|
||||
otherFeesMinor: 0,
|
||||
},
|
||||
saleItem: {
|
||||
id: 'sale-item-1',
|
||||
inventoryItemId: 'inventory-1',
|
||||
id: "sale-item-1",
|
||||
inventoryItemId: "inventory-1",
|
||||
soldPriceMinor: 2499,
|
||||
costBasisMinor: 1200,
|
||||
},
|
||||
inventoryItem: { id: 'inventory-1', status: 'SOLD' },
|
||||
inventoryItem: { id: "inventory-1", status: "SOLD" },
|
||||
};
|
||||
|
||||
function createTestApp({ authenticateAdmin, authorizeUnsafeRequest } = {}) {
|
||||
@@ -31,70 +31,74 @@ function createTestApp({ authenticateAdmin, authorizeUnsafeRequest } = {}) {
|
||||
authorizeUnsafeRequest,
|
||||
saleService: {
|
||||
async create(input) {
|
||||
assert.equal(input.sellerId, 'seller-1');
|
||||
assert.equal(input.sellerId, "seller-1");
|
||||
return saleResult;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test('creates a sale for the authenticated seller with CSRF protection', async (testContext) => {
|
||||
test("creates a sale for the authenticated seller with CSRF protection", async (testContext) => {
|
||||
let csrfChecks = 0;
|
||||
const app = createTestApp({
|
||||
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
|
||||
authorizeUnsafeRequest: async () => { csrfChecks += 1; },
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {
|
||||
csrfChecks += 1;
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/sales',
|
||||
headers: { 'x-csrf-token': 'csrf-token' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/sales",
|
||||
headers: { "x-csrf-token": "csrf-token" },
|
||||
payload: {
|
||||
inventoryItemId: 'd2f67d74-9647-48d5-b3a4-74d3b16ea9f8',
|
||||
inventoryItemId: "d2f67d74-9647-48d5-b3a4-74d3b16ea9f8",
|
||||
costBasisMinor: 1200,
|
||||
saleDate: '2026-09-09',
|
||||
saleDate: "2026-09-09",
|
||||
soldPriceMinor: 2499,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 201);
|
||||
assert.equal(response.json().data.inventoryItem.status, 'SOLD');
|
||||
assert.equal(response.json().data.inventoryItem.status, "SOLD");
|
||||
assert.equal(csrfChecks, 1);
|
||||
});
|
||||
|
||||
test('rejects invalid sale requests and unauthenticated requests', async (testContext) => {
|
||||
const app = createTestApp({ authenticateAdmin: async () => ({ sellerId: 'seller-1' }) });
|
||||
test("rejects invalid sale requests and unauthenticated requests", async (testContext) => {
|
||||
const app = createTestApp({
|
||||
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const invalidResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/sales',
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/sales",
|
||||
payload: {
|
||||
inventoryItemId: 'not-a-uuid',
|
||||
inventoryItemId: "not-a-uuid",
|
||||
costBasisMinor: -1,
|
||||
saleDate: 'tomorrow',
|
||||
saleDate: "tomorrow",
|
||||
soldPriceMinor: 0,
|
||||
},
|
||||
});
|
||||
assert.equal(invalidResponse.statusCode, 400);
|
||||
assert.equal(invalidResponse.json().error.code, 'VALIDATION_ERROR');
|
||||
assert.equal(invalidResponse.json().error.code, "VALIDATION_ERROR");
|
||||
|
||||
const unauthenticatedApp = createTestApp();
|
||||
testContext.after(() => unauthenticatedApp.close());
|
||||
const unauthenticatedResponse = await unauthenticatedApp.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/sales',
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/sales",
|
||||
payload: {},
|
||||
});
|
||||
assert.equal(unauthenticatedResponse.statusCode, 401);
|
||||
assert.equal(unauthenticatedResponse.json().error.code, 'UNAUTHORIZED');
|
||||
assert.equal(unauthenticatedResponse.json().error.code, "UNAUTHORIZED");
|
||||
});
|
||||
|
||||
test('voids a sale with CSRF protection and actor identity', async (testContext) => {
|
||||
test("voids a sale with CSRF protection and actor identity", async (testContext) => {
|
||||
let voidInput;
|
||||
const app = buildApp({
|
||||
authenticateAdmin: async () => ({ userId: 'user-1', sellerId: 'seller-1' }),
|
||||
authenticateAdmin: async () => ({ userId: "user-1", sellerId: "seller-1" }),
|
||||
authorizeUnsafeRequest: async () => {},
|
||||
saleService: {
|
||||
async void(input) {
|
||||
@@ -106,13 +110,13 @@ test('voids a sale with CSRF protection and actor identity', async (testContext)
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/sales/sale-1/void',
|
||||
headers: { 'x-csrf-token': 'csrf-token' },
|
||||
payload: { reason: 'Duplicate entry' },
|
||||
method: "POST",
|
||||
url: "/api/v1/admin/sales/sale-1/void",
|
||||
headers: { "x-csrf-token": "csrf-token" },
|
||||
payload: { reason: "Duplicate entry" },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(voidInput.actorUserId, 'user-1');
|
||||
assert.equal(response.json().data.inventoryItem.status, 'SOLD');
|
||||
});
|
||||
assert.equal(voidInput.actorUserId, "user-1");
|
||||
assert.equal(response.json().data.inventoryItem.status, "SOLD");
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test, { after, beforeEach } from 'node:test';
|
||||
import assert from "node:assert/strict";
|
||||
import test, { after, beforeEach } from "node:test";
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
import { createDatabase } from '../../src/infrastructure/database/client.js';
|
||||
import { createDatabase } from "../../src/infrastructure/database/client.js";
|
||||
import {
|
||||
categories,
|
||||
conditionProfiles,
|
||||
@@ -13,76 +13,103 @@ import {
|
||||
securityAuditEvents,
|
||||
sellers,
|
||||
users,
|
||||
} from '../../src/infrastructure/database/schema.js';
|
||||
import { createAcquisitionRepository } from '../../src/modules/acquisitions/repositories/acquisition-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 { createProviderMappingRepository } from '../../src/modules/pricing/repositories/provider-mapping-repository.js';
|
||||
import { createSaleRepository } from '../../src/modules/sales/repositories/sale-repository.js';
|
||||
} from "../../src/infrastructure/database/schema.js";
|
||||
import { createAcquisitionRepository } from "../../src/modules/acquisitions/repositories/acquisition-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 { createProviderMappingRepository } from "../../src/modules/pricing/repositories/provider-mapping-repository.js";
|
||||
import { createSaleRepository } from "../../src/modules/sales/repositories/sale-repository.js";
|
||||
|
||||
const connectionString = process.env.TEST_DATABASE_URL;
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error('TEST_DATABASE_URL is required to run integration tests.');
|
||||
throw new Error("TEST_DATABASE_URL is required to run integration tests.");
|
||||
}
|
||||
|
||||
if (!new URL(connectionString).pathname.endsWith('_test')) {
|
||||
throw new Error('TEST_DATABASE_URL must reference a database whose name ends in "_test".');
|
||||
if (!new URL(connectionString).pathname.endsWith("_test")) {
|
||||
throw new Error(
|
||||
'TEST_DATABASE_URL must reference a database whose name ends in "_test".',
|
||||
);
|
||||
}
|
||||
|
||||
const database = createDatabase(connectionString);
|
||||
const acquisitionRepository = createAcquisitionRepository(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);
|
||||
|
||||
async function createFixture() {
|
||||
const [user] = await database.db.insert(users).values({
|
||||
email: 'transactions@example.test',
|
||||
passwordHash: 'not-a-real-password',
|
||||
}).returning();
|
||||
const [seller] = await database.db.insert(sellers).values({
|
||||
userId: user.id,
|
||||
displayName: 'Transactions Owner',
|
||||
}).returning();
|
||||
const [category] = await database.db.insert(categories).values({
|
||||
name: 'Transactions Cards',
|
||||
slug: 'transactions-cards',
|
||||
}).returning();
|
||||
const [profile] = await database.db.insert(conditionProfiles).values({
|
||||
categoryId: category.id,
|
||||
name: 'Card',
|
||||
}).returning();
|
||||
const [condition] = await database.db.insert(conditions).values({
|
||||
conditionProfileId: profile.id,
|
||||
name: 'Near Mint',
|
||||
code: 'NM',
|
||||
qualityRank: 1,
|
||||
}).returning();
|
||||
const [product] = await database.db.insert(products).values({
|
||||
sellerId: seller.id,
|
||||
categoryId: category.id,
|
||||
name: 'Integration Card',
|
||||
slug: 'integration-card',
|
||||
}).returning();
|
||||
const inventory = await database.db.insert(inventoryItems).values([
|
||||
{ productId: product.id, conditionId: condition.id },
|
||||
{ productId: product.id, conditionId: condition.id },
|
||||
]).returning();
|
||||
const [user] = await database.db
|
||||
.insert(users)
|
||||
.values({
|
||||
email: "transactions@example.test",
|
||||
passwordHash: "not-a-real-password",
|
||||
})
|
||||
.returning();
|
||||
const [seller] = await database.db
|
||||
.insert(sellers)
|
||||
.values({
|
||||
userId: user.id,
|
||||
displayName: "Transactions Owner",
|
||||
})
|
||||
.returning();
|
||||
const [category] = await database.db
|
||||
.insert(categories)
|
||||
.values({
|
||||
name: "Transactions Cards",
|
||||
slug: "transactions-cards",
|
||||
})
|
||||
.returning();
|
||||
const [profile] = await database.db
|
||||
.insert(conditionProfiles)
|
||||
.values({
|
||||
categoryId: category.id,
|
||||
name: "Card",
|
||||
})
|
||||
.returning();
|
||||
const [condition] = await database.db
|
||||
.insert(conditions)
|
||||
.values({
|
||||
conditionProfileId: profile.id,
|
||||
name: "Near Mint",
|
||||
code: "NM",
|
||||
qualityRank: 1,
|
||||
})
|
||||
.returning();
|
||||
const [product] = await database.db
|
||||
.insert(products)
|
||||
.values({
|
||||
sellerId: seller.id,
|
||||
categoryId: category.id,
|
||||
name: "Integration Card",
|
||||
slug: "integration-card",
|
||||
})
|
||||
.returning();
|
||||
const inventory = await database.db
|
||||
.insert(inventoryItems)
|
||||
.values([
|
||||
{ productId: product.id, conditionId: condition.id },
|
||||
{ productId: product.id, conditionId: condition.id },
|
||||
])
|
||||
.returning();
|
||||
|
||||
return { user, seller, product, condition, inventory };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
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`);
|
||||
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 () => {
|
||||
await database.close();
|
||||
});
|
||||
|
||||
test('persists acquisitions, valuations, multi-item sales, voids, and audit events transactionally', async () => {
|
||||
test("persists acquisitions, valuations, multi-item sales, voids, and audit events transactionally", async () => {
|
||||
const { user, seller, product, condition, inventory } = await createFixture();
|
||||
const [firstItem, secondItem] = inventory;
|
||||
|
||||
@@ -90,47 +117,50 @@ test('persists acquisitions, valuations, multi-item sales, voids, and audit even
|
||||
sellerId: seller.id,
|
||||
acquisition: {
|
||||
inventoryItemId: firstItem.id,
|
||||
method: 'PURCHASE',
|
||||
method: "PURCHASE",
|
||||
costMinor: 1000,
|
||||
currency: 'USD',
|
||||
currency: "USD",
|
||||
shippingMinor: 100,
|
||||
taxMinor: 50,
|
||||
otherFeesMinor: 0,
|
||||
estimatedTradeValueMinor: 0,
|
||||
},
|
||||
});
|
||||
assert.equal(acquisition.status, 'CREATED');
|
||||
assert.equal(acquisition.status, "CREATED");
|
||||
|
||||
for (const [item, price] of [[firstItem, 2500], [secondItem, 3000]]) {
|
||||
for (const [item, price] of [
|
||||
[firstItem, 2500],
|
||||
[secondItem, 3000],
|
||||
]) {
|
||||
const observation = await pricingRepository.addObservationForSeller({
|
||||
sellerId: seller.id,
|
||||
observation: {
|
||||
productId: product.id,
|
||||
inventoryItemId: item.id,
|
||||
providerId: 'integration',
|
||||
providerId: "integration",
|
||||
priceMinor: price,
|
||||
currency: 'USD',
|
||||
currency: "USD",
|
||||
sourceIdentifier: `source-${item.id}`,
|
||||
conditionId: condition.id,
|
||||
},
|
||||
});
|
||||
assert.equal(observation.status, 'CREATED');
|
||||
assert.equal(observation.status, "CREATED");
|
||||
}
|
||||
|
||||
const valuation = await pricingRepository.calculateForSeller({
|
||||
sellerId: seller.id,
|
||||
inventoryItemId: firstItem.id,
|
||||
});
|
||||
assert.equal(valuation.status, 'CALCULATED');
|
||||
assert.equal(valuation.status, "CALCULATED");
|
||||
assert.equal(valuation.valuation.valueMinor, 2500);
|
||||
assert.equal(valuation.valuation.sourceCount, 1);
|
||||
|
||||
const sale = await saleRepository.createForInventoryItems({
|
||||
sale: {
|
||||
sellerId: seller.id,
|
||||
saleDate: '2026-09-09',
|
||||
saleDate: "2026-09-09",
|
||||
soldPriceMinor: 5500,
|
||||
currency: 'USD',
|
||||
currency: "USD",
|
||||
platformFeesMinor: 200,
|
||||
paymentProcessingFeesMinor: 0,
|
||||
shippingCostMinor: 0,
|
||||
@@ -138,58 +168,92 @@ test('persists acquisitions, valuations, multi-item sales, voids, and audit even
|
||||
otherFeesMinor: 0,
|
||||
},
|
||||
items: [
|
||||
{ inventoryItemId: firstItem.id, soldPriceMinor: 2500, costBasisMinor: 1150 },
|
||||
{ inventoryItemId: secondItem.id, soldPriceMinor: 3000, costBasisMinor: 900 },
|
||||
{
|
||||
inventoryItemId: firstItem.id,
|
||||
soldPriceMinor: 2500,
|
||||
costBasisMinor: 1150,
|
||||
},
|
||||
{
|
||||
inventoryItemId: secondItem.id,
|
||||
soldPriceMinor: 3000,
|
||||
costBasisMinor: 900,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(sale.status, 'CREATED');
|
||||
assert.equal(sale.status, "CREATED");
|
||||
assert.equal(sale.saleItems.length, 2);
|
||||
assert.deepEqual(sale.inventoryItems.map(({ status }) => status), ['SOLD', 'SOLD']);
|
||||
assert.deepEqual(
|
||||
sale.inventoryItems.map(({ status }) => status),
|
||||
["SOLD", "SOLD"],
|
||||
);
|
||||
|
||||
const voided = await saleRepository.voidForSeller({
|
||||
saleId: sale.sale.id,
|
||||
sellerId: seller.id,
|
||||
actorUserId: user.id,
|
||||
reason: 'Integration correction',
|
||||
reason: "Integration correction",
|
||||
});
|
||||
assert.equal(voided.status, 'VOIDED');
|
||||
assert.equal(voided.sale.status, 'VOIDED');
|
||||
assert.equal(voided.status, "VOIDED");
|
||||
assert.equal(voided.sale.status, "VOIDED");
|
||||
assert.equal(voided.saleItems.length, 2);
|
||||
|
||||
const [auditEvent] = await database.db.select().from(securityAuditEvents).where(sql`event = 'SALE_VOIDED'`);
|
||||
const [auditEvent] = await database.db
|
||||
.select()
|
||||
.from(securityAuditEvents)
|
||||
.where(sql`event = 'SALE_VOIDED'`);
|
||||
assert.equal(auditEvent.userId, user.id);
|
||||
});
|
||||
|
||||
test('claims and completes a durable pricing refresh job', async () => {
|
||||
test("claims and completes a durable pricing refresh job", async () => {
|
||||
const { user, seller, product, condition, inventory } = await createFixture();
|
||||
const [job] = await pricingRefreshJobRepository.create({
|
||||
sellerId: seller.id,
|
||||
inventoryItemId: inventory[0].id,
|
||||
productId: product.id,
|
||||
conditionId: condition.id,
|
||||
providerId: 'integration',
|
||||
}).then((created) => [created]);
|
||||
const [job] = await pricingRefreshJobRepository
|
||||
.create({
|
||||
sellerId: seller.id,
|
||||
inventoryItemId: inventory[0].id,
|
||||
productId: product.id,
|
||||
conditionId: condition.id,
|
||||
providerId: "integration",
|
||||
})
|
||||
.then((created) => [created]);
|
||||
|
||||
const claimed = await pricingRefreshJobRepository.claimNext();
|
||||
assert.equal(claimed.id, job.id);
|
||||
assert.equal(claimed.status, 'PROCESSING');
|
||||
const completed = await pricingRefreshJobRepository.complete(job.id, [{ priceMinor: 2500 }], { valueMinor: 2500 });
|
||||
assert.equal(completed.status, 'COMPLETED');
|
||||
assert.equal(claimed.status, "PROCESSING");
|
||||
const completed = await pricingRefreshJobRepository.complete(
|
||||
job.id,
|
||||
[{ priceMinor: 2500 }],
|
||||
{ valueMinor: 2500 },
|
||||
);
|
||||
assert.equal(completed.status, "COMPLETED");
|
||||
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 () => {
|
||||
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);
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user