test: cover transaction persistence integrations
Quality / test (push) Canceled after 0s

Add PostgreSQL integration coverage for acquisitions, pricing valuations, multi-item sales, audited voiding, and document the expanded integration baseline.
This commit is contained in:
2026-09-09 09:10:51 -04:00
parent 2feb5006cb
commit 13f0f3a565
3 changed files with 173 additions and 1 deletions
@@ -0,0 +1,157 @@
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 { 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 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 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);
});
+1 -1
View File
@@ -118,7 +118,7 @@ docker context use orbstack
| HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management | | HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management |
| Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling | | Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling |
Latest result: API, web, build, Storybook build, and Storybook interaction/accessibility checks pass locally. The authenticated E2E workflow covers acquisition, image upload, pricing, listing publication, sale recording, dashboard loading, and audited sale voiding. Latest result: API, web, build, Storybook build, and Storybook interaction/accessibility checks pass locally. The isolated integration suite passes 20 tests, including acquisition, pricing, multi-item sales, audited sale voiding, and seller ownership persistence. The authenticated E2E workflow covers acquisition, image upload, pricing, listing publication, sale recording, dashboard loading, and audited sale voiding.
## Next Milestone ## Next Milestone
+15
View File
@@ -25,6 +25,21 @@ Test:
- sales transactions - sales transactions
- public/private DTO boundaries - public/private DTO boundaries
Run the isolated PostgreSQL/Redis integration suite with the test environment
loaded into the shell:
```sh
set -a
. ./.env
set +a
npm run test:integration
```
The PostgreSQL database name must end in `_test`. The suite covers catalog,
session, image derivative, listing visibility, Redis login throttling,
acquisition, pricing observations/valuations, multi-item sales, audited sale
voiding, and seller ownership persistence.
## Component ## Component
Test important Vue components and composables. Every new or changed visual component must include a colocated Storybook story or an update to an existing story before its vertical slice is committed. Test important Vue components and composables. Every new or changed visual component must include a colocated Storybook story or an update to an existing story before its vertical slice is committed.