Add products, inventory units, stock receipts, and price history
- product table (catalog definition, no cost column — cost tracked per receipt/unit) - inventory_unit table (serialized items with serial number, condition, status) - stock_receipt table (FIFO cost tracking — records every stock receive event with cost_per_unit, supplier, date) - price_history table (logs every retail price change for margin analysis over time) - product_supplier join table (many-to-many, tracks supplier SKU and preferred supplier) - Full CRUD routes + search (name, SKU, UPC, brand) - Inventory unit routes nested under products - Price changes auto-logged on product update - 33 tests passing
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
CREATE TYPE "public"."item_condition" AS ENUM('new', 'excellent', 'good', 'fair', 'poor');--> statement-breakpoint
|
||||
CREATE TYPE "public"."unit_status" AS ENUM('available', 'sold', 'rented', 'in_repair', 'retired');--> statement-breakpoint
|
||||
CREATE TABLE "inventory_unit" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"product_id" uuid NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"location_id" uuid,
|
||||
"serial_number" varchar(255),
|
||||
"condition" "item_condition" DEFAULT 'new' NOT NULL,
|
||||
"status" "unit_status" DEFAULT 'available' NOT NULL,
|
||||
"purchase_date" date,
|
||||
"purchase_cost" numeric(10, 2),
|
||||
"notes" text,
|
||||
"legacy_id" varchar(255),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "price_history" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"product_id" uuid NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"previous_price" numeric(10, 2),
|
||||
"new_price" numeric(10, 2) NOT NULL,
|
||||
"previous_min_price" numeric(10, 2),
|
||||
"new_min_price" numeric(10, 2),
|
||||
"reason" text,
|
||||
"changed_by" uuid,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product_supplier" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"product_id" uuid NOT NULL,
|
||||
"supplier_id" uuid NOT NULL,
|
||||
"supplier_sku" varchar(100),
|
||||
"is_preferred" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "product" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"location_id" uuid,
|
||||
"sku" varchar(100),
|
||||
"upc" varchar(100),
|
||||
"name" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"brand" varchar(255),
|
||||
"model" varchar(255),
|
||||
"category_id" uuid,
|
||||
"is_serialized" boolean DEFAULT false NOT NULL,
|
||||
"is_rental" boolean DEFAULT false NOT NULL,
|
||||
"is_dual_use_repair" boolean DEFAULT false NOT NULL,
|
||||
"price" numeric(10, 2),
|
||||
"min_price" numeric(10, 2),
|
||||
"rental_rate_monthly" numeric(10, 2),
|
||||
"qty_on_hand" integer DEFAULT 0 NOT NULL,
|
||||
"qty_reorder_point" integer,
|
||||
"is_active" boolean DEFAULT true NOT NULL,
|
||||
"legacy_id" varchar(255),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "stock_receipt" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"location_id" uuid,
|
||||
"product_id" uuid NOT NULL,
|
||||
"supplier_id" uuid,
|
||||
"inventory_unit_id" uuid,
|
||||
"qty" integer DEFAULT 1 NOT NULL,
|
||||
"cost_per_unit" numeric(10, 2) NOT NULL,
|
||||
"total_cost" numeric(10, 2) NOT NULL,
|
||||
"received_date" date NOT NULL,
|
||||
"received_by" uuid,
|
||||
"invoice_number" varchar(100),
|
||||
"notes" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "inventory_unit" ADD CONSTRAINT "inventory_unit_product_id_product_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."product"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "inventory_unit" ADD CONSTRAINT "inventory_unit_company_id_company_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."company"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "inventory_unit" ADD CONSTRAINT "inventory_unit_location_id_location_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "price_history" ADD CONSTRAINT "price_history_product_id_product_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."product"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "price_history" ADD CONSTRAINT "price_history_company_id_company_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."company"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "product_supplier" ADD CONSTRAINT "product_supplier_product_id_product_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."product"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "product_supplier" ADD CONSTRAINT "product_supplier_supplier_id_supplier_id_fk" FOREIGN KEY ("supplier_id") REFERENCES "public"."supplier"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "product" ADD CONSTRAINT "product_company_id_company_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."company"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "product" ADD CONSTRAINT "product_location_id_location_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "product" ADD CONSTRAINT "product_category_id_category_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."category"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "stock_receipt" ADD CONSTRAINT "stock_receipt_company_id_company_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."company"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "stock_receipt" ADD CONSTRAINT "stock_receipt_location_id_location_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "stock_receipt" ADD CONSTRAINT "stock_receipt_product_id_product_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."product"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "stock_receipt" ADD CONSTRAINT "stock_receipt_supplier_id_supplier_id_fk" FOREIGN KEY ("supplier_id") REFERENCES "public"."supplier"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "stock_receipt" ADD CONSTRAINT "stock_receipt_inventory_unit_id_inventory_unit_id_fk" FOREIGN KEY ("inventory_unit_id") REFERENCES "public"."inventory_unit"("id") ON DELETE no action ON UPDATE no action;
|
||||
1516
packages/backend/src/db/migrations/meta/0005_snapshot.json
Normal file
1516
packages/backend/src/db/migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
||||
"when": 1774652800605,
|
||||
"tag": "0004_peaceful_wendell_rand",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1774653515690,
|
||||
"tag": "0005_add_products_units_receipts",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
numeric,
|
||||
date,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { companies, locations } from './stores.js'
|
||||
|
||||
export const categories = pgTable('category', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -33,7 +44,130 @@ export const suppliers = pgTable('supplier', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const conditionEnum = pgEnum('item_condition', [
|
||||
'new',
|
||||
'excellent',
|
||||
'good',
|
||||
'fair',
|
||||
'poor',
|
||||
])
|
||||
|
||||
export const unitStatusEnum = pgEnum('unit_status', [
|
||||
'available',
|
||||
'sold',
|
||||
'rented',
|
||||
'in_repair',
|
||||
'retired',
|
||||
])
|
||||
|
||||
export const products = pgTable('product', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
sku: varchar('sku', { length: 100 }),
|
||||
upc: varchar('upc', { length: 100 }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
brand: varchar('brand', { length: 255 }),
|
||||
model: varchar('model', { length: 255 }),
|
||||
categoryId: uuid('category_id').references(() => categories.id),
|
||||
isSerialized: boolean('is_serialized').notNull().default(false),
|
||||
isRental: boolean('is_rental').notNull().default(false),
|
||||
isDualUseRepair: boolean('is_dual_use_repair').notNull().default(false),
|
||||
price: numeric('price', { precision: 10, scale: 2 }),
|
||||
minPrice: numeric('min_price', { precision: 10, scale: 2 }),
|
||||
rentalRateMonthly: numeric('rental_rate_monthly', { precision: 10, scale: 2 }),
|
||||
qtyOnHand: integer('qty_on_hand').notNull().default(0),
|
||||
qtyReorderPoint: integer('qty_reorder_point'),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
legacyId: varchar('legacy_id', { length: 255 }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const inventoryUnits = pgTable('inventory_unit', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
serialNumber: varchar('serial_number', { length: 255 }),
|
||||
condition: conditionEnum('condition').notNull().default('new'),
|
||||
status: unitStatusEnum('status').notNull().default('available'),
|
||||
purchaseDate: date('purchase_date'),
|
||||
purchaseCost: numeric('purchase_cost', { precision: 10, scale: 2 }),
|
||||
notes: text('notes'),
|
||||
legacyId: varchar('legacy_id', { length: 255 }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const productSuppliers = pgTable('product_supplier', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
supplierId: uuid('supplier_id')
|
||||
.notNull()
|
||||
.references(() => suppliers.id),
|
||||
supplierSku: varchar('supplier_sku', { length: 100 }),
|
||||
isPreferred: boolean('is_preferred').notNull().default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export type Category = typeof categories.$inferSelect
|
||||
export type CategoryInsert = typeof categories.$inferInsert
|
||||
export type Supplier = typeof suppliers.$inferSelect
|
||||
export type SupplierInsert = typeof suppliers.$inferInsert
|
||||
export const stockReceipts = pgTable('stock_receipt', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
supplierId: uuid('supplier_id').references(() => suppliers.id),
|
||||
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
|
||||
qty: integer('qty').notNull().default(1),
|
||||
costPerUnit: numeric('cost_per_unit', { precision: 10, scale: 2 }).notNull(),
|
||||
totalCost: numeric('total_cost', { precision: 10, scale: 2 }).notNull(),
|
||||
receivedDate: date('received_date').notNull(),
|
||||
receivedBy: uuid('received_by'),
|
||||
invoiceNumber: varchar('invoice_number', { length: 100 }),
|
||||
notes: text('notes'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const priceHistory = pgTable('price_history', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
previousPrice: numeric('previous_price', { precision: 10, scale: 2 }),
|
||||
newPrice: numeric('new_price', { precision: 10, scale: 2 }).notNull(),
|
||||
previousMinPrice: numeric('previous_min_price', { precision: 10, scale: 2 }),
|
||||
newMinPrice: numeric('new_min_price', { precision: 10, scale: 2 }),
|
||||
reason: text('reason'),
|
||||
changedBy: uuid('changed_by'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export type PriceHistory = typeof priceHistory.$inferSelect
|
||||
|
||||
export type StockReceipt = typeof stockReceipts.$inferSelect
|
||||
export type StockReceiptInsert = typeof stockReceipts.$inferInsert
|
||||
|
||||
export type Product = typeof products.$inferSelect
|
||||
export type ProductInsert = typeof products.$inferInsert
|
||||
export type InventoryUnit = typeof inventoryUnits.$inferSelect
|
||||
export type InventoryUnitInsert = typeof inventoryUnits.$inferInsert
|
||||
export type ProductSupplier = typeof productSuppliers.$inferSelect
|
||||
|
||||
Reference in New Issue
Block a user