fix(inventory-opt): inventory optimization: pagination, queries, filters for 10k+ products
This commit is contained in:
186
project/src/app/tests/inventory-admin-overview.itest.ts
Normal file
186
project/src/app/tests/inventory-admin-overview.itest.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { DestinationStream } from 'pino';
|
||||
import type pg from 'pg';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { buildApp } from '../build-app.js';
|
||||
import { createPool } from '../../infrastructure/db/pool.js';
|
||||
import { createLogger } from '../../infrastructure/logging/logger.js';
|
||||
import {
|
||||
getTestDbUrl,
|
||||
recreateDatabase,
|
||||
runMigrations,
|
||||
} from '../../infrastructure/db/tests/db-test-support.js';
|
||||
import { SESSION_COOKIE_NAME } from '../../modules/identity/index.js';
|
||||
|
||||
const hasDb = Boolean(process.env.TEST_DATABASE_URL);
|
||||
|
||||
function silentLogger() {
|
||||
const destination: DestinationStream = { write: () => undefined };
|
||||
return createLogger({ level: 'info', destination });
|
||||
}
|
||||
|
||||
function cookieValue(setCookieHeader: string | string[] | undefined): string {
|
||||
const raw = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader;
|
||||
expect(raw).toBeDefined();
|
||||
const pair = (raw as string).split(';')[0] as string;
|
||||
return pair.slice(pair.indexOf('=') + 1);
|
||||
}
|
||||
|
||||
describe.skipIf(!hasDb)('inventory admin overview (real PostgreSQL)', () => {
|
||||
const url = hasDb ? getTestDbUrl() : '';
|
||||
let pool: pg.Pool;
|
||||
let app: Awaited<ReturnType<typeof buildApp>>;
|
||||
let adminCookie = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
await recreateDatabase(url);
|
||||
await runMigrations(url, 'up');
|
||||
pool = createPool(url);
|
||||
app = await buildApp({ logger: silentLogger(), pool, cookieSecure: true });
|
||||
|
||||
const user = { email: 'inventory-admin@example.com', password: 'correct horse battery staple' };
|
||||
const registered = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/auth/register',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: user,
|
||||
});
|
||||
const id = (registered.json() as { id: string }).id;
|
||||
await pool.query(
|
||||
`UPDATE identity_users
|
||||
SET role = $1,
|
||||
email_confirmed = true,
|
||||
confirmed_at = now(),
|
||||
confirmation_token = null
|
||||
WHERE id = $2`,
|
||||
['admin', id],
|
||||
);
|
||||
const login = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/auth/login',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: user,
|
||||
});
|
||||
adminCookie = cookieValue(login.headers['set-cookie']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app?.close();
|
||||
await pool?.end();
|
||||
});
|
||||
|
||||
it('returns paginated inventory rows with server-side search, filters and stats', async () => {
|
||||
const createProduct = async (name: string, slug: string, expirationDate?: string | null) => {
|
||||
const product = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/products',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
payload: { name, slug, state: 'active', expirationDate: expirationDate ?? null },
|
||||
});
|
||||
expect(product.statusCode).toBe(201);
|
||||
return product.json() as { id: string };
|
||||
};
|
||||
|
||||
const productA = await createProduct('Avena bio', `avena-${randomUUID().slice(0, 8)}`);
|
||||
const productB = await createProduct(
|
||||
'Semillas lino',
|
||||
`lino-${randomUUID().slice(0, 8)}`,
|
||||
new Date(Date.now() + 10 * 86_400_000).toISOString().slice(0, 10),
|
||||
);
|
||||
|
||||
const createVariant = async (productId: string, sku: string, ean: string) => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/products/${productId}/variants`,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
payload: { sku, ean },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
return response.json() as { id: string };
|
||||
};
|
||||
|
||||
const variantA = await createVariant(productA.id, `SKU-${randomUUID().slice(0, 8)}`, '111');
|
||||
const variantB = await createVariant(productB.id, `SKU-${randomUUID().slice(0, 8)}`, '222');
|
||||
|
||||
const setPrice = async (
|
||||
variantId: string,
|
||||
netUnitAmountCents: number,
|
||||
costCents: number,
|
||||
) => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: `/pricing/variants/${variantId}`,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
payload: { netUnitAmountCents, costCents, vatRate: 'general' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
};
|
||||
|
||||
const setStock = async (variantId: string, quantity: number) => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: `/inventory/${variantId}/stock`,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
payload: { quantity },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
};
|
||||
|
||||
await setPrice(variantA.id, 1000, 600);
|
||||
await setPrice(variantB.id, 1000, 950);
|
||||
await setStock(variantA.id, 8);
|
||||
await setStock(variantB.id, 2);
|
||||
|
||||
const search = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/inventory/admin/overview?q=111&limit=10&offset=0',
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
});
|
||||
expect(search.statusCode).toBe(200);
|
||||
expect(search.json()).toMatchObject({
|
||||
total: 1,
|
||||
stats: { inStock: 1, lowStock: 0, outOfStock: 0 },
|
||||
items: [
|
||||
{
|
||||
productName: 'Avena bio',
|
||||
variant: { ean: '111' },
|
||||
stock: { availableQuantity: 8 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const lowMargin = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/inventory/admin/overview?filter=low_margin&limit=10&offset=0',
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
});
|
||||
expect(lowMargin.statusCode).toBe(200);
|
||||
expect(lowMargin.json()).toMatchObject({
|
||||
total: 1,
|
||||
items: [
|
||||
{
|
||||
productName: 'Semillas lino',
|
||||
stock: { availableQuantity: 2 },
|
||||
marginPercent: 21,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const expiring = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/inventory/admin/overview?filter=expiring&limit=10&offset=0',
|
||||
cookies: { [SESSION_COOKIE_NAME]: adminCookie },
|
||||
});
|
||||
expect(expiring.statusCode).toBe(200);
|
||||
const expiringBody = expiring.json() as {
|
||||
total: number;
|
||||
items: Array<{ productName: string }>;
|
||||
};
|
||||
expect(expiringBody.total).toBe(2);
|
||||
expect(expiringBody.items.every((item) => item.productName === 'Semillas lino')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user