Compare commits

..

13 Commits

Author SHA1 Message Date
ahasani 636daca703 fix: improve Redis client shutdown handling to avoid errors 2026-06-17 14:57:52 +03:30
ahasani 9bf294a1f3 refactor: comment out payment validation for invoice amount correction 2026-06-17 10:03:09 +03:30
ahasani 839f6de691 debug for build 2026-06-17 09:07:53 +03:30
ahasani 826041b07a update return from sale invoices jurney and set nama return from sale 2026-06-17 08:50:35 +03:30
ahasani f87e5b9d8e feat: refactor Prisma client initialization and update related services 2026-06-16 15:47:29 +03:30
ahasani 652177862d update config 2026-06-16 10:09:22 +03:30
ahasani ac2e7f5dab feat: comment out CORS configuration in bootstrap function 2026-06-15 21:50:29 +03:30
ahasani f94a108948 feat: update activation expiration handling and enhance payload structure in transaction services 2026-06-15 21:35:53 +03:30
ahasani d51b677f26 feat: update invoice processing to use last_tsp_status for status mapping 2026-06-15 19:34:21 +03:30
ahasani 9170d8cd5a feat: enhance sales invoice processing with correction and return functionalities 2026-06-15 17:14:54 +03:30
ahasani 5f70b95589 feat: refactor sales invoice services and introduce pagination and filtering
- Added SharedSaleInvoicePaginationService for handling pagination logic.
- Introduced SharedSaleInvoiceFilterService to centralize filtering logic for sales invoices.
- Updated SalesInvoicesService to utilize the new pagination and filtering services.
- Refactored findAll methods in SalesInvoicesService, CustomerSaleInvoicesService, and other related services to support pagination and filtering.
- Enhanced DTOs for sales invoice filtering to extend shared filter properties.
- Updated module imports to include new services.
- Cleaned up redundant code related to filtering and pagination across various services.
2026-06-14 16:34:00 +03:30
ahasani d2bd576277 feat(pos): enhance password update process with current password validation 2026-06-11 17:53:08 +03:30
ahasani 23bfe1ecbe Refactor: Remove stored procedures and replace with direct database operations
feat: Add economic code and guild information to sales invoice selection

fix: Update error messages for invoice access and creation to use "صورت‌حساب"

fix: Change error messages in SaleInvoicesService to use "صورت‌حساب" instead of "فاکتور"

fix: Update error messages in SalesInvoicesService for not found cases to use "صورت‌حساب"

fix: Modify TSP service to handle invoice updates and error messages consistently with "صورت‌حساب"

fix: Update common DTO descriptions to refer to "صورت‌حساب" instead of "فاکتور"

