generated from wiz4apps/templates.OnlineShop
Implement the data layer from docs/PLAN.md §4 (card #55): - db/migrations/0001_init.sql: idempotent DDL for the 16 entities (category, product, product_category, product_image, product_variant, product_feature, cart, cart_item, customer, address, orders, order_item, payment, promo_code, content_page, delivery_option) with relations and indexes; explicit index on orders.number (product.slug/category.slug are unique-indexed by their keys). - Currency BYN (R4/Q1): STORE_CURRENCY = 'BYN', money as integer minor units (BYN * 100), no float, no interim currency, no FX conversion. - Idempotent seed: 14 categories (11 visible + 3 hidden visible=false, R13) and 93 products from docs/CATALOG.md §3 (broken #59 excluded, R14) with prices, old prices and badges; cross-listing, images, variants, PDP fields (R9), store constants (CATALOG §6), 1 admin + 1 customer and 2 orders. - scripts/{migrate,seed,verify}.ts + src/db: CLI entry points and an in-process Postgres verification harness (satisfies `migrate` + `seed` on a clean clone). - db/seed/0001_seed.sql: generated single-file SQL for the platform Data Tables (`/db-forms`, run_sql) to converge tenant_1 without a Node process. - Placeholder dev server + storefront currency/constants and money helpers. Verified on a clean Postgres (PGlite): product = 93; category = 14 with visible = 11; orders = 2; money integer BYN minor units (0 float rows).
97 lines
5.1 KiB
TypeScript
97 lines
5.1 KiB
TypeScript
import { PGlite } from '@electric-sql/pglite';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { migrate } from '../src/db/migrate';
|
|
import { seed } from '../src/db/seed';
|
|
import { STORE_CURRENCY } from '../src/store/constants';
|
|
import { toMinor, formatMinor } from '../src/store/money';
|
|
|
|
/**
|
|
* Verification harness for the data layer.
|
|
*
|
|
* Runs the exact same migrations + idempotent seed against an in-process Postgres
|
|
* (PGlite) so the acceptance criteria can be checked on a clean database without a
|
|
* running server:
|
|
* • migrate + seed run clean (twice, proving idempotency)
|
|
* • select count(*) from product = 93
|
|
* • select count(*) from category = 14
|
|
* • select count(*) from category visible = 11
|
|
* • money is integer BYN minor units, no float
|
|
*/
|
|
|
|
function assertEqual(actual: unknown, expected: unknown, label: string): void {
|
|
if (actual !== expected) {
|
|
throw new Error(`verify: ${label} = ${String(actual)}, expected ${String(expected)}`);
|
|
}
|
|
}
|
|
|
|
const db = new PGlite();
|
|
|
|
// Run twice: the second pass proves the seed is idempotent.
|
|
await migrate(db);
|
|
const first = await seed(db);
|
|
await migrate(db);
|
|
const second = await seed(db);
|
|
|
|
const productCount = (await db.query<{ c: number }>('select count(*)::int as c from product')).rows[0].c;
|
|
const categoryCount = (await db.query<{ c: number }>('select count(*)::int as c from category')).rows[0].c;
|
|
const visibleCount = (await db.query<{ c: number }>('select count(*)::int as c from category where visible = true')).rows[0].c;
|
|
const hiddenCount = (await db.query<{ c: number }>('select count(*)::int as c from category where visible = false')).rows[0].c;
|
|
const variantCount = (await db.query<{ c: number }>('select count(*)::int as c from product_variant')).rows[0].c;
|
|
const orderCount = (await db.query<{ c: number }>('select count(*)::int as c from orders')).rows[0].c;
|
|
const admins = (await db.query<{ c: number }>('select count(*)::int as c from customer where role = $1', ['admin'])).rows[0].c;
|
|
const customers = (await db.query<{ c: number }>('select count(*)::int as c from customer where role = $1', ['customer'])).rows[0].c;
|
|
|
|
// Money must be integer minor units in BYN — reject any non-integer / non-BYN row.
|
|
const badMoney = (await db.query<{ c: number }>(
|
|
`select count(*)::int as c from product
|
|
where currency <> $1
|
|
or price_minor <> floor(price_minor)
|
|
or (old_price_minor is not null and old_price_minor <> floor(old_price_minor))`,
|
|
[STORE_CURRENCY],
|
|
)).rows[0].c;
|
|
const badOrderMoney = (await db.query<{ c: number }>(
|
|
`select count(*)::int as c from orders
|
|
where currency <> $1 or total_minor <> floor(total_minor) or subtotal_minor <> floor(subtotal_minor)`,
|
|
[STORE_CURRENCY],
|
|
)).rows[0].c;
|
|
|
|
assertEqual(STORE_CURRENCY, 'BYN', 'STORE_CURRENCY');
|
|
assertEqual(first.products, 93, 'seed products (first run)');
|
|
assertEqual(second.products, 93, 'seed products (second run)');
|
|
assertEqual(productCount, 93, 'select count(*) from product');
|
|
assertEqual(categoryCount, 14, 'select count(*) from category');
|
|
assertEqual(visibleCount, 11, 'select count(*) from category where visible = true');
|
|
assertEqual(hiddenCount, 3, 'hidden categories');
|
|
assertEqual(variantCount, 3, 'product_variant (15/19/25 роз)');
|
|
assertEqual(orderCount, 2, 'orders');
|
|
assertEqual(admins >= 1, true, 'at least one admin');
|
|
assertEqual(customers >= 1, true, 'at least one customer');
|
|
assertEqual(badMoney, 0, 'non-BYN / non-integer product prices');
|
|
assertEqual(badOrderMoney, 0, 'non-BYN / non-integer order amounts');
|
|
assertEqual(toMinor(71), 7100, 'toMinor(71)');
|
|
assertEqual(formatMinor(7100).includes('71'), true, 'formatMinor(7100) contains 71');
|
|
|
|
// Same checks again, but through the generated single-file SQL seed
|
|
// (db/seed/0001_seed.sql) used by the W4C Data Tables module (`run_sql`).
|
|
const sqlDb = new PGlite();
|
|
await migrate(sqlDb);
|
|
const seedSql = readFileSync(fileURLToPath(new URL('../db/seed/0001_seed.sql', import.meta.url)), 'utf8');
|
|
await sqlDb.exec(seedSql);
|
|
const sqlProducts = (await sqlDb.query<{ c: number }>('select count(*)::int as c from product')).rows[0].c;
|
|
const sqlCategories = (await sqlDb.query<{ c: number }>('select count(*)::int as c from category')).rows[0].c;
|
|
const sqlVisible = (await sqlDb.query<{ c: number }>('select count(*)::int as c from category where visible = true')).rows[0].c;
|
|
assertEqual(sqlProducts, 93, 'generated SQL seed: product');
|
|
assertEqual(sqlCategories, 14, 'generated SQL seed: category');
|
|
assertEqual(sqlVisible, 11, 'generated SQL seed: visible category');
|
|
await sqlDb.close();
|
|
|
|
console.log('verify: OK');
|
|
console.log(` select count(*) from product = ${productCount}`);
|
|
console.log(` select count(*) from category where visible = true = ${visibleCount} (total ${categoryCount}, hidden ${hiddenCount})`);
|
|
console.log(` generated SQL seed (db/seed/0001_seed.sql) = product ${sqlProducts}, category ${sqlCategories} (visible ${sqlVisible})`);
|
|
console.log(` product_variant=${variantCount} orders=${orderCount} admins=${admins} customers=${customers}`);
|
|
console.log(` money: integer ${STORE_CURRENCY} minor units (0 float rows)`);
|
|
|
|
await db.close();
|