diff --git a/prisma/migrations/20260603100143_init/migration.sql b/prisma/migrations/20260603100143_init/migration.sql index d8fa7e4..1c7f685 100644 --- a/prisma/migrations/20260603100143_init/migration.sql +++ b/prisma/migrations/20260603100143_init/migration.sql @@ -1,66 +1,111 @@ -- CreateEnum CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER'); +-- CreateEnum +CREATE TYPE "UserStatus" AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DISABLED'); + -- CreateEnum CREATE TYPE "PersonDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER'); -- CreateEnum -CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED'); +CREATE TYPE "ItemTrackingType" AS ENUM ('QUANTITY', 'SERIALIZED'); -- 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 CREATE TABLE "User" ( - "id" TEXT NOT NULL, + "id" UUID NOT NULL, "name" TEXT NOT NULL, "email" TEXT NOT NULL, - "password" TEXT NOT NULL, + "emailNormalized" TEXT NOT NULL, + "passwordHash" TEXT, "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, "updatedAt" TIMESTAMP(3) NOT NULL, 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 CREATE TABLE "Person" ( - "id" TEXT NOT NULL, + "id" UUID NOT NULL, "firstName" TEXT NOT NULL, "lastName" TEXT NOT NULL, "department" "PersonDepartment", "email" TEXT, "phone" TEXT, - "userId" TEXT, - "isActive" BOOLEAN NOT NULL DEFAULT true, + "userId" UUID, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), CONSTRAINT "Person_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Category" ( - "id" TEXT NOT NULL, + "id" UUID NOT NULL, "name" TEXT NOT NULL, "description" TEXT, - "isActive" BOOLEAN NOT NULL DEFAULT true, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), CONSTRAINT "Category_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Item" ( - "id" TEXT NOT NULL, + "id" UUID NOT NULL, + "sku" TEXT NOT NULL, "name" TEXT NOT NULL, "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, "minStock" INTEGER, - "maxStock" INTEGER, + "targetStock" INTEGER, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "deletedAt" TIMESTAMP(3), @@ -70,29 +115,38 @@ CREATE TABLE "Item" ( -- CreateTable CREATE TABLE "Asset" ( - "id" TEXT NOT NULL, - "itemId" TEXT, + "id" UUID NOT NULL, + "assetTag" TEXT, "serialNumber" TEXT NOT NULL, + "itemId" UUID NOT NULL, + "status" "AssetStatus" NOT NULL DEFAULT 'AVAILABLE', + "manufacturer" TEXT, + "model" TEXT, "deliveryNote" TEXT, - "status" "ItemStatus" NOT NULL DEFAULT 'AVAILABLE', + "invoiceNumber" TEXT, + "purchaseDate" TIMESTAMP(3), + "purchasePrice" DECIMAL(12,2), + "warrantyEndsAt" TIMESTAMP(3), "notes" TEXT, + "retiredAt" TIMESTAMP(3), "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), CONSTRAINT "Asset_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Assignment" ( - "id" TEXT NOT NULL, - "quantity" INTEGER, + "id" UUID NOT NULL, + "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, - "itemId" TEXT, - "assetId" TEXT, - "personId" TEXT, - "assignmentDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "returnDate" TIMESTAMP(3), - "createdBy" TEXT NOT NULL, + "createdById" UUID NOT NULL, + "closedById" UUID, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -100,29 +154,135 @@ CREATE TABLE "Assignment" ( ); -- CreateTable -CREATE TABLE "Movement" ( - "id" TEXT NOT NULL, - "type" "MovementType" NOT NULL DEFAULT 'IN', +CREATE TABLE "AssignmentStockLine" ( + "id" UUID NOT NULL, + "assignmentId" UUID NOT NULL, + "itemId" UUID 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, - "itemId" TEXT, - "assetId" TEXT, - "previousStock" INTEGER, - "newStock" INTEGER, - "personId" TEXT, - "assignmentId" TEXT, - "userId" TEXT NOT NULL, "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 -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +CREATE UNIQUE INDEX "User_emailNormalized_key" ON "User"("emailNormalized"); -- 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 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"); -- 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 CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); -- CreateIndex -CREATE INDEX "Category_name_idx" ON "Category"("name"); +CREATE INDEX "Category_deletedAt_idx" ON "Category"("deletedAt"); -- CreateIndex -CREATE UNIQUE INDEX "Item_name_key" ON "Item"("name"); +CREATE UNIQUE INDEX "Item_sku_key" ON "Item"("sku"); -- 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 CREATE UNIQUE INDEX "Asset_serialNumber_key" ON "Asset"("serialNumber"); -- CreateIndex -CREATE INDEX "Asset_serialNumber_idx" ON "Asset"("serialNumber"); - --- CreateIndex -CREATE INDEX "Asset_itemId_idx" ON "Asset"("itemId"); +CREATE INDEX "Asset_itemId_status_idx" ON "Asset"("itemId", "status"); -- CreateIndex CREATE INDEX "Asset_status_idx" ON "Asset"("status"); -- CreateIndex -CREATE UNIQUE INDEX "Assignment_assetId_key" ON "Assignment"("assetId"); +CREATE INDEX "Asset_createdAt_idx" ON "Asset"("createdAt"); -- CreateIndex -CREATE INDEX "Assignment_itemId_idx" ON "Assignment"("itemId"); +CREATE INDEX "Asset_deletedAt_idx" ON "Asset"("deletedAt"); -- CreateIndex -CREATE INDEX "Assignment_assetId_idx" ON "Assignment"("assetId"); +CREATE INDEX "Assignment_personId_status_idx" ON "Assignment"("personId", "status"); -- CreateIndex -CREATE INDEX "Assignment_personId_idx" ON "Assignment"("personId"); +CREATE INDEX "Assignment_personId_assignedAt_idx" ON "Assignment"("personId", "assignedAt"); -- CreateIndex -CREATE INDEX "Assignment_createdBy_idx" ON "Assignment"("createdBy"); +CREATE INDEX "Assignment_status_assignedAt_idx" ON "Assignment"("status", "assignedAt"); -- CreateIndex -CREATE INDEX "Movement_itemId_idx" ON "Movement"("itemId"); +CREATE INDEX "Assignment_dueAt_idx" ON "Assignment"("dueAt"); -- CreateIndex -CREATE INDEX "Movement_assetId_idx" ON "Movement"("assetId"); +CREATE INDEX "Assignment_createdById_createdAt_idx" ON "Assignment"("createdById", "createdAt"); -- CreateIndex -CREATE INDEX "Movement_personId_idx" ON "Movement"("personId"); +CREATE INDEX "AssignmentStockLine_assignmentId_idx" ON "AssignmentStockLine"("assignmentId"); -- CreateIndex -CREATE INDEX "Movement_type_idx" ON "Movement"("type"); +CREATE INDEX "AssignmentStockLine_itemId_createdAt_idx" ON "AssignmentStockLine"("itemId", "createdAt"); -- 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 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; -- 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 -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 -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 -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 -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 -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 -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 -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 -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 -ALTER TABLE "Movement" ADD CONSTRAINT "Movement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; \ No newline at end of file +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'); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 026b072..70d0038 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,6 +14,10 @@ datasource db { provider = "postgresql" } +// ====================================================== +// USERS +// ====================================================== + enum UserRole { ADMIN MANAGER @@ -21,20 +25,91 @@ enum UserRole { VIEWER } -model User { - id String @id @default(uuid()) - name String - email String @unique - password String - role UserRole @default(STAFF) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - movements Movement[] - assignments Assignment[] - person Person? +enum UserStatus { + INVITED + ACTIVE + SUSPENDED + DISABLED } +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 { IT ENGINEERING @@ -47,139 +122,458 @@ enum PersonDepartment { } model Person { - id String @id @default(uuid()) - firstName String - lastName String - department PersonDepartment? - email String? @unique - phone String? - userId String? @unique - user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid(7)) @db.Uuid + firstName String + lastName String + department PersonDepartment? + + email String? + phone String? + + userId String? @unique @db.Uuid + user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + assignments Assignment[] - movements Movement[] @@index([lastName, firstName]) - @@index([department]) + @@index([department, deletedAt]) + @@index([deletedAt]) } -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 +// ====================================================== +// CATALOG +// ====================================================== - @@index([name]) +enum ItemTrackingType { + QUANTITY + SERIALIZED } enum ItemStatus { - AVAILABLE - ASSIGNED - RESERVED - IN_REPAIR - BROKEN - STOLEN - DISPOSED + 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()) - name String @unique + id String @id @default(uuid(7)) @db.Uuid + sku String @unique + name 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 { - id String @id @default(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? + id String @id @default(uuid(7)) @db.Uuid - @@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([createdAt]) + @@index([deletedAt]) +} + +// ====================================================== +// ASSIGNMENTS +// ====================================================== + +enum AssignmentStatus { + OPEN + PARTIALLY_RETURNED + RETURNED + CANCELLED } model Assignment { - id String @id @default(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[] + id String @id @default(uuid(7)) @db.Uuid - @@index([itemId]) - @@index([assetId]) - @@index([personId]) - @@index([createdBy]) + 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? + + 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 - OUT +// ====================================================== +// QUANTITY ASSIGNMENTS +// ====================================================== + +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 RETURN ADJUSTMENT - DELETED + STATUS_CHANGE + DISPOSAL + INITIAL_LOAD } -model Movement { - id String @id @default(uuid()) - type MovementType @default(IN) - quantity Int - details String? - notes String? - itemId String? - item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) - assetId String? - asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade) - previousStock Int? - 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()) - - @@index([itemId]) - @@index([assetId]) - @@index([personId]) - @@index([type]) - @@index([userId]) +enum InventoryMovementReason { + PURCHASE + MANUAL_ENTRY + 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? + 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]) }