fix: Adjust utility functions to handle invoice references and error messages with "صورت‌حساب"
2026-06-11 16:13:17 +03:30
59 changed files with 2037 additions and 3229 deletions
+4 -1
View File
@@ -1,7 +1,10 @@
import { PasswordUtil } from '@/common/utils/password.util' import { PasswordUtil } from '@/common/utils/password.util'
import { GoodPricingModel } from '@/generated/prisma/enums' import { GoodPricingModel } from '@/generated/prisma/enums'
import { GoodCreateManyInput } from '@/generated/prisma/models' import { GoodCreateManyInput } from '@/generated/prisma/models'
import { prisma } from '../src/lib/prisma' import { PrismaClient } from '../src/generated/prisma/client'
import { prismaAdapter } from '../src/lib/prisma'
const prisma = new PrismaClient({ adapter: prismaAdapter })
async function main() { async function main() {
const password = await PasswordUtil.hash('123456') const password = await PasswordUtil.hash('123456')
-548
View File
@@ -1,548 +0,0 @@
-- AUTO-GENERATED MYSQL TRIGGER DUMP
-- Generated at: 2026-01-06T16:09:38.959Z
-- ------------------------------------------
-- index: 1
-- Trigger: trg_bank_account_transaction_after_delete
-- Event: DELETE
-- Table: Bank_Account_Transactions
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_bank_account_transaction_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_bank_account_transaction_after_delete` AFTER DELETE ON `Bank_Account_Transactions` FOR EACH ROW BEGIN
UPDATE `Bank_Account_Balance` SET balance = balance - OLD.amount WHERE `bankAccountId` = OLD.bankAccountId;
END;
-- ------------------------------------------
-- index: 2
-- 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
DECLARE fromInv INT;
DECLARE toInv INT;
DECLARE _avgCost DECIMAL(10,2);
DECLARE latestQuantityInOrigin DECIMAL(10,2);
DECLARE latestQuantityInDestination DECIMAL(10,2);
SELECT fromInventoryId, toInventoryId INTO fromInv, toInv
FROM Inventory_Transfers WHERE id = NEW.transferId;
SELECT COALESCE(avgCost, 0), COALESCE(quantity, 0) INTO _avgCost, latestQuantityInOrigin FROM Stock_Balance
WHERE ProductId = NEW.productId AND inventoryId = fromInv LIMIT 1;
SELECT COALESCE(quantity, 0) INTO latestQuantityInDestination FROM Stock_Balance
WHERE ProductId = NEW.productId AND inventoryId = toInv LIMIT 1;
-- OUT from source
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('OUT', NEW.count, _avgCost, _avgCost*NEW.count, _avgCost, 'INVENTORY_TRANSFER', NEW.transferId, NEW.productId, fromInv, toInv, NOW(), latestQuantityInOrigin-NEW.count);
-- IN to destination
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('IN', NEW.count,_avgCost, _avgCost*NEW.count,_avgCost, 'INVENTORY_TRANSFER', NEW.transferId, NEW.productId, toInv, fromInv, NOW(), latestQuantityInOrigin-NEW.count);
end;
-- ------------------------------------------
-- index: 3
-- Trigger: trg_order_item_after_update
-- Event: UPDATE
-- Table: Order_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_item_after_update`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_item_after_update` AFTER UPDATE ON `Order_Items` FOR EACH ROW BEGIN
UPDATE Stock_Reservations
SET quantity = quantity - OLD.quantity + NEW.quantity
WHERE orderId = NEW.orderId AND productId = NEW.productId;
END;
-- ------------------------------------------
-- index: 4
-- Trigger: trg_order_item_after_delete
-- Event: DELETE
-- Table: Order_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_item_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_item_after_delete` AFTER DELETE ON `Order_Items` FOR EACH ROW BEGIN
DELETE From Stock_Reservations
WHERE orderId = OLD.orderId AND productId = OLD.productId;
END;
-- ------------------------------------------
-- index: 5
-- Trigger: trg_order_after_cancel
-- Event: UPDATE
-- Table: Orders
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_after_cancel`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_after_cancel` AFTER UPDATE ON `Orders` FOR EACH ROW BEGIN
IF NEW.status = 'CANCELED' OR NEW.status = 'REJECTED' OR NEW.status = 'DONE' THEN
UPDATE Stock_Reservations sr SET quantity = 0
WHERE sr.orderId = NEW.id;
END IF;
END;
-- ------------------------------------------
-- index: 6
-- Trigger: trg_purchase_receipt_item_after_insert
-- Event: INSERT
-- Table: Purchase_Receipt_Items
-- ------------------------------------------
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;
DECLARE suppId INT;
-- Get inventory & supplier from
SELECT inventoryId, supplierId
INTO invId, suppId
FROM Purchase_Receipts
WHERE id = NEW.receiptId;
-- Get current stock quantity (if exists)
SELECT COALESCE(quantity, 0)
INTO latestQuantity
FROM Stock_Balance sb
WHERE sb.inventoryId = invId
AND sb.productId = NEW.productId
LIMIT 1;
-- Insert stock movement
INSERT INTO Stock_Movements (
type,
quantity,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
supplierId,
remainedInStock,
createdAt
)
VALUES (
'IN',
NEW.count,
NEW.unitPrice,
NEW.totalAmount,
'PURCHASE',
NEW.receiptId,
NEW.productId,
invId,
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.totalAmount / NEW.count
END
,
suppId,
latestQuantity + NEW.count,
NOW()
);
END;
-- ------------------------------------------
-- index: 7
-- Trigger: trg_pr_payment_after_delete
-- Event: DELETE
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pr_payment_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pr_payment_after_delete` AFTER DELETE ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE receiptTotal DECIMAL(14,2);
DECLARE newPaid DECIMAL(14,2);
SELECT totalAmount, paidAmount
INTO receiptTotal, newPaid
FROM Purchase_Receipts
WHERE id = OLD.receiptId
FOR UPDATE;
IF OLD.type = 'PAYMENT' THEN
SET newPaid = newPaid - OLD.amount;
ELSE
SET newPaid = newPaid + OLD.amount;
END IF;
UPDATE Purchase_Receipts
SET
paidAmount = newPaid,
status =
CASE
WHEN newPaid = 0 THEN 'UNPAID'
WHEN newPaid < receiptTotal THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = OLD.receiptId;
END;
-- ------------------------------------------
-- index: 8
-- Trigger: trg_sales_invoice_items_before_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_before_insert`;
CREATE DEFINER=`pos`@`%` 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 pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
SELECT COALESCE(sav.availableQuantity, 0) INTO current_stock
FROM Stock_Available_View sav
WHERE productId = NEW.productId AND sav.inventoryId = inventory_id;
IF NEW.count > current_stock THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'موجودی کالا در انبار کافی نیست.';
END IF;
end;
-- ------------------------------------------
-- index: 9
-- Trigger: trg_sales_invoice_items_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_after_insert`;
CREATE DEFINER=`pos`@`%` 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;
DECLARE customer_id INT;
DECLARE pos_id INT;
SELECT posAccountId, customerId INTO pos_id, customer_id
FROM Sales_Invoices si
WHERE si.id = NEW.invoiceId
LIMIT 1;
INSERT INTO Trigger_Logs (name , message) VALUES ('pos_id', pos_id);
INSERT INTO Trigger_Logs (name , message) VALUES ('customer_id', customer_id);
SELECT pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
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,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
remainedInStock,
customerId,
createdAt
)
VALUES (
'OUT',
NEW.count,
NEW.unitPrice,
NEW.totalAmount,
'SALES',
NEW.invoiceId,
NEW.productId,
inventory_id,
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.totalAmount / NEW.count
END,
current_stock - NEW.count,
customer_id,
NOW()
);
END;
-- ------------------------------------------
-- index: 10
-- Trigger: trg_pos_account_payment_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pos_account_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pos_account_payment_after_insert` AFTER INSERT ON `Sales_Invoice_Payments` FOR EACH ROW BEGIN
DECLARE _bankAccountId INT;
IF(NEW.paymentMethod != 'CASH') THEN
SELECT cashBankAccountId INTO _bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
End IF;
INSERT INTO Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES(
_bankAccountId,
'DEPOSIT',
NEW.amount,
0,
'POS_SALE',
NEW.id
);
END;
-- ------------------------------------------
-- index: 11
-- Trigger: trg_sales_invoice_payment_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_sales_invoice_payment_after_insert` AFTER INSERT ON `Sales_Invoice_Payments` FOR EACH ROW BEGIN
DECLARE currentBalance DECIMAL(15,2);
DECLARE bankAccountId INT;
SELECT pa.bankAccountId INTO bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
SELECT balance INTO currentBalance
FROM Bank_Account_Balance
WHERE bankAccountId = bankAccountId
FOR UPDATE;
IF currentBalance IS NULL THEN
SET currentBalance = 0;
INSERT INTO Bank_Account_Balance (bankAccountId, balance)
VALUES (bankAccountId, 0);
END IF;
SET currentBalance = currentBalance + NEW.amount;
INSERT INTO Bank_Account_Transactions
(bankAccountId, type, amount, balanceAfter, referenceType, referenceId)
VALUES
(bankAccountId, 'DEPOSIT', NEW.amount, currentBalance, 'POS_SALE', NEW.id);
UPDATE Bank_Account_Balance
SET balance = currentBalance
WHERE bankAccountId = bankAccountId;
END;
-- ------------------------------------------
-- index: 12
-- Trigger: trg_stock_sale_insert
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_stock_sale_insert`;
CREATE DEFINER=`pos`@`%` 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.unitPrice,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity - NEW.quantity,
totalCost = totalCost - NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END;
-- ------------------------------------------
-- index: 13
-- Trigger: trg_stock_purchase_insert
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
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
INSERT INTO
Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
NEW.productId,
NEW.quantity,
NEW.unitPrice,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END;
-- ------------------------------------------
-- index: 14
-- Trigger: trg_stock_transfer
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
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 (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
NEW.quantity,
NEW.totalCost,
CASE
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;
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;
ELSE
INSERT INTO
Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
- NEW.quantity,
- COALESCE(NEW.unitPrice, 0) * NEW.quantity,
COALESCE(NEW.unitPrice, 0),
NOW()
);
END IF;
END IF;
END IF;
END;
-- ------------------------------------------
-- index: 15
-- Trigger: trg_no_negative_available_stock
-- Event: INSERT
-- Table: Stock_Reservations
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_no_negative_available_stock`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_no_negative_available_stock` BEFORE INSERT ON `Stock_Reservations` FOR EACH ROW BEGIN
DECLARE available DECIMAL(14,3);
SELECT availableQuantity
INTO available
FROM Stock_Available_View
WHERE productId = NEW.productId
AND inventoryId = NEW.inventoryId;
IF available < NEW.quantity THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'موجودی کافی نیست';
END IF;
END;
-825
View File
@@ -1,825 +0,0 @@
-- AUTO-GENERATED MYSQL TRIGGER DUMP
-- Generated at: 2026-01-04T09:46:30.365Z
-- ------------------------------------------
-- index: 1
-- Trigger: trg_bank_account_transaction_after_insert
-- Event: INSERT
-- Table: Bank_Account_Transactions
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_bank_account_transaction_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_bank_account_transaction_after_insert` AFTER INSERT ON `Bank_Account_Transactions` FOR EACH ROW BEGIN
IF NEW.type = 'DEPOSIT' THEN
UPDATE Bank_Account_Balance SET balance = balance + NEW.amount WHERE bankAccountId = NEW.bankAccountId;
ELSEIF NEW.type = 'WITHDRAWAL' THEN
UPDATE Bank_Account_Balance SET balance = balance - NEW.amount WHERE bankAccountId = NEW.bankAccountId;
END IF;
END;
-- ------------------------------------------
-- index: 2
-- Trigger: trg_bank_account_transaction_after_delete
-- Event: DELETE
-- Table: Bank_Account_Transactions
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_bank_account_transaction_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_bank_account_transaction_after_delete` AFTER DELETE ON `Bank_Account_Transactions` FOR EACH ROW BEGIN
UPDATE Bank_Accounts SET balance = balance - OLD.amount;
END;
-- ------------------------------------------
-- index: 3
-- 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
DECLARE fromInv INT;
DECLARE toInv INT;
DECLARE _avgCost DECIMAL(10,2);
DECLARE latestQuantityInOrigin DECIMAL(10,2);
DECLARE latestQuantityInDestination DECIMAL(10,2);
SELECT fromInventoryId, toInventoryId INTO fromInv, toInv
FROM Inventory_Transfers WHERE id = NEW.transferId;
SELECT COALESCE(avgCost, 0), COALESCE(quantity, 0) INTO _avgCost, latestQuantityInOrigin FROM Stock_Balance
WHERE ProductId = NEW.productId AND inventoryId = fromInv LIMIT 1;
SELECT COALESCE(quantity, 0) INTO latestQuantityInDestination FROM Stock_Balance
WHERE ProductId = NEW.productId AND inventoryId = toInv LIMIT 1;
-- OUT from source
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('OUT', NEW.count, _avgCost, _avgCost*NEW.count, _avgCost, 'INVENTORY_TRANSFER', NEW.transferId, NEW.productId, fromInv, toInv, NOW(), latestQuantityInOrigin-NEW.count);
-- IN to destination
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('IN', NEW.count,_avgCost, _avgCost*NEW.count,_avgCost, 'INVENTORY_TRANSFER', NEW.transferId, NEW.productId, toInv, fromInv, NOW(), latestQuantityInOrigin-NEW.count);
end;
-- ------------------------------------------
-- index: 4
-- Trigger: trg_order_item_after_insert
-- Event: INSERT
-- Table: Order_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_item_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_item_after_insert` AFTER INSERT ON `Order_Items` FOR EACH ROW BEGIN
UPDATE Stock_Reservations SET quantity = quantity + NEW.quantity
WHERE orderId = NEW.orderId AND productId = NEW.productId;
END;
-- ------------------------------------------
-- index: 5
-- Trigger: trg_order_item_after_update
-- Event: UPDATE
-- Table: Order_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_item_after_update`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_item_after_update` AFTER UPDATE ON `Order_Items` FOR EACH ROW BEGIN
UPDATE Stock_Reservations
SET quantity = quantity - OLD.quantity + NEW.quantity
WHERE orderId = NEW.orderId AND productId = NEW.productId;
END;
-- ------------------------------------------
-- index: 6
-- Trigger: trg_order_item_after_delete
-- Event: DELETE
-- Table: Order_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_item_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_item_after_delete` AFTER DELETE ON `Order_Items` FOR EACH ROW BEGIN
UPDATE Stock_Reservations SET quantity = quantity - OLD.quantity
WHERE orderId = OLD.orderId AND productId = OLD.productId;
END;
-- ------------------------------------------
-- index: 7
-- Trigger: trg_order_after_cancel
-- Event: UPDATE
-- Table: Orders
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_order_after_cancel`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_order_after_cancel` AFTER UPDATE ON `Orders` FOR EACH ROW BEGIN
IF NEW.status = 'CANCELED' OR NEW.status = 'REJECTED' OR NEW.status = 'DONE' THEN
UPDATE Stock_Reservations sr SET quantity = 0
WHERE sr.orderId = NEW.id;
END IF;
END;
-- ------------------------------------------
-- index: 8
-- Trigger: trg_purchase_receipt_item_after_insert
-- Event: INSERT
-- Table: Purchase_Receipt_Items
-- ------------------------------------------
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;
DECLARE suppId INT;
-- Get inventory & supplier from
SELECT inventoryId, supplierId
INTO invId, suppId
FROM Purchase_Receipts
WHERE id = NEW.receiptId;
-- Get current stock quantity (if exists)
SELECT COALESCE(quantity, 0)
INTO latestQuantity
FROM Stock_Balance sb
WHERE sb.inventoryId = invId
AND sb.productId = NEW.productId
LIMIT 1;
-- Insert stock movement
INSERT INTO Stock_Movements (
type,
quantity,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
supplierId,
remainedInStock,
createdAt
)
VALUES (
'IN',
NEW.count,
NEW.unitPrice,
NEW.totalAmount,
'PURCHASE',
NEW.receiptId,
NEW.productId,
invId,
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.totalAmount / NEW.count
END
,
suppId,
latestQuantity + NEW.count,
NOW()
);
END;
-- ------------------------------------------
-- index: 9
-- Trigger: trg_pr_payment_before_insert
-- Event: INSERT
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pr_payment_before_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pr_payment_before_insert` BEFORE INSERT ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE receiptTotal DECIMAL(14,2);
DECLARE paid DECIMAL(14,2);
SELECT totalAmount, paidAmount
INTO receiptTotal, paid
FROM Purchase_Receipts
WHERE id = NEW.receiptId
FOR UPDATE;
IF NEW.type = 'PAYMENT' AND paid + NEW.amount > receiptTotal THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'مجموع مبلغ پرداختی بیشتر از مبلغ فاکتور است.';
END IF;
END;
-- ------------------------------------------
-- index: 10
-- Trigger: trg_purchase_payment_update_receipt
-- Event: INSERT
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_purchase_payment_update_receipt`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_purchase_payment_update_receipt` AFTER INSERT ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE paid DECIMAL(15,2);
DECLARE total DECIMAL(15,2);
SELECT
COALESCE(SUM(
CASE WHEN type = 'PAYMENT' THEN amount ELSE -amount END
),0)
INTO paid
FROM Purchase_Receipt_Payments
WHERE receiptId = NEW.receiptId;
SELECT totalAmount INTO total
FROM Purchase_Receipts
WHERE id = NEW.receiptId;
UPDATE Purchase_Receipts
SET
paidAmount = paid,
status = CASE
WHEN paid = 0 THEN 'UNPAID'
WHEN paid < total THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = NEW.receiptId;
END;
-- ------------------------------------------
-- index: 11
-- Trigger: trg_purchase_payment_after_insert
-- Event: INSERT
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_purchase_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_purchase_payment_after_insert` AFTER INSERT ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE currentBalance DECIMAL(15, 2);
SELECT balance INTO currentBalance
FROM Bank_Account_Balance
WHERE
bankAccountId = NEW.bankAccountId FOR
UPDATE;
IF currentBalance IS NULL THEN SET currentBalance = 0;
INSERT INTO
Bank_Account_Balance (bankAccountId, balance, updatedAt)
VALUES (NEW.bankAccountId, 0, NOW());
END IF;
IF NEW.type = 'PAYMENT' THEN
SET
currentBalance = currentBalance - NEW.amount;
INSERT INTO
Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES (
NEW.bankAccountId,
'WITHDRAWAL',
NEW.amount,
currentBalance,
'PURCHASE_PAYMENT',
NEW.id
);
ELSE SET currentBalance = currentBalance + NEW.amount;
INSERT INTO
Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES (
NEW.bankAccountId,
'DEPOSIT',
NEW.amount,
currentBalance,
'PURCHASE_REFUND',
NEW.id
);
END IF;
UPDATE Bank_Account_Balance
SET
balance = currentBalance
WHERE
bankAccountId = NEW.bankAccountId;
END;
-- ------------------------------------------
-- index: 12
-- Trigger: trg_pr_payment_after_insert
-- Event: INSERT
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pr_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pr_payment_after_insert` AFTER INSERT ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE receiptTotal DECIMAL(14,2) Default 0;
DECLARE newPaid DECIMAL(14,2) Default 0;
DECLARE _supplierId INT;
DECLARE lastBalance DECIMAL(14,2) Default 0;
-- Lock receipt row
SELECT COALESCE(totalAmount, 0), COALESCE(paidAmount, 0), supplierId
INTO receiptTotal, newPaid, _supplierId
FROM Purchase_Receipts
WHERE id = NEW.receiptId
FOR UPDATE;
-- Apply payment or refund
IF NEW.type = 'PAYMENT' THEN
SET newPaid = newPaid + NEW.amount;
ELSE
SET newPaid = newPaid - NEW.amount;
END IF;
-- Update receipt
UPDATE Purchase_Receipts
SET
paidAmount = newPaid,
status =
CASE
WHEN newPaid = 0 THEN 'UNPAID'
WHEN newPaid < receiptTotal THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = NEW.receiptId;
-- Get last supplier balance
SELECT IFNULL(balance, 0)
INTO lastBalance
FROM Supplier_Ledger
WHERE supplierId = _supplierId
ORDER BY id DESC
LIMIT 1;
-- Insert supplier ledger
INSERT INTO Supplier_Ledger
(
supplierId,
debit,
credit,
balance,
sourceType,
sourceId,
createdAt
)
VALUES
(
_supplierId,
IF(NEW.type = 'REFUND', NEW.amount, 0),
IF(NEW.type = 'PAYMENT', NEW.amount, 0),
lastBalance
+ IF(NEW.type = 'PAYMENT', NEW.amount, 0)
- IF(NEW.type = 'REFUND', NEW.amount, 0),
'PAYMENT',
NEW.id,
NOW()
);
END;
-- ------------------------------------------
-- index: 13
-- Trigger: trg_pr_payment_after_delete
-- Event: DELETE
-- Table: Purchase_Receipt_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pr_payment_after_delete`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pr_payment_after_delete` AFTER DELETE ON `Purchase_Receipt_Payments` FOR EACH ROW BEGIN
DECLARE receiptTotal DECIMAL(14,2);
DECLARE newPaid DECIMAL(14,2);
SELECT totalAmount, paidAmount
INTO receiptTotal, newPaid
FROM Purchase_Receipts
WHERE id = OLD.receiptId
FOR UPDATE;
IF OLD.type = 'PAYMENT' THEN
SET newPaid = newPaid - OLD.amount;
ELSE
SET newPaid = newPaid + OLD.amount;
END IF;
UPDATE Purchase_Receipts
SET
paidAmount = newPaid,
status =
CASE
WHEN newPaid = 0 THEN 'UNPAID'
WHEN newPaid < receiptTotal THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = OLD.receiptId;
END;
-- ------------------------------------------
-- index: 14
-- Trigger: trg_purchase_receipt_after_insert
-- Event: INSERT
-- Table: Purchase_Receipts
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_purchase_receipt_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_purchase_receipt_after_insert` AFTER INSERT ON `Purchase_Receipts` FOR EACH ROW BEGIN
DECLARE lastBalance DECIMAL(14,2) DEFAULT 0;
SELECT COALESCE(balance, 0)
INTO lastBalance
FROM Supplier_Ledger
WHERE supplierId = NEW.supplierId
ORDER BY id DESC
LIMIT 1;
INSERT INTO Supplier_Ledger
(
supplierId,
debit,
credit,
balance,
sourceType,
sourceId,
createdAt
)
VALUES
(
NEW.supplierId,
NEW.totalAmount,
0,
lastBalance - NEW.totalAmount,
'PURCHASE',
NEW.id,
NOW()
);
END;
-- ------------------------------------------
-- index: 15
-- Trigger: trg_sales_invoice_items_before_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_before_insert`;
CREATE DEFINER=`pos`@`%` 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 pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
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;
-- ------------------------------------------
-- index: 16
-- Trigger: trg_sales_invoice_items_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Items
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_items_after_insert`;
CREATE DEFINER=`pos`@`%` 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;
DECLARE customer_id INT;
DECLARE pos_id INT;
SELECT posAccountId, customerId INTO pos_id, customer_id
FROM Sales_Invoices si
WHERE si.id = NEW.invoiceId
LIMIT 1;
INSERT INTO Trigger_Logs (name , message) VALUES ('pos_id', pos_id);
INSERT INTO Trigger_Logs (name , message) VALUES ('customer_id', customer_id);
SELECT pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
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,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
remainedInStock,
customerId,
createdAt
)
VALUES (
'OUT',
NEW.count,
NEW.unitPrice,
NEW.totalAmount,
'SALES',
NEW.invoiceId,
NEW.productId,
inventory_id,
CASE
WHEN NEW.count = 0 THEN 0
ELSE NEW.totalAmount / NEW.count
END,
current_stock - NEW.count,
customer_id,
NOW()
);
END;
-- ------------------------------------------
-- index: 17
-- Trigger: trg_sales_invoice_payment_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_sales_invoice_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_sales_invoice_payment_after_insert` AFTER INSERT ON `Sales_Invoice_Payments` FOR EACH ROW BEGIN
DECLARE currentBalance DECIMAL(15,2);
DECLARE bankAccountId INT;
SELECT pa.bankAccountId INTO bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
SELECT balance INTO currentBalance
FROM Bank_Account_Balance
WHERE bankAccountId = bankAccountId
FOR UPDATE;
IF currentBalance IS NULL THEN
SET currentBalance = 0;
INSERT INTO Bank_Account_Balance (bankAccountId, balance)
VALUES (bankAccountId, 0);
END IF;
SET currentBalance = currentBalance + NEW.amount;
INSERT INTO Bank_Account_Transactions
(bankAccountId, type, amount, balanceAfter, referenceType, referenceId)
VALUES
(bankAccountId, 'DEPOSIT', NEW.amount, currentBalance, 'POS_SALE', NEW.id);
UPDATE Bank_Account_Balance
SET balance = currentBalance
WHERE bankAccountId = bankAccountId;
END;
-- ------------------------------------------
-- index: 18
-- Trigger: trg_pos_account_payment_after_insert
-- Event: INSERT
-- Table: Sales_Invoice_Payments
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_pos_account_payment_after_insert`;
CREATE DEFINER=`pos`@`%` TRIGGER `trg_pos_account_payment_after_insert` AFTER INSERT ON `Sales_Invoice_Payments` FOR EACH ROW BEGIN
DECLARE _bankAccountId INT;
IF(NEW.paymentMethod != 'CASH') THEN
SELECT cashBankAccountId INTO _bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = NEW.invoiceId;
End IF;
INSERT INTO Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES(
_bankAccountId,
'DEPOSIT',
NEW.amount,
0,
'POS_SALE',
NEW.id
);
END;
-- ------------------------------------------
-- index: 19
-- Trigger: trg_stock_transfer
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
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 (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
NEW.quantity,
NEW.totalCost,
CASE
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;
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;
ELSE
INSERT INTO
Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
NEW.productId,
NEW.inventoryId,
- NEW.quantity,
- COALESCE(NEW.unitPrice, 0) * NEW.quantity,
COALESCE(NEW.unitPrice, 0),
NOW()
);
END IF;
END IF;
END IF;
END;
-- ------------------------------------------
-- index: 20
-- Trigger: trg_stock_purchase_insert
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
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
INSERT INTO
Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
NEW.productId,
NEW.quantity,
NEW.unitPrice,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + NEW.quantity,
totalCost = totalCost + NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END;
-- ------------------------------------------
-- index: 21
-- Trigger: trg_stock_sale_insert
-- Event: INSERT
-- Table: Stock_Movements
-- ------------------------------------------
DROP TRIGGER IF EXISTS `trg_stock_sale_insert`;
CREATE DEFINER=`pos`@`%` 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.unitPrice,
NEW.totalCost,
NEW.inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity - NEW.quantity,
totalCost = totalCost - NEW.totalCost,
avgCost = totalCost / quantity;
END IF;
END;
-657
View File
@@ -1,657 +0,0 @@
-- Stored Procedures equivalent to triggers
DELIMITER / /
-- Procedure for trg_bank_account_transaction_after_insert
CREATE PROCEDURE update_bank_balance(IN p_bankAccountId INT, IN p_type VARCHAR(20), IN p_amount DECIMAL(15,2))
BEGIN
START TRANSACTION;
IF p_type = 'DEPOSIT' THEN
UPDATE Bank_Account_Balance SET balance = balance + p_amount WHERE bankAccountId = p_bankAccountId;
ELSEIF p_type = 'WITHDRAWAL' THEN
UPDATE Bank_Account_Balance SET balance = balance - p_amount WHERE bankAccountId = p_bankAccountId;
END IF;
COMMIT;
END //
-- Procedure for trg_bank_account_transaction_after_delete
CREATE PROCEDURE update_bank_balance_on_delete(IN p_bankAccountId INT, IN p_amount DECIMAL(15,2))
BEGIN
START TRANSACTION;
UPDATE Bank_Accounts SET balance = balance - p_amount WHERE id = p_bankAccountId;
COMMIT;
END //
-- Procedure for trg_transfer_item_after_insert
CREATE PROCEDURE process_transfer_item(IN p_transferId INT, IN p_productId INT, IN p_count DECIMAL(10,2))
BEGIN
START TRANSACTION;
DECLARE fromInv INT;
DECLARE toInv INT;
DECLARE _avgCost DECIMAL(10,2);
DECLARE latestQuantityInOrigin DECIMAL(10,2);
DECLARE latestQuantityInDestination DECIMAL(10,2);
SELECT fromInventoryId, toInventoryId INTO fromInv, toInv
FROM Inventory_Transfers WHERE id = p_transferId;
SELECT COALESCE(avgCost, 0), COALESCE(quantity, 0) INTO _avgCost, latestQuantityInOrigin FROM Stock_Balance
WHERE ProductId = p_productId AND inventoryId = fromInv LIMIT 1;
SELECT COALESCE(quantity, 0) INTO latestQuantityInDestination FROM Stock_Balance
WHERE ProductId = p_productId AND inventoryId = toInv LIMIT 1;
-- OUT from source
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('OUT', p_count, _avgCost, _avgCost*p_count, _avgCost, 'INVENTORY_TRANSFER', p_transferId, p_productId, fromInv, toInv, NOW(), latestQuantityInOrigin-p_count);
-- IN to destination
INSERT INTO Stock_Movements
(type, quantity, unitPrice, totalCost, avgCost, referenceType, referenceId, productId, inventoryId, counterInventoryId, createdAt, remainedInStock)
VALUES
('IN', p_count, _avgCost, _avgCost*p_count, _avgCost, 'INVENTORY_TRANSFER', p_transferId, p_productId, toInv, fromInv, NOW(), latestQuantityInOrigin-p_count);
COMMIT;
END //
-- Procedure for trg_order_item_after_insert
CREATE PROCEDURE update_stock_reservation_insert(IN p_orderId INT, IN p_productId INT, IN p_quantity DECIMAL(10,2))
BEGIN
START TRANSACTION;
UPDATE Stock_Reservations SET quantity = quantity + p_quantity
WHERE orderId = p_orderId AND productId = p_productId;
COMMIT;
END //
-- Procedure for trg_order_item_after_update
CREATE PROCEDURE update_stock_reservation_update(IN p_orderId INT, IN p_productId INT, IN p_old_quantity DECIMAL(10,2), IN p_new_quantity DECIMAL(10,2))
BEGIN
START TRANSACTION;
UPDATE Stock_Reservations
SET quantity = quantity - p_old_quantity + p_new_quantity
WHERE orderId = p_orderId AND productId = p_productId;
COMMIT;
END //
-- Procedure for trg_order_item_after_delete
CREATE PROCEDURE update_stock_reservation_delete(IN p_orderId INT, IN p_productId INT, IN p_quantity DECIMAL(10,2))
BEGIN
START TRANSACTION;
UPDATE Stock_Reservations SET quantity = quantity - p_quantity
WHERE orderId = p_orderId AND productId = p_productId;
COMMIT;
END //
-- Procedure for trg_order_after_cancel
CREATE PROCEDURE cancel_order_stock(IN p_orderId INT, IN p_status VARCHAR(20))
BEGIN
START TRANSACTION;
IF p_status = 'CANCELED' OR p_status = 'REJECTED' OR p_status = 'DONE' THEN
UPDATE Stock_Reservations sr SET quantity = 0
WHERE sr.orderId = p_orderId;
END IF;
COMMIT;
END //
-- Procedure for trg_purchase_receipt_item_after_insert
CREATE PROCEDURE process_purchase_item(IN p_receiptId INT, IN p_productId INT, IN p_count DECIMAL(10,2), IN p_unitPrice DECIMAL(15,2), IN p_totalAmount DECIMAL(15,2))
BEGIN
START TRANSACTION;
DECLARE latestQuantity DECIMAL(10, 2) DEFAULT 0;
DECLARE invId INT;
DECLARE suppId INT;
-- Get inventory & supplier from
SELECT inventoryId, supplierId
INTO invId, suppId
FROM Purchase_Receipts
WHERE id = p_receiptId;
-- Get current stock quantity (if exists)
SELECT COALESCE(quantity, 0)
INTO latestQuantity
FROM Stock_Balance sb
WHERE sb.inventoryId = invId
AND sb.productId = p_productId
LIMIT 1;
-- Insert stock movement
INSERT INTO Stock_Movements (
type,
quantity,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
supplierId,
remainedInStock,
createdAt
)
VALUES (
'IN',
p_count,
p_unitPrice,
p_totalAmount,
'PURCHASE',
p_receiptId,
p_productId,
invId,
CASE
WHEN p_count = 0 THEN 0
ELSE p_totalAmount / p_count
END,
suppId,
latestQuantity + p_count,
NOW()
);
COMMIT;
END //
-- Procedure for trg_pr_payment_before_insert
CREATE PROCEDURE validate_payment_before_insert(IN p_receiptId INT, IN p_type VARCHAR(20), IN p_amount DECIMAL(15,2))
BEGIN
START TRANSACTION;
DECLARE receiptTotal DECIMAL(14,2);
DECLARE paid DECIMAL(14,2);
SELECT totalAmount, paidAmount
INTO receiptTotal, paid
FROM Purchase_Receipts
WHERE id = p_receiptId
FOR UPDATE;
IF p_type = 'PAYMENT' AND paid + p_amount > receiptTotal THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'مجموع مبلغ پرداختی بیشتر از مبلغ فاکتور است.';
END IF;
COMMIT;
END //
-- Procedure for trg_purchase_payment_update_receipt
CREATE PROCEDURE update_receipt_payment(IN p_receiptId INT)
BEGIN
START TRANSACTION;
DECLARE paid DECIMAL(15,2);
DECLARE total DECIMAL(15,2);
SELECT
COALESCE(SUM(
CASE WHEN type = 'PAYMENT' THEN amount ELSE -amount END
),0)
INTO paid
FROM Purchase_Receipt_Payments
WHERE receiptId = p_receiptId;
SELECT totalAmount INTO total
FROM Purchase_Receipts
WHERE id = p_receiptId;
UPDATE Purchase_Receipts
SET
paidAmount = paid,
status = CASE
WHEN paid = 0 THEN 'UNPAID'
WHEN paid < total THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = p_receiptId;
COMMIT;
END //
-- Procedure for trg_purchase_payment_after_insert
CREATE PROCEDURE process_purchase_payment(IN p_bankAccountId INT, IN p_type VARCHAR(20), IN p_amount DECIMAL(15,2), IN p_id INT)
BEGIN
START TRANSACTION;
DECLARE currentBalance DECIMAL(15, 2);
SELECT balance INTO currentBalance
FROM Bank_Account_Balance
WHERE bankAccountId = p_bankAccountId FOR UPDATE;
IF currentBalance IS NULL THEN SET currentBalance = 0;
INSERT INTO Bank_Account_Balance (bankAccountId, balance, updatedAt)
VALUES (p_bankAccountId, 0, NOW());
END IF;
IF p_type = 'PAYMENT' THEN
SET currentBalance = currentBalance - p_amount;
INSERT INTO Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES (
p_bankAccountId,
'WITHDRAWAL',
p_amount,
currentBalance,
'PURCHASE_PAYMENT',
p_id
);
ELSE
SET currentBalance = currentBalance + p_amount;
INSERT INTO Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES (
p_bankAccountId,
'DEPOSIT',
p_amount,
currentBalance,
'PURCHASE_REFUND',
p_id
);
END IF;
UPDATE Bank_Account_Balance
SET balance = currentBalance
WHERE bankAccountId = p_bankAccountId;
COMMIT;
END //
-- Procedure for trg_pr_payment_after_insert
CREATE PROCEDURE update_supplier_ledger(IN p_receiptId INT, IN p_type VARCHAR(20), IN p_amount DECIMAL(15,2), IN p_id INT)
BEGIN
START TRANSACTION;
DECLARE receiptTotal DECIMAL(14,2) DEFAULT 0;
DECLARE newPaid DECIMAL(14,2) DEFAULT 0;
DECLARE _supplierId INT;
DECLARE lastBalance DECIMAL(14,2) DEFAULT 0;
-- Lock receipt row
SELECT COALESCE(totalAmount, 0), COALESCE(paidAmount, 0), supplierId
INTO receiptTotal, newPaid, _supplierId
FROM Purchase_Receipts
WHERE id = p_receiptId
FOR UPDATE;
-- Apply payment or refund
IF p_type = 'PAYMENT' THEN
SET newPaid = newPaid + p_amount;
ELSE
SET newPaid = newPaid - p_amount;
END IF;
-- Update receipt
UPDATE Purchase_Receipts
SET
paidAmount = newPaid,
status =
CASE
WHEN newPaid = 0 THEN 'UNPAID'
WHEN newPaid < receiptTotal THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = p_receiptId;
-- Get last supplier balance
SELECT IFNULL(balance, 0)
INTO lastBalance
FROM Supplier_Ledger
WHERE supplierId = _supplierId
ORDER BY id DESC
LIMIT 1;
-- Insert supplier ledger
INSERT INTO Supplier_Ledger
(
supplierId,
debit,
credit,
balance,
sourceType,
sourceId,
createdAt
)
VALUES
(
_supplierId,
IF(p_type = 'REFUND', p_amount, 0),
IF(p_type = 'PAYMENT', p_amount, 0),
lastBalance
+ IF(p_type = 'PAYMENT', p_amount, 0)
- IF(p_type = 'REFUND', p_amount, 0),
'PAYMENT',
p_id,
NOW()
);
COMMIT;
END //
-- Procedure for trg_pr_payment_after_delete
CREATE PROCEDURE update_receipt_on_payment_delete(IN p_receiptId INT, IN p_type VARCHAR(20), IN p_amount DECIMAL(15,2))
BEGIN
DECLARE receiptTotal DECIMAL(14,2);
DECLARE newPaid DECIMAL(14,2);
SELECT totalAmount, paidAmount
INTO receiptTotal, newPaid
FROM Purchase_Receipts
WHERE id = p_receiptId
FOR UPDATE;
IF p_type = 'PAYMENT' THEN
SET newPaid = newPaid - p_amount;
ELSE
SET newPaid = newPaid + p_amount;
END IF;
UPDATE Purchase_Receipts
SET
paidAmount = newPaid,
status =
CASE
WHEN newPaid = 0 THEN 'UNPAID'
WHEN newPaid < receiptTotal THEN 'PARTIALLY_PAID'
ELSE 'PAID'
END
WHERE id = p_receiptId;
END //
-- Procedure for trg_purchase_receipt_after_insert
CREATE PROCEDURE insert_supplier_ledger_purchase(IN p_supplierId INT, IN p_totalAmount DECIMAL(15,2), IN p_id INT)
BEGIN
DECLARE lastBalance DECIMAL(14,2) DEFAULT 0;
SELECT COALESCE(balance, 0)
INTO lastBalance
FROM Supplier_Ledger
WHERE supplierId = p_supplierId
ORDER BY id DESC
LIMIT 1;
INSERT INTO Supplier_Ledger
(
supplierId,
debit,
credit,
balance,
sourceType,
sourceId,
createdAt
)
VALUES
(
p_supplierId,
p_totalAmount,
0,
lastBalance - p_totalAmount,
'PURCHASE',
p_id,
NOW()
);
END //
-- Procedure for trg_sales_invoice_items_before_insert
CREATE PROCEDURE validate_stock_before_sale(IN p_invoiceId INT, IN p_productId INT, IN p_count DECIMAL(10,2))
BEGIN
DECLARE current_stock DECIMAL(10, 2);
DECLARE inventory_id INT;
SELECT pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = p_invoiceId;
SELECT COALESCE(quantity, 0) INTO current_stock
FROM Stock_Balance sb
WHERE productId = p_productId AND sb.inventoryId = inventory_id
LIMIT 1;
IF p_count > current_stock THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'موجودی کالا در انبار کافی نیست.';
END IF;
END //
-- Procedure for trg_sales_invoice_items_after_insert
CREATE PROCEDURE process_sale_item(IN p_invoiceId INT, IN p_productId INT, IN p_count DECIMAL(10,2), IN p_unitPrice DECIMAL(15,2), IN p_totalAmount DECIMAL(15,2))
BEGIN
DECLARE current_stock DECIMAL(10, 2);
DECLARE inventory_id INT;
DECLARE customer_id INT;
DECLARE pos_id INT;
SELECT posAccountId, customerId INTO pos_id, customer_id
FROM Sales_Invoices si
WHERE si.id = p_invoiceId
LIMIT 1;
SELECT pa.inventoryId INTO inventory_id
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = p_invoiceId;
SELECT COALESCE(quantity, 0) INTO current_stock
FROM Stock_Balance sb
WHERE productId = p_productId AND sb.inventoryId = inventory_id
LIMIT 1;
INSERT INTO Stock_Movements (
type,
quantity,
unitPrice,
totalCost,
referenceType,
referenceId,
productId,
inventoryId,
avgCost,
remainedInStock,
customerId,
createdAt
)
VALUES (
'OUT',
p_count,
p_unitPrice,
p_totalAmount,
'SALES',
p_invoiceId,
p_productId,
inventory_id,
CASE
WHEN p_count = 0 THEN 0
ELSE p_totalAmount / p_count
END,
current_stock - p_count,
customer_id,
NOW()
);
END //
-- Procedure for trg_sales_invoice_payment_after_insert
CREATE PROCEDURE process_sale_payment(IN p_invoiceId INT, IN p_amount DECIMAL(15,2), IN p_id INT)
BEGIN
DECLARE currentBalance DECIMAL(15,2);
DECLARE bankAccountId INT;
SELECT pa.bankAccountId INTO bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = p_invoiceId;
SELECT balance INTO currentBalance
FROM Bank_Account_Balance
WHERE bankAccountId = bankAccountId
FOR UPDATE;
IF currentBalance IS NULL THEN
SET currentBalance = 0;
INSERT INTO Bank_Account_Balance (bankAccountId, balance, updatedAt)
VALUES (bankAccountId, 0, NOW());
END IF;
SET currentBalance = currentBalance + p_amount;
INSERT INTO Bank_Account_Transactions
(bankAccountId, type, amount, balanceAfter, referenceType, referenceId)
VALUES
(bankAccountId, 'DEPOSIT', p_amount, currentBalance, 'POS_SALE', p_id);
UPDATE Bank_Account_Balance
SET balance = currentBalance
WHERE bankAccountId = bankAccountId;
END //
-- Procedure for trg_pos_account_payment_after_insert
CREATE PROCEDURE process_pos_payment(IN p_invoiceId INT, IN p_paymentMethod VARCHAR(20), IN p_amount DECIMAL(15,2), IN p_id INT)
BEGIN
DECLARE _bankAccountId INT;
IF(p_paymentMethod != 'CASH') THEN
SELECT cashBankAccountId INTO _bankAccountId
FROM Pos_Accounts pa
INNER JOIN Sales_Invoices si ON pa.id = si.posAccountId
WHERE si.id = p_invoiceId;
END IF;
INSERT INTO Bank_Account_Transactions (
bankAccountId,
type,
amount,
balanceAfter,
referenceType,
referenceId
)
VALUES(
_bankAccountId,
'DEPOSIT',
p_amount,
0,
'POS_SALE',
p_id
);
END //
-- Procedure for trg_stock_transfer
CREATE PROCEDURE update_stock_balance_transfer(IN p_productId INT, IN p_inventoryId INT, IN p_quantity DECIMAL(10,2), IN p_unitPrice DECIMAL(15,2), IN p_totalCost DECIMAL(15,2), IN p_type VARCHAR(10))
BEGIN
IF p_type = 'IN' THEN
INSERT INTO Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
p_productId,
p_inventoryId,
p_quantity,
p_totalCost,
CASE
WHEN p_quantity = 0 THEN 0
ELSE p_totalCost / p_quantity
END,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + p_quantity,
totalCost = totalCost + p_totalCost,
avgCost = CASE
WHEN (quantity + p_quantity) = 0 THEN 0
ELSE (totalCost + p_totalCost) / (quantity + p_quantity)
END,
updatedAt = NOW();
END IF;
IF p_type = 'OUT' THEN
IF EXISTS (
SELECT 1
FROM Stock_Balance sb
WHERE sb.productId = p_productId AND sb.inventoryId = p_inventoryId
) THEN
UPDATE Stock_Balance sb
SET
sb.quantity = sb.quantity - p_quantity,
sb.totalCost = sb.totalCost - (sb.avgCost * p_quantity),
sb.updatedAt = NOW()
WHERE
sb.productId = p_productId
AND sb.inventoryId = p_inventoryId;
ELSE
INSERT INTO Stock_Balance (
productId,
inventoryId,
quantity,
totalCost,
avgCost,
updatedAt
)
VALUES (
p_productId,
p_inventoryId,
- p_quantity,
- COALESCE(p_unitPrice, 0) * p_quantity,
COALESCE(p_unitPrice, 0),
NOW()
);
END IF;
END IF;
END //
-- Procedure for trg_stock_purchase_insert
CREATE PROCEDURE update_stock_balance_purchase(IN p_productId INT, IN p_quantity DECIMAL(10,2), IN p_unitPrice DECIMAL(15,2), IN p_totalCost DECIMAL(15,2), IN p_inventoryId INT)
BEGIN
INSERT INTO Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
p_productId,
p_quantity,
p_unitPrice,
p_totalCost,
p_inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity + p_quantity,
totalCost = totalCost + p_totalCost,
avgCost = totalCost / quantity;
END //
-- Procedure for trg_stock_sale_insert
CREATE PROCEDURE update_stock_balance_sale(IN p_productId INT, IN p_quantity DECIMAL(10,2), IN p_unitPrice DECIMAL(15,2), IN p_totalCost DECIMAL(15,2), IN p_inventoryId INT)
BEGIN
INSERT INTO Stock_Balance (
productId,
quantity,
avgCost,
totalCost,
inventoryId,
updatedAt
)
VALUES (
p_productId,
p_quantity,
p_unitPrice,
p_totalCost,
p_inventoryId,
NOW()
)
ON DUPLICATE KEY UPDATE
quantity = quantity - p_quantity,
totalCost = totalCost - p_totalCost,
avgCost = totalCost / quantity;
END //
DELIMITER;
+55 -56
View File
@@ -1,66 +1,65 @@
#!/usr/bin/env ts-node // #!/usr/bin/env ts-node
import * as fs from 'fs' // import * as fs from 'fs'
import * as path from 'path' // import * as path from 'path'
import { prisma } from '../src/lib/prisma'
function findModules(dir: string): string[] { // function findModules(dir: string): string[] {
const results: string[] = [] // const results: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true }) // const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const e of entries) { // for (const e of entries) {
const full = path.join(dir, e.name) // const full = path.join(dir, e.name)
if (e.isDirectory()) { // if (e.isDirectory()) {
results.push(...findModules(full)) // results.push(...findModules(full))
} else if (e.isFile() && e.name.endsWith('.module.ts')) { // } else if (e.isFile() && e.name.endsWith('.module.ts')) {
results.push(full) // results.push(full)
} // }
} // }
return results // return results
} // }
function moduleNameFromFile(filePath: string) { // function moduleNameFromFile(filePath: string) {
const name = path.basename(filePath).replace('.module.ts', '') // const name = path.basename(filePath).replace('.module.ts', '')
return name.replace(/-module$|\.module$/i, '') // return name.replace(/-module$|\.module$/i, '')
} // }
function makePermissionsFor(moduleName: string) { // function makePermissionsFor(moduleName: string) {
const base = moduleName.replace(/\W+/g, '_').toLowerCase() // const base = moduleName.replace(/\W+/g, '_').toLowerCase()
return [`${base}:create`, `${base}:read`, `${base}:update`, `${base}:delete`] // return [`${base}:create`, `${base}:read`, `${base}:update`, `${base}:delete`]
} // }
async function main() { // async function main() {
// const srcDir = path.resolve(__dirname, '..', 'src') // // const srcDir = path.resolve(__dirname, '..', 'src')
// const moduleFiles = findModules(srcDir) // // const moduleFiles = findModules(srcDir)
// const modules = moduleFiles.map(moduleNameFromFile) // // const modules = moduleFiles.map(moduleNameFromFile)
// const permsMap: Record<string, boolean> = {} // // const permsMap: Record<string, boolean> = {}
// for (const m of modules) { // // for (const m of modules) {
// for (const p of makePermissionsFor(m)) permsMap[p] = false // // for (const p of makePermissionsFor(m)) permsMap[p] = false
// } // // }
// // Upsert roles: ensure admin has full permissions, others get entries added // // // Upsert roles: ensure admin has full permissions, others get entries added
// const roles = await prisma.role.findMany() // // const roles = await prisma.role.findMany()
// for (const r of roles) { // // for (const r of roles) {
// const current: Record<string, any> = (r.permissions as any) || {} // // const current: Record<string, any> = (r.permissions as any) || {}
// const merged = { ...permsMap, ...current } // // const merged = { ...permsMap, ...current }
// // If role name is admin (case-insensitive), set all permissions to true // // // If role name is admin (case-insensitive), set all permissions to true
// if (r.name && r.name.toLowerCase() === 'admin') { // // if (r.name && r.name.toLowerCase() === 'admin') {
// for (const key of Object.keys(merged)) merged[key] = true // // for (const key of Object.keys(merged)) merged[key] = true
// } else { // // } else {
// // keep existing truthy values, otherwise false // // // keep existing truthy values, otherwise false
// for (const key of Object.keys(merged)) merged[key] = merged[key] || false // // for (const key of Object.keys(merged)) merged[key] = merged[key] || false
// } // // }
// await prisma.role.update({ where: { id: r.id }, data: { permissions: merged } }) // // await prisma.role.update({ where: { id: r.id }, data: { permissions: merged } })
// console.log( // // console.log(
// `Updated role ${r.name} (id=${r.id}) with ${Object.keys(merged).length} permissions`, // // `Updated role ${r.name} (id=${r.id}) with ${Object.keys(merged).length} permissions`,
// ) // // )
// } // // }
await prisma.$disconnect() // await prisma.$disconnect()
} // }
main().catch(e => { // main().catch(e => {
console.error(e) // console.error(e)
process.exit(1) // process.exit(1)
}) // })
+121 -122
View File
@@ -1,144 +1,143 @@
import fs from 'node:fs' // import fs from 'node:fs'
import path from 'node:path' // import path from 'node:path'
import { prisma } from '../src/lib/prisma'
type CsvRow = { // type CsvRow = {
ID?: string // ID?: string
DescriptionOfID?: string // DescriptionOfID?: string
Vat?: string // Vat?: string
Type?: string // Type?: string
} // }
function parseCsvLine(line: string): string[] { // function parseCsvLine(line: string): string[] {
const result: string[] = [] // const result: string[] = []
let current = '' // let current = ''
let inQuotes = false // let inQuotes = false
for (let index = 0; index < line.length; index++) { // for (let index = 0; index < line.length; index++) {
const char = line[index] // const char = line[index]
if (char === '"') { // if (char === '"') {
if (inQuotes && line[index + 1] === '"') { // if (inQuotes && line[index + 1] === '"') {
current += '"' // current += '"'
index++ // index++
} else { // } else {
inQuotes = !inQuotes // inQuotes = !inQuotes
} // }
continue // continue
} // }
if (char === ',' && !inQuotes) { // if (char === ',' && !inQuotes) {
result.push(current) // result.push(current)
current = '' // current = ''
continue // continue
} // }
current += char // current += char
} // }
result.push(current) // result.push(current)
return result.map(value => value.trim()) // return result.map(value => value.trim())
} // }
function parseCsv(content: string): CsvRow[] { // function parseCsv(content: string): CsvRow[] {
const lines = content // const lines = content
.split(/\r?\n/) // .split(/\r?\n/)
.map(line => line.trim()) // .map(line => line.trim())
.filter(Boolean) // .filter(Boolean)
if (!lines.length) return [] // if (!lines.length) return []
const headers = parseCsvLine(lines[0]) // const headers = parseCsvLine(lines[0])
return lines.slice(1).map(line => { // return lines.slice(1).map(line => {
const cols = parseCsvLine(line) // const cols = parseCsvLine(line)
const row: Record<string, string> = {} // const row: Record<string, string> = {}
for (let index = 0; index < headers.length; index++) { // for (let index = 0; index < headers.length; index++) {
row[headers[index]] = cols[index] || '' // row[headers[index]] = cols[index] || ''
} // }
return row // return row
}) // })
} // }
function toBooleanFlags(typeValue: string) { // function toBooleanFlags(typeValue: string) {
const normalized = typeValue || '' // const normalized = typeValue || ''
const isPublic = normalized.includes('شناسه عمومی') // const isPublic = normalized.includes('شناسه عمومی')
const isImported = normalized.includes('وارداتی') // const isImported = normalized.includes('وارداتی')
const isDomestic = !isImported // const isDomestic = !isImported
return { isPublic, isDomestic } // return { isPublic, isDomestic }
} // }
function parseVat(vatValue: string) { // function parseVat(vatValue: string) {
const parsed = Number(vatValue || '0') // const parsed = Number(vatValue || '0')
if (!Number.isFinite(parsed)) return 0 // if (!Number.isFinite(parsed)) return 0
return parsed // return parsed
} // }
async function main() { // async function main() {
const defaultPath = // const defaultPath =
'/Users/ahasani/Desktop/product_good_2026-05-01T11-35-57_part_1_28812448-0dd9-411c-99e1-334c28d781fe.csv' // '/Users/ahasani/Desktop/product_good_2026-05-01T11-35-57_part_1_28812448-0dd9-411c-99e1-334c28d781fe.csv'
const csvPath = process.argv[2] || defaultPath // const csvPath = process.argv[2] || defaultPath
const absolutePath = path.resolve(csvPath) // const absolutePath = path.resolve(csvPath)
if (!fs.existsSync(absolutePath)) { // if (!fs.existsSync(absolutePath)) {
throw new Error(`CSV file not found: ${absolutePath}`) // throw new Error(`CSV file not found: ${absolutePath}`)
} // }
const raw = fs.readFileSync(absolutePath, 'utf8').replace(/^\uFEFF/, '') // const raw = fs.readFileSync(absolutePath, 'utf8').replace(/^\uFEFF/, '')
const rows = parseCsv(raw) // const rows = parseCsv(raw)
let upserted = 0 // let upserted = 0
let skipped = 0 // let skipped = 0
const guild = await prisma.guild.findFirst({}) // const guild = await prisma.guild.findFirst({})
for (const row of rows) { // for (const row of rows) {
const code = (row.ID || '').trim() // const code = (row.ID || '').trim()
const name = (row.DescriptionOfID || '').trim() // const name = (row.DescriptionOfID || '').trim()
const vat = parseVat((row.Vat || '').trim()) // const vat = parseVat((row.Vat || '').trim())
const type = (row.Type || '').trim() // const type = (row.Type || '').trim()
const { isPublic, isDomestic } = toBooleanFlags(type) // const { isPublic, isDomestic } = toBooleanFlags(type)
if (!code || !name) { // if (!code || !name) {
skipped++ // skipped++
continue // continue
} // }
await prisma.stockKeepingUnits.upsert({ // await prisma.stockKeepingUnits.upsert({
where: { code }, // where: { code },
create: { // create: {
code, // code,
name, // name,
VAT: vat, // VAT: vat,
guild: { // guild: {
connect: { // connect: {
id: guild?.id, // id: guild?.id,
}, // },
}, // },
is_public: isPublic, // is_public: isPublic,
is_domestic: isDomestic, // is_domestic: isDomestic,
}, // },
update: { // update: {
name, // name,
VAT: vat, // VAT: vat,
guild: { // guild: {
connect: { // connect: {
id: guild?.id, // id: guild?.id,
}, // },
}, // },
is_public: isPublic, // is_public: isPublic,
is_domestic: isDomestic, // is_domestic: isDomestic,
}, // },
}) // })
upserted++ // upserted++
} // }
} // }
main() // main()
.catch(error => { // .catch(error => {
console.error(error) // console.error(error)
process.exit(1) // process.exit(1)
}) // })
.finally(async () => { // .finally(async () => {
await prisma.$disconnect() // await prisma.$disconnect()
}) // })
@@ -24,9 +24,10 @@ export const summarySelect: BusinessActivitySelect = {
select: { select: {
activation: { activation: {
select: { select: {
_count: { account_allocations: {
select: { select: {
account_allocations: true, id: true,
account_id: true,
}, },
}, },
}, },
@@ -44,13 +45,18 @@ export const select: BusinessActivitySelect = {
export const mappedData = (businessActivity: any) => { export const mappedData = (businessActivity: any) => {
const { license_activation, ...rest } = businessActivity const { license_activation, ...rest } = businessActivity
const { license, ...license_activation_rest } = license_activation const { license, ...license_activation_rest } = license_activation
const { _count } = license.activation const { account_allocations } = license.activation
console.log('license_activation', license_activation)
return { return {
...rest, ...rest,
license_info: { license_info: {
...license_activation_rest, ...license_activation_rest,
accounts_limit: _count.account_allocations, accounts_limit: account_allocations.length,
allocated_account_count: account_allocations.filter(
allocation => allocation.account_id,
).length,
}, },
} }
} }
+13 -24
View File
@@ -13,6 +13,8 @@ export const summarySelect: SalesInvoiceSelect = {
created_at: true, created_at: true,
settlement_type: true, settlement_type: true,
unknown_customer: true, unknown_customer: true,
last_attempt_no: true,
last_tsp_status: true,
customer: { customer: {
select: { select: {
type: true, type: true,
@@ -36,18 +38,13 @@ export const summarySelect: SalesInvoiceSelect = {
}, },
}, },
}, },
tsp_attempts: { reference_invoice: {
orderBy: {
created_at: 'desc',
},
take: 1,
select: { select: {
status: true, id: true,
sent_at: true, invoice_number: true,
message: true,
}, },
}, },
reference_invoice: { referenced_by: {
select: { select: {
id: true, id: true,
invoice_number: true, invoice_number: true,
@@ -61,6 +58,7 @@ export const select: SalesInvoiceSelect = {
tax_amount: true, tax_amount: true,
updated_at: true, updated_at: true,
unknown_customer: true, unknown_customer: true,
pos: { pos: {
select: { select: {
id: true, id: true,
@@ -73,6 +71,12 @@ export const select: SalesInvoiceSelect = {
select: { select: {
id: true, id: true,
name: true, name: true,
economic_code: true,
guild: {
select: {
name: true,
},
},
}, },
}, },
}, },
@@ -127,19 +131,4 @@ export const select: SalesInvoiceSelect = {
}, },
}, },
}, },
tsp_attempts: {
orderBy: {
created_at: 'desc',
},
select: {
id: true,
attempt_no: true,
status: true,
message: true,
sent_at: true,
received_at: true,
created_at: true,
},
take: 1,
},
} }
@@ -21,7 +21,7 @@ export class SharedSaleInvoiceAccessService {
}) })
if (!consumer) { if (!consumer) {
throw new BadRequestException('شما دسترسی لازم برای ارسال فاکتور را ندارید.') throw new BadRequestException('شما دسترسی لازم برای ارسال صورت‌حساب را ندارید.')
} }
return consumer.consumer_id return consumer.consumer_id
@@ -1,13 +1,25 @@
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { PosCorrectionSalesInvoiceDto } from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto' import {
PosCorrectionSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service'
import { Injectable } from '@nestjs/common' import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import {
CustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/enums'
import { SharedSaleInvoiceCreateService } from './sale-invoice-create.service'
@Injectable() @Injectable()
export class SharedSaleInvoiceActionsService { export class SharedSaleInvoiceActionsService {
constructor( constructor(
private prisma: PrismaService,
private readonly salesInvoiceTspService: SalesInvoiceTspService, private readonly salesInvoiceTspService: SalesInvoiceTspService,
private readonly saleInvoiceAccessService: SharedSaleInvoiceAccessService, private readonly saleInvoiceAccessService: SharedSaleInvoiceAccessService,
private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService,
) {} ) {}
async send(consumerAccountId: string, posId: string, invoiceId: string) { async send(consumerAccountId: string, posId: string, invoiceId: string) {
@@ -58,14 +70,464 @@ export class SharedSaleInvoiceActionsService {
consumerAccountId, consumerAccountId,
posId, posId,
) )
return this.salesInvoiceTspService.correctionSend(
const newInvoice = await this.prisma.$transaction(async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
total_amount: true,
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان اصلاح این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
)
}
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
const originalUnitPrice = Number(originalItem.unit_price)
const requestedUnitPrice = Number(item.unit_price)
const originalTotalAmount = this.roundAmount(Number(originalItem.total_amount))
const requestedTotalAmount = this.roundAmount(Number(item.total_amount))
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام اصلاحی باید بیشتر از صفر باشد.',
)
}
// if (requestedUnitPrice < originalUnitPrice) {
// throw new BadRequestException(
// 'مبلغ واحد اقلام اصلاحی نمی‌تواند کمتر از صورت‌حساب اصلی باشد.',
// )
// }
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: requestedUnitPrice,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(item.discount_amount || 0)),
tax_amount: this.roundAmount(Number(item.tax_amount || 0)),
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
notes: item.notes || undefined,
}
})
if (usedItemKeys.size !== relatedInvoice.items.length) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedAmount = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return (
this.roundAmount(Number(originalItem.total_amount)) !==
this.roundAmount(Number(item.total_amount))
)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasChangedQuantity && !hasChangedAmount && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const requestedTotalAmount = this.roundAmount(Number(data.total_amount))
const originalTotalAmount = this.roundAmount(Number(relatedInvoice.total_amount))
const totalDiff = this.roundAmount(requestedTotalAmount - originalTotalAmount)
const paymentsAmount = this.roundAmount(this.getPaymentsAmount(data.payments))
if (totalDiff > 0 && paymentsAmount !== totalDiff) {
throw new BadRequestException(
'جمع پرداختی باید برابر با اختلاف مبلغ صورت‌حساب اصلاحی و صورت‌حساب مرجع باشد.',
)
}
if (totalDiff === 0 && paymentsAmount !== 0) {
throw new BadRequestException('در صورت عدم افزایش مبلغ، پرداختی نباید ثبت شود.')
}
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(data.discount_amount)),
tax_amount: this.roundAmount(Number(data.tax_amount)),
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: data.payments,
} as any,
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.correctionSend(posId, newInvoice.id)
}
async return(
data: PosReturnSalesInvoiceDto,
consumerAccountId: string,
pos_id: string,
complex_id: string,
business_id: string,
invoiceId: string,
) {
await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId, consumerAccountId,
posId, pos_id,
complexId,
businessId,
invoiceId,
data,
) )
const newInvoice = await this.prisma.$transaction(async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id,
},
select: {
id: true,
type: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان برگشت از خرید روی این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
)
}
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getReturnItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getReturnItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
// if (usedItemKeys.has(itemKey)) {
// throw new BadRequestException('اقلام تکراری در صورت‌حساب بازگشتی مجاز نیستند.')
// }
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل بازگشت هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی باید بیشتر از صفر باشد.',
)
}
if (requestedQuantity > originalQuantity) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی نمی‌تواند از مقدار صورت‌حساب اصلی بیشتر باشد.',
)
}
const quantityRatio = requestedQuantity / originalQuantity
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: Number(originalItem.unit_price),
total_amount: this.roundAmount(
Number(originalItem.total_amount) * quantityRatio,
),
discount_amount: this.roundAmount(
Number(originalItem.discount_amount || 0) * quantityRatio,
),
tax_amount: this.roundAmount(
Number(originalItem.tax_amount || 0) * quantityRatio,
),
payload: originalItem.payload
? JSON.parse(JSON.stringify(originalItem.payload))
: undefined,
notes: originalItem.notes || undefined,
}
})
if (!normalizedItems.length) {
throw new BadRequestException(
'حداقل یک قلم باید در صورت‌حساب بازگشتی باقی بماند.',
)
}
const hasRemovedItem = relatedInvoice.items.some(
item => !usedItemKeys.has(this.getReturnItemKey(item)),
)
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem =>
this.getReturnItemKey(relatedItem) === this.getReturnItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasRemovedItem && !hasChangedQuantity && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const totalAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.total_amount),
0,
)
const discountAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.discount_amount || 0),
0,
)
const taxAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.tax_amount || 0),
0,
)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: totalAmount,
discount_amount: discountAmount,
tax_amount: taxAmount,
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: undefined,
} as any,
businessId: business_id,
complexId: complex_id,
posId: pos_id,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.RETURN,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.returnFromSaleSend(
pos_id,
business_id,
newInvoice.id,
)
}
private normalizeInvoiceDate(invoiceDate: Date | string) {
return new Date(invoiceDate).toISOString()
}
private getItemKey(item: { good_id?: string | null; service_id?: string | null }) {
return item.good_id || item.service_id || ''
}
private getPaymentsAmount(payments?: {
terminals?: { amount?: number }
cash?: number
set_off?: number
card?: number
bank?: number
check?: number
other?: number
}) {
return (
Number(payments?.terminals?.amount || 0) +
Number(payments?.cash || 0) +
Number(payments?.set_off || 0) +
Number(payments?.card || 0) +
Number(payments?.bank || 0) +
Number(payments?.check || 0) +
Number(payments?.other || 0)
)
}
private roundAmount(amount: number) {
return Number(amount.toFixed(2))
} }
async inquiry(consumerAccountId: string, posId: string, invoiceId: string) { async inquiry(consumerAccountId: string, posId: string, invoiceId: string) {
@@ -75,4 +537,11 @@ export class SharedSaleInvoiceActionsService {
) )
return this.salesInvoiceTspService.get(invoiceId, posId, consumerId) return this.salesInvoiceTspService.get(invoiceId, posId, consumerId)
} }
private getReturnItemKey(item: {
good_id?: string | null
service_id?: string | null
}) {
return item.good_id || item.service_id || ''
}
} }
@@ -83,7 +83,7 @@ export class SharedCreateTerminalPayment {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ required: true }) @ApiProperty({ required: true })
terminalId: string terminal_id: string
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -196,6 +196,11 @@ export class SharedCreateSalesInvoiceDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
items: SharedCreateSalesInvoiceItemDto[] items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty({ required: false })
@IsOptional()
@IsString()
ref_invoice_id?: string
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -230,5 +235,13 @@ export class SharedCreateSalesInvoiceDto {
export class SharedCorrectionSalesInvoiceDto extends OmitType( export class SharedCorrectionSalesInvoiceDto extends OmitType(
SharedCreateSalesInvoiceDto, SharedCreateSalesInvoiceDto,
['customer', 'customer_id', 'send_to_tsp', 'customer_type', 'settlement_type'], ['customer', 'customer_id', 'customer_type', 'settlement_type'],
) {} ) {}
export class SharedReturnSalesInvoiceDto extends OmitType(SharedCreateSalesInvoiceDto, [
'customer',
'customer_id',
'customer_type',
'settlement_type',
'payments',
]) {}
@@ -23,6 +23,7 @@ interface TerminalPaymentInfo {
interface NormalizedPayment { interface NormalizedPayment {
method: PaymentMethodType method: PaymentMethodType
amount: number amount: number
terminalInfo?: TerminalPaymentInfo
} }
interface CreateSharedSaleInvoiceInput { interface CreateSharedSaleInvoiceInput {
@@ -57,10 +58,10 @@ export class SharedSaleInvoiceCreateService {
} = input } = input
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date) const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const { payments, terminalInfo } = this.buildPaymentsData( const payments =
data.payments, type === TspProviderRequestType.ORIGINAL || data.payments
data.total_amount, ? this.buildPaymentsData(data.payments, data.total_amount)
) : []
for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) { for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) {
try { try {
@@ -88,13 +89,14 @@ export class SharedSaleInvoiceCreateService {
select: { ...QUERY_CONSTANTS.SALE_INVOICE.select }, select: { ...QUERY_CONSTANTS.SALE_INVOICE.select },
}) })
await this.createPayments( if (payments.length) {
$tx, await this.createPayments(
salesInvoice.id, $tx,
payments, salesInvoice.id,
terminalInfo, payments,
normalizedInvoiceDate, normalizedInvoiceDate,
) )
}
return salesInvoice return salesInvoice
}) })
@@ -109,7 +111,7 @@ export class SharedSaleInvoiceCreateService {
} }
} }
throw new BadRequestException('ایجاد فاکتور با خطا مواجه شد.') throw new BadRequestException('ایجاد صورت‌حساب با خطا مواجه شد.')
} }
private isRetryableInvoiceConflict(error: unknown) { private isRetryableInvoiceConflict(error: unknown) {
@@ -148,7 +150,7 @@ export class SharedSaleInvoiceCreateService {
} }
const rawPayments = (paymentsData || {}) as Record<string, unknown> const rawPayments = (paymentsData || {}) as Record<string, unknown>
const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined const terminalPayments = rawPayments.terminals as TerminalPaymentInfo[] | undefined
const payments: NormalizedPayment[] = Object.entries(rawPayments) const payments: NormalizedPayment[] = Object.entries(rawPayments)
.filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0) .filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0)
@@ -158,54 +160,31 @@ export class SharedSaleInvoiceCreateService {
})) }))
.filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[] .filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[]
const hasTerminalPayment = payments.some( const hasTerminalPayment = terminalPayments && terminalPayments.length
payment => payment.method === PaymentMethodType.TERMINAL,
)
const nonTerminalTotal = payments
.filter(payment => payment.method !== PaymentMethodType.TERMINAL)
.reduce((sum, payment) => sum + payment.amount, 0)
if (!hasTerminalPayment && terminalInfo) { if (hasTerminalPayment) {
const terminalAmount = for (const terminal of terminalPayments) {
typeof terminalInfo.amount === 'number'
? terminalInfo.amount
: Math.max(0, Number(totalAmount) - nonTerminalTotal)
if (terminalAmount > 0) {
payments.push({ payments.push({
method: PaymentMethodType.TERMINAL, method: PaymentMethodType.TERMINAL,
amount: terminalAmount, amount: terminal.amount,
terminalInfo: terminal,
}) })
} }
} }
this.validatePayments(payments, totalAmount, terminalInfo) //TODO: for correction i need to validate payments with diff of total amount and original invoice amount
// this.validatePayments(payments, totalAmount)
return { return payments
payments,
terminalInfo,
}
} }
private validatePayments( private validatePayments(payments: NormalizedPayment[], totalAmount: number) {
payments: NormalizedPayment[],
totalAmount: number,
terminalInfo?: TerminalPaymentInfo,
) {
const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0) const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0)
const roundedTotalPayments = Number(totalPayments.toFixed(2)) const roundedTotalPayments = Number(totalPayments.toFixed(2))
const roundedTotalAmount = Number(Number(totalAmount).toFixed(2)) const roundedTotalAmount = Number(Number(totalAmount).toFixed(2))
if (roundedTotalPayments !== roundedTotalAmount) { if (roundedTotalPayments !== roundedTotalAmount) {
throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل فاکتور باشد.') throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل صورت‌حساب باشد.')
}
const terminalPayments = payments.filter(
payment => payment.method === PaymentMethodType.TERMINAL,
)
if (terminalPayments.length > 0 && !terminalInfo) {
throw new BadRequestException('برای پرداخت ترمینال اطلاعات ترمینال الزامی است.')
} }
} }
@@ -277,9 +256,7 @@ export class SharedSaleInvoiceCreateService {
} }
return customerIndividualId return customerIndividualId
} } else if (customer_type === CustomerType.LEGAL && customer?.customer_legal) {
if (customer_type === CustomerType.LEGAL && customer?.customer_legal) {
const { registration_number, economic_code, postal_code } = customer.customer_legal const { registration_number, economic_code, postal_code } = customer.customer_legal
const foundedCustomer = await tx.customerLegal.findFirst({ const foundedCustomer = await tx.customerLegal.findFirst({
where: { where: {
@@ -424,6 +401,7 @@ export class SharedSaleInvoiceCreateService {
customer, customer,
payments, payments,
settlement_type, settlement_type,
send_to_tsp,
...invoiceData ...invoiceData
} = data } = data
@@ -431,7 +409,7 @@ export class SharedSaleInvoiceCreateService {
type !== TspProviderRequestType.ORIGINAL && type !== TspProviderRequestType.ORIGINAL &&
!(main_invoice_id || ref_invoice_id) !(main_invoice_id || ref_invoice_id)
) { ) {
throw new BadRequestException('متاسفانه مشکلی در اطلاعات فاکتور وجود دارد.') throw new BadRequestException('متاسفانه مشکلی در اطلاعات صورت‌حساب وجود دارد.')
} }
const salesInvoiceData: SalesInvoiceCreateInput = { const salesInvoiceData: SalesInvoiceCreateInput = {
@@ -486,6 +464,8 @@ export class SharedSaleInvoiceCreateService {
id: customerId, id: customerId,
}, },
} }
} else if (data.customer?.customer_unknown) {
salesInvoiceData.unknown_customer = data.customer.customer_unknown
} }
if (type !== TspProviderRequestType.ORIGINAL) { if (type !== TspProviderRequestType.ORIGINAL) {
@@ -500,7 +480,7 @@ export class SharedSaleInvoiceCreateService {
return salesInvoiceData return salesInvoiceData
} }
private async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) { async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) {
const latestInvoice = await tx.salesInvoice.findFirst({ const latestInvoice = await tx.salesInvoice.findFirst({
where: { where: {
pos: { pos: {
@@ -541,7 +521,6 @@ export class SharedSaleInvoiceCreateService {
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
invoiceId: string, invoiceId: string,
payments: NormalizedPayment[], payments: NormalizedPayment[],
terminalInfo: TerminalPaymentInfo | undefined,
paidAt: Date, paidAt: Date,
) { ) {
for (const payment of payments) { for (const payment of payments) {
@@ -557,16 +536,27 @@ export class SharedSaleInvoiceCreateService {
}, },
}) })
if (payment.method === PaymentMethodType.TERMINAL && terminalInfo) { if (payment.method === PaymentMethodType.TERMINAL && payment.terminalInfo) {
const {
terminal_id,
stan,
rrn,
transaction_date_time,
customer_card_no,
description,
} = payment.terminalInfo
await tx.salesInvoicePaymentTerminalInfo.create({ await tx.salesInvoicePaymentTerminalInfo.create({
data: { data: {
payment_id: createdPayment.id, payment_id: createdPayment.id,
terminal_id: terminalInfo.terminal_id, terminal_id,
stan: terminalInfo.stan, stan: stan,
rrn: terminalInfo.rrn, rrn: rrn,
transaction_date_time: new Date(terminalInfo.transaction_date_time || ''), transaction_date_time: transaction_date_time
customer_card_no: terminalInfo.customer_card_no || null, ? new Date(transaction_date_time)
description: terminalInfo.description || null, : new Date(),
customer_card_no: '1234567890123456',
// customer_card_no: customer_card_no || null,
description: description || null,
}, },
}) })
} }
@@ -0,0 +1,90 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator'
import { TspProviderResponseStatus } from 'generated/prisma/enums'
export class SharedSaleInvoicesFilterDto {
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number
@ApiPropertyOptional({ default: 10 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
perPage?: number
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_mobile?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_national_id?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_economic_code?: string
@ApiPropertyOptional({ enum: TspProviderResponseStatus })
@IsOptional()
@IsEnum(TspProviderResponseStatus)
status?: TspProviderResponseStatus
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_from?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_to?: number
}
@@ -0,0 +1,137 @@
import { TspProviderResponseStatus } from '@/generated/prisma/enums'
import { SalesInvoiceWhereInput } from '@/generated/prisma/models'
import { Injectable } from '@nestjs/common'
import { SharedSaleInvoicesFilterDto } from './sale-invoice-filter.dto'
@Injectable()
export class SharedSaleInvoiceFilterService {
buildWhere(filter: SharedSaleInvoicesFilterDto): SalesInvoiceWhereInput {
const where: SalesInvoiceWhereInput = {}
if (filter.invoice_date_from || filter.invoice_date_to) {
where.invoice_date = {
...(filter.invoice_date_from ? { gte: new Date(filter.invoice_date_from) } : {}),
...(filter.invoice_date_to ? { lte: new Date(filter.invoice_date_to) } : {}),
}
}
if (filter.created_at_from || filter.created_at_to) {
where.created_at = {
...(filter.created_at_from ? { gte: new Date(filter.created_at_from) } : {}),
...(filter.created_at_to ? { lte: new Date(filter.created_at_to) } : {}),
}
}
if (
filter.total_amount !== undefined ||
filter.total_amount_from !== undefined ||
filter.total_amount_to !== undefined
) {
where.total_amount = {
...(filter.total_amount !== undefined ? { equals: filter.total_amount } : {}),
...(filter.total_amount_from !== undefined
? { gte: filter.total_amount_from }
: {}),
...(filter.total_amount_to !== undefined ? { lte: filter.total_amount_to } : {}),
}
}
if (
filter.customer_name?.trim() ||
filter.customer_mobile?.trim() ||
filter.customer_national_id?.trim() ||
filter.customer_economic_code?.trim()
) {
where.customer = {
is: {
OR: [
...(filter.customer_name?.trim()
? [
{
individual: {
is: {
OR: [
{ first_name: { contains: filter.customer_name.trim() } },
{ last_name: { contains: filter.customer_name.trim() } },
],
},
},
},
{
legal: {
is: {
name: {
contains: filter.customer_name.trim(),
},
},
},
},
]
: []),
...(filter.customer_mobile?.trim()
? [
{
individual: {
is: {
mobile_number: { contains: filter.customer_mobile.trim() },
},
},
},
]
: []),
...(filter.customer_national_id?.trim()
? [
{
individual: {
is: {
national_id: { contains: filter.customer_national_id.trim() },
},
},
},
]
: []),
...(filter.customer_economic_code?.trim()
? [
{
legal: {
is: {
OR: [
{
economic_code: {
contains: filter.customer_economic_code.trim(),
},
},
{
registration_number: {
contains: filter.customer_economic_code.trim(),
},
},
],
},
},
},
]
: []),
],
},
}
}
if (filter.status) {
if (filter.status === TspProviderResponseStatus.NOT_SEND) {
where.OR = [
{
last_tsp_status: null,
},
{
last_tsp_status: TspProviderResponseStatus.NOT_SEND,
},
]
} else {
where.last_tsp_status = filter.status
}
}
return where
}
}
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class SharedSaleInvoicePaginationService {
normalize(
page: number = 1,
perPage: number = 10,
defaultPerPage: number = 10,
maxPerPage: number = 50,
) {
const normalizedPageValue = Number(page ?? 1)
const normalizedPage = Number.isFinite(normalizedPageValue)
? Math.max(1, Math.floor(normalizedPageValue))
: 1
const requestedPerPageValue = Number(perPage ?? defaultPerPage)
const requestedPerPage = Number.isFinite(requestedPerPageValue)
? Math.max(1, Math.floor(requestedPerPageValue))
: defaultPerPage
const normalizedPerPage = Math.min(requestedPerPage, maxPerPage)
return {
page: normalizedPage,
perPage: normalizedPerPage,
skip: (normalizedPage - 1) * normalizedPerPage,
take: normalizedPerPage,
}
}
}
+7 -5
View File
@@ -2,18 +2,20 @@ import { PrismaMariaDb } from '@prisma/adapter-mariadb'
import 'dotenv/config' import 'dotenv/config'
import { env } from 'prisma/config' import { env } from 'prisma/config'
import { PrismaClient } from '../generated/prisma/client'
const adapter = new PrismaMariaDb({ const adapter = new PrismaMariaDb({
host: env('DATABASE_HOST'), host: env('DATABASE_HOST'),
user: env('DATABASE_USER'), user: env('DATABASE_USER'),
password: env('DATABASE_PASSWORD'), password: env('DATABASE_PASSWORD'),
database: env('DATABASE_NAME'), database: env('DATABASE_NAME'),
ssl: false,
connectionLimit: 5, connectionLimit: 5,
port: Number(env('DATABASE_PORT')) || 3306, port: Number(env('DATABASE_PORT')) || 3306,
allowPublicKeyRetrieval: true,
ssl: {
rejectUnauthorized: false,
},
connectTimeout: 10000,
}) })
const prisma = new PrismaClient({ adapter }) // const prisma = new PrismaClient({ adapter })
export { prisma } export { adapter as prismaAdapter }
+3 -2
View File
@@ -17,6 +17,8 @@ const cookieParser = require('cookie-parser')
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule) const app = await NestFactory.create(AppModule)
app.enableShutdownHooks()
app.use(cookieParser()) app.use(cookieParser())
const config = new DocumentBuilder() const config = new DocumentBuilder()
@@ -33,6 +35,7 @@ async function bootstrap() {
in: 'header', in: 'header',
}) })
.build() .build()
const documentFactory = () => SwaggerModule.createDocument(app, config) const documentFactory = () => SwaggerModule.createDocument(app, config)
SwaggerModule.setup('swagger', app, documentFactory, { SwaggerModule.setup('swagger', app, documentFactory, {
swaggerOptions: { swaggerOptions: {
@@ -47,14 +50,12 @@ async function bootstrap() {
}, },
}, },
}) })
// Set API prefix and enable URI versioning so alls routes live under /api/v1/* // Set API prefix and enable URI versioning so alls routes live under /api/v1/*
app.setGlobalPrefix('api') app.setGlobalPrefix('api')
app.enableVersioning({ app.enableVersioning({
type: VersioningType.URI, type: VersioningType.URI,
defaultVersion: '1', defaultVersion: '1',
}) })
// Enable CORS. You can set `CORS_ORIGINS` to a comma-separated list of allowed origins. // Enable CORS. You can set `CORS_ORIGINS` to a comma-separated list of allowed origins.
// Defaults include common localhost origins used by front-end dev servers. // Defaults include common localhost origins used by front-end dev servers.
const defaultOrigins: string[] = [] const defaultOrigins: string[] = []
@@ -1,7 +1,6 @@
import { SKUGuildType } from '@/common/enums/enums'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer' import { Type } from 'class-transformer'
import { IsBoolean, IsEnum, IsNumber, IsString, Min } from 'class-validator' import { IsBoolean, IsNumber, IsString, Min } from 'class-validator'
export class CreateStockKeepingUnitDto { export class CreateStockKeepingUnitDto {
@ApiProperty() @ApiProperty()
@@ -18,9 +17,9 @@ export class CreateStockKeepingUnitDto {
@Min(0) @Min(0)
VAT: number VAT: number
@ApiPropertyOptional({ enum: SKUGuildType, default: SKUGuildType.GOLD }) // @ApiPropertyOptional({ enum: SKUGuildType, default: SKUGuildType.GOLD })
@IsEnum(SKUGuildType) // @IsEnum(SKUGuildType)
type: SKUGuildType = SKUGuildType.GOLD // type: SKUGuildType = SKUGuildType.GOLD
@ApiPropertyOptional({ default: true }) @ApiPropertyOptional({ default: true })
@IsBoolean() @IsBoolean()
@@ -105,7 +105,9 @@ export class PartnerAccountChargeTransactionService {
try { try {
createdTransaction = await tx.partnerAccountQuotaChargeTransaction.create({ createdTransaction = await tx.partnerAccountQuotaChargeTransaction.create({
data: { data: {
activation_expires_at: data.activated_expires_at, activation_expires_at: data.activated_expires_at
? new Date(data.activated_expires_at).toISOString()
: new Date().toISOString(),
tracking_code: generateTrackingCode('AQC', this.TRACKING_CODE_LENGTH), tracking_code: generateTrackingCode('AQC', this.TRACKING_CODE_LENGTH),
purchased_count: data.quantity, purchased_count: data.quantity,
partner: { connect: { id: partner_id } }, partner: { connect: { id: partner_id } },
@@ -121,7 +121,9 @@ export class PartnerLicenseChargeTransactionService {
try { try {
createdTransaction = await tx.licenseChargeTransaction.create({ createdTransaction = await tx.licenseChargeTransaction.create({
data: { data: {
activation_expires_at: data.activated_expires_at, activation_expires_at: data.activated_expires_at
? new Date(data.activated_expires_at).toISOString()
: new Date().toISOString(),
tracking_code: generateTrackingCode('LIC', this.TRACKING_CODE_LENGTH), tracking_code: generateTrackingCode('LIC', this.TRACKING_CODE_LENGTH),
purchased_count: data.quantity, purchased_count: data.quantity,
partner: { connect: { id: partner_id } }, partner: { connect: { id: partner_id } },
@@ -71,7 +71,7 @@ export class BusinessActivitiesService {
async findOne(consumer_id: string, id: string) { async findOne(consumer_id: string, id: string) {
const cacheKey = RedisKeyMaker.consumerBusinessActivityInfo(consumer_id, id) const cacheKey = RedisKeyMaker.consumerBusinessActivityInfo(consumer_id, id)
return await this.redisService.getAndSet(cacheKey, 'list', async () => { return await this.redisService.getAndSet(cacheKey, 'single', async () => {
return await this.businessActivitiesQueryService.findOneByConsumer( return await this.businessActivitiesQueryService.findOneByConsumer(
consumer_id, consumer_id,
id, id,
@@ -1,4 +1,4 @@
import { Controller, Get, Param, Post } from '@nestjs/common' import { Controller, Get, Param, Post, Query } from '@nestjs/common'
import { TokenAccount } from '@/common/decorators/tokenInfo.decorator' import { TokenAccount } from '@/common/decorators/tokenInfo.decorator'
import { ApiTags } from '@nestjs/swagger' import { ApiTags } from '@nestjs/swagger'
@@ -14,8 +14,10 @@ export class SalesInvoicesController {
@TokenAccount('userId') userId: string, @TokenAccount('userId') userId: string,
@Param('complexId') complexId: string, @Param('complexId') complexId: string,
@Param('posId') posId: string, @Param('posId') posId: string,
@Query('page') page: number,
@Query('perPage') perPage: number,
) { ) {
return this.salesInvoicesService.findAll(userId, complexId, posId) return this.salesInvoicesService.findAll(userId, complexId, posId, page, perPage)
} }
@Get(':id') @Get(':id')
@@ -1,5 +1,6 @@
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module' import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { SalesInvoicesController } from './sales-invoices.controller' import { SalesInvoicesController } from './sales-invoices.controller'
@@ -12,6 +13,7 @@ import { SalesInvoicesService } from './sales-invoices.service'
SalesInvoicesService, SalesInvoicesService,
SharedSaleInvoiceActionsService, SharedSaleInvoiceActionsService,
SharedSaleInvoiceAccessService, SharedSaleInvoiceAccessService,
SharedSaleInvoicePaginationService,
], ],
}) })
export class ConsumerPosSalesInvoicesModule {} export class ConsumerPosSalesInvoicesModule {}
@@ -1,5 +1,8 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { ResponseMapper } from '@/common/response/response-mapper' import { ResponseMapper } from '@/common/response/response-mapper'
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service' import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { translateEnumValue } from '@/common/utils'
import { SalesInvoiceWhereInput } from '@/generated/prisma/models' import { SalesInvoiceWhereInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable, NotFoundException } from '@nestjs/common' import { Injectable, NotFoundException } from '@nestjs/common'
@@ -9,9 +12,19 @@ export class SalesInvoicesService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private readonly sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService, private readonly sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService,
private readonly sharedSaleInvoicePaginationService: SharedSaleInvoicePaginationService,
) {} ) {}
async findAll(consumer_id: string, complex_id: string, pos_id: string) { async findAll(
consumer_id: string,
complex_id: string,
pos_id: string,
requestedPage: number = 1,
requestedPerPage: number = 10,
) {
const { page, perPage, skip, take } =
this.sharedSaleInvoicePaginationService.normalize(requestedPage, requestedPerPage)
const defaultWhere: SalesInvoiceWhereInput = { const defaultWhere: SalesInvoiceWhereInput = {
pos_id, pos_id,
pos: { pos: {
@@ -24,98 +37,41 @@ export class SalesInvoicesService {
}, },
} }
const perPage = 10 const [saleInvoices, total] = await this.prisma.$transaction([
const page = 1
const [items, total] = await this.prisma.$transaction([
this.prisma.salesInvoice.findMany({ this.prisma.salesInvoice.findMany({
where: defaultWhere, where: defaultWhere,
select: { select: {
id: true, ...QUERY_CONSTANTS.SALE_INVOICE.select,
code: true, _count: {
invoice_date: true,
notes: true,
total_amount: true,
items: {
select: { select: {
measure_unit_code: true, items: true,
measure_unit_text: true,
sku_code: true,
discount_amount: true,
tax_amount: true,
notes: true,
quantity: true,
total_amount: true,
unit_price: true,
payload: true,
good: {
select: {
id: true,
name: true,
sku: {
select: {
id: true,
name: true,
},
},
barcode: true,
local_sku: true,
pricing_model: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
id: true,
name: true,
},
},
},
},
}, },
}, },
payments: {
select: {
amount: true,
paid_at: true,
payment_method: true,
},
},
customer: {
select: {
type: true,
individual: {
select: {
economic_code: true,
first_name: true,
last_name: true,
postal_code: true,
national_id: true,
},
},
legal: {
select: {
economic_code: true,
postal_code: true,
registration_number: true,
},
},
},
},
unknown_customer: true,
}, },
skip: (page - 1) * perPage, orderBy: {
take: perPage, created_at: 'desc',
},
skip,
take,
}), }),
this.prisma.salesInvoice.count({ this.prisma.salesInvoice.count({
where: defaultWhere, where: defaultWhere,
}), }),
]) ])
return ResponseMapper.paginate(items, { total, page, perPage }) const mappedAccounts = saleInvoices.map(saleInvoice => {
const { _count, consumer_account, last_tsp_status, ...rest } = saleInvoice
return {
...rest,
items_count: _count.items,
status: translateEnumValue('TspProviderResponseStatus', last_tsp_status),
}
})
return ResponseMapper.paginate(mappedAccounts, {
total,
page,
perPage,
})
} }
findOne(complex_id: string, pos_id: string, id: string) { findOne(complex_id: string, pos_id: string, id: string) {
@@ -1,3 +1,4 @@
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { PrismaModule } from '@/prisma/prisma.module' import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { consumerCustomersController } from './customers.controller' import { consumerCustomersController } from './customers.controller'
@@ -7,6 +8,6 @@ import { ConsumerSaleInvoicesModule } from './sale-invoices/sale-invoices.module
@Module({ @Module({
imports: [PrismaModule, ConsumerSaleInvoicesModule], imports: [PrismaModule, ConsumerSaleInvoicesModule],
controllers: [consumerCustomersController], controllers: [consumerCustomersController],
providers: [consumerCustomersService], providers: [consumerCustomersService, SharedSaleInvoicePaginationService],
}) })
export class ConsumerCustomersModule {} export class ConsumerCustomersModule {}
@@ -1,3 +1,4 @@
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { CustomerSelect, CustomerWhereInput } from '@/generated/prisma/models' import { CustomerSelect, CustomerWhereInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@@ -9,7 +10,10 @@ import {
@Injectable() @Injectable()
export class consumerCustomersService { export class consumerCustomersService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly sharedSaleInvoicePaginationService: SharedSaleInvoicePaginationService,
) {}
defaultSelect: CustomerSelect = { defaultSelect: CustomerSelect = {
id: true, id: true,
@@ -56,13 +60,15 @@ export class consumerCustomersService {
} }
} }
async findAll(consumer_id: string, page = 1, perPage = 10) { async findAll(consumer_id: string, requestedPage = 1, requestedPerPage = 10) {
const { page, perPage, skip, take } =
this.sharedSaleInvoicePaginationService.normalize(requestedPage, requestedPerPage)
const [customers, total] = await this.prisma.$transaction(async tx => [ const [customers, total] = await this.prisma.$transaction(async tx => [
await tx.customer.findMany({ await tx.customer.findMany({
where: this.defaultWhere(consumer_id), where: this.defaultWhere(consumer_id),
select: this.defaultSelect, select: this.defaultSelect,
skip: (page - 1) * perPage, skip,
take: 10, take,
}), }),
await tx.customer.count({ await tx.customer.count({
where: this.defaultWhere(consumer_id), where: this.defaultWhere(consumer_id),
@@ -1,3 +1,4 @@
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { PrismaModule } from '@/prisma/prisma.module' import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { CustomerSaleInvoicesController } from './sale-invoices.controller' import { CustomerSaleInvoicesController } from './sale-invoices.controller'
@@ -6,6 +7,6 @@ import { CustomerSaleInvoicesService } from './sale-invoices.service'
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
controllers: [CustomerSaleInvoicesController], controllers: [CustomerSaleInvoicesController],
providers: [CustomerSaleInvoicesService], providers: [CustomerSaleInvoicesService, SharedSaleInvoicePaginationService],
}) })
export class ConsumerSaleInvoicesModule {} export class ConsumerSaleInvoicesModule {}
@@ -1,57 +1,28 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import consumer_mappersUtil from '@/common/utils/mappers/consumer_mappers.util' import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { SalesInvoiceSelect, SalesInvoiceWhereInput } from '@/generated/prisma/models' import { translateEnumValue } from '@/common/utils'
import { TspProviderResponseStatus } from '@/generated/prisma/enums'
import { SalesInvoiceWhereInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable, NotFoundException } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
export class CustomerSaleInvoicesService { export class CustomerSaleInvoicesService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly sharedSaleInvoicePaginationService: SharedSaleInvoicePaginationService,
) {}
private readonly defaultSelect: SalesInvoiceSelect = { async findAll(
id: true, consumer_id: string,
code: true, customer_id: string,
invoice_date: true, requestedPage = 1,
notes: true, requestedPerPage = 10,
total_amount: true, ) {
pos: { const { page, perPage, skip, take } =
select: { this.sharedSaleInvoicePaginationService.normalize(requestedPage, requestedPerPage)
id: true,
name: true,
complex: {
select: {
id: true,
name: true,
business_activity: {
select: {
id: true,
name: true,
},
},
},
},
},
},
consumer_account: {
select: {
role: true,
consumer: {
select: {
...QUERY_CONSTANTS.CONSUMER.infoSelect,
},
},
account: {
select: {
username: true,
},
},
},
},
created_at: true,
}
async findAll(consumer_id: string, customer_id: string, page = 1, perPage = 10) {
const salesWhere: SalesInvoiceWhereInput = { const salesWhere: SalesInvoiceWhereInput = {
customer_id, customer_id,
pos: { pos: {
@@ -67,15 +38,30 @@ export class CustomerSaleInvoicesService {
await tx.salesInvoice.findMany({ await tx.salesInvoice.findMany({
where: salesWhere, where: salesWhere,
select: { select: {
...this.defaultSelect, ...QUERY_CONSTANTS.SALE_INVOICE.summarySelect,
pos: {
select: {
name: true,
complex: {
select: {
name: true,
business_activity: {
select: {
name: true,
},
},
},
},
},
},
_count: { _count: {
select: { select: {
items: true, items: true,
}, },
}, },
}, },
skip: (page - 1) * perPage, skip,
take: 10, take,
}), }),
await tx.salesInvoice.count({ await tx.salesInvoice.count({
where: salesWhere, where: salesWhere,
@@ -83,16 +69,11 @@ export class CustomerSaleInvoicesService {
]) ])
const mappedAccounts = saleInvoices.map(saleInvoice => { const mappedAccounts = saleInvoices.map(saleInvoice => {
const { _count, consumer_account, ...rest } = saleInvoice const { _count, consumer_account, last_tsp_status, ...rest } = saleInvoice
const { consumer, ...restConsumerAccount } = consumer_account as any
const mappedConsumer = consumer_mappersUtil(consumer)
return { return {
...rest, ...rest,
items_count: _count.items, items_count: _count.items,
consumer_account: { status: translateEnumValue('TspProviderResponseStatus', last_tsp_status),
...restConsumerAccount,
consumer: mappedConsumer,
},
} }
}) })
@@ -104,7 +85,7 @@ export class CustomerSaleInvoicesService {
} }
async findOne(consumer_id: string, customer_id: string, id: string) { async findOne(consumer_id: string, customer_id: string, id: string) {
const saleInvoice = await this.prisma.salesInvoice.findUniqueOrThrow({ const invoice = await this.prisma.salesInvoice.findUnique({
where: { where: {
id, id,
customer_id, customer_id,
@@ -117,59 +98,26 @@ export class CustomerSaleInvoicesService {
}, },
}, },
select: { select: {
...this.defaultSelect, ...QUERY_CONSTANTS.SALE_INVOICE.select,
items: {
select: {
id: true,
notes: true,
unit_price: true,
quantity: true,
discount_amount: true,
tax_amount: true,
total_amount: true,
payload: true,
good: {
select: {
id: true,
name: true,
pricing_model: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
id: true,
name: true,
image_url: true,
},
},
},
},
},
},
payments: {
select: {
amount: true,
paid_at: true,
payment_method: true,
},
},
}, },
}) })
const { _count, consumer_account, ...rest } = saleInvoice
const { consumer, ...restConsumerAccount } = consumer_account as any
const mappedConsumer = consumer_mappersUtil(consumer)
return ResponseMapper.single({ if (invoice) {
...rest, const { type, ...rest } = invoice
items_count: _count.items, const mappedInvoice = {
consumer_account: { ...rest,
...restConsumerAccount, type: translateEnumValue('TspProviderRequestType', type),
consumer: mappedConsumer, status: translateEnumValue(
}, 'TspProviderResponseStatus',
}) invoice.last_tsp_status || TspProviderResponseStatus.NOT_SEND,
),
settlement_type: translateEnumValue(
'InvoiceSettlementType',
invoice.settlement_type,
),
}
return ResponseMapper.single(mappedInvoice)
}
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { UpdateBusinessActivityDto } from './dto/poses.dto'
@Injectable() @Injectable()
export class PosesService { export class PosesService {
@@ -43,7 +44,7 @@ export class PosesService {
}) })
} }
async update(consumer_id: string, id: string, data: any) { async update(consumer_id: string, id: string, data: UpdateBusinessActivityDto) {
const pos = await this.prisma.pos.update({ const pos = await this.prisma.pos.update({
where: { ...this.defaultWhere(consumer_id), id }, where: { ...this.defaultWhere(consumer_id), id },
data, data,
@@ -1,95 +1,12 @@
import { SharedSaleInvoicesFilterDto } from '@/common/services/saleInvoices/sale-invoice-filter.dto'
import { ApiPropertyOptional } from '@nestjs/swagger' import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer' import { Max } from 'class-validator'
import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator'
import { TspProviderResponseStatus } from 'generated/prisma/enums'
export class ConsumerSaleInvoicesFilterDto {
@ApiPropertyOptional({ type: Number, example: 1 })
@Type(() => Number)
@Min(1)
@IsOptional()
page?: number
export class ConsumerSaleInvoicesFilterDto extends SharedSaleInvoicesFilterDto {
@ApiPropertyOptional({ type: Number, example: 10, description: 'Max value is 50.' }) @ApiPropertyOptional({ type: Number, example: 10, description: 'Max value is 50.' })
@Type(() => Number)
@Min(1)
@Max(50) @Max(50)
@IsOptional() declare perPage?: number
perPage?: number
@ApiPropertyOptional() @ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
code?: string code?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_mobile?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_national_id?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_economic_code?: string
@ApiPropertyOptional({ enum: TspProviderResponseStatus })
@IsOptional()
@IsEnum(TspProviderResponseStatus)
status?: TspProviderResponseStatus
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_from?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_to?: number
} }
@@ -4,6 +4,7 @@ import type { IPosPayload } from '@/common/models'
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common' import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger' import { ApiTags } from '@nestjs/swagger'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto' import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
import type { import type {
SaleInvoicesServiceFindAllResponseDto, SaleInvoicesServiceFindAllResponseDto,
SaleInvoicesServiceFindOneResponseDto, SaleInvoicesServiceFindOneResponseDto,
@@ -59,4 +60,18 @@ export class StatisticsController {
revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) { revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.service.revoke(id, posInfo) return this.service.revoke(id, posInfo)
} }
@Post(':id/correction')
correction(
@Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosCorrectionSalesInvoiceDto,
) {
return this.service.correction(id, posInfo, data)
}
@Post(':id/return')
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.service.return(id, posInfo)
}
} }
@@ -1,5 +1,7 @@
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service' import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { SharedSaleInvoiceFilterService } from '@/common/services/saleInvoices/sale-invoice-filter.service'
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module' import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module'
import { PrismaModule } from '@/prisma/prisma.module' import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
@@ -9,7 +11,13 @@ import { SaleInvoicesService } from './saleInvoices.service'
@Module({ @Module({
imports: [PrismaModule, SaleInvoiceTspModule], imports: [PrismaModule, SaleInvoiceTspModule],
controllers: [StatisticsController], controllers: [StatisticsController],
providers: [SaleInvoicesService, SharedSaleInvoiceActionsService, SharedSaleInvoiceAccessService], providers: [
SaleInvoicesService,
SharedSaleInvoiceActionsService,
SharedSaleInvoiceAccessService,
SharedSaleInvoiceFilterService,
SharedSaleInvoicePaginationService,
],
exports: [SaleInvoicesService], exports: [SaleInvoicesService],
}) })
export class ConsumerSaleInvoicesModule {} export class ConsumerSaleInvoicesModule {}
@@ -1,6 +1,8 @@
import { IPosPayload } from '@/common/models' import { IPosPayload } from '@/common/models'
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service' import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoiceFilterService } from '@/common/services/saleInvoices/sale-invoice-filter.service'
import { SharedSaleInvoicePaginationService } from '@/common/services/saleInvoices/sale-invoice-pagination.service'
import { translateEnumValue } from '@/common/utils' import { translateEnumValue } from '@/common/utils'
import { TspProviderResponseStatus } from '@/generated/prisma/enums' import { TspProviderResponseStatus } from '@/generated/prisma/enums'
import { import {
@@ -11,6 +13,7 @@ import {
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto' import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
@@ -20,11 +23,10 @@ export class SaleInvoicesService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private salesInvoiceTaxService: SalesInvoiceTspService, private salesInvoiceTaxService: SalesInvoiceTspService,
private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService, private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService,
private sharedSaleInvoiceFilterService: SharedSaleInvoiceFilterService,
private sharedSaleInvoicePaginationService: SharedSaleInvoicePaginationService,
) {} ) {}
private readonly defaultPerPage = 10
private readonly maxPerPage = 50
private readonly defaultSelect: SalesInvoiceSelect = { private readonly defaultSelect: SalesInvoiceSelect = {
pos: { pos: {
select: { select: {
@@ -47,29 +49,21 @@ export class SaleInvoicesService {
} }
private readonly invoiceMapper = (invoice: any) => { private readonly invoiceMapper = (invoice: any) => {
const { tsp_attempts, ...rest } = invoice || {} const { last_tsp_status, ...rest } = invoice || {}
return { return {
...rest, ...rest,
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
} }
async findAll(consumer_id: string, filter: ConsumerSaleInvoicesFilterDto) { async findAll(consumer_id: string, filter: ConsumerSaleInvoicesFilterDto) {
const invoicesWhere = this.buildFindAllWhere(consumer_id, filter) const invoicesWhere = this.buildFindAllWhere(consumer_id, filter)
const normalizedPageValue = Number(filter.page ?? 1) const { page, perPage, skip, take } =
const normalizedPage = Number.isFinite(normalizedPageValue) this.sharedSaleInvoicePaginationService.normalize(filter.page, filter.perPage)
? Math.max(1, Math.floor(normalizedPageValue))
: 1
const requestedPerPageValue = Number(filter.perPage ?? this.defaultPerPage)
const requestedPerPage = Number.isFinite(requestedPerPageValue)
? Math.max(1, Math.floor(requestedPerPageValue))
: this.defaultPerPage
const normalizedPerPage = Math.min(requestedPerPage, this.maxPerPage)
const [invoices, total] = await this.prisma.$transaction(async tx => [ const [invoices, total] = await this.prisma.$transaction(async tx => [
await tx.salesInvoice.findMany({ await tx.salesInvoice.findMany({
@@ -77,8 +71,8 @@ export class SaleInvoicesService {
orderBy: { orderBy: {
created_at: 'desc', created_at: 'desc',
}, },
skip: (normalizedPage - 1) * normalizedPerPage, skip,
take: normalizedPerPage, take,
select: { select: {
...QUERY_CONSTANTS.SALE_INVOICE.summarySelect, ...QUERY_CONSTANTS.SALE_INVOICE.summarySelect,
...this.defaultSelect, ...this.defaultSelect,
@@ -89,8 +83,8 @@ export class SaleInvoicesService {
const mappedInvoices = invoices.map(this.invoiceMapper) const mappedInvoices = invoices.map(this.invoiceMapper)
return ResponseMapper.paginate(mappedInvoices, { return ResponseMapper.paginate(mappedInvoices, {
page: normalizedPage, page,
perPage: normalizedPerPage, perPage,
total, total,
}) })
} }
@@ -100,9 +94,11 @@ export class SaleInvoicesService {
filter: ConsumerSaleInvoicesFilterDto, filter: ConsumerSaleInvoicesFilterDto,
): SalesInvoiceWhereInput { ): SalesInvoiceWhereInput {
const where: SalesInvoiceWhereInput = { const where: SalesInvoiceWhereInput = {
...this.sharedSaleInvoiceFilterService.buildWhere(filter),
consumer_account: { consumer_account: {
consumer_id, consumer_id,
}, },
referenced_by: null,
} }
if (filter.code?.trim()) { if (filter.code?.trim()) {
@@ -111,127 +107,6 @@ export class SaleInvoicesService {
} }
} }
if (filter.invoice_date_from || filter.invoice_date_to) {
where.invoice_date = {
...(filter.invoice_date_from ? { gte: new Date(filter.invoice_date_from) } : {}),
...(filter.invoice_date_to ? { lte: new Date(filter.invoice_date_to) } : {}),
}
}
if (filter.created_at_from || filter.created_at_to) {
where.created_at = {
...(filter.created_at_from ? { gte: new Date(filter.created_at_from) } : {}),
...(filter.created_at_to ? { lte: new Date(filter.created_at_to) } : {}),
}
}
if (
filter.total_amount !== undefined ||
filter.total_amount_from !== undefined ||
filter.total_amount_to !== undefined
) {
where.total_amount = {
...(filter.total_amount !== undefined ? { equals: filter.total_amount } : {}),
...(filter.total_amount_from !== undefined
? { gte: filter.total_amount_from }
: {}),
...(filter.total_amount_to !== undefined ? { lte: filter.total_amount_to } : {}),
}
}
if (
filter.customer_name?.trim() ||
filter.customer_mobile?.trim() ||
filter.customer_national_id?.trim() ||
filter.customer_economic_code?.trim()
) {
where.customer = {
is: {
OR: [
...(filter.customer_name?.trim()
? [
{
individual: {
is: {
OR: [
{ first_name: { contains: filter.customer_name.trim() } },
{ last_name: { contains: filter.customer_name.trim() } },
],
},
},
},
{
legal: {
is: {
name: {
contains: filter.customer_name.trim(),
},
},
},
},
]
: []),
...(filter.customer_mobile?.trim()
? [
{
individual: {
is: {
mobile_number: { contains: filter.customer_mobile.trim() },
},
},
},
]
: []),
...(filter.customer_national_id?.trim()
? [
{
individual: {
is: {
national_id: { contains: filter.customer_national_id.trim() },
},
},
},
]
: []),
...(filter.customer_economic_code?.trim()
? [
{
legal: {
is: {
OR: [
{
economic_code: {
contains: filter.customer_economic_code.trim(),
},
},
{
registration_number: {
contains: filter.customer_economic_code.trim(),
},
},
],
},
},
},
]
: []),
],
},
}
}
if (filter.status) {
if (filter.status === TspProviderResponseStatus.NOT_SEND) {
where.tsp_attempts = undefined
} else {
where.tsp_attempts = {
some: {
status: filter.status,
},
}
}
}
return where return where
} }
@@ -242,7 +117,7 @@ export class SaleInvoicesService {
consumer_id, consumer_id,
}, },
} }
const invoice = await this.prisma.salesInvoice.findUniqueOrThrow({ const invoice = await this.prisma.salesInvoice.findUnique({
where: invoicesWhere, where: invoicesWhere,
select: { select: {
...QUERY_CONSTANTS.SALE_INVOICE.select, ...QUERY_CONSTANTS.SALE_INVOICE.select,
@@ -251,9 +126,22 @@ export class SaleInvoicesService {
}) })
if (invoice) { if (invoice) {
return ResponseMapper.single(this.invoiceMapper(invoice)) const { type, ...rest } = invoice
const mappedInvoice = {
...rest,
type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue(
'TspProviderResponseStatus',
invoice.last_tsp_status || TspProviderResponseStatus.NOT_SEND,
),
settlement_type: translateEnumValue(
'InvoiceSettlementType',
invoice.settlement_type,
),
}
return ResponseMapper.single(mappedInvoice)
} }
throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.') throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
} }
async send(invoiceId: string, posInfo: IPosPayload) { async send(invoiceId: string, posInfo: IPosPayload) {
@@ -290,6 +178,39 @@ export class SaleInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async correction(
invoiceId: string,
posInfo: IPosPayload,
data: PosCorrectionSalesInvoiceDto,
) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.correction(
data,
consumer_account_id,
pos_id,
complex_id,
business_id,
invoiceId,
)
return ResponseMapper.single(invoice)
}
async return(invoiceId: string, posInfo: IPosPayload) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke(
consumer_account_id,
pos_id,
complex_id,
business_id,
invoiceId,
)
return ResponseMapper.single(invoice)
}
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) { async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
const invoice = await this.sharedSaleInvoiceActionsService.inquiry( const invoice = await this.sharedSaleInvoiceActionsService.inquiry(
consumer_account_id, consumer_account_id,
@@ -301,7 +222,7 @@ export class SaleInvoicesService {
async sendBulk(consumer_id: string, invoiceIds: string[]) { async sendBulk(consumer_id: string, invoiceIds: string[]) {
if (!invoiceIds.length) { if (!invoiceIds.length) {
throw new BadRequestException('لیست شناسه فاکتورها نمی‌تواند خالی باشد.') throw new BadRequestException('لیست شناسه ‌صورت‌حساب‌ها نمی‌تواند خالی باشد.')
} }
await this.salesInvoiceTaxService.sendBulk(consumer_id, invoiceIds) await this.salesInvoiceTaxService.sendBulk(consumer_id, invoiceIds)
@@ -10,13 +10,13 @@ export class StatisticsService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
private readonly invoiceMapper = (invoice: any) => { private readonly invoiceMapper = (invoice: any) => {
const { tsp_attempts, ...rest } = invoice || {} const { last_tsp_status, ...rest } = invoice || {}
return { return {
...rest, ...rest,
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
} }
+2 -2
View File
@@ -25,7 +25,7 @@ import {
TspProviderType, TspProviderType,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { GoldKarat, SKUGuildType, TspProviderCustomerType } from 'common/enums/enums' import { GoldKarat, TspProviderCustomerType } from 'common/enums/enums'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
@@ -80,7 +80,7 @@ export class EnumsService {
TspProviderCustomerType, TspProviderCustomerType,
'TspProviderCustomerType', 'TspProviderCustomerType',
), ),
SKUGuildType: this.prepareData(SKUGuildType, 'SKUGuildType'), // SKUGuildType: this.prepareData(SKUGuildType, 'SKUGuildType'),
InvoiceTemplateType: this.prepareData(InvoiceTemplateType, 'InvoiceTemplateType'), InvoiceTemplateType: this.prepareData(InvoiceTemplateType, 'InvoiceTemplateType'),
} }
} }
+2 -2
View File
@@ -115,14 +115,14 @@ export class InvoicesService {
private readonly where = () => ({}) private readonly where = () => ({})
private readonly invoiceMapper = (invoice: any) => { private readonly invoiceMapper = (invoice: any) => {
const { tsp_attempts, type, ...rest } = invoice || {} const { last_tsp_status, type, ...rest } = invoice || {}
return { return {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
} }
@@ -1,10 +1,14 @@
import { RedisKeyMaker } from '@/common/utils' import { RedisKeyMaker } from '@/common/utils'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { RedisService } from '@/redis/redis.service' import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@Injectable() @Injectable()
export class PartnersCacheInvalidationService { export class PartnersCacheInvalidationService {
constructor(private readonly redisService: RedisService) {} constructor(
private readonly redisService: RedisService,
private readonly PosCacheInvalidationService: PosCacheInvalidationService,
) {}
async invalidatePartnerSummary(partnerId: string): Promise<void> { async invalidatePartnerSummary(partnerId: string): Promise<void> {
await this.invalidatePartnersList() await this.invalidatePartnersList()
@@ -6,6 +6,10 @@ import { Injectable } from '@nestjs/common'
export class PosCacheInvalidationService { export class PosCacheInvalidationService {
constructor(private readonly redisService: RedisService) {} constructor(private readonly redisService: RedisService) {}
async invalidatePosInfo(posId: string): Promise<void> {
this.redisService.delete(PosKeyMaker.info(posId))
}
async invalidatePosMiddleware(token: string): Promise<void> { async invalidatePosMiddleware(token: string): Promise<void> {
this.redisService.delete(PosKeyMaker.middleware(token)) this.redisService.delete(PosKeyMaker.middleware(token))
} }
@@ -1,8 +1,13 @@
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator' import { IsString, Length } from 'class-validator'
export class UpdatePosAccountPasswordDto { export class UpdatePosAccountPasswordDto {
@IsString()
@ApiProperty({ required: true }) @ApiProperty({ required: true })
@IsString()
currentPassword: string
@ApiProperty({ required: true })
@IsString()
@Length(6, 32)
password: string password: string
} }
+25 -3
View File
@@ -5,7 +5,7 @@ import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { ConsumerStatus } from '@/generated/prisma/enums' import { ConsumerStatus } from '@/generated/prisma/enums'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service' import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { UpdatePosAccountPasswordDto } from './dto/update-password-request.dto' import { UpdatePosAccountPasswordDto } from './dto/update-password-request.dto'
@@ -218,7 +218,29 @@ export class PosService {
accountId: string, accountId: string,
data: UpdatePosAccountPasswordDto, data: UpdatePosAccountPasswordDto,
) { ) {
const consumer = await this.prisma.consumerAccount.update({ const currentCustomerAccount = await this.prisma.consumerAccount.findUniqueOrThrow({
where: {
id: accountId,
consumer_id,
},
select: {
account: {
select: {
password: true,
},
},
},
})
const isCurrentPasswordValid = await PasswordUtil.compare(
data.currentPassword,
currentCustomerAccount.account.password,
)
if (!isCurrentPasswordValid) {
throw new BadRequestException('رمز عبور فعلی اشتباه است')
}
const consumerAccount = await this.prisma.consumerAccount.update({
where: { where: {
id: accountId, id: accountId,
consumer_id, consumer_id,
@@ -232,6 +254,6 @@ export class PosService {
}, },
}) })
return ResponseMapper.update(consumer) return ResponseMapper.update(consumerAccount)
} }
} }
@@ -2,6 +2,51 @@ import {
SharedCorrectionSalesInvoiceDto, SharedCorrectionSalesInvoiceDto,
SharedCreateSalesInvoiceDto, SharedCreateSalesInvoiceDto,
} from '@/common/services/saleInvoices/sale-invoice-create.dto' } from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsOptional,
IsString,
Min,
ValidateNested,
} from 'class-validator'
export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {} export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {}
export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {} export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {}
export class PosReturnSalesInvoiceItemDto {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
good_id?: string
@ApiProperty({ required: false })
@IsOptional()
@IsString()
service_id?: string
@ApiProperty({ required: true, default: 1 })
@IsNumber()
@Min(1)
quantity: number
}
export class PosReturnSalesInvoiceDto {
@ApiProperty({ required: true })
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
invoice_date: Date
@ApiProperty({ required: true, type: [PosReturnSalesInvoiceItemDto] })
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => PosReturnSalesInvoiceItemDto)
items: PosReturnSalesInvoiceItemDto[]
}
@@ -1,95 +1,10 @@
import { SharedSaleInvoicesFilterDto } from '@/common/services/saleInvoices/sale-invoice-filter.dto'
import { ApiPropertyOptional } from '@nestjs/swagger' import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer' import { IsNumber, IsOptional } from 'class-validator'
import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator'
import { TspProviderResponseStatus } from 'generated/prisma/enums'
export class SalesInvoicesFilterDto {
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number
@ApiPropertyOptional({ default: 10 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
perPage?: number
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_to?: string
export class SalesInvoicesFilterDto extends SharedSaleInvoicesFilterDto {
@ApiPropertyOptional() @ApiPropertyOptional()
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
invoice_number?: number invoice_number?: number
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_mobile?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_national_id?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_economic_code?: string
@ApiPropertyOptional({ enum: TspProviderResponseStatus })
@IsOptional()
@IsEnum(TspProviderResponseStatus)
status?: TspProviderResponseStatus
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_from?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_to?: number
} }
@@ -5,6 +5,7 @@ import type { IPosPayload } from '@/common/models/posPayload.model'
import { import {
PosCorrectionSalesInvoiceDto, PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto, PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto' } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
import { SalesInvoicesService } from './sales-invoices.service' import { SalesInvoicesService } from './sales-invoices.service'
@@ -60,9 +61,13 @@ export class SalesInvoicesController {
) { ) {
return this.salesInvoicesService.correction(id, posInfo, data) return this.salesInvoicesService.correction(id, posInfo, data)
} }
@Post(':id/return') @Post(':id/return_from_sale')
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) { return(
return this.salesInvoicesService.return(id, posInfo) @Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosReturnSalesInvoiceDto,
) {
return this.salesInvoicesService.return(id, posInfo, data)
} }
// @Post('send/bulk') // @Post('send/bulk')
@@ -1,6 +1,7 @@
import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service' import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service'
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service' import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { SharedSaleInvoiceFilterService } from '@/common/services/saleInvoices/sale-invoice-filter.service'
import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module' import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { SalesInvoicesController } from './sales-invoices.controller' import { SalesInvoicesController } from './sales-invoices.controller'
@@ -14,6 +15,7 @@ import { SalesInvoicesService } from './sales-invoices.service'
SharedSaleInvoiceCreateService, SharedSaleInvoiceCreateService,
SharedSaleInvoiceActionsService, SharedSaleInvoiceActionsService,
SharedSaleInvoiceAccessService, SharedSaleInvoiceAccessService,
SharedSaleInvoiceFilterService,
], ],
}) })
export class PosSalesInvoicesModule {} export class PosSalesInvoicesModule {}
@@ -2,6 +2,7 @@ import { IPosPayload } from '@/common/models/posPayload.model'
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service' import { SharedSaleInvoiceActionsService } from '@/common/services/saleInvoices/sale-invoice-actions.service'
import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service' import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service'
import { SharedSaleInvoiceFilterService } from '@/common/services/saleInvoices/sale-invoice-filter.service'
import { translateEnumValue } from '@/common/utils' import { translateEnumValue } from '@/common/utils'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable, NotFoundException } from '@nestjs/common' import { Injectable, NotFoundException } from '@nestjs/common'
@@ -12,6 +13,7 @@ import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.ser
import { import {
PosCorrectionSalesInvoiceDto, PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto, PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto' } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
@@ -22,6 +24,7 @@ export class SalesInvoicesService {
private salesInvoiceTaxService: SalesInvoiceTspService, private salesInvoiceTaxService: SalesInvoiceTspService,
private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService, private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService,
private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService, private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService,
private sharedSaleInvoiceFilterService: SharedSaleInvoiceFilterService,
) {} ) {}
async findAll(posInfo: IPosPayload, query: SalesInvoicesFilterDto = {}) { async findAll(posInfo: IPosPayload, query: SalesInvoicesFilterDto = {}) {
@@ -45,13 +48,13 @@ export class SalesInvoicesService {
]) ])
const summaryItems = items.map(invoice => { const summaryItems = items.map(invoice => {
const { tsp_attempts, type, ...rest } = invoice const { last_tsp_status, type, ...rest } = invoice
return { return {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
}) })
@@ -76,13 +79,13 @@ export class SalesInvoicesService {
}) })
if (invoice) { if (invoice) {
const { tsp_attempts, type, ...rest } = invoice const { last_tsp_status, type, ...rest } = invoice
const mappedInvoice = { const mappedInvoice = {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
settlement_type: translateEnumValue( settlement_type: translateEnumValue(
'InvoiceSettlementType', 'InvoiceSettlementType',
@@ -91,7 +94,7 @@ export class SalesInvoicesService {
} }
return ResponseMapper.single(mappedInvoice) return ResponseMapper.single(mappedInvoice)
} else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.') } else throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
} }
async create(data: PosCreateSalesInvoiceDto, posInfo: IPosPayload) { async create(data: PosCreateSalesInvoiceDto, posInfo: IPosPayload) {
@@ -170,10 +173,11 @@ export class SalesInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async return(invoiceId: string, posInfo: IPosPayload) { async return(invoiceId: string, posInfo: IPosPayload, data: PosReturnSalesInvoiceDto) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke( const invoice = await this.sharedSaleInvoiceActionsService.return(
data,
consumer_account_id, consumer_account_id,
pos_id, pos_id,
complex_id, complex_id,
@@ -181,7 +185,7 @@ export class SalesInvoicesService {
invoiceId, invoiceId,
) )
return ResponseMapper.single(invoice) return ResponseMapper.create(invoice)
} }
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) { async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
@@ -195,6 +199,7 @@ export class SalesInvoicesService {
private buildFindAllWhere(posInfo: IPosPayload, query: SalesInvoicesFilterDto) { private buildFindAllWhere(posInfo: IPosPayload, query: SalesInvoicesFilterDto) {
const where: Prisma.SalesInvoiceWhereInput = { const where: Prisma.SalesInvoiceWhereInput = {
...this.sharedSaleInvoiceFilterService.buildWhere(query),
pos: { pos: {
id: posInfo.pos_id, id: posInfo.pos_id,
complex: { complex: {
@@ -208,142 +213,6 @@ export class SalesInvoicesService {
where.invoice_number = parseInt(query.invoice_number + '') where.invoice_number = parseInt(query.invoice_number + '')
} }
if (query.invoice_date_from || query.invoice_date_to) {
where.invoice_date = {
...(query.invoice_date_from ? { gte: new Date(query.invoice_date_from) } : {}),
...(query.invoice_date_to ? { lte: new Date(query.invoice_date_to) } : {}),
}
}
if (query.created_at_from || query.created_at_to) {
where.created_at = {
...(query.created_at_from ? { gte: new Date(query.created_at_from) } : {}),
...(query.created_at_to ? { lte: new Date(query.created_at_to) } : {}),
}
}
if (
query.total_amount !== undefined ||
query.total_amount_from !== undefined ||
query.total_amount_to !== undefined
) {
where.total_amount = {
...(query.total_amount !== undefined ? { equals: query.total_amount } : {}),
...(query.total_amount_from !== undefined
? { gte: query.total_amount_from }
: {}),
...(query.total_amount_to !== undefined ? { lte: query.total_amount_to } : {}),
}
}
if (
query.customer_name?.trim() ||
query.customer_mobile?.trim() ||
query.customer_national_id?.trim() ||
query.customer_economic_code?.trim()
) {
where.customer = {
is: {
OR: [
...(query.customer_name?.trim()
? [
{
individual: {
is: {
OR: [
{
first_name: {
contains: query.customer_name.trim(),
},
},
{
last_name: {
contains: query.customer_name.trim(),
},
},
],
},
},
},
{
legal: {
is: {
name: {
contains: query.customer_name.trim(),
},
},
},
},
]
: []),
...(query.customer_mobile?.trim()
? [
{
individual: {
is: {
mobile_number: {
contains: query.customer_mobile.trim(),
},
},
},
},
]
: []),
...(query.customer_national_id?.trim()
? [
{
individual: {
is: {
national_id: {
contains: query.customer_national_id.trim(),
},
},
},
},
]
: []),
...(query.customer_economic_code?.trim()
? [
{
legal: {
is: {
OR: [
{
economic_code: {
contains: query.customer_economic_code.trim(),
},
},
{
registration_number: {
contains: query.customer_economic_code.trim(),
},
},
],
},
},
},
]
: []),
],
},
}
}
if (query.status) {
if (query.status === TspProviderResponseStatus.NOT_SEND) {
where.OR = [
{
last_tsp_status: null,
},
{
last_tsp_status: TspProviderResponseStatus.NOT_SEND,
},
]
} else {
where.last_tsp_status = query.status
}
}
return where return where
} }
} }
@@ -46,8 +46,9 @@ export class TspProviderCorrectionInvoicePayloadDto {
@IsObject() @IsObject()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoicePaymentsDto) @Type(() => SharedCreateSalesInvoicePaymentsDto)
payments: SharedCreateSalesInvoicePaymentsDto payments?: SharedCreateSalesInvoicePaymentsDto
} }
export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSendPayloadDto { export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true }) @ApiProperty({ required: true })
@IsString() @IsString()
+1
View File
@@ -3,4 +3,5 @@ export * from './correction.dto'
export * from './get.dto' export * from './get.dto'
export * from './original.dto' export * from './original.dto'
export * from './provider-switch.dto' export * from './provider-switch.dto'
export * from './return.dto'
export * from './revoke.dto' export * from './revoke.dto'
@@ -0,0 +1,48 @@
import { SharedCreateSalesInvoiceItemDto } from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsString,
ValidateNested,
} from 'class-validator'
import {
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
} from './original.dto'
export class TspProviderReturnInvoicePayloadDto {
@ApiProperty({ required: true, nullable: true })
@IsDateString()
invoice_date: Date
@ApiProperty({ type: [SharedCreateSalesInvoiceItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoiceItemDto)
@ArrayMinSize(1)
items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty()
@IsNumber()
discount_amount: number
@ApiProperty()
@IsNumber()
tax_amount: number
}
export class TspProviderReturnSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true })
@IsString()
last_tax_id: string
}
export class TspProviderReturnSendResponseDto extends TspProviderOriginalResponseDto {}
@@ -6,6 +6,8 @@ import {
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto, TspProviderRevokeResponseDto,
} from './dto' } from './dto'
@@ -48,6 +50,14 @@ export class SalesInvoiceTspSwitchService {
return adapter.correctionSend(payload) return adapter.correctionSend(payload)
} }
async returnFromSale(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const adapter = this.resolveSwitch(payload.tsp_provider)
return adapter.returnSend(payload)
}
async sendBulk( async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[], payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> { ): Promise<TspProviderBulkSendResultDto[]> {
@@ -6,19 +6,15 @@ import {
TspProviderRequestType, TspProviderRequestType,
TspProviderResponseStatus, TspProviderResponseStatus,
} from 'generated/prisma/client' } from 'generated/prisma/client'
import { import { TspProviderActionResponseDto } from './dto'
TspProviderActionResponseDto,
TspProviderCorrectionInvoicePayloadDto,
} from './dto'
import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service' import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service'
import { import {
buildCorrectionPayload, buildCorrectionPayload,
buildOriginalPayload, buildOriginalPayload,
buildReturnFromSalePayload,
buildRevokePayload, buildRevokePayload,
getOriginalResendAttemptNumber, getOriginalResendAttemptNumber,
getRelatedInvoiceForCorrection,
onResult, onResult,
trySend,
} from './utils/sales-invoice-tsp.utils' } from './utils/sales-invoice-tsp.utils'
type ItemTspRow = { type ItemTspRow = {
@@ -42,20 +38,28 @@ export class SalesInvoiceTspService {
const attemptNumber = await getOriginalResendAttemptNumber(this.prisma, invoice_id) const attemptNumber = await getOriginalResendAttemptNumber(this.prisma, invoice_id)
const attempt = await this.prisma.saleInvoiceTspAttempts.create({ const invoice = await this.prisma.salesInvoice.update({
where: {
id: invoice_id,
},
data: { data: {
attempt_no: attemptNumber, last_tsp_status: TspProviderResponseStatus.QUEUED,
invoice_id, last_attempt_no: attemptNumber,
provider_request_payload: {}, tsp_attempts: {
status: TspProviderResponseStatus.QUEUED, create: {
raw_request_payload: JSON.parse(JSON.stringify(payload)), attempt_no: attemptNumber,
sent_at: new Date().toISOString(), provider_request_payload: {},
message: 'در حال ارسال به سامانه مالیاتی...', status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
},
},
}, },
}) })
const result = await this.tspSwitchService.send(payload) const result = await this.tspSwitchService.send(payload)
return await onResult(this.prisma, result, attempt.id) return await onResult(this.prisma, result, invoice.id)
} }
async sendBulk(consumer_id: string, invoice_ids: string[]): Promise<void> { async sendBulk(consumer_id: string, invoice_ids: string[]): Promise<void> {
@@ -75,18 +79,17 @@ export class SalesInvoiceTspService {
pos_id: string, pos_id: string,
consumer_id: string, consumer_id: string,
): Promise<TspProviderActionResponseDto> { ): Promise<TspProviderActionResponseDto> {
const [attempt, pos] = await this.prisma.$transaction(async tx => [ const [invoice, pos] = await this.prisma.$transaction(async tx => [
await tx.saleInvoiceTspAttempts.findFirst({ await tx.salesInvoice.findUnique({
where: { where: {
invoice_id, id: invoice_id,
invoice: { pos: {
pos: { id: pos_id,
id: pos_id,
},
}, },
}, },
orderBy: { select: {
attempt_no: 'desc', last_tsp_status: true,
id: true,
}, },
}), }),
@@ -135,12 +138,25 @@ export class SalesInvoiceTspService {
}), }),
]) ])
if (!attempt) { if (!invoice) {
throw new NotFoundException('صورت‌حساب ارسالی فاکتور شما به سامانه یافت نشد.') throw new NotFoundException('صورت‌حساب ارسالی صورت‌حساب شما به سامانه یافت نشد.')
} }
if (!pos) { if (!pos) {
throw new NotFoundException('مشکلی در ساختار اطلاعات ورودی شما وجود دارد.') throw new NotFoundException('مشکلی در ساختار اطلاعات ورودی شما وجود دارد.')
} }
if (
!invoice.last_tsp_status ||
invoice.last_tsp_status === TspProviderResponseStatus.NOT_SEND
) {
throw new BadRequestException(
'صورت‌حساب شما هنوز به سامانه مالیاتی ارسال نشده است. لطفا چند لحظه دیگر مجددا تلاش کنید.',
)
}
if (invoice.last_tsp_status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException(
'صورت‌حساب شما در صف ارسال به سامانه مالیاتی قرار دارد. لطفا چند لحظه دیگر مجددا تلاش کنید.',
)
}
const { business_activity } = pos.complex const { business_activity } = pos.complex
@@ -153,98 +169,72 @@ export class SalesInvoiceTspService {
business_activity.partner_token, business_activity.partner_token,
) )
return await onResult(this.prisma, result, attempt.id) return await onResult(this.prisma, result, invoice.id)
} }
async correctionSend( async correctionSend(
consumerAccountId: string,
posId: string, posId: string,
complexId: string, invoice_id: string,
businessId: string,
ref_invoice_id: string,
dataToUpdate: TspProviderCorrectionInvoicePayloadDto,
): Promise<TspProviderActionResponseDto> { ): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction( const payload = await buildCorrectionPayload(this.prisma, invoice_id, posId)
async tx => {
const relatedInvoice = await getRelatedInvoiceForCorrection(tx, ref_invoice_id)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({ const attemptNumber = 1
tx,
data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: dataToUpdate.invoice_date,
payments: dataToUpdate.payments,
items: dataToUpdate.items,
total_amount: dataToUpdate.total_amount,
discount_amount: dataToUpdate.discount_amount,
tax_amount: dataToUpdate.tax_amount,
customer_id: relatedInvoice.customer_id || undefined,
settlement_type: relatedInvoice.settlement_type,
},
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
const correctionPayload = await buildCorrectionPayload(tx, newInvoice.id) const invoice = await this.prisma.salesInvoice.update({
where: {
const attempt = await tx.saleInvoiceTspAttempts.create({ id: invoice_id,
data: {
attempt_no: 1,
invoice_id: newInvoice.id,
message: 'در حال ارسال به سامانه مالیاتی...',
status: TspProviderResponseStatus.QUEUED,
sent_at: new Date().toISOString(),
raw_request_payload: JSON.parse(JSON.stringify(correctionPayload)),
provider_request_payload: {},
},
})
return [newInvoice, attempt, correctionPayload]
}, },
) data: {
last_tsp_status: TspProviderResponseStatus.QUEUED,
last_attempt_no: attemptNumber,
type: TspProviderRequestType.CORRECTION,
tsp_attempts: {
create: {
attempt_no: attemptNumber,
provider_request_payload: {},
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
},
},
},
})
const result = await this.runProviderCall(() => const result = await this.tspSwitchService.send(payload)
trySend(this.tspSwitchService, correctionPayload), return await onResult(this.prisma, result, invoice.id)
) }
if (result?.hasError) {
result.provider_request_payload =
result.provider_request_payload || JSON.parse(JSON.stringify(correctionPayload))
result.sent_at = result.sent_at || new Date().toISOString()
result.received_at = result.received_at || new Date().toISOString()
}
return onResult(this.prisma, result, attempt.id) async returnFromSaleSend(
pos_id: string,
business_id: string,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const payload = await buildReturnFromSalePayload(this.prisma, invoice_id, pos_id)
// const countByGoodId = (goodIds: string[]): Map<string, number> => { const attemptNumber = 1
// const counts = new Map<string, number>()
// for (const goodId of goodIds) {
// counts.set(goodId, (counts.get(goodId) ?? 0) + 1)
// }
// return counts
// }
// const updatedGoodIds = dataToUpdate.items.map(item => item.good_id!) const invoice = await this.prisma.salesInvoice.update({
// const previousGoodIds = lastAttempt.invoice.items.map(item => item.good_id) where: {
id: invoice_id,
// const updatedCounts = countByGoodId(updatedGoodIds) },
// const previousCounts = countByGoodId(previousGoodIds) data: {
last_tsp_status: TspProviderResponseStatus.QUEUED,
// let isBackFromSale = false last_attempt_no: attemptNumber,
// if (updatedCounts.size !== previousCounts.size) { tsp_attempts: {
// isBackFromSale = true create: {
// } else { attempt_no: attemptNumber,
// for (const [goodId, count] of updatedCounts) { provider_request_payload: {},
// if (previousCounts.get(goodId) !== count) { status: TspProviderResponseStatus.QUEUED,
// isBackFromSale = true raw_request_payload: JSON.parse(JSON.stringify(payload)),
// break sent_at: new Date().toISOString(),
// } message: 'در حال ارسال به سامانه مالیاتی...',
// } },
// } },
},
})
const result = await this.tspSwitchService.returnFromSale(payload)
return await onResult(this.prisma, result, invoice.id)
} }
async revoke( async revoke(
@@ -282,86 +272,87 @@ export class SalesInvoiceTspService {
}) })
if (!relatedInvoice) { if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.') throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
} }
if (relatedInvoice.type === TspProviderRequestType.REVOKE) { if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.', 'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
) )
} }
if (!relatedInvoice.tax_id) { if (!relatedInvoice.tax_id) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.', 'صورت‌حساب قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
) )
} }
const payments = relatedInvoice.payments.reduce( // const payments = relatedInvoice.payments.reduce(
(acc, payment) => { // (acc, payment) => {
const amount = Number(payment.amount) // const amount = Number(payment.amount)
switch (payment.payment_method) { // switch (payment.payment_method) {
case 'CASH': // case 'CASH':
acc.cash = (acc.cash || 0) + amount // acc.cash = (acc.cash || 0) + amount
break // break
case 'SET_OFF': // case 'SET_OFF':
acc.set_off = (acc.set_off || 0) + amount // acc.set_off = (acc.set_off || 0) + amount
break // break
case 'CARD': // case 'CARD':
acc.card = (acc.card || 0) + amount // acc.card = (acc.card || 0) + amount
break // break
case 'BANK': // case 'BANK':
acc.bank = (acc.bank || 0) + amount // acc.bank = (acc.bank || 0) + amount
break // break
case 'CHEQUE': // case 'CHEQUE':
acc.check = (acc.check || 0) + amount // acc.check = (acc.check || 0) + amount
break // break
case 'OTHER': // case 'OTHER':
acc.other = (acc.other || 0) + amount // acc.other = (acc.other || 0) + amount
break // break
case 'TERMINAL': // case 'TERMINAL':
acc.terminals = payment.terminal_info // acc.terminals = payment.terminal_info
? { // ? {
amount, // amount,
terminalId: payment.terminal_info.terminal_id, // terminal_id: payment.terminal_info.terminal_id,
stan: payment.terminal_info.stan, // stan: payment.terminal_info.stan,
rrn: payment.terminal_info.rrn, // rrn: payment.terminal_info.rrn,
customer_card_no: // customer_card_no:
payment.terminal_info.customer_card_no || undefined, // payment.terminal_info.customer_card_no || undefined,
transaction_date_time: payment.terminal_info.transaction_date_time, // transaction_date_time: payment.terminal_info.transaction_date_time,
description: payment.terminal_info.description || undefined, // description: payment.terminal_info.description || undefined,
} // }
: acc.terminals // : acc.terminals
break // break
default: // default:
break // break
} // }
return acc // return acc
}, // },
{} as { // {} as {
terminals?: { // terminals?: {
amount?: number // amount?: number
terminalId: string // terminalId: string
stan: string // stan: string
rrn: string // rrn: string
customer_card_no?: string // customer_card_no?: string
transaction_date_time: Date // transaction_date_time: Date
description?: string // description?: string
} // }
cash?: number // cash?: number
set_off?: number // set_off?: number
card?: number // card?: number
bank?: number // bank?: number
check?: number // check?: number
other?: number // other?: number
}, // },
) // )
const newInvoice = await this.sharedSaleInvoiceCreateService.create({ const newInvoice = await this.sharedSaleInvoiceCreateService.create({
data: { data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN, customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: new Date(), invoice_date: new Date(),
payments, // @ts-ignore
payments: undefined,
settlement_type: relatedInvoice.settlement_type, settlement_type: relatedInvoice.settlement_type,
items: relatedInvoice.items.map(item => ({ items: relatedInvoice.items.map(item => ({
unit_price: Number(item.unit_price), unit_price: Number(item.unit_price),
@@ -226,7 +226,7 @@ export class NamaProviderValidationErrorDto {
} }
export class NamaProviderResponseDto { export class NamaProviderResponseDto {
@ApiProperty({ description: 'شناسه پیگیری فاکتور' }) @ApiProperty({ description: 'شناسه پیگیری صورت‌حساب' })
@IsString() @IsString()
uuid: string uuid: string
@@ -2,4 +2,5 @@ export * from './common.dto'
export * from './correction.dto' export * from './correction.dto'
export * from './get.dto' export * from './get.dto'
export * from './original.dto' export * from './original.dto'
export * from './returnFromSale.dto'
export * from './revoke.dto' export * from './revoke.dto'
@@ -0,0 +1,60 @@
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsArray, IsString, Length, ValidateNested } from 'class-validator'
import {
NamaProviderCommonBodyDto,
NamaProviderCommonHeaderDto,
NamaProviderPaymentInfoDto,
NamaProviderResponseDto,
} from './common.dto'
export class NamaProviderReturnHeaderDto extends NamaProviderCommonHeaderDto {
@ApiProperty({ required: true, description: 'موضوع صورتحساب (3- برگشت از فروش)' })
@IsString()
ins: '3'
@ApiProperty({
required: true,
description: 'شناسه مالیاتی صورت‌حساب مرجع',
minLength: 22,
maxLength: 22,
})
@IsString()
@Length(22, 22)
irtaxid: string
}
export class NamaProviderReturnBodyItemDto extends NamaProviderCommonBodyDto {}
export class NamaProviderReturnRequestDto {
@ApiProperty({ type: [NamaProviderReturnBodyItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnBodyItemDto)
body: NamaProviderReturnBodyItemDto[]
@ApiProperty({ type: [NamaProviderPaymentInfoDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderPaymentInfoDto)
payment: NamaProviderPaymentInfoDto[]
@ApiProperty({ type: NamaProviderReturnHeaderDto })
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnHeaderDto)
header: NamaProviderReturnHeaderDto
@ApiProperty()
@IsString()
uuid: string
@ApiProperty()
@IsString()
economic_code: string
@ApiProperty()
@IsString()
fiscal_id: string
}
export class NamaProviderReturnResponseDto extends NamaProviderResponseDto {}
@@ -21,7 +21,7 @@ export class NamaProviderRevokeHeaderDto {
@ApiProperty({ @ApiProperty({
required: true, required: true,
description: 'آخرین شماره مالیاتی دریافت شده مربوط به فاکتور', description: 'آخرین شماره مالیاتی دریافت شده مربوط به صورت‌حساب',
}) })
@IsString() @IsString()
irtaxid: string irtaxid: string
@@ -12,6 +12,8 @@ import {
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto, TspProviderRevokeResponseDto,
} from '../../dto' } from '../../dto'
@@ -27,6 +29,7 @@ import {
NamaProviderRevokeRequestDto, NamaProviderRevokeRequestDto,
NamaProviderRevokeResponseDto, NamaProviderRevokeResponseDto,
} from './dto' } from './dto'
import { NamaProviderReturnResponseDto } from './dto/returnFromSale.dto'
import { NamaProviderUtils } from './nama-provider.util' import { NamaProviderUtils } from './nama-provider.util'
@Injectable() @Injectable()
@@ -40,6 +43,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
private readonly sendPath = '/api/v1/invoices/single-send' private readonly sendPath = '/api/v1/invoices/single-send'
private readonly correctionPath = this.sendPath private readonly correctionPath = this.sendPath
private readonly returnFromSalePath = this.sendPath
private readonly sendBulkPath = '/api/v1/invoices' private readonly sendBulkPath = '/api/v1/invoices'
private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids' private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids'
private readonly getPath = '/api/v1/invoices' private readonly getPath = '/api/v1/invoices'
@@ -63,6 +67,10 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return `${this.baseUrl}${this.correctionPath}` return `${this.baseUrl}${this.correctionPath}`
} }
private buildReturnFromSaleUrl() {
return `${this.baseUrl}${this.returnFromSalePath}`
}
private buildGetUrl(invoiceId: string) { private buildGetUrl(invoiceId: string) {
return `${this.baseUrl}${this.getPath}/${invoiceId}` return `${this.baseUrl}${this.getPath}/${invoiceId}`
} }
@@ -176,6 +184,51 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
} }
} }
async returnSend(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const mappedRequest = this.namaProviderUtils.mapToNamaReturnDto(payload)
try {
const response = await this.httpClient.request(
this.buildReturnFromSaleUrl(),
{
method: 'POST',
body: JSON.stringify(mappedRequest),
},
this.createRequestInterceptors(payload.token),
)
const providerResponse: NamaProviderReturnResponseDto = await response.json()
this.logger.debug('NAMA provider return response', providerResponse)
const result: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: !response.ok,
message: providerResponse.message,
provider_response: JSON.parse(JSON.stringify(providerResponse)),
received_at: new Date().toISOString(),
tax_id: providerResponse.tax_id,
status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus,
),
}
return result
} catch (err) {
this.logger.error('NAMA return from sale send failed', err)
const failure: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: true,
message: (err as Error).message,
received_at: new Date().toISOString(),
status: TspProviderResponseStatus.SEND_FAILURE,
}
return failure
}
}
async sendBulk( async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[], payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> { ): Promise<TspProviderBulkSendResultDto[]> {
@@ -12,6 +12,7 @@ import {
PaymentInfoDto, PaymentInfoDto,
TspProviderCorrectionSendPayloadDto, TspProviderCorrectionSendPayloadDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
} from '../../dto' } from '../../dto'
import { import {
@@ -22,6 +23,7 @@ import {
NamaProviderResponseStatus, NamaProviderResponseStatus,
NamaProviderRevokeRequestDto, NamaProviderRevokeRequestDto,
} from './dto' } from './dto'
import { NamaProviderReturnRequestDto } from './dto/returnFromSale.dto'
export class NamaProviderUtils { export class NamaProviderUtils {
mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus { mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus {
@@ -68,7 +70,7 @@ export class NamaProviderUtils {
sstid: item.sku, sstid: item.sku,
vra: item.sku_vat, vra: item.sku_vat,
fee: String(item.unit_price), fee: String(item.unit_price),
dis: String(item.discount_amount), dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit, mu: item.measure_unit,
am: String(item.quantity), am: String(item.quantity),
consfee: '0', consfee: '0',
@@ -120,7 +122,58 @@ export class NamaProviderUtils {
sstid: item.sku, sstid: item.sku,
vra: item.sku_vat, vra: item.sku_vat,
fee: String(item.unit_price), fee: String(item.unit_price),
dis: String(item.discount_amount), dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit,
am: String(item.quantity),
consfee: '0',
bros: '0',
spro: '0',
}
if (item.gold_type_payload) {
const { profit = '0', commission = '0', wages = '0' } = item.gold_type_payload
data = {
...data,
consfee: wages,
bros: commission,
spro: profit,
tcpbs: (Number(profit) + Number(commission) + Number(wages)).toString(),
}
}
return data
}),
}
}
mapToNamaReturnDto(
payload: TspProviderReturnSendPayloadDto,
): NamaProviderReturnRequestDto {
return {
uuid: payload.id,
economic_code: payload.economic_code,
fiscal_id: payload.fiscal_id,
payment: this.mapPayments(payload.payments),
header: {
ins: this.mapRequestType(TspProviderRequestType.RETURN),
inp: this.mapInvoiceTemplate(payload.template),
inty: this.mapTspProviderCustomerType(payload.customer?.type),
inno: payload.invoice_number.toString(),
tins: '',
indatim: payload.invoice_date.getTime() + '',
irtaxid: payload.last_tax_id,
...this.mapSettlementBased(
payload.settlement_type,
payload.payments,
payload.total_amount,
),
...this.mapCustomerInfo(payload.customer),
},
body: payload.items.map(item => {
let data: NamaProviderOriginalBodyItemDto = {
sstid: item.sku,
vra: item.sku_vat,
fee: String(item.unit_price),
dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit, mu: item.measure_unit,
am: String(item.quantity), am: String(item.quantity),
consfee: '0', consfee: '0',
@@ -297,6 +350,9 @@ export class NamaProviderUtils {
} }
private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] { private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] {
if (!payments) {
return []
}
return payments.map(payment => ({ return payments.map(payment => ({
pmt: this.mapPaymentMethod(payment.payment_method), pmt: this.mapPaymentMethod(payment.payment_method),
pv: payment.amount, pv: payment.amount,
@@ -9,12 +9,13 @@ import {
} from 'generated/prisma/client' } from 'generated/prisma/client'
import { import {
goldTypePayload, goldTypePayload,
TspProviderCorrectionSendPayloadDto,
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
} from '../dto' } from '../dto'
import { TspProviderCorrectionSendPayloadDto } from '../dto/correction.dto'
import { TspProviderActionResponseDto } from '../dto/provider-switch.dto' import { TspProviderActionResponseDto } from '../dto/provider-switch.dto'
export async function getOriginalResendAttemptNumber( export async function getOriginalResendAttemptNumber(
@@ -23,44 +24,46 @@ export async function getOriginalResendAttemptNumber(
): Promise<number> { ): Promise<number> {
let attemptNumber = 1 let attemptNumber = 1
const existingAttempt = await prisma.saleInvoiceTspAttempts.findFirst({ const invoice = await prisma.salesInvoice.findFirst({
where: { where: {
invoice_id, id: invoice_id,
}, },
include: { select: {
invoice: { type: true,
select: { last_attempt_no: true,
type: true, last_tsp_status: true,
},
},
}, },
orderBy: { orderBy: {
created_at: 'desc', created_at: 'desc',
}, },
}) })
if (existingAttempt) { if (invoice) {
attemptNumber = existingAttempt.attempt_no + 1 const { last_attempt_no, type, last_tsp_status } = invoice
if (existingAttempt.invoice.type !== TspProviderRequestType.ORIGINAL) { attemptNumber = (last_attempt_no ?? 0) + 1
if (type !== TspProviderRequestType.ORIGINAL) {
throw new BadRequestException( throw new BadRequestException(
'فقط فاکتورهای اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.', 'فقط ‌صورت‌حساب‌های اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.',
) )
} }
if (existingAttempt.status === TspProviderResponseStatus.SUCCESS) { if (last_tsp_status === TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.', 'صورت‌حساب تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.',
) )
} }
if ( if (last_tsp_status === TspProviderResponseStatus.QUEUED) {
existingAttempt.status === TspProviderResponseStatus.QUEUED ||
existingAttempt.status === TspProviderResponseStatus.FISCAL_QUEUED
) {
throw new BadRequestException( throw new BadRequestException(
'در حال حاضر فاکتور شما در حال بررسی توسط سازمان مالیاتی است.', 'در حال حاضر صورت‌حساب شما در صف ارسال به سازمان مالیاتی است.',
) )
} }
} if (last_tsp_status === TspProviderResponseStatus.FISCAL_QUEUED) {
throw new BadRequestException(
'در حال حاضر صورت‌حساب شما در حال بررسی توسط سازمان مالیاتی است.',
)
}
} else throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
return attemptNumber return attemptNumber
} }
@@ -138,7 +141,7 @@ export async function buildRevokePayload(
}) })
if (!invoice) { if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.') throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
} }
const { partner } = (invoice.pos.complex.business_activity.consumer.legal || const { partner } = (invoice.pos.complex.business_activity.consumer.legal ||
@@ -158,10 +161,12 @@ export async function buildRevokePayload(
export async function buildCorrectionPayload( export async function buildCorrectionPayload(
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
invoice_id: string, invoice_id: string,
pos_id: string,
): Promise<TspProviderCorrectionSendPayloadDto> { ): Promise<TspProviderCorrectionSendPayloadDto> {
const invoice = await tx.salesInvoice.findUnique({ const invoice = await tx.salesInvoice.findUnique({
where: { where: {
id: invoice_id, id: invoice_id,
pos_id,
}, },
select: { select: {
id: true, id: true,
@@ -321,6 +326,217 @@ export async function buildCorrectionPayload(
} }
} }
export async function buildReturnFromSalePayload(
prisma: PrismaService,
invoiceId: string,
posId: string,
): Promise<TspProviderReturnSendPayloadDto> {
const invoice = await prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
settlement_type: true,
discount_amount: true,
tax_amount: true,
tax_id: true,
items: {
select: {
id: true,
quantity: true,
unit_price: true,
total_amount: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
good_id: true,
service_id: true,
discount_amount: true,
tax_amount: true,
payload: true,
good_snapshot: true,
},
},
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
partner_token: true,
guild: {
select: {
invoice_template: true,
},
},
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
},
unknown_customer: true,
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
payments: {
select: {
amount: true,
payment_method: true,
paid_at: true,
terminal_info: {
select: {
stan: true,
rrn: true,
transaction_date_time: true,
customer_card_no: true,
terminal_id: true,
},
},
},
},
reference_invoice: {
select: {
tax_id: true,
},
},
},
})
if (!invoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (!invoice.reference_invoice) {
throw new NotFoundException('صورت‌حساب مرجع برای برگشت از فروش یافت نشد.')
}
const {
pos,
id,
invoice_number,
invoice_date,
total_amount,
discount_amount,
tax_amount,
settlement_type,
} = invoice
const { business_activity: ba } = pos.complex
const unknown_customer = (invoice.unknown_customer || {}) as Record<string, string>
const { partner } = (ba.consumer.legal || ba.consumer.individual)!
return {
id,
invoice_number,
invoice_date,
settlement_type,
total_amount: Number(total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
economic_code: ba.economic_code,
fiscal_id: ba.fiscal_id,
template: ba.guild.invoice_template,
token: ba.partner_token,
tsp_provider: partner.tsp_provider!,
last_tax_id: invoice.reference_invoice.tax_id!,
payments: invoice.payments.map(payment => ({
amount: Number(payment.amount),
payment_method: payment.payment_method,
paid_at: payment.paid_at,
terminal_info: {
card_number: payment.terminal_info?.customer_card_no
? payment.terminal_info.customer_card_no
: undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
},
})),
customer:
invoice.customer && invoice.customer.type !== CustomerType.UNKNOWN
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal
? {
name: invoice.customer.legal.name ?? undefined,
registration_number:
invoice.customer.legal.registration_number ?? undefined,
postal_code: invoice.customer.legal.postal_code ?? undefined,
economic_code: invoice.customer.legal.economic_code ?? undefined,
}
: undefined,
individual_info: invoice.customer.individual
? {
first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: invoice.customer.individual.economic_code ?? undefined,
}
: undefined,
}
: {
type: CustomerType.UNKNOWN,
unknown_info: {
name: unknown_customer?.name || '',
economic_code: unknown_customer?.economic_code || '',
},
},
items: invoice.items.map(item => ({
invoice_item_id: item.id,
quantity: Number(item.quantity),
unit_price: Number(item.unit_price),
total_amount: Number(item.total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount: String((item.payload as Record<string, any>)?.discount || '0'),
good_id: item.good_id,
service_id: item.service_id,
good_snapshot: item.good_snapshot,
gold_type_payload: isGoldTypePayload(item.payload) ? item.payload : undefined,
})),
}
}
export async function buildOriginalPayload( export async function buildOriginalPayload(
prisma: PrismaService, prisma: PrismaService,
invoiceId: string, invoiceId: string,
@@ -417,6 +633,9 @@ export async function buildOriginalPayload(
select: { select: {
stan: true, stan: true,
rrn: true, rrn: true,
transaction_date_time: true,
customer_card_no: true,
terminal_id: true,
}, },
}, },
}, },
@@ -425,7 +644,7 @@ export async function buildOriginalPayload(
}) })
if (!invoice) { if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.') throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
} }
const { const {
@@ -463,7 +682,9 @@ export async function buildOriginalPayload(
payment_method: payment.payment_method, payment_method: payment.payment_method,
paid_at: payment.paid_at, paid_at: payment.paid_at,
terminal_info: { terminal_info: {
card_number: payment.terminal_info ? payment.terminal_info.stan : undefined, card_number: payment.terminal_info?.customer_card_no
? payment.terminal_info.customer_card_no
: undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined, tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
}, },
})), })),
@@ -552,12 +773,12 @@ export async function getRelatedInvoiceForModification(
}) })
if (!relatedInvoice) { if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.') throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
} }
if (relatedInvoice.type === TspProviderRequestType.REVOKE) { if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.', 'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
) )
} }
@@ -566,7 +787,7 @@ export async function getRelatedInvoiceForModification(
relatedInvoice.tsp_attempts?.[0].status !== TspProviderResponseStatus.SUCCESS relatedInvoice.tsp_attempts?.[0].status !== TspProviderResponseStatus.SUCCESS
) { ) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.', 'صورت‌حساب قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.',
) )
} }
@@ -587,11 +808,11 @@ export async function trySend(
export async function onResult( export async function onResult(
prisma: PrismaService, prisma: PrismaService,
result: TspProviderOriginalResponseDto | TspProviderGetResponseDto, result: TspProviderOriginalResponseDto | TspProviderGetResponseDto,
attempt_id: string, invoice_id: string,
): Promise<TspProviderActionResponseDto> { ): Promise<TspProviderActionResponseDto> {
let attemptUpdatedData: SaleInvoiceTspAttemptsUpdateInput = {} let attemptUpdatedData: SaleInvoiceTspAttemptsUpdateInput = {}
console.log('attempt', result, attempt_id) console.log('attempt', result)
const resultMessage = result.message const resultMessage = result.message
@@ -606,7 +827,7 @@ export async function onResult(
? resultMessage ? resultMessage
: result.hasError : result.hasError
? 'وجود مشکل در ارسال به سامانه مالیاتی' ? 'وجود مشکل در ارسال به سامانه مالیاتی'
: 'فاکتور با موفقیت به سامانه مالیاتی ارسال شد.', : 'صورت‌حساب با موفقیت به سامانه مالیاتی ارسال شد.',
invoice: { invoice: {
update: { update: {
tax_id: result['tax_id'] ? result['tax_id'] : undefined, tax_id: result['tax_id'] ? result['tax_id'] : undefined,
@@ -633,26 +854,40 @@ export async function onResult(
attemptUpdatedData = { attemptUpdatedData = {
status: TspProviderResponseStatus.SEND_FAILURE, status: TspProviderResponseStatus.SEND_FAILURE,
message: message:
'متاسفانه امکان ارسال فاکتور به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.', 'متاسفانه امکان ارسال صورت‌حساب به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.',
received_at: new Date().toISOString(), received_at: new Date().toISOString(),
} }
} }
const updatedAttempt = await prisma.saleInvoiceTspAttempts.update({ const invoice = await prisma.$transaction(async tx => {
where: { const lastAttempt = await tx.saleInvoiceTspAttempts.findFirst({
id: attempt_id, where: { invoice_id },
}, orderBy: { attempt_no: 'desc' },
data: attemptUpdatedData, select: { id: true },
select: { })
status: true,
invoice: true, if (!lastAttempt) throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
message: true,
}, await tx.saleInvoiceTspAttempts.update({
where: { id: lastAttempt.id },
data: attemptUpdatedData,
})
console.log('attemptUpdatedData', attemptUpdatedData)
const updatedInvoice = await tx.salesInvoice.update({
where: { id: invoice_id },
data: {
last_tsp_status: attemptUpdatedData.status,
last_attempt_no: attemptUpdatedData.attempt_no,
},
})
return updatedInvoice
}) })
return { return {
invoice: updatedAttempt.invoice, invoice,
status: updatedAttempt.status, status: attemptUpdatedData.status as TspProviderResponseStatus,
message: updatedAttempt.message, message: attemptUpdatedData.message as string,
} }
} }
+2 -10
View File
@@ -1,21 +1,13 @@
import { prismaAdapter } from '@/lib/prisma'
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common' import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
import 'dotenv/config' import 'dotenv/config'
import { env } from 'prisma/config'
import { PrismaClient } from '../generated/prisma/client' import { PrismaClient } from '../generated/prisma/client'
import { getPrismaOptions } from './prisma-config.service' import { getPrismaOptions } from './prisma-config.service'
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() { constructor() {
const adapter = new PrismaMariaDb({ const adapter = prismaAdapter
host: env('DATABASE_HOST'),
user: env('DATABASE_USER'),
password: env('DATABASE_PASSWORD'),
database: env('DATABASE_NAME'),
port: env('DATABASE_PORT') ? Number(env('DATABASE_PORT')) : 3306,
connectionLimit: 10,
})
const prismaOptions = getPrismaOptions() const prismaOptions = getPrismaOptions()
super({ ...prismaOptions, adapter }) super({ ...prismaOptions, adapter })
+12 -1
View File
@@ -252,6 +252,17 @@ export class RedisService implements OnModuleDestroy {
} }
async onModuleDestroy() { async onModuleDestroy() {
await this.client.quit() if (this.client.status === 'end') {
return
}
try {
await this.client.quit()
} catch (error) {
const message = (error as Error)?.message || ''
if (!message.includes('Connection is closed')) {
this.logger.warn(`Redis shutdown failed: ${message}`)
}
}
} }
} }