import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField, type TailorAnyDBType } from "@tailor-platform/sdk"; import { STOCK_TYPES } from "./stockLevel"; import { storageLocation } from "./storageLocation"; export const LEDGER_DIRECTIONS = ["IN", "OUT"] as const; // What the movement changes: net on-hand quantity, location, or stock type. export const INVENTORY_LEDGER_ACTIONS = [ "QUANTITY_CHANGE", "TRANSFER", "STOCK_TYPE_CHANGE", ] as const; // The document that caused the movement. export const INVENTORY_LEDGER_SOURCE_TYPES = [ "INBOUND_SHIPMENT", "OUTBOUND_SHIPMENT", "TRANSFER_ORDER", "STOCK_ADJUSTMENT", ] as const; // The demand/supply document the movement fulfills. export const INVENTORY_LEDGER_ORDER_DOCUMENT_TYPES = [ "PURCHASE_ORDER", "SALES_ORDER", "TRANSFER_ORDER", ] as const; const builtins = (params: { itemType?: TailorAnyDBType }) => { const itemId = db.uuid().description("Foreign key to Item"); return { sourceType: db .enum([...INVENTORY_LEDGER_SOURCE_TYPES] as [string, ...string[]]) .description( "Cause-document type: INBOUND_SHIPMENT, OUTBOUND_SHIPMENT, TRANSFER_ORDER, or STOCK_ADJUSTMENT", ), sourceId: db .uuid() .description("Cause-document ID. Generic reference, not a typed foreign key."), sourceLineId: db .uuid({ optional: true }) .description("Cause-document line ID. Generic reference, not a typed foreign key."), orderDocumentType: db .enum([...INVENTORY_LEDGER_ORDER_DOCUMENT_TYPES] as [string, ...string[]], { optional: true, }) .description("Ordering-document type this movement fulfills"), orderDocumentId: db.uuid({ optional: true }).description("Ordering-document ID"), orderDocumentLineId: db.uuid({ optional: true }).description("Ordering-document line ID"), direction: db .enum([...LEDGER_DIRECTIONS] as [string, ...string[]]) .description("Whether the row increases or decreases stock"), action: db .enum([...INVENTORY_LEDGER_ACTIONS] as [string, ...string[]]) .description( "What the movement changes; with sourceType and direction it determines the costing treatment", ), itemId: params.itemType ? itemId.relation({ type: "n-1", toward: { type: params.itemType, as: "item" }, backward: "inventoryLedgerEntries", }) : itemId, storageLocationId: db .uuid() .relation({ type: "n-1", toward: { type: storageLocation, as: "storageLocation" }, backward: "inventoryLedgerEntries", }) .description("The storage location where the stock change occurs"), stockType: db .enum([...STOCK_TYPES] as [string, ...string[]]) .description("Stock type affected by this ledger row: AVAILABLE, BLOCKED, or IN_TRANSIT"), quantity: db .decimal() .description( "Quantity in the item's primary unit (always positive); direction determines the stock effect", ), executedAt: db.datetime().description("Timestamp when the stock change was executed"), effectiveDate: db .date() .description("Business effective date used for inventory reporting and period controls"), }; }; type ReservedFields = keyof ReturnType | CommonReservedFields; export interface CreateInventoryLedgerTypeParams> { fields?: NoReservedFields; itemType?: TailorAnyDBType; } export function createInventoryLedgerType>( params: CreateInventoryLedgerTypeParams, ) { return db .table("InventoryLedger", { ...builtins(params), ...((params.fields ?? {}) as F), createdAt: db.fields.timestamps().createdAt, }) .indexes( { fields: ["sourceType", "sourceId"], unique: false, name: "inventory_ledger_source_idx", }, { fields: ["itemId", "effectiveDate"], unique: false, name: "inventory_ledger_item_effective_idx", }, ) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } export const inventoryLedger = createInventoryLedgerType({});