feat(cardex): implement cardex controller, service, and DTO for stock movements

feat(pos): add POS controller and service for order creation and stock retrieval
refactor(pos): enhance stock and product category retrieval with pagination and mapping
This commit is contained in:
2025-12-21 19:09:41 +03:30
parent d514267f58
commit c6a86719dd
42 changed files with 3769 additions and 119 deletions
@@ -15,17 +15,13 @@ CREATE INDEX `Sales_Invoices_inventoryId_fkey` ON `Sales_Invoices` (`inventoryId
ALTER TABLE `Sales_Invoices`
ADD CONSTRAINT `Sales_Invoices_inventoryId_fkey` FOREIGN KEY (`inventoryId`) REFERENCES `Inventories` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- ------------------------------------------
-- Trigger: trg_transfer_item_after_insert
-- Event: INSERT
-- Table: Inventory_Transfer_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_transfer_item_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_transfer_item_after_insert` AFTER INSERT ON `Inventory_Transfer_Items` FOR EACH ROW begin
CREATE TRIGGER `trg_transfer_item_after_insert` AFTER
INSERT
ON `Inventory_Transfer_Items` FOR EACH ROW begin
DECLARE fromInv INT;
DECLARE fromInv INT;
DECLARE toInv INT;
DECLARE _avgCost DECIMAL(10,2);
DECLARE latestQuantityInOrigin DECIMAL(10,2);
@@ -61,9 +57,11 @@ end;
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_purchase_receipt_item_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_purchase_receipt_item_after_insert` AFTER INSERT ON `Purchase_Receipt_Items` FOR EACH ROW BEGIN
DECLARE latestQuantity DECIMAL(10,2) DEFAULT 0;
DECLARE invId INT;
CREATE TRIGGER `trg_purchase_receipt_item_after_insert` AFTER
INSERT
ON `Purchase_Receipt_Items` FOR EACH ROW BEGIN DECLARE latestQuantity DECIMAL(10, 2) DEFAULT 0;
DECLARE invId INT;
DECLARE suppId INT;
-- Get inventory & supplier from receipt
@@ -107,11 +105,103 @@ CREATE DEFINER=`pos`@`%` TRIGGER `trg_purchase_receipt_item_after_insert` AFTER
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.total / NEW.count
END,
END
,
suppId,
latestQuantity + NEW.count,
NOW()
);
END;
-- ------------------------------------------
-- Trigger: trg_sales_invoice_items_before_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_before_insert`;
CREATE TRIGGER `trg_sales_invoice_items_before_insert` BEFORE
INSERT
ON `Sales_Invoice_Items` FOR EACH ROW begin DECLARE current_stock DECIMAL(10, 2);
DECLARE inventory_id INT;
SELECT inventoryId INTO inventory_id
FROM Sales_Invoices si
WHERE si.id = NEW.invoiceId
LIMIT 1;
SELECT COALESCE(quantity, 0) INTO current_stock
FROM Stock_Balance sb
WHERE productId = NEW.productId AND sb.inventoryId = inventory_id
LIMIT 1;
IF NEW.count > current_stock THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'موجودی کالا در انبار کافی نیست.';
END IF;
end;
-- ------------------------------------------
-- Trigger: trg_sales_invoice_items_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_after_insert`;
CREATE TRIGGER `trg_sales_invoice_items_after_insert` AFTER
INSERT
ON `Sales_Invoice_Items` FOR EACH ROW begin DECLARE current_stock DECIMAL(10, 2);
DECLARE inventory_id INT;
SELECT inventoryId INTO inventory_id
FROM Sales_Invoices si
WHERE si.id = NEW.invoiceId
LIMIT 1;
SELECT COALESCE(quantity, 0) INTO current_stock
FROM Stock_Balance sb
WHERE productId = NEW.productId AND sb.inventoryId = inventory_id
LIMIT 1;
INSERT INTO Stock_Movements (
type,
quantity,
fee,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
remainedInStock,
createdAt
)
VALUES (
'OUT',
NEW.count,
NEW.fee,
NEW.total,
'SALES',
NEW.invoiceId,
NEW.productId,
inventory_id,
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.total / NEW.count
END
, current_stock + NEW.count, NOW() );
END;
-- ------------------------------------------
@@ -121,81 +211,82 @@ END;
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_stock_transfer`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_stock_transfer` AFTER INSERT ON `Stock_Movements` FOR EACH ROW BEGIN
IF NEW.referenceType = 'INVENTORY_TRANSFER' THEN
IF NEW.type = 'IN' THEN
INSERT INTO Stock_Balance (
CREATE TRIGGER `trg_stock_transfer` AFTER
INSERT
ON `Stock_Movements` FOR EACH ROW BEGIN IF NEW.referenceType = 'INVENTORY_TRANSFER' THEN IF NEW.type = 'IN' THEN
INSERT INTO
Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
)
VALUES (
NEW.productId,
NEW.inventoryId,
NEW.quantity,
NEW.totalCost,
CASE
WHEN NEW.quantity = 0 THEN 0
ELSE NEW.totalCost / NEW.quantity
WHEN NEW.quantity = 0 THEN 0
ELSE NEW.totalCost / NEW.quantity
END,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = CASE
WHEN (quantity + NEW.quantity) = 0 THEN 0
ELSE (totalCost + NEW.totalCost) / (quantity + NEW.quantity)
END,
updatedAt = NOW();
END IF;
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = CASE
WHEN (quantity + NEW.quantity) = 0 THEN 0
ELSE (totalCost + NEW.totalCost) / (quantity + NEW.quantity)
END,
updatedAt = NOW();
END IF;
IF NEW.type = 'OUT' THEN
IF NEW.type = 'OUT' THEN IF EXISTS (
SELECT 1
FROM Stock_Balance sb
WHERE
sb.productId = NEW.productId
AND sb.inventoryId = NEW.inventoryId
) THEN
UPDATE Stock_Balance sb
SET
sb.quantity = sb.quantity - NEW.quantity,
sb.totalCost = sb.totalCost - (sb.avgCost * NEW.quantity),
sb.updatedAt = NOW()
WHERE
sb.productId = NEW.productId
AND sb.inventoryId = NEW.inventoryId;
IF EXISTS(
SELECT 1 FROM Stock_Balance sb
WHERE sb.productId = NEW.productId
AND sb.inventoryId = NEW.inventoryId
) THEN
ELSE
INSERT INTO
Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
- NEW.quantity,
- COALESCE(NEW.fee, 0) * NEW.quantity,
COALESCE(NEW.fee, 0),
NOW()
);
END IF;
UPDATE Stock_Balance sb
SET
sb.quantity = sb.quantity - NEW.quantity,
sb.totalCost = sb.totalCost - (sb.avgCost * NEW.quantity),
sb.updatedAt = NOW()
WHERE sb.productId = NEW.productId
AND sb.inventoryId = NEW.inventoryId;
END IF;
ELSE
INSERT INTO Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
-NEW.quantity,
-COALESCE(NEW.fee, 0) * NEW.quantity,
COALESCE(NEW.fee, 0),
NOW()
);
END IF;
END IF;
END IF;
END IF;
END;
-- ------------------------------------------
@@ -205,22 +296,69 @@ END;
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_stock_purchase_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_stock_purchase_insert` AFTER INSERT ON `Stock_Movements` FOR EACH ROW BEGIN
IF NEW.referenceType = 'PURCHASE' THEN
CREATE TRIGGER `trg_stock_purchase_insert` AFTER
INSERT
ON `Stock_Movements` FOR EACH ROW BEGIN IF NEW.referenceType = 'PURCHASE' THEN
INSERT INTO Stock_Balance (productId, quantity, avgCost, totalCost, inventoryId, updatedAt)
VALUES (
NEW.productId,
NEW.quantity,
NEW.fee,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = totalCost / quantity;
INSERT INTO
Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
NEW.productId,
NEW.quantity,
NEW.fee,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END;
-- ------------------------------------------
-- Trigger: trg_stock_sale_insert
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_stock_sale_insert`;
CREATE TRIGGER `trg_stock_sale_insert` AFTER
INSERT
ON `Stock_Movements` FOR EACH ROW BEGIN IF NEW.referenceType = 'SALES' THEN
INSERT INTO
Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
NEW.productId,
NEW.quantity,
NEW.fee,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity - NEW.quantity,
totalCost = totalCost - NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END IF;
END;
@@ -0,0 +1,37 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Roles` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateTable
CREATE TABLE `Otp_Codes` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`mobileNumber` VARCHAR(20) NOT NULL,
`code` VARCHAR(10) NOT NULL,
`used` BOOLEAN NOT NULL DEFAULT false,
`expiresAt` TIMESTAMP(0) NOT NULL,
`createdAt` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
INDEX `Otp_Codes_mobileNumber_idx`(`mobileNumber`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Refresh_Tokens` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`tokenHash` VARCHAR(255) NOT NULL,
`userId` INTEGER NOT NULL,
`revoked` BOOLEAN NOT NULL DEFAULT false,
`expiresAt` TIMESTAMP(0) NOT NULL,
`createdAt` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
INDEX `Refresh_Tokens_userId_idx`(`userId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateIndex
CREATE UNIQUE INDEX `Roles_name_key` ON `Roles`(`name`);
-- AddForeignKey
ALTER TABLE `Refresh_Tokens` ADD CONSTRAINT `Refresh_Tokens_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `Users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
+38 -11
View File
@@ -10,16 +10,17 @@ datasource db {
}
model User {
id Int @id @default(autoincrement())
mobileNumber String @unique @db.Char(11)
password String
firstName String
lastName String
roleId Int
createdAt DateTime @default(now()) @db.Timestamp(0)
deletedAt DateTime? @db.Timestamp(0)
updatedAt DateTime @updatedAt @db.Timestamp(0)
role Role @relation("User_Role", fields: [roleId], references: [id], onUpdate: NoAction)
id Int @id @default(autoincrement())
mobileNumber String @unique @db.Char(11)
password String
firstName String
lastName String
roleId Int
createdAt DateTime @default(now()) @db.Timestamp(0)
deletedAt DateTime? @db.Timestamp(0)
updatedAt DateTime @updatedAt @db.Timestamp(0)
role Role @relation("User_Role", fields: [roleId], references: [id], onUpdate: NoAction)
refreshTokens RefreshToken[]
@@index([roleId], map: "Users_roleId_fkey")
@@map("Users")
@@ -27,7 +28,7 @@ model User {
model Role {
id Int @id @default(autoincrement())
name String @db.VarChar(100)
name String @unique() @db.VarChar(100)
description String? @db.Text
permissions Json?
createdAt DateTime @default(now()) @db.Timestamp(0)
@@ -345,6 +346,32 @@ model StockBalance {
@@map("Stock_Balance")
}
model OtpCode {
id Int @id @default(autoincrement())
mobileNumber String @db.VarChar(20)
code String @db.VarChar(10)
used Boolean @default(false)
expiresAt DateTime @db.Timestamp(0)
createdAt DateTime @default(now()) @db.Timestamp(0)
@@index([mobileNumber])
@@map("Otp_Codes")
}
model RefreshToken {
id Int @id @default(autoincrement())
tokenHash String @db.VarChar(255)
userId Int
revoked Boolean @default(false)
expiresAt DateTime @db.Timestamp(0)
createdAt DateTime @default(now()) @db.Timestamp(0)
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@map("Refresh_Tokens")
}
model InventoryTransfer {
id Int @id @default(autoincrement())
code String @unique @db.VarChar(100)
+18
View File
@@ -11,6 +11,24 @@ async function main() {
})
}
if ((await prisma.user.count()) === 0) {
const adminRole = await prisma.role.findUnique({ where: { name: 'Admin' } })
if (!adminRole) {
return
}
await prisma.user.upsert({
where: { id: 1, firstName: 'عباس', lastName: 'حسنی' },
update: {},
create: {
firstName: 'عباس',
lastName: 'حسنی',
roleId: adminRole.id,
mobileNumber: '09120258156',
password: '12345678',
},
})
}
if ((await prisma.inventory.count()) === 0) {
await prisma.inventory.createMany({
data: [
+1 -1
View File
@@ -1,5 +1,5 @@
-- AUTO-GENERATED MYSQL TRIGGER DUMP
-- Generated at: 2025-12-15T15:21:49.148Z
-- Generated at: 2025-12-21T08:07:11.406Z
-- ------------------------------------------
-- Trigger: trg_transfer_item_after_insert