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
+2
View File
@@ -3,6 +3,7 @@
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.3",
"@prisma/adapter-mariadb": "^7.1.0",
@@ -12,6 +13,7 @@
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"jalaliday": "^3.1.1",
"jsonwebtoken": "^9.0.3",
"mysql2": "^3.15.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
+111
View File
@@ -14,6 +14,9 @@ importers:
'@nestjs/core':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/jwt':
specifier: ^11.0.2
version: 11.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))
'@nestjs/platform-express':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
@@ -41,6 +44,9 @@ importers:
jalaliday:
specifier: ^3.1.1
version: 3.1.1(dayjs@1.11.19)
jsonwebtoken:
specifier: ^9.0.3
version: 9.0.3
mysql2:
specifier: ^3.15.3
version: 3.15.3
@@ -915,6 +921,11 @@ packages:
'@nestjs/websockets':
optional: true
'@nestjs/jwt@11.0.2':
resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==}
peerDependencies:
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
'@nestjs/mapped-types@2.1.0':
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
peerDependencies:
@@ -1144,9 +1155,15 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/methods@1.1.4':
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@22.19.1':
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
@@ -1575,6 +1592,9 @@ packages:
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1854,6 +1874,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -2555,6 +2578,16 @@ packages:
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2592,12 +2625,33 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -4441,6 +4495,12 @@ snapshots:
optionalDependencies:
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/jwt@11.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@types/jsonwebtoken': 9.0.10
jsonwebtoken: 9.0.3
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -4711,8 +4771,15 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 22.19.1
'@types/methods@1.1.4': {}
'@types/ms@2.1.0': {}
'@types/node@22.19.1':
dependencies:
undici-types: 6.21.0
@@ -5193,6 +5260,8 @@ snapshots:
dependencies:
node-int64: 0.4.0
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -5427,6 +5496,10 @@ snapshots:
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
ee-first@1.1.1: {}
effect@3.18.4:
@@ -6368,6 +6441,30 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.3
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.1:
dependencies:
jwa: 2.0.1
safe-buffer: 5.2.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -6397,10 +6494,24 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
lodash@4.17.21: {}
log-symbols@4.1.0:
@@ -15,15 +15,11 @@ 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 toInv INT;
@@ -61,8 +57,10 @@ 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;
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;
@@ -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,12 +211,11 @@ 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,
@@ -153,29 +242,29 @@ CREATE DEFINER=`pos`@`%` TRIGGER `trg_stock_transfer` AFTER INSERT ON `Stock_Mov
ELSE (totalCost + NEW.totalCost) / (quantity + NEW.quantity)
END,
updatedAt = NOW();
END IF;
IF NEW.type = 'OUT' THEN
IF EXISTS(
SELECT 1 FROM Stock_Balance sb
WHERE sb.productId = NEW.productId
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
WHERE
sb.productId = NEW.productId
AND sb.inventoryId = NEW.inventoryId;
ELSE
INSERT INTO Stock_Balance (
INSERT INTO
Stock_Balance (
productId,
inventoryId,
quantity,
@@ -191,11 +280,13 @@ CREATE DEFINER=`pos`@`%` TRIGGER `trg_stock_transfer` AFTER INSERT ON `Stock_Mov
COALESCE(NEW.fee, 0),
NOW()
);
END IF;
END IF;
END IF;
END IF;
END;
-- ------------------------------------------
@@ -205,10 +296,19 @@ 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)
INSERT INTO
Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
NEW.productId,
NEW.quantity,
@@ -223,4 +323,42 @@ CREATE DEFINER=`pos`@`%` TRIGGER `trg_stock_purchase_insert` AFTER INSERT ON `St
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;
@@ -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;
+28 -1
View File
@@ -20,6 +20,7 @@ model User {
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
+5 -1
View File
@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { AuthModule } from './auth/auth.module'
import { CustomersModule } from './customers/customers.module'
import { InventoriesModule } from './inventories/inventories.module'
import { InventoryTransferItemsModule } from './inventory-transfer-items/inventory-transfer-items.module'
import { InventoryTransfersModule } from './inventory-transfers/inventory-transfers.module'
import { PosModule } from './pos/pos.module'
import { CardexModule } from './modules/cardex/cardex.module'
import { PosModule } from './modules/pos/pos.module'
import { PrismaModule } from './prisma/prisma.module'
import { ProductBrandsModule } from './product-brands/product-brands.module'
import { ProductCategoriesModule } from './product-categories/product-categories.module'
@@ -28,6 +30,7 @@ import { UsersModule } from './users/users.module'
imports: [
PrismaModule,
UsersModule,
AuthModule,
RolesModule,
ProductsModule,
ProductVariantsModule,
@@ -48,6 +51,7 @@ import { UsersModule } from './users/users.module'
StockAdjustmentsModule,
StockBalanceModule,
PosModule,
CardexModule,
],
controllers: [AppController],
providers: [AppService],
+41
View File
@@ -0,0 +1,41 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { ResponseMapper } from '../common/response/response-mapper'
import { AuthService } from './auth.service'
import { CurrentUser } from './current-user.decorator'
import { RefreshTokenDto } from './dto/refresh-token.dto'
import { SendCodeDto } from './dto/send-code.dto'
import { VerifyCodeDto } from './dto/verify-code.dto'
import { JwtAuthGuard } from './jwt-auth.guard'
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Post('send-code')
async sendCode(@Body() dto: SendCodeDto) {
return this.auth.sendCode(dto)
}
@Post('login')
async login(@Body() dto: VerifyCodeDto) {
return this.auth.loginWithCode(dto)
}
@Post('refresh')
async refresh(@Body() dto: RefreshTokenDto) {
return this.auth.refreshToken(dto)
}
@Post('logout')
async logout(@Body() dto: RefreshTokenDto) {
return this.auth.logout(dto.refreshToken)
}
@Get('me')
@UseGuards(JwtAuthGuard)
async me(@CurrentUser() user: any) {
return ResponseMapper.single(user)
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import 'dotenv/config'
import { env } from 'prisma/config'
import { PrismaModule } from '../prisma/prisma.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { JwtAuthGuard } from './jwt-auth.guard'
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: env('JWT_SECRET') || 'secret',
signOptions: { expiresIn: Number(env('JWT_EXPIRES_IN')) || '15m' },
}),
],
providers: [AuthService, JwtAuthGuard],
controllers: [AuthController],
exports: [AuthService, JwtAuthGuard],
})
export class AuthModule {}
+126
View File
@@ -0,0 +1,126 @@
import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { createHash, randomBytes } from 'crypto'
import 'dotenv/config'
import { env } from 'prisma/config'
import { ResponseMapper } from '../common/response/response-mapper'
import { PrismaService } from '../prisma/prisma.service'
import { RefreshTokenDto } from './dto/refresh-token.dto'
import { SendCodeDto } from './dto/send-code.dto'
import { VerifyCodeDto } from './dto/verify-code.dto'
@Injectable()
export class AuthService {
private readonly OTP_TTL_MINUTES = 2
private readonly ACCESS_TOKEN_EXP = Number(env('JWT_EXPIRES_IN')) || '15m'
private readonly REFRESH_TOKEN_EXP_DAYS = 30
constructor(
private prisma: PrismaService,
private jwt: JwtService,
) {}
private generateCode() {
// For development you may want fixed code via env, otherwise random 5-digit
const staticCode = process.env.OTP_STATIC_CODE
if (staticCode) return staticCode
return Math.floor(10000 + Math.random() * 90000).toString()
}
async sendCode(dto: SendCodeDto) {
const { mobileNumber } = dto
const user = await this.prisma.user.findFirst({ where: { mobileNumber } })
if (!user) {
throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
}
const code = this.generateCode()
const expiresAt = new Date(Date.now() + this.OTP_TTL_MINUTES * 60 * 1000)
await this.prisma.otpCode.create({
data: {
mobileNumber,
code,
expiresAt,
},
})
// TODO: integrate SMS provider. For now return the code for dev.
return {
ok: true,
}
}
async loginWithCode(dto: VerifyCodeDto) {
const { mobileNumber, code } = dto
const user = await this.prisma.user.findFirst({
where: { mobileNumber },
include: {
role: true,
},
})
if (!user) {
throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
}
const otp = await this.prisma.otpCode.findFirst({
where: { mobileNumber, used: false },
})
if (!otp) throw new BadRequestException('اطلاعات ورودی درست نیست')
if (otp.used) throw new BadRequestException('کد قبلن استفاده شده است')
if (otp.expiresAt.getTime() < Date.now())
throw new BadRequestException('کد وارد شده منقضی شده است')
if (otp.code !== code) throw new BadRequestException('کد وارد شده اشتباه است')
// mark used
await this.prisma.otpCode.update({ where: { id: otp.id }, data: { used: true } })
const accessToken = this.jwt.sign(
{ sub: user.id, mobileNumber },
{ expiresIn: this.ACCESS_TOKEN_EXP },
)
const refreshTokenPlain = randomBytes(48).toString('hex')
const tokenHash = createHash('sha256').update(refreshTokenPlain).digest('hex')
const expiresAt = new Date(
Date.now() + this.REFRESH_TOKEN_EXP_DAYS * 24 * 60 * 60 * 1000,
)
await this.prisma.refreshToken.create({
data: { tokenHash, userId: user.id, expiresAt },
})
return ResponseMapper.single({ accessToken, refreshToken: refreshTokenPlain, user })
}
async refreshToken(dto: RefreshTokenDto) {
const { refreshToken } = dto
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
const record = await this.prisma.refreshToken.findFirst({ where: { tokenHash } })
if (!record || record.revoked || record.expiresAt.getTime() < Date.now()) {
throw new UnauthorizedException('Invalid refresh token')
}
const user = await this.prisma.user.findUnique({ where: { id: record.userId } })
if (!user) throw new UnauthorizedException('User not found')
const accessToken = this.jwt.sign(
{ sub: user.id, mobileNumber: user.mobileNumber },
{ expiresIn: this.ACCESS_TOKEN_EXP },
)
return { accessToken }
}
async logout(refreshToken: string) {
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
await this.prisma.refreshToken.updateMany({
where: { tokenHash },
data: { revoked: true },
})
return { ok: true }
}
}
+8
View File
@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest()
return req.user
},
)
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class LoginDto {
@ApiProperty({ description: 'Mobile number used as username', example: '09123456789' })
@IsString()
mobileNumber: string
@ApiProperty({ description: 'OTP password (one-time password)', example: '12345' })
@IsString()
password: string
}
+8
View File
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class RefreshTokenDto {
@ApiProperty()
@IsString()
refreshToken: string
}
+8
View File
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class SendCodeDto {
@ApiProperty({ example: '09123456789' })
@IsString()
mobileNumber: string
}
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class VerifyCodeDto {
@ApiProperty({ example: '09123456789' })
@IsString()
mobileNumber: string
@ApiProperty({ example: '12345' })
@IsString()
code: string
}
+35
View File
@@ -0,0 +1,35 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwt: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest<Request>()
const auth = req.headers.authorization
if (!auth) throw new UnauthorizedException('Missing Authorization header')
const parts = auth.split(' ')
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new UnauthorizedException('Invalid Authorization header format')
}
const token = parts[1]
try {
const payload = this.jwt.verify(token, {
secret: process.env.JWT_SECRET || 'secret',
})
;(req as any).user = payload
return true
} catch (err) {
throw new UnauthorizedException('Invalid or expired token')
}
}
}
+10
View File
@@ -107,6 +107,16 @@ export type StockMovement = Prisma.StockMovementModel
*
*/
export type StockBalance = Prisma.StockBalanceModel
/**
* Model OtpCode
*
*/
export type OtpCode = Prisma.OtpCodeModel
/**
* Model RefreshToken
*
*/
export type RefreshToken = Prisma.RefreshTokenModel
/**
* Model InventoryTransfer
*
+10
View File
@@ -127,6 +127,16 @@ export type StockMovement = Prisma.StockMovementModel
*
*/
export type StockBalance = Prisma.StockBalanceModel
/**
* Model OtpCode
*
*/
export type OtpCode = Prisma.OtpCodeModel
/**
* Model RefreshToken
*
*/
export type RefreshToken = Prisma.RefreshTokenModel
/**
* Model InventoryTransfer
*
File diff suppressed because one or more lines are too long
@@ -402,6 +402,8 @@ export const ModelName = {
SalesInvoiceItem: 'SalesInvoiceItem',
StockMovement: 'StockMovement',
StockBalance: 'StockBalance',
OtpCode: 'OtpCode',
RefreshToken: 'RefreshToken',
InventoryTransfer: 'InventoryTransfer',
InventoryTransferItem: 'InventoryTransferItem',
StockAdjustment: 'StockAdjustment',
@@ -424,7 +426,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "role" | "productVariant" | "product" | "productBrand" | "productCategory" | "supplier" | "customer" | "inventory" | "store" | "productCharge" | "order" | "purchaseReceipt" | "purchaseReceiptItem" | "salesInvoice" | "salesInvoiceItem" | "stockMovement" | "stockBalance" | "inventoryTransfer" | "inventoryTransferItem" | "stockAdjustment" | "triggerLog" | "inventory_overview" | "stock_cardex" | "stock_view"
modelProps: "user" | "role" | "productVariant" | "product" | "productBrand" | "productCategory" | "supplier" | "customer" | "inventory" | "store" | "productCharge" | "order" | "purchaseReceipt" | "purchaseReceiptItem" | "salesInvoice" | "salesInvoiceItem" | "stockMovement" | "stockBalance" | "otpCode" | "refreshToken" | "inventoryTransfer" | "inventoryTransferItem" | "stockAdjustment" | "triggerLog" | "inventory_overview" | "stock_cardex" | "stock_view"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1616,6 +1618,138 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
OtpCode: {
payload: Prisma.$OtpCodePayload<ExtArgs>
fields: Prisma.OtpCodeFieldRefs
operations: {
findUnique: {
args: Prisma.OtpCodeFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload> | null
}
findUniqueOrThrow: {
args: Prisma.OtpCodeFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
findFirst: {
args: Prisma.OtpCodeFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload> | null
}
findFirstOrThrow: {
args: Prisma.OtpCodeFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
findMany: {
args: Prisma.OtpCodeFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>[]
}
create: {
args: Prisma.OtpCodeCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
createMany: {
args: Prisma.OtpCodeCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.OtpCodeDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
update: {
args: Prisma.OtpCodeUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
deleteMany: {
args: Prisma.OtpCodeDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.OtpCodeUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.OtpCodeUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
aggregate: {
args: Prisma.OtpCodeAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateOtpCode>
}
groupBy: {
args: Prisma.OtpCodeGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OtpCodeGroupByOutputType>[]
}
count: {
args: Prisma.OtpCodeCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OtpCodeCountAggregateOutputType> | number
}
}
}
RefreshToken: {
payload: Prisma.$RefreshTokenPayload<ExtArgs>
fields: Prisma.RefreshTokenFieldRefs
operations: {
findUnique: {
args: Prisma.RefreshTokenFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload> | null
}
findUniqueOrThrow: {
args: Prisma.RefreshTokenFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
findFirst: {
args: Prisma.RefreshTokenFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload> | null
}
findFirstOrThrow: {
args: Prisma.RefreshTokenFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
findMany: {
args: Prisma.RefreshTokenFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>[]
}
create: {
args: Prisma.RefreshTokenCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
createMany: {
args: Prisma.RefreshTokenCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.RefreshTokenDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
update: {
args: Prisma.RefreshTokenUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
deleteMany: {
args: Prisma.RefreshTokenDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.RefreshTokenUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.RefreshTokenUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
aggregate: {
args: Prisma.RefreshTokenAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateRefreshToken>
}
groupBy: {
args: Prisma.RefreshTokenGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefreshTokenGroupByOutputType>[]
}
count: {
args: Prisma.RefreshTokenCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefreshTokenCountAggregateOutputType> | number
}
}
}
InventoryTransfer: {
payload: Prisma.$InventoryTransferPayload<ExtArgs>
fields: Prisma.InventoryTransferFieldRefs
@@ -2289,6 +2423,30 @@ export const StockBalanceScalarFieldEnum = {
export type StockBalanceScalarFieldEnum = (typeof StockBalanceScalarFieldEnum)[keyof typeof StockBalanceScalarFieldEnum]
export const OtpCodeScalarFieldEnum = {
id: 'id',
mobileNumber: 'mobileNumber',
code: 'code',
used: 'used',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type OtpCodeScalarFieldEnum = (typeof OtpCodeScalarFieldEnum)[keyof typeof OtpCodeScalarFieldEnum]
export const RefreshTokenScalarFieldEnum = {
id: 'id',
tokenHash: 'tokenHash',
userId: 'userId',
revoked: 'revoked',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum]
export const InventoryTransferScalarFieldEnum = {
id: 'id',
code: 'code',
@@ -2553,6 +2711,21 @@ export const StockMovementOrderByRelevanceFieldEnum = {
export type StockMovementOrderByRelevanceFieldEnum = (typeof StockMovementOrderByRelevanceFieldEnum)[keyof typeof StockMovementOrderByRelevanceFieldEnum]
export const OtpCodeOrderByRelevanceFieldEnum = {
mobileNumber: 'mobileNumber',
code: 'code'
} as const
export type OtpCodeOrderByRelevanceFieldEnum = (typeof OtpCodeOrderByRelevanceFieldEnum)[keyof typeof OtpCodeOrderByRelevanceFieldEnum]
export const RefreshTokenOrderByRelevanceFieldEnum = {
tokenHash: 'tokenHash'
} as const
export type RefreshTokenOrderByRelevanceFieldEnum = (typeof RefreshTokenOrderByRelevanceFieldEnum)[keyof typeof RefreshTokenOrderByRelevanceFieldEnum]
export const InventoryTransferOrderByRelevanceFieldEnum = {
code: 'code',
description: 'description'
@@ -2778,6 +2951,8 @@ export type GlobalOmitConfig = {
salesInvoiceItem?: Prisma.SalesInvoiceItemOmit
stockMovement?: Prisma.StockMovementOmit
stockBalance?: Prisma.StockBalanceOmit
otpCode?: Prisma.OtpCodeOmit
refreshToken?: Prisma.RefreshTokenOmit
inventoryTransfer?: Prisma.InventoryTransferOmit
inventoryTransferItem?: Prisma.InventoryTransferItemOmit
stockAdjustment?: Prisma.StockAdjustmentOmit
@@ -69,6 +69,8 @@ export const ModelName = {
SalesInvoiceItem: 'SalesInvoiceItem',
StockMovement: 'StockMovement',
StockBalance: 'StockBalance',
OtpCode: 'OtpCode',
RefreshToken: 'RefreshToken',
InventoryTransfer: 'InventoryTransfer',
InventoryTransferItem: 'InventoryTransferItem',
StockAdjustment: 'StockAdjustment',
@@ -374,6 +376,30 @@ export const StockBalanceScalarFieldEnum = {
export type StockBalanceScalarFieldEnum = (typeof StockBalanceScalarFieldEnum)[keyof typeof StockBalanceScalarFieldEnum]
export const OtpCodeScalarFieldEnum = {
id: 'id',
mobileNumber: 'mobileNumber',
code: 'code',
used: 'used',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type OtpCodeScalarFieldEnum = (typeof OtpCodeScalarFieldEnum)[keyof typeof OtpCodeScalarFieldEnum]
export const RefreshTokenScalarFieldEnum = {
id: 'id',
tokenHash: 'tokenHash',
userId: 'userId',
revoked: 'revoked',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum]
export const InventoryTransferScalarFieldEnum = {
id: 'id',
code: 'code',
@@ -638,6 +664,21 @@ export const StockMovementOrderByRelevanceFieldEnum = {
export type StockMovementOrderByRelevanceFieldEnum = (typeof StockMovementOrderByRelevanceFieldEnum)[keyof typeof StockMovementOrderByRelevanceFieldEnum]
export const OtpCodeOrderByRelevanceFieldEnum = {
mobileNumber: 'mobileNumber',
code: 'code'
} as const
export type OtpCodeOrderByRelevanceFieldEnum = (typeof OtpCodeOrderByRelevanceFieldEnum)[keyof typeof OtpCodeOrderByRelevanceFieldEnum]
export const RefreshTokenOrderByRelevanceFieldEnum = {
tokenHash: 'tokenHash'
} as const
export type RefreshTokenOrderByRelevanceFieldEnum = (typeof RefreshTokenOrderByRelevanceFieldEnum)[keyof typeof RefreshTokenOrderByRelevanceFieldEnum]
export const InventoryTransferOrderByRelevanceFieldEnum = {
code: 'code',
description: 'description'
+2
View File
@@ -26,6 +26,8 @@ export type * from './models/SalesInvoice.js'
export type * from './models/SalesInvoiceItem.js'
export type * from './models/StockMovement.js'
export type * from './models/StockBalance.js'
export type * from './models/OtpCode.js'
export type * from './models/RefreshToken.js'
export type * from './models/InventoryTransfer.js'
export type * from './models/InventoryTransferItem.js'
export type * from './models/StockAdjustment.js'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -245,17 +245,17 @@ export type RoleOrderByWithRelationInput = {
export type RoleWhereUniqueInput = Prisma.AtLeast<{
id?: number
name?: string
AND?: Prisma.RoleWhereInput | Prisma.RoleWhereInput[]
OR?: Prisma.RoleWhereInput[]
NOT?: Prisma.RoleWhereInput | Prisma.RoleWhereInput[]
name?: Prisma.StringFilter<"Role"> | string
description?: Prisma.StringNullableFilter<"Role"> | string | null
permissions?: Prisma.JsonNullableFilter<"Role">
createdAt?: Prisma.DateTimeFilter<"Role"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Role"> | Date | string
deletedAt?: Prisma.DateTimeNullableFilter<"Role"> | Date | string | null
users?: Prisma.UserListRelationFilter
}, "id">
}, "id" | "name">
export type RoleOrderByWithAggregationInput = {
id?: Prisma.SortOrder
+151
View File
@@ -253,6 +253,7 @@ export type UserWhereInput = {
deletedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.XOR<Prisma.RoleScalarRelationFilter, Prisma.RoleWhereInput>
refreshTokens?: Prisma.RefreshTokenListRelationFilter
}
export type UserOrderByWithRelationInput = {
@@ -266,6 +267,7 @@ export type UserOrderByWithRelationInput = {
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.RoleOrderByWithRelationInput
refreshTokens?: Prisma.RefreshTokenOrderByRelationAggregateInput
_relevance?: Prisma.UserOrderByRelevanceInput
}
@@ -283,6 +285,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
deletedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.XOR<Prisma.RoleScalarRelationFilter, Prisma.RoleWhereInput>
refreshTokens?: Prisma.RefreshTokenListRelationFilter
}, "id" | "mobileNumber">
export type UserOrderByWithAggregationInput = {
@@ -326,6 +329,7 @@ export type UserCreateInput = {
deletedAt?: Date | string | null
updatedAt?: Date | string
role: Prisma.RoleCreateNestedOneWithoutUsersInput
refreshTokens?: Prisma.RefreshTokenCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateInput = {
@@ -338,6 +342,7 @@ export type UserUncheckedCreateInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedCreateNestedManyWithoutUserInput
}
export type UserUpdateInput = {
@@ -349,6 +354,7 @@ export type UserUpdateInput = {
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.RoleUpdateOneRequiredWithoutUsersNestedInput
refreshTokens?: Prisma.RefreshTokenUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateInput = {
@@ -361,6 +367,7 @@ export type UserUncheckedUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateManyInput = {
@@ -459,6 +466,11 @@ export type UserOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type UserScalarRelationFilter = {
is?: Prisma.UserWhereInput
isNot?: Prisma.UserWhereInput
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
@@ -521,6 +533,20 @@ export type UserUncheckedUpdateManyWithoutRoleNestedInput = {
deleteMany?: Prisma.UserScalarWhereInput | Prisma.UserScalarWhereInput[]
}
export type UserCreateNestedOneWithoutRefreshTokensInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutRefreshTokensInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutRefreshTokensNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutRefreshTokensInput
upsert?: Prisma.UserUpsertWithoutRefreshTokensInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutRefreshTokensInput, Prisma.UserUpdateWithoutRefreshTokensInput>, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
}
export type UserCreateWithoutRoleInput = {
mobileNumber: string
password: string
@@ -529,6 +555,7 @@ export type UserCreateWithoutRoleInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutRoleInput = {
@@ -540,6 +567,7 @@ export type UserUncheckedCreateWithoutRoleInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutRoleInput = {
@@ -583,6 +611,68 @@ export type UserScalarWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
}
export type UserCreateWithoutRefreshTokensInput = {
mobileNumber: string
password: string
firstName: string
lastName: string
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
role: Prisma.RoleCreateNestedOneWithoutUsersInput
}
export type UserUncheckedCreateWithoutRefreshTokensInput = {
id?: number
mobileNumber: string
password: string
firstName: string
lastName: string
roleId: number
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
}
export type UserCreateOrConnectWithoutRefreshTokensInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
}
export type UserUpsertWithoutRefreshTokensInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutRefreshTokensInput, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
create: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutRefreshTokensInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutRefreshTokensInput, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
}
export type UserUpdateWithoutRefreshTokensInput = {
mobileNumber?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.RoleUpdateOneRequiredWithoutUsersNestedInput
}
export type UserUncheckedUpdateWithoutRefreshTokensInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
mobileNumber?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
roleId?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type UserCreateManyRoleInput = {
id?: number
mobileNumber: string
@@ -602,6 +692,7 @@ export type UserUpdateWithoutRoleInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutRoleInput = {
@@ -613,6 +704,7 @@ export type UserUncheckedUpdateWithoutRoleInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateManyWithoutRoleInput = {
@@ -627,6 +719,35 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
}
/**
* Count Type UserCountOutputType
*/
export type UserCountOutputType = {
refreshTokens: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
refreshTokens?: boolean | UserCountOutputTypeCountRefreshTokensArgs
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the UserCountOutputType
*/
select?: Prisma.UserCountOutputTypeSelect<ExtArgs> | null
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountRefreshTokensArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RefreshTokenWhereInput
}
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -639,6 +760,8 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
deletedAt?: boolean
updatedAt?: boolean
role?: boolean | Prisma.RoleDefaultArgs<ExtArgs>
refreshTokens?: boolean | Prisma.User$refreshTokensArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
@@ -658,12 +781,15 @@ export type UserSelectScalar = {
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "mobileNumber" | "password" | "firstName" | "lastName" | "roleId" | "createdAt" | "deletedAt" | "updatedAt", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
role?: boolean | Prisma.RoleDefaultArgs<ExtArgs>
refreshTokens?: boolean | Prisma.User$refreshTokensArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "User"
objects: {
role: Prisma.$RolePayload<ExtArgs>
refreshTokens: Prisma.$RefreshTokenPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number
@@ -1016,6 +1142,7 @@ readonly fields: UserFieldRefs;
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
role<T extends Prisma.RoleDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.RoleDefaultArgs<ExtArgs>>): Prisma.Prisma__RoleClient<runtime.Types.Result.GetResult<Prisma.$RolePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
refreshTokens<T extends Prisma.User$refreshTokensArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$refreshTokensArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefreshTokenPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1396,6 +1523,30 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* User.refreshTokens
*/
export type User$refreshTokensArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RefreshToken
*/
select?: Prisma.RefreshTokenSelect<ExtArgs> | null
/**
* Omit specific fields from the RefreshToken
*/
omit?: Prisma.RefreshTokenOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RefreshTokenInclude<ExtArgs> | null
where?: Prisma.RefreshTokenWhereInput
orderBy?: Prisma.RefreshTokenOrderByWithRelationInput | Prisma.RefreshTokenOrderByWithRelationInput[]
cursor?: Prisma.RefreshTokenWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RefreshTokenScalarFieldEnum | Prisma.RefreshTokenScalarFieldEnum[]
}
/**
* User without action
*/
+27
View File
@@ -0,0 +1,27 @@
import { Controller, Get, Query } from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { CardexService } from './cardex.service'
@Controller('cardex')
export class CardexController {
constructor(private readonly service: CardexService) {}
@Get('')
@ApiQuery({ name: 'startDate', required: true, type: Date })
@ApiQuery({ name: 'endDate', required: false, type: Date })
@ApiQuery({ name: 'inventoryId', required: false, type: Number })
@ApiQuery({ name: 'productId', required: false, type: Number })
async getStock(
@Query('startDate') startDate: Date,
@Query('endDate') endDate: Date,
@Query('inventoryId') inventoryId: number,
@Query('productId') productId: number,
) {
return this.service.getCardex({
startDate,
endDate,
inventoryId,
productId,
})
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { CardexController } from './cardex.controller'
import { CardexService } from './cardex.service'
@Module({
imports: [PrismaModule],
controllers: [CardexController],
providers: [CardexService],
})
export class CardexModule {}
+56
View File
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common'
import dayjs from 'dayjs'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { ReadCardexDto } from './dto/read-pos.dto'
@Injectable()
export class CardexService {
constructor(private prisma: PrismaService) {}
async getCardex({ startDate, endDate, inventoryId, productId }: ReadCardexDto) {
if (!endDate) {
endDate = dayjs(startDate).add(1, 'months').toDate()
}
const where: Prisma.StockMovementWhereInput = {}
if (inventoryId !== undefined) where.inventoryId = inventoryId
if (productId !== undefined) where.productId = productId
if (startDate || endDate) {
where.createdAt = {}
if (startDate) where.createdAt.gte = new Date(startDate)
if (endDate) where.createdAt.lte = new Date(endDate)
}
const items = await this.prisma.stockMovement.findMany({
where,
include: { product: true, inventory: true },
orderBy: { createdAt: 'desc' },
})
const mapped = items.map(i => ({
id: i.id,
type: i.type,
quantity: Number(i.quantity),
fee: Number(i.fee),
totalCost: Number(i.totalCost),
referenceType: i.referenceType,
referenceId: i.referenceId,
createdAt: i.createdAt,
avgCost: Number(i.avgCost),
product: {
name: i.product.name,
sku: i.product.sku,
id: i.productId,
},
inventory: {
name: i.inventory.name,
id: i.inventoryId,
},
}))
return ResponseMapper.list(mapped)
}
}
+23
View File
@@ -0,0 +1,23 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { IsDateString, IsNumber, IsOptional } from 'class-validator'
export class ReadCardexDto {
@ApiPropertyOptional()
@IsDateString()
startDate: Date
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
endDate?: Date
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
inventoryId?: number
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
productId?: number
}
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../prisma/prisma.module'
import { PrismaModule } from '../../prisma/prisma.module'
import { PosController } from './pos.controller'
import { PosService } from './pos.service'
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../common/response/response-mapper'
import { Prisma } from '../generated/prisma/client'
import { PrismaService } from '../prisma/prisma.service'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateOrderDto } from './dto/create-pos.dto'
@Injectable()
+6 -3
View File
@@ -1,4 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { CreateProductDto } from './dto/create-product.dto'
import { UpdateProductDto } from './dto/update-product.dto'
import { ProductsService } from './products.service'
@@ -13,8 +14,10 @@ export class ProductsController {
}
@Get()
findAll() {
return this.productsService.findAll()
@ApiQuery({ name: 'page', required: false, type: Number, default: 1 })
@ApiQuery({ name: 'pageSize', required: false, type: Number, default: 30 })
findAll(@Query('page') page: number = 1, @Query('pageSize') pageSize: number = 30) {
return this.productsService.findAll(page, pageSize)
}
@Get(':id')
+12 -4
View File
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'
import { ShortEntity } from '../common/interfaces/response-models'
import { ResponseMapper } from '../common/response/response-mapper'
import { Prisma } from '../generated/prisma/client'
import { PrismaService } from '../prisma/prisma.service'
import { CreateProductDto } from './dto/create-product.dto'
@@ -28,10 +29,17 @@ export class ProductsService {
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.product.findMany({
async findAll(page = 1, pageSize = 20) {
const query: Prisma.ProductFindManyArgs = {}
const [items, count] = await this.prisma.$transaction([
this.prisma.product.findMany({
include: { brand: true, category: true, stockBalances: true },
})
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.product.count(),
])
const mapped = items.map(p => {
const { brandId, categoryId, ...rest } = p as any
@@ -48,7 +56,7 @@ export class ProductsService {
return { ...rest, brand, category, stock }
})
return ResponseMapper.list(mapped)
return ResponseMapper.paginate(mapped, count, page, pageSize)
}
async findOne(id: number) {
@@ -23,8 +23,8 @@ export class PurchaseReceiptItemsService {
const item = await this.prisma.purchaseReceiptItem.create({ data: payload })
return ResponseMapper.create(item)
}
async findAll(receiptId?: number) {
const where = receiptId ? { receiptId: Number(receiptId) } : undefined
async findAll(receiptId: number) {
const where = { receiptId: Number(receiptId) }
const items = await this.prisma.purchaseReceiptItem.findMany({
where,
include: { product: true },
@@ -47,7 +47,22 @@ export class PurchaseReceiptsService {
async findOne(id: number) {
const item = await this.prisma.purchaseReceipt.findUnique({
where: { id },
include: { supplier: true, inventory: true, items: true },
include: {
supplier: {
select: {
id: true,
firstName: true,
lastName: true,
},
},
inventory: {
select: {
id: true,
name: true,
},
},
items: true,
},
})
if (!item) return null
return ResponseMapper.single(item)
+16 -3
View File
@@ -17,9 +17,22 @@ export class SuppliersController {
return this.suppliersService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.suppliersService.findOne(Number(id))
@Get(':supplierId')
findOne(@Param('supplierId') supplierId: string) {
return this.suppliersService.findOne(Number(supplierId))
}
@Get(':supplierId/invoices')
getInvoices(@Param('supplierId') supplierId: string) {
return this.suppliersService.getInvoices(Number(supplierId))
}
@Get(':supplierId/invoices/:invoiceId')
getInvoice(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
) {
return this.suppliersService.getInvoice(Number(supplierId), Number(invoiceId))
}
@Patch(':id')
+76 -1
View File
@@ -18,7 +18,82 @@ export class SuppliersService {
async findOne(id: number) {
const item = await this.prisma.supplier.findUnique({ where: { id } })
if (!item) return null
return ResponseMapper.single(item)
const receiptsInfo = await this.prisma.purchaseReceipt.aggregate({
where: {
supplierId: id,
},
_count: true,
_sum: {
totalAmount: true,
},
})
return ResponseMapper.single({
...item,
receiptsOverview: {
totalPayment: receiptsInfo._sum.totalAmount,
count: receiptsInfo._count,
},
})
}
async getInvoices(supplierId: number) {
const items = await this.prisma.purchaseReceipt.findMany({
where: {
supplierId,
},
include: {
inventory: {
select: {
id: true,
name: true,
},
},
items: {
select: {
id: true,
count: true,
fee: true,
total: true,
product: {
select: {
id: true,
name: true,
sku: true,
},
},
},
},
},
omit: {
inventoryId: true,
supplierId: true,
},
})
return ResponseMapper.list(items)
}
async getInvoice(supplierId: number, invoiceId: number) {
const invoice = await this.prisma.purchaseReceipt.findUnique({
where: {
id: invoiceId,
supplierId,
},
include: {
inventory: {
select: {
id: true,
name: true,
},
},
items: true,
},
omit: {
inventoryId: true,
supplierId: true,
},
})
return ResponseMapper.single(invoice)
}
async update(id: number, data: any) {