Quality / test (push) Canceled after 0s
Schedule configurable completed pricing-job cleanup in the worker, broaden repository cleanup across sellers, add unit and integration coverage, and update pricing operational documentation.
299 lines
8.8 KiB
JavaScript
299 lines
8.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test, { after, beforeEach } from "node:test";
|
|
|
|
import { sql } from "drizzle-orm";
|
|
|
|
import { createDatabase } from "../../src/infrastructure/database/client.js";
|
|
import {
|
|
categories,
|
|
conditionProfiles,
|
|
conditions,
|
|
inventoryItems,
|
|
products,
|
|
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";
|
|
|
|
const connectionString = process.env.TEST_DATABASE_URL;
|
|
|
|
if (!connectionString) {
|
|
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".',
|
|
);
|
|
}
|
|
|
|
const database = createDatabase(connectionString);
|
|
const acquisitionRepository = createAcquisitionRepository(database.db);
|
|
const pricingRepository = createPricingRepository(database.db);
|
|
const pricingRefreshJobRepository = createPricingRefreshJobRepository(
|
|
database.db,
|
|
);
|
|
const providerMappingRepository = createProviderMappingRepository(database.db);
|
|
const saleRepository = createSaleRepository(database.db);
|
|
|
|
async function createFixture(suffix = "") {
|
|
const fixtureName = suffix ? `transactions-${suffix}` : "transactions";
|
|
const [user] = await database.db
|
|
.insert(users)
|
|
.values({
|
|
email: `${fixtureName}@example.test`,
|
|
passwordHash: "not-a-real-password",
|
|
})
|
|
.returning();
|
|
const [seller] = await database.db
|
|
.insert(sellers)
|
|
.values({
|
|
userId: user.id,
|
|
displayName: `${fixtureName} Owner`,
|
|
})
|
|
.returning();
|
|
const [category] = await database.db
|
|
.insert(categories)
|
|
.values({
|
|
name: `${fixtureName} Cards`,
|
|
slug: `${fixtureName}-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: `${fixtureName} Card`,
|
|
slug: `${fixtureName}-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`,
|
|
);
|
|
});
|
|
|
|
after(async () => {
|
|
await database.close();
|
|
});
|
|
|
|
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;
|
|
|
|
const acquisition = await acquisitionRepository.createForSeller({
|
|
sellerId: seller.id,
|
|
acquisition: {
|
|
inventoryItemId: firstItem.id,
|
|
method: "PURCHASE",
|
|
costMinor: 1000,
|
|
currency: "USD",
|
|
shippingMinor: 100,
|
|
taxMinor: 50,
|
|
otherFeesMinor: 0,
|
|
estimatedTradeValueMinor: 0,
|
|
},
|
|
});
|
|
assert.equal(acquisition.status, "CREATED");
|
|
|
|
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",
|
|
priceMinor: price,
|
|
currency: "USD",
|
|
sourceIdentifier: `source-${item.id}`,
|
|
conditionId: condition.id,
|
|
},
|
|
});
|
|
assert.equal(observation.status, "CREATED");
|
|
}
|
|
|
|
const valuation = await pricingRepository.calculateForSeller({
|
|
sellerId: seller.id,
|
|
inventoryItemId: firstItem.id,
|
|
});
|
|
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",
|
|
soldPriceMinor: 5500,
|
|
currency: "USD",
|
|
platformFeesMinor: 200,
|
|
paymentProcessingFeesMinor: 0,
|
|
shippingCostMinor: 0,
|
|
taxesMinor: 0,
|
|
otherFeesMinor: 0,
|
|
},
|
|
items: [
|
|
{
|
|
inventoryItemId: firstItem.id,
|
|
soldPriceMinor: 2500,
|
|
costBasisMinor: 1150,
|
|
},
|
|
{
|
|
inventoryItemId: secondItem.id,
|
|
soldPriceMinor: 3000,
|
|
costBasisMinor: 900,
|
|
},
|
|
],
|
|
});
|
|
assert.equal(sale.status, "CREATED");
|
|
assert.equal(sale.saleItems.length, 2);
|
|
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",
|
|
});
|
|
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'`);
|
|
assert.equal(auditEvent.userId, user.id);
|
|
});
|
|
|
|
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 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.deepEqual(completed.valuation, { valueMinor: 2500 });
|
|
assert.equal(user.email, "transactions@example.test");
|
|
});
|
|
|
|
test("purges completed pricing jobs across sellers before the cutoff", async () => {
|
|
const first = await createFixture();
|
|
const second = await createFixture("second");
|
|
const jobs = await Promise.all(
|
|
[first, second].map(({ seller, product, condition, inventory }) =>
|
|
pricingRefreshJobRepository.create({
|
|
sellerId: seller.id,
|
|
inventoryItemId: inventory[0].id,
|
|
productId: product.id,
|
|
conditionId: condition.id,
|
|
providerId: "integration",
|
|
}),
|
|
),
|
|
);
|
|
|
|
for (const job of jobs) {
|
|
await pricingRefreshJobRepository.claimNext();
|
|
await pricingRefreshJobRepository.complete(
|
|
job.id,
|
|
[{ priceMinor: 2500 }],
|
|
{ valueMinor: 2500 },
|
|
);
|
|
}
|
|
|
|
const deleted = await pricingRefreshJobRepository.purgeCompletedBefore(
|
|
new Date(Date.now() + 1000),
|
|
);
|
|
assert.equal(deleted, 2);
|
|
assert.equal(
|
|
(await pricingRefreshJobRepository.listForSeller(first.seller.id)).length,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
(await pricingRefreshJobRepository.listForSeller(second.seller.id)).length,
|
|
0,
|
|
);
|
|
});
|
|
|
|
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,
|
|
);
|
|
});
|