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
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;
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"
}
// ======================================================
// USERS
// ======================================================
enum UserRole {
ADMIN
MANAGER
@@ -21,20 +25,91 @@ enum UserRole {
VIEWER
}
enum UserStatus {
INVITED
ACTIVE
SUSPENDED
DISABLED
}
model User {
id String @id @default(uuid())
id String @id @default(uuid(7)) @db.Uuid
name String
email String @unique
password String
email String
emailNormalized String @unique
/**
* Nulo mientras el usuario no haya aceptado la invitación.
*/
passwordHash String?
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())
updatedAt DateTime @updatedAt
movements Movement[]
assignments Assignment[]
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())
id String @id @default(uuid(7)) @db.Uuid
firstName String
lastName String
department PersonDepartment?
email String? @unique
email String?
phone String?
userId String? @unique
userId String? @unique @db.Uuid
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())
updatedAt DateTime @updatedAt
deletedAt DateTime?
movements Movement[]
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[]
@@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 {
id String @id @default(uuid())
itemId String?
item Item? @relation(fields: [itemId], references: [id])
id String @id @default(uuid(7)) @db.Uuid
/**
* 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?
status ItemStatus @default(AVAILABLE)
invoiceNumber String?
purchaseDate DateTime?
purchasePrice Decimal? @db.Decimal(12, 2)
warrantyEndsAt DateTime?
notes String?
retiredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
movements Movement[]
assignment Assignment?
@@index([serialNumber])
@@index([itemId])
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?
id String @id @default(uuid(7)) @db.Uuid
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?
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])
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
movement Movement[]
@@index([itemId])
@@index([assetId])
@@index([personId])
@@index([createdBy])
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
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?
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])
performedById String @db.Uuid
performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict, onUpdate: Cascade)
occurredAt DateTime @default(now())
createdAt DateTime @default(now())
@@index([itemId])
@@index([assetId])
@@index([personId])
@@index([type])
@@index([userId])
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])
}