Files
pokemon-inventory/apps/api/tests/integration/transactions-pricing-sales.test.js
T
fragginwagon b8f089a5d9 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.
2026-09-09 22:12:53 -04:00

260 lines
7.7 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() {
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`,
);
});
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("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,
);
});