feat(db): reshape user and inventory schema

This commit is contained in:
2026-06-19 01:05:33 +02:00
parent 1a95bf4613
commit 2ed9445f7f
2 changed files with 1107 additions and 183 deletions
@@ -1,66 +1,111 @@
-- CreateEnum -- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER'); CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER');
-- CreateEnum
CREATE TYPE "UserStatus" AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DISABLED');
-- CreateEnum -- CreateEnum
CREATE TYPE "PersonDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER'); CREATE TYPE "PersonDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER');
-- CreateEnum -- CreateEnum
CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED'); CREATE TYPE "ItemTrackingType" AS ENUM ('QUANTITY', 'SERIALIZED');
-- CreateEnum -- CreateEnum
CREATE TYPE "MovementType" AS ENUM ('IN', 'OUT', 'ASSIGNMENT', 'RETURN', 'ADJUSTMENT', 'DELETED'); CREATE TYPE "ItemStatus" AS ENUM ('ACTIVE', 'DISCONTINUED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "AssetStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'IN_REPAIR', 'BROKEN', 'LOST', 'STOLEN', 'DISPOSED', 'RETIRED');
-- CreateEnum
CREATE TYPE "AssignmentStatus" AS ENUM ('OPEN', 'PARTIALLY_RETURNED', 'RETURNED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "InventoryMovementType" AS ENUM ('RECEIPT', 'ISSUE', 'ASSIGNMENT', 'RETURN', 'ADJUSTMENT', 'STATUS_CHANGE', 'DISPOSAL', 'INITIAL_LOAD');
-- CreateEnum
CREATE TYPE "InventoryMovementReason" AS ENUM ('PURCHASE', 'MANUAL_ENTRY', 'EMPLOYEE_ASSIGNMENT', 'EMPLOYEE_RETURN', 'INVENTORY_CORRECTION', 'DAMAGE', 'REPAIR', 'REPAIR_RETURN', 'LOSS', 'THEFT', 'DISPOSAL', 'INITIAL_LOAD', 'OTHER');
-- CreateEnum
CREATE TYPE "StockAlertStatus" AS ENUM ('OPEN', 'ACKNOWLEDGED', 'RESOLVED');
-- CreateEnum
CREATE TYPE "StockAlertTrigger" AS ENUM ('BELOW_MINIMUM', 'OUT_OF_STOCK');
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"email" TEXT NOT NULL, "email" TEXT NOT NULL,
"password" TEXT NOT NULL, "emailNormalized" TEXT NOT NULL,
"passwordHash" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'STAFF', "role" "UserRole" NOT NULL DEFAULT 'STAFF',
"isActive" BOOLEAN NOT NULL DEFAULT true, "status" "UserStatus" NOT NULL DEFAULT 'INVITED',
"deletedAt" TIMESTAMP(3),
"invitedAt" TIMESTAMP(3),
"activatedAt" TIMESTAMP(3),
"passwordChangedAt" TIMESTAMP(3),
"lastLoginAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id") CONSTRAINT "User_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "UserInvitation" (
"id" UUID NOT NULL,
"userId" UUID NOT NULL,
"tokenHash" TEXT NOT NULL,
"invitedById" UUID NOT NULL,
"email" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"acceptedAt" TIMESTAMP(3),
"revokedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserInvitation_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "Person" ( CREATE TABLE "Person" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"firstName" TEXT NOT NULL, "firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL, "lastName" TEXT NOT NULL,
"department" "PersonDepartment", "department" "PersonDepartment",
"email" TEXT, "email" TEXT,
"phone" TEXT, "phone" TEXT,
"userId" TEXT, "userId" UUID,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "Person_pkey" PRIMARY KEY ("id") CONSTRAINT "Person_pkey" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "Category" ( CREATE TABLE "Category" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"description" TEXT, "description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "Category_pkey" PRIMARY KEY ("id") CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "Item" ( CREATE TABLE "Item" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"sku" TEXT NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"description" TEXT, "description" TEXT,
"categoryId" TEXT NOT NULL, "trackingType" "ItemTrackingType" NOT NULL,
"status" "ItemStatus" NOT NULL DEFAULT 'ACTIVE',
"categoryId" UUID NOT NULL,
"stock" INTEGER NOT NULL DEFAULT 0, "stock" INTEGER NOT NULL DEFAULT 0,
"minStock" INTEGER, "minStock" INTEGER,
"maxStock" INTEGER, "targetStock" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3), "deletedAt" TIMESTAMP(3),
@@ -70,29 +115,38 @@ CREATE TABLE "Item" (
-- CreateTable -- CreateTable
CREATE TABLE "Asset" ( CREATE TABLE "Asset" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"itemId" TEXT, "assetTag" TEXT,
"serialNumber" TEXT NOT NULL, "serialNumber" TEXT NOT NULL,
"itemId" UUID NOT NULL,
"status" "AssetStatus" NOT NULL DEFAULT 'AVAILABLE',
"manufacturer" TEXT,
"model" TEXT,
"deliveryNote" TEXT, "deliveryNote" TEXT,
"status" "ItemStatus" NOT NULL DEFAULT 'AVAILABLE', "invoiceNumber" TEXT,
"purchaseDate" TIMESTAMP(3),
"purchasePrice" DECIMAL(12,2),
"warrantyEndsAt" TIMESTAMP(3),
"notes" TEXT, "notes" TEXT,
"retiredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "Asset_pkey" PRIMARY KEY ("id") CONSTRAINT "Asset_pkey" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "Assignment" ( CREATE TABLE "Assignment" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"quantity" INTEGER, "personId" UUID NOT NULL,
"status" "AssignmentStatus" NOT NULL DEFAULT 'OPEN',
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"dueAt" TIMESTAMP(3),
"closedAt" TIMESTAMP(3),
"notes" TEXT, "notes" TEXT,
"itemId" TEXT, "createdById" UUID NOT NULL,
"assetId" TEXT, "closedById" UUID,
"personId" TEXT,
"assignmentDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"returnDate" TIMESTAMP(3),
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
@@ -100,29 +154,135 @@ CREATE TABLE "Assignment" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "Movement" ( CREATE TABLE "AssignmentStockLine" (
"id" TEXT NOT NULL, "id" UUID NOT NULL,
"type" "MovementType" NOT NULL DEFAULT 'IN', "assignmentId" UUID NOT NULL,
"itemId" UUID NOT NULL,
"quantity" INTEGER NOT NULL, "quantity" INTEGER NOT NULL,
"details" TEXT, "returnedQuantity" INTEGER NOT NULL DEFAULT 0,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AssignmentStockLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AssignmentStockReturn" (
"id" UUID NOT NULL,
"assignmentLineId" UUID NOT NULL,
"quantity" INTEGER NOT NULL,
"returnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"receivedById" UUID NOT NULL,
"notes" TEXT, "notes" TEXT,
"itemId" TEXT,
"assetId" TEXT,
"previousStock" INTEGER,
"newStock" INTEGER,
"personId" TEXT,
"assignmentId" TEXT,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Movement_pkey" PRIMARY KEY ("id") CONSTRAINT "AssignmentStockReturn_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AssignmentAssetLine" (
"id" UUID NOT NULL,
"assignmentId" UUID NOT NULL,
"assetId" UUID NOT NULL,
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"returnedAt" TIMESTAMP(3),
"returnedById" UUID,
"returnStatus" "AssetStatus",
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AssignmentAssetLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InventoryMovement" (
"id" UUID NOT NULL,
"type" "InventoryMovementType" NOT NULL,
"reason" "InventoryMovementReason" NOT NULL,
"assignmentId" UUID,
"reference" TEXT,
"details" TEXT,
"notes" TEXT,
"performedById" UUID NOT NULL,
"occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InventoryMovement_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StockMovementLine" (
"id" UUID NOT NULL,
"movementId" UUID NOT NULL,
"itemId" UUID NOT NULL,
"stockDelta" INTEGER NOT NULL,
"previousStock" INTEGER NOT NULL,
"newStock" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "StockMovementLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AssetMovementLine" (
"id" UUID NOT NULL,
"movementId" UUID NOT NULL,
"assetId" UUID NOT NULL,
"previousStatus" "AssetStatus",
"newStatus" "AssetStatus" NOT NULL,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AssetMovementLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StockAlert" (
"id" UUID NOT NULL,
"itemId" UUID NOT NULL,
"trigger" "StockAlertTrigger" NOT NULL,
"status" "StockAlertStatus" NOT NULL DEFAULT 'OPEN',
"availableStock" INTEGER NOT NULL,
"minimumStock" INTEGER NOT NULL,
"suggestedPurchase" INTEGER,
"triggeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"acknowledgedAt" TIMESTAMP(3),
"acknowledgedById" UUID,
"resolvedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "StockAlert_pkey" PRIMARY KEY ("id")
); );
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); CREATE UNIQUE INDEX "User_emailNormalized_key" ON "User"("emailNormalized");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Person_email_key" ON "Person"("email"); CREATE INDEX "User_status_idx" ON "User"("status");
-- CreateIndex
CREATE INDEX "User_deletedAt_idx" ON "User"("deletedAt");
-- CreateIndex
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "UserInvitation_tokenHash_key" ON "UserInvitation"("tokenHash");
-- CreateIndex
CREATE INDEX "UserInvitation_userId_idx" ON "UserInvitation"("userId");
-- CreateIndex
CREATE INDEX "UserInvitation_expiresAt_idx" ON "UserInvitation"("expiresAt");
-- CreateIndex
CREATE INDEX "UserInvitation_acceptedAt_idx" ON "UserInvitation"("acceptedAt");
-- CreateIndex
CREATE INDEX "UserInvitation_revokedAt_idx" ON "UserInvitation"("revokedAt");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId"); CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId");
@@ -131,61 +291,127 @@ CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId");
CREATE INDEX "Person_lastName_firstName_idx" ON "Person"("lastName", "firstName"); CREATE INDEX "Person_lastName_firstName_idx" ON "Person"("lastName", "firstName");
-- CreateIndex -- CreateIndex
CREATE INDEX "Person_department_idx" ON "Person"("department"); CREATE INDEX "Person_department_deletedAt_idx" ON "Person"("department", "deletedAt");
-- CreateIndex
CREATE INDEX "Person_deletedAt_idx" ON "Person"("deletedAt");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
-- CreateIndex -- CreateIndex
CREATE INDEX "Category_name_idx" ON "Category"("name"); CREATE INDEX "Category_deletedAt_idx" ON "Category"("deletedAt");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Item_name_key" ON "Item"("name"); CREATE UNIQUE INDEX "Item_sku_key" ON "Item"("sku");
-- CreateIndex -- CreateIndex
CREATE INDEX "Item_categoryId_idx" ON "Item"("categoryId"); CREATE INDEX "Item_categoryId_status_idx" ON "Item"("categoryId", "status");
-- CreateIndex
CREATE INDEX "Item_trackingType_status_idx" ON "Item"("trackingType", "status");
-- CreateIndex
CREATE INDEX "Item_name_idx" ON "Item"("name");
-- CreateIndex
CREATE INDEX "Item_deletedAt_idx" ON "Item"("deletedAt");
-- CreateIndex
CREATE UNIQUE INDEX "Asset_assetTag_key" ON "Asset"("assetTag");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Asset_serialNumber_key" ON "Asset"("serialNumber"); CREATE UNIQUE INDEX "Asset_serialNumber_key" ON "Asset"("serialNumber");
-- CreateIndex -- CreateIndex
CREATE INDEX "Asset_serialNumber_idx" ON "Asset"("serialNumber"); CREATE INDEX "Asset_itemId_status_idx" ON "Asset"("itemId", "status");
-- CreateIndex
CREATE INDEX "Asset_itemId_idx" ON "Asset"("itemId");
-- CreateIndex -- CreateIndex
CREATE INDEX "Asset_status_idx" ON "Asset"("status"); CREATE INDEX "Asset_status_idx" ON "Asset"("status");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Assignment_assetId_key" ON "Assignment"("assetId"); CREATE INDEX "Asset_createdAt_idx" ON "Asset"("createdAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Assignment_itemId_idx" ON "Assignment"("itemId"); CREATE INDEX "Asset_deletedAt_idx" ON "Asset"("deletedAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Assignment_assetId_idx" ON "Assignment"("assetId"); CREATE INDEX "Assignment_personId_status_idx" ON "Assignment"("personId", "status");
-- CreateIndex -- CreateIndex
CREATE INDEX "Assignment_personId_idx" ON "Assignment"("personId"); CREATE INDEX "Assignment_personId_assignedAt_idx" ON "Assignment"("personId", "assignedAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Assignment_createdBy_idx" ON "Assignment"("createdBy"); CREATE INDEX "Assignment_status_assignedAt_idx" ON "Assignment"("status", "assignedAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_itemId_idx" ON "Movement"("itemId"); CREATE INDEX "Assignment_dueAt_idx" ON "Assignment"("dueAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_assetId_idx" ON "Movement"("assetId"); CREATE INDEX "Assignment_createdById_createdAt_idx" ON "Assignment"("createdById", "createdAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_personId_idx" ON "Movement"("personId"); CREATE INDEX "AssignmentStockLine_assignmentId_idx" ON "AssignmentStockLine"("assignmentId");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_type_idx" ON "Movement"("type"); CREATE INDEX "AssignmentStockLine_itemId_createdAt_idx" ON "AssignmentStockLine"("itemId", "createdAt");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_userId_idx" ON "Movement"("userId"); CREATE INDEX "AssignmentStockReturn_assignmentLineId_returnedAt_idx" ON "AssignmentStockReturn"("assignmentLineId", "returnedAt");
-- CreateIndex
CREATE INDEX "AssignmentStockReturn_receivedById_returnedAt_idx" ON "AssignmentStockReturn"("receivedById", "returnedAt");
-- CreateIndex
CREATE INDEX "AssignmentAssetLine_assignmentId_idx" ON "AssignmentAssetLine"("assignmentId");
-- CreateIndex
CREATE INDEX "AssignmentAssetLine_assetId_assignedAt_idx" ON "AssignmentAssetLine"("assetId", "assignedAt");
-- CreateIndex
CREATE INDEX "AssignmentAssetLine_returnedAt_idx" ON "AssignmentAssetLine"("returnedAt");
-- CreateIndex
CREATE INDEX "InventoryMovement_type_occurredAt_idx" ON "InventoryMovement"("type", "occurredAt");
-- CreateIndex
CREATE INDEX "InventoryMovement_reason_occurredAt_idx" ON "InventoryMovement"("reason", "occurredAt");
-- CreateIndex
CREATE INDEX "InventoryMovement_assignmentId_idx" ON "InventoryMovement"("assignmentId");
-- CreateIndex
CREATE INDEX "InventoryMovement_performedById_occurredAt_idx" ON "InventoryMovement"("performedById", "occurredAt");
-- CreateIndex
CREATE INDEX "InventoryMovement_occurredAt_idx" ON "InventoryMovement"("occurredAt");
-- CreateIndex
CREATE INDEX "StockMovementLine_movementId_idx" ON "StockMovementLine"("movementId");
-- CreateIndex
CREATE INDEX "StockMovementLine_itemId_createdAt_idx" ON "StockMovementLine"("itemId", "createdAt");
-- CreateIndex
CREATE INDEX "AssetMovementLine_assetId_createdAt_idx" ON "AssetMovementLine"("assetId", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "AssetMovementLine_movementId_assetId_key" ON "AssetMovementLine"("movementId", "assetId");
-- CreateIndex
CREATE INDEX "StockAlert_itemId_status_idx" ON "StockAlert"("itemId", "status");
-- CreateIndex
CREATE INDEX "StockAlert_status_triggeredAt_idx" ON "StockAlert"("status", "triggeredAt");
-- CreateIndex
CREATE INDEX "StockAlert_trigger_triggeredAt_idx" ON "StockAlert"("trigger", "triggeredAt");
-- AddForeignKey
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Person" ADD CONSTRAINT "Person_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Person" ADD CONSTRAINT "Person_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -194,31 +420,335 @@ ALTER TABLE "Person" ADD CONSTRAINT "Person_userId_fkey" FOREIGN KEY ("userId")
ALTER TABLE "Item" ADD CONSTRAINT "Item_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Item" ADD CONSTRAINT "Item_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Asset" ADD CONSTRAINT "Asset_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Asset" ADD CONSTRAINT "Asset_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "AssignmentStockLine" ADD CONSTRAINT "AssignmentStockLine_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "AssignmentStockLine" ADD CONSTRAINT "AssignmentStockLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "AssignmentStockReturn" ADD CONSTRAINT "AssignmentStockReturn_assignmentLineId_fkey" FOREIGN KEY ("assignmentLineId") REFERENCES "AssignmentStockLine"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "AssignmentStockReturn" ADD CONSTRAINT "AssignmentStockReturn_receivedById_fkey" FOREIGN KEY ("receivedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "AssignmentAssetLine" ADD CONSTRAINT "AssignmentAssetLine_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "AssignmentAssetLine" ADD CONSTRAINT "AssignmentAssetLine_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AssignmentAssetLine" ADD CONSTRAINT "AssignmentAssetLine_returnedById_fkey" FOREIGN KEY ("returnedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InventoryMovement" ADD CONSTRAINT "InventoryMovement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InventoryMovement" ADD CONSTRAINT "InventoryMovement_performedById_fkey" FOREIGN KEY ("performedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockMovementLine" ADD CONSTRAINT "StockMovementLine_movementId_fkey" FOREIGN KEY ("movementId") REFERENCES "InventoryMovement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockMovementLine" ADD CONSTRAINT "StockMovementLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AssetMovementLine" ADD CONSTRAINT "AssetMovementLine_movementId_fkey" FOREIGN KEY ("movementId") REFERENCES "InventoryMovement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AssetMovementLine" ADD CONSTRAINT "AssetMovementLine_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockAlert" ADD CONSTRAINT "StockAlert_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockAlert" ADD CONSTRAINT "StockAlert_acknowledgedById_fkey" FOREIGN KEY ("acknowledgedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- =====================================================
-- USER INVITATION / ACTIVATION
-- =====================================================
ALTER TABLE "User"
ADD CONSTRAINT "User_invited_without_password"
CHECK (
"status" <> 'INVITED'
OR "passwordHash" IS NULL
);
ALTER TABLE "User"
ADD CONSTRAINT "User_active_requires_password"
CHECK (
"status" <> 'ACTIVE'
OR "passwordHash" IS NOT NULL
);
ALTER TABLE "User"
ADD CONSTRAINT "User_active_requires_activation_date"
CHECK (
"status" <> 'ACTIVE'
OR "activatedAt" IS NOT NULL
);
ALTER TABLE "User"
ADD CONSTRAINT "User_activation_date_after_invitation"
CHECK (
"activatedAt" IS NULL
OR "invitedAt" IS NULL
OR "activatedAt" >= "invitedAt"
);
ALTER TABLE "User"
ADD CONSTRAINT "User_password_changed_after_invitation"
CHECK (
"passwordChangedAt" IS NULL
OR "invitedAt" IS NULL
OR "passwordChangedAt" >= "invitedAt"
);
ALTER TABLE "UserInvitation"
ADD CONSTRAINT "UserInvitation_expiry_after_creation"
CHECK ("expiresAt" > "createdAt");
ALTER TABLE "UserInvitation"
ADD CONSTRAINT "UserInvitation_accepted_or_revoked"
CHECK (
"acceptedAt" IS NULL
OR "revokedAt" IS NULL
);
ALTER TABLE "UserInvitation"
ADD CONSTRAINT "UserInvitation_accepted_after_creation"
CHECK (
"acceptedAt" IS NULL
OR "acceptedAt" >= "createdAt"
);
ALTER TABLE "UserInvitation"
ADD CONSTRAINT "UserInvitation_revoked_after_creation"
CHECK (
"revokedAt" IS NULL
OR "revokedAt" >= "createdAt"
);
CREATE UNIQUE INDEX "UserInvitation_active_user_key"
ON "UserInvitation" ("userId")
WHERE "acceptedAt" IS NULL
AND "revokedAt" IS NULL;
-- =====================================================
-- ITEM STOCK
-- =====================================================
ALTER TABLE "Item"
ADD CONSTRAINT "Item_stock_non_negative"
CHECK ("stock" >= 0);
ALTER TABLE "Item"
ADD CONSTRAINT "Item_min_stock_non_negative"
CHECK (
"minStock" IS NULL
OR "minStock" >= 0
);
ALTER TABLE "Item"
ADD CONSTRAINT "Item_target_stock_non_negative"
CHECK (
"targetStock" IS NULL
OR "targetStock" >= 0
);
ALTER TABLE "Item"
ADD CONSTRAINT "Item_target_not_below_minimum"
CHECK (
"minStock" IS NULL
OR "targetStock" IS NULL
OR "targetStock" >= "minStock"
);
ALTER TABLE "Item"
ADD CONSTRAINT "Item_serialized_stock_zero"
CHECK (
"trackingType" <> 'SERIALIZED'
OR "stock" = 0
);
-- =====================================================
-- ASSET DATA
-- =====================================================
ALTER TABLE "Asset"
ADD CONSTRAINT "Asset_purchase_price_non_negative"
CHECK (
"purchasePrice" IS NULL
OR "purchasePrice" >= 0
);
ALTER TABLE "Asset"
ADD CONSTRAINT "Asset_warranty_date_valid"
CHECK (
"warrantyEndsAt" IS NULL
OR "purchaseDate" IS NULL
OR "warrantyEndsAt" >= "purchaseDate"
);
ALTER TABLE "Asset"
ADD CONSTRAINT "Asset_retired_date_valid"
CHECK (
"retiredAt" IS NULL
OR "retiredAt" >= "createdAt"
);
-- =====================================================
-- ASSIGNMENTS
-- =====================================================
ALTER TABLE "Assignment"
ADD CONSTRAINT "Assignment_due_date_valid"
CHECK (
"dueAt" IS NULL
OR "dueAt" >= "assignedAt"
);
ALTER TABLE "Assignment"
ADD CONSTRAINT "Assignment_closed_date_valid"
CHECK (
"closedAt" IS NULL
OR "closedAt" >= "assignedAt"
);
-- =====================================================
-- QUANTITY ASSIGNMENTS
-- =====================================================
ALTER TABLE "AssignmentStockLine"
ADD CONSTRAINT "AssignmentStockLine_quantity_positive"
CHECK ("quantity" > 0);
ALTER TABLE "AssignmentStockLine"
ADD CONSTRAINT "AssignmentStockLine_returned_non_negative"
CHECK ("returnedQuantity" >= 0);
ALTER TABLE "AssignmentStockLine"
ADD CONSTRAINT "AssignmentStockLine_returned_not_greater"
CHECK ("returnedQuantity" <= "quantity");
ALTER TABLE "AssignmentStockReturn"
ADD CONSTRAINT "AssignmentStockReturn_quantity_positive"
CHECK ("quantity" > 0);
-- =====================================================
-- SERIALIZED ASSET ASSIGNMENTS
-- =====================================================
ALTER TABLE "AssignmentAssetLine"
ADD CONSTRAINT "AssignmentAssetLine_return_date_valid"
CHECK (
"returnedAt" IS NULL
OR "returnedAt" >= "assignedAt"
);
ALTER TABLE "AssignmentAssetLine"
ADD CONSTRAINT "AssignmentAssetLine_return_data_consistent"
CHECK (
(
"returnedAt" IS NULL
AND "returnedById" IS NULL
AND "returnStatus" IS NULL
)
OR
(
"returnedAt" IS NOT NULL
AND "returnedById" IS NOT NULL
AND "returnStatus" IS NOT NULL
)
);
CREATE UNIQUE INDEX "AssignmentAssetLine_active_asset_key"
ON "AssignmentAssetLine" ("assetId")
WHERE "returnedAt" IS NULL;
-- =====================================================
-- STOCK MOVEMENTS
-- =====================================================
ALTER TABLE "StockMovementLine"
ADD CONSTRAINT "StockMovementLine_stock_consistency"
CHECK (
"newStock" = "previousStock" + "stockDelta"
);
ALTER TABLE "StockMovementLine"
ADD CONSTRAINT "StockMovementLine_previous_stock_non_negative"
CHECK ("previousStock" >= 0);
ALTER TABLE "StockMovementLine"
ADD CONSTRAINT "StockMovementLine_new_stock_non_negative"
CHECK ("newStock" >= 0);
ALTER TABLE "StockMovementLine"
ADD CONSTRAINT "StockMovementLine_delta_not_zero"
CHECK ("stockDelta" <> 0);
-- =====================================================
-- STOCK ALERTS
-- =====================================================
ALTER TABLE "StockAlert"
ADD CONSTRAINT "StockAlert_available_stock_non_negative"
CHECK ("availableStock" >= 0);
ALTER TABLE "StockAlert"
ADD CONSTRAINT "StockAlert_minimum_stock_non_negative"
CHECK ("minimumStock" >= 0);
ALTER TABLE "StockAlert"
ADD CONSTRAINT "StockAlert_suggested_purchase_non_negative"
CHECK (
"suggestedPurchase" IS NULL
OR "suggestedPurchase" >= 0
);
ALTER TABLE "StockAlert"
ADD CONSTRAINT "StockAlert_acknowledgement_consistent"
CHECK (
(
"acknowledgedAt" IS NULL
AND "acknowledgedById" IS NULL
)
OR
(
"acknowledgedAt" IS NOT NULL
AND "acknowledgedById" IS NOT NULL
)
);
ALTER TABLE "StockAlert"
ADD CONSTRAINT "StockAlert_resolution_date_valid"
CHECK (
"resolvedAt" IS NULL
OR "resolvedAt" >= "triggeredAt"
);
CREATE UNIQUE INDEX "StockAlert_active_item_trigger_key"
ON "StockAlert" ("itemId", "trigger")
WHERE "status" IN ('OPEN', 'ACKNOWLEDGED');
+511 -117
View File
@@ -14,6 +14,10 @@ datasource db {
provider = "postgresql" provider = "postgresql"
} }
// ======================================================
// USERS
// ======================================================
enum UserRole { enum UserRole {
ADMIN ADMIN
MANAGER MANAGER
@@ -21,20 +25,91 @@ enum UserRole {
VIEWER VIEWER
} }
model User { enum UserStatus {
id String @id @default(uuid()) INVITED
name String ACTIVE
email String @unique SUSPENDED
password String DISABLED
role UserRole @default(STAFF)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
movements Movement[]
assignments Assignment[]
person Person?
} }
model User {
id String @id @default(uuid(7)) @db.Uuid
name String
email String
emailNormalized String @unique
/**
* Nulo mientras el usuario no haya aceptado la invitación.
*/
passwordHash String?
role UserRole @default(STAFF)
status UserStatus @default(INVITED)
deletedAt DateTime?
invitedAt DateTime?
activatedAt DateTime?
passwordChangedAt DateTime?
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
person Person?
createdAssignments Assignment[] @relation("AssignmentCreatedBy")
closedAssignments Assignment[] @relation("AssignmentClosedBy")
receivedStockReturns AssignmentStockReturn[]
receivedAssetReturns AssignmentAssetLine[] @relation("AssetReturnedBy")
movements InventoryMovement[]
acknowledgedStockAlerts StockAlert[] @relation("StockAlertAcknowledgedBy")
sentInvitations UserInvitation[] @relation("UserInvitationInvitedBy")
invitations UserInvitation[]
@@index([status])
@@index([deletedAt])
@@index([createdAt])
}
model UserInvitation {
id String @id @default(uuid(7)) @db.Uuid
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
/**
* Hash del token de invitación.
* Nunca guardar el token plano.
*/
tokenHash String @unique
invitedById String @db.Uuid
invitedBy User @relation("UserInvitationInvitedBy", fields: [invitedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
email String
expiresAt DateTime
acceptedAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
@@index([acceptedAt])
@@index([revokedAt])
}
// ======================================================
// PEOPLE
// ======================================================
enum PersonDepartment { enum PersonDepartment {
IT IT
ENGINEERING ENGINEERING
@@ -47,139 +122,458 @@ enum PersonDepartment {
} }
model Person { model Person {
id String @id @default(uuid()) id String @id @default(uuid(7)) @db.Uuid
firstName String firstName String
lastName String lastName String
department PersonDepartment? department PersonDepartment?
email String? @unique
phone String? email String?
userId String? @unique phone String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
isActive Boolean @default(true) userId String? @unique @db.Uuid
createdAt DateTime @default(now()) user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
assignments Assignment[] assignments Assignment[]
movements Movement[]
@@index([lastName, firstName]) @@index([lastName, firstName])
@@index([department]) @@index([department, deletedAt])
@@index([deletedAt])
} }
model Category { // ======================================================
id String @id @default(uuid()) // CATALOG
name String @unique // ======================================================
description String?
isActive Boolean @default(true)
items Item[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name]) enum ItemTrackingType {
QUANTITY
SERIALIZED
} }
enum ItemStatus { enum ItemStatus {
AVAILABLE ACTIVE
ASSIGNED DISCONTINUED
RESERVED ARCHIVED
IN_REPAIR }
BROKEN
STOLEN model Category {
DISPOSED id String @id @default(uuid(7)) @db.Uuid
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
items Item[]
@@index([deletedAt])
} }
model Item { model Item {
id String @id @default(uuid()) id String @id @default(uuid(7)) @db.Uuid
name String @unique sku String @unique
name String
description String? description String?
categoryId String
category Category @relation(fields: [categoryId], references: [id])
stock Int @default(0)
minStock Int?
maxStock Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
movements Movement[]
assignments Assignment[]
assets Asset[]
@@index([categoryId]) trackingType ItemTrackingType
status ItemStatus @default(ACTIVE)
categoryId String @db.Uuid
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict, onUpdate: Cascade)
/**
* Solo se utiliza para artículos QUANTITY.
* Para artículos SERIALIZED, las existencias se obtienen
* contando los activos AVAILABLE.
*/
stock Int @default(0)
/**
* Umbral de alerta.
* QUANTITY:
* Se compara contra Item.stock.
* SERIALIZED:
* Se compara contra número de Asset AVAILABLE.
*/
minStock Int?
/**
* Nivel deseado tras reposición.
* Compra sugerida:
* targetStock - stock disponible.
*/
targetStock Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
assets Asset[]
assignmentStockLines AssignmentStockLine[]
stockMovementLines StockMovementLine[]
stockAlerts StockAlert[]
@@index([categoryId, status])
@@index([trackingType, status])
@@index([name])
@@index([deletedAt])
}
// ======================================================
// SERIALIZED ASSETS
// ======================================================
enum AssetStatus {
AVAILABLE
ASSIGNED
IN_REPAIR
BROKEN
LOST
STOLEN
DISPOSED
RETIRED
} }
model Asset { model Asset {
id String @id @default(uuid()) id String @id @default(uuid(7)) @db.Uuid
itemId String?
item Item? @relation(fields: [itemId], references: [id])
serialNumber String @unique
deliveryNote String?
status ItemStatus @default(AVAILABLE)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
movements Movement[]
assignment Assignment?
@@index([serialNumber]) /**
@@index([itemId]) * Identificador interno visible.
* Ejemplos:
* IT-000001
* LAP-000042
* MON-000117
*/
assetTag String? @unique
/**
* Número de serie del fabricante.
* Puede ser nulo.
*/
serialNumber String @unique
itemId String @db.Uuid
item Item @relation(fields: [itemId], references: [id], onDelete: Restrict, onUpdate: Cascade)
status AssetStatus @default(AVAILABLE)
manufacturer String?
model String?
deliveryNote String?
invoiceNumber String?
purchaseDate DateTime?
purchasePrice Decimal? @db.Decimal(12, 2)
warrantyEndsAt DateTime?
notes String?
retiredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
assignmentLines AssignmentAssetLine[]
movementLines AssetMovementLine[]
@@index([itemId, status])
@@index([status]) @@index([status])
@@index([createdAt])
@@index([deletedAt])
}
// ======================================================
// ASSIGNMENTS
// ======================================================
enum AssignmentStatus {
OPEN
PARTIALLY_RETURNED
RETURNED
CANCELLED
} }
model Assignment { model Assignment {
id String @id @default(uuid()) id String @id @default(uuid(7)) @db.Uuid
quantity Int?
notes String?
itemId String?
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
assetId String? @unique
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade, onUpdate: Cascade)
assignmentDate DateTime @default(now())
returnDate DateTime?
createdBy String
createdUser User @relation(fields: [createdBy], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
movement Movement[]
@@index([itemId]) personId String @db.Uuid
@@index([assetId]) person Person @relation(fields: [personId], references: [id], onDelete: Restrict, onUpdate: Cascade)
@@index([personId])
@@index([createdBy]) status AssignmentStatus @default(OPEN)
assignedAt DateTime @default(now())
dueAt DateTime?
closedAt DateTime?
notes String?
createdById String @db.Uuid
createdBy User @relation("AssignmentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict, onUpdate: Cascade)
closedById String? @db.Uuid
closedBy User? @relation("AssignmentClosedBy", fields: [closedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
stockLines AssignmentStockLine[]
assetLines AssignmentAssetLine[]
movements InventoryMovement[]
@@index([personId, status])
@@index([personId, assignedAt])
@@index([status, assignedAt])
@@index([dueAt])
@@index([createdById, createdAt])
} }
enum MovementType { // ======================================================
IN // QUANTITY ASSIGNMENTS
OUT // ======================================================
model AssignmentStockLine {
id String @id @default(uuid(7)) @db.Uuid
assignmentId String @db.Uuid
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Restrict, onUpdate: Cascade)
itemId String @db.Uuid
item Item @relation(fields: [itemId], references: [id], onDelete: Restrict, onUpdate: Cascade)
quantity Int
returnedQuantity Int @default(0)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
returns AssignmentStockReturn[]
@@index([assignmentId])
@@index([itemId, createdAt])
}
model AssignmentStockReturn {
id String @id @default(uuid(7)) @db.Uuid
assignmentLineId String @db.Uuid
assignmentLine AssignmentStockLine @relation(fields: [assignmentLineId], references: [id], onDelete: Restrict, onUpdate: Cascade)
quantity Int
returnedAt DateTime @default(now())
receivedById String @db.Uuid
receivedBy User @relation(fields: [receivedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
notes String?
createdAt DateTime @default(now())
@@index([assignmentLineId, returnedAt])
@@index([receivedById, returnedAt])
}
// ======================================================
// SERIALIZED ASSET ASSIGNMENTS
// ======================================================
model AssignmentAssetLine {
id String @id @default(uuid(7)) @db.Uuid
assignmentId String @db.Uuid
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Restrict, onUpdate: Cascade)
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id], onDelete: Restrict, onUpdate: Cascade)
assignedAt DateTime @default(now())
returnedAt DateTime?
returnedById String? @db.Uuid
returnedBy User? @relation("AssetReturnedBy", fields: [returnedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
returnStatus AssetStatus?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/**
* La unicidad de asignación activa se protege
* mediante índice único parcial en PostgreSQL.
*/
@@index([assignmentId])
@@index([assetId, assignedAt])
@@index([returnedAt])
}
// ======================================================
// INVENTORY MOVEMENTS
// ======================================================
enum InventoryMovementType {
RECEIPT
ISSUE
ASSIGNMENT ASSIGNMENT
RETURN RETURN
ADJUSTMENT ADJUSTMENT
DELETED STATUS_CHANGE
DISPOSAL
INITIAL_LOAD
} }
model Movement { enum InventoryMovementReason {
id String @id @default(uuid()) PURCHASE
type MovementType @default(IN) MANUAL_ENTRY
quantity Int EMPLOYEE_ASSIGNMENT
details String? EMPLOYEE_RETURN
notes String? INVENTORY_CORRECTION
itemId String? DAMAGE
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) REPAIR
assetId String? REPAIR_RETURN
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade) LOSS
previousStock Int? THEFT
newStock Int? DISPOSAL
personId String? INITIAL_LOAD
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull, onUpdate: Cascade) OTHER
assignmentId String? }
assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
userId String model InventoryMovement {
user User @relation(fields: [userId], references: [id]) id String @id @default(uuid(7)) @db.Uuid
createdAt DateTime @default(now())
type InventoryMovementType
@@index([itemId]) reason InventoryMovementReason
@@index([assetId])
@@index([personId]) assignmentId String? @db.Uuid
@@index([type]) assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: Restrict, onUpdate: Cascade)
@@index([userId])
reference String?
details String?
notes String?
performedById String @db.Uuid
performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
occurredAt DateTime @default(now())
createdAt DateTime @default(now())
stockLines StockMovementLine[]
assetLines AssetMovementLine[]
@@index([type, occurredAt])
@@index([reason, occurredAt])
@@index([assignmentId])
@@index([performedById, occurredAt])
@@index([occurredAt])
}
// ======================================================
// QUANTITY MOVEMENTS
// ======================================================
model StockMovementLine {
id String @id @default(uuid(7)) @db.Uuid
movementId String @db.Uuid
movement InventoryMovement @relation(fields: [movementId], references: [id], onDelete: Cascade, onUpdate: Cascade)
itemId String @db.Uuid
item Item @relation(fields: [itemId], references: [id], onDelete: Restrict, onUpdate: Cascade)
/**
* Positivo: entrada/devolución/ajuste positivo.
* Negativo: salida/asignación/ajuste negativo.
*/
stockDelta Int
previousStock Int
newStock Int
createdAt DateTime @default(now())
@@index([movementId])
@@index([itemId, createdAt])
}
// ======================================================
// SERIALIZED ASSET MOVEMENTS
// ======================================================
model AssetMovementLine {
id String @id @default(uuid(7)) @db.Uuid
movementId String @db.Uuid
movement InventoryMovement @relation(fields: [movementId], references: [id], onDelete: Cascade, onUpdate: Cascade)
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id], onDelete: Restrict, onUpdate: Cascade)
previousStatus AssetStatus?
newStatus AssetStatus
notes String?
createdAt DateTime @default(now())
@@unique([movementId, assetId])
@@index([assetId, createdAt])
}
// ======================================================
// STOCK ALERTS
// ======================================================
enum StockAlertStatus {
OPEN
ACKNOWLEDGED
RESOLVED
}
enum StockAlertTrigger {
BELOW_MINIMUM
OUT_OF_STOCK
}
model StockAlert {
id String @id @default(uuid(7)) @db.Uuid
itemId String @db.Uuid
item Item @relation(fields: [itemId], references: [id], onDelete: Restrict, onUpdate: Cascade)
trigger StockAlertTrigger
status StockAlertStatus @default(OPEN)
availableStock Int
minimumStock Int
suggestedPurchase Int?
triggeredAt DateTime @default(now())
acknowledgedAt DateTime?
acknowledgedById String? @db.Uuid
acknowledgedBy User? @relation("StockAlertAcknowledgedBy", fields: [acknowledgedById], references: [id], onDelete: SetNull, onUpdate: Cascade)
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([itemId, status])
@@index([status, triggeredAt])
@@index([trigger, triggeredAt])
} }