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');
+495 -101
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
} }
enum UserStatus {
INVITED
ACTIVE
SUSPENDED
DISABLED
}
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid(7)) @db.Uuid
name String name String
email String @unique email String
password String emailNormalized String @unique
/**
* Nulo mientras el usuario no haya aceptado la invitación.
*/
passwordHash String?
role UserRole @default(STAFF) role UserRole @default(STAFF)
isActive Boolean @default(true) status UserStatus @default(INVITED)
deletedAt DateTime?
invitedAt DateTime?
activatedAt DateTime?
passwordChangedAt DateTime?
lastLoginAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
movements Movement[]
assignments Assignment[]
person Person? 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
email String?
phone String? phone String?
userId String? @unique
userId String? @unique @db.Uuid
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments Assignment[]
movements Movement[]
@@index([lastName, firstName])
@@index([department])
}
model Category {
id String @id @default(uuid())
name String @unique
description String?
isActive Boolean @default(true)
items Item[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name])
}
enum ItemStatus {
AVAILABLE
ASSIGNED
RESERVED
IN_REPAIR
BROKEN
STOLEN
DISPOSED
}
model Item {
id String @id @default(uuid())
name String @unique
description String?
categoryId String
category Category @relation(fields: [categoryId], references: [id])
stock Int @default(0)
minStock Int?
maxStock Int?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
movements Movement[]
assignments Assignment[] assignments Assignment[]
@@index([lastName, firstName])
@@index([department, deletedAt])
@@index([deletedAt])
}
// ======================================================
// CATALOG
// ======================================================
enum ItemTrackingType {
QUANTITY
SERIALIZED
}
enum ItemStatus {
ACTIVE
DISCONTINUED
ARCHIVED
}
model Category {
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 {
id String @id @default(uuid(7)) @db.Uuid
sku String @unique
name String
description String?
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[] assets Asset[]
@@index([categoryId]) 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]) /**
* 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 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? deliveryNote String?
status ItemStatus @default(AVAILABLE) invoiceNumber String?
purchaseDate DateTime?
purchasePrice Decimal? @db.Decimal(12, 2)
warrantyEndsAt DateTime?
notes String? notes String?
retiredAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
movements Movement[]
assignment Assignment?
@@index([serialNumber]) deletedAt DateTime?
@@index([itemId])
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?
personId String @db.Uuid
person Person @relation(fields: [personId], references: [id], onDelete: Restrict, onUpdate: Cascade)
status AssignmentStatus @default(OPEN)
assignedAt DateTime @default(now())
dueAt DateTime?
closedAt DateTime?
notes String? notes String?
itemId String?
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) createdById String @db.Uuid
assetId String? @unique createdBy User @relation("AssignmentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict, onUpdate: Cascade)
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
personId String? closedById String? @db.Uuid
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade, onUpdate: Cascade) closedBy User? @relation("AssignmentClosedBy", fields: [closedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
assignmentDate DateTime @default(now())
returnDate DateTime?
createdBy String
createdUser User @relation(fields: [createdBy], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
movement Movement[]
@@index([itemId]) stockLines AssignmentStockLine[]
@@index([assetId]) assetLines AssignmentAssetLine[]
@@index([personId]) movements InventoryMovement[]
@@index([createdBy])
@@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
EMPLOYEE_RETURN
INVENTORY_CORRECTION
DAMAGE
REPAIR
REPAIR_RETURN
LOSS
THEFT
DISPOSAL
INITIAL_LOAD
OTHER
}
model InventoryMovement {
id String @id @default(uuid(7)) @db.Uuid
type InventoryMovementType
reason InventoryMovementReason
assignmentId String? @db.Uuid
assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: Restrict, onUpdate: Cascade)
reference String?
details String? details String?
notes String? notes String?
itemId String?
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) performedById String @db.Uuid
assetId String? performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
previousStock Int? occurredAt DateTime @default(now())
newStock Int?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull, onUpdate: Cascade)
assignmentId String?
assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@index([itemId]) stockLines StockMovementLine[]
@@index([assetId]) assetLines AssetMovementLine[]
@@index([personId])
@@index([type]) @@index([type, occurredAt])
@@index([userId]) @@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])
} }