@goweekdays/core 2.15.6 → 2.15.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +301 -1
- package/dist/index.js +2608 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2609 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3465,6 +3465,11 @@ function useAppService() {
|
|
|
3465
3465
|
name: "Finance",
|
|
3466
3466
|
description: "Finance and billing services."
|
|
3467
3467
|
},
|
|
3468
|
+
{
|
|
3469
|
+
code: "assets",
|
|
3470
|
+
name: "Assets",
|
|
3471
|
+
description: "Asset management and tracking."
|
|
3472
|
+
},
|
|
3468
3473
|
{
|
|
3469
3474
|
code: "marketplace",
|
|
3470
3475
|
name: "Marketplace",
|
|
@@ -22496,6 +22501,2567 @@ function useJournalLineController() {
|
|
|
22496
22501
|
getById
|
|
22497
22502
|
};
|
|
22498
22503
|
}
|
|
22504
|
+
|
|
22505
|
+
// src/resources/asset-item/asset.item.model.ts
|
|
22506
|
+
import { BadRequestError as BadRequestError108 } from "@goweekdays/utils";
|
|
22507
|
+
import Joi96 from "joi";
|
|
22508
|
+
import { ObjectId as ObjectId58 } from "mongodb";
|
|
22509
|
+
var assetItemCategories = [
|
|
22510
|
+
"supply",
|
|
22511
|
+
"tool",
|
|
22512
|
+
"vehicle",
|
|
22513
|
+
"facility",
|
|
22514
|
+
"equipment"
|
|
22515
|
+
];
|
|
22516
|
+
var assetItemClasses = [
|
|
22517
|
+
"consumable",
|
|
22518
|
+
"semi_expendable",
|
|
22519
|
+
"ppe"
|
|
22520
|
+
];
|
|
22521
|
+
var assetItemPurposes = ["internal", "for-sale", "for-rent"];
|
|
22522
|
+
var assetItemTrackingTypes = ["quantity", "individual"];
|
|
22523
|
+
var assetItemStatuses = ["active", "archived"];
|
|
22524
|
+
var schemaAssetItemBase = {
|
|
22525
|
+
orgId: Joi96.string().hex().length(24).required(),
|
|
22526
|
+
name: Joi96.string().required(),
|
|
22527
|
+
sku: Joi96.string().optional().allow("", null).default(""),
|
|
22528
|
+
price: Joi96.number().min(0).optional().allow("", null).default(0),
|
|
22529
|
+
unit: Joi96.string().required(),
|
|
22530
|
+
description: Joi96.string().optional().allow("", null),
|
|
22531
|
+
assetCategory: Joi96.string().valid(...assetItemCategories).required(),
|
|
22532
|
+
assetClass: Joi96.string().valid(...assetItemClasses).optional().allow("", null),
|
|
22533
|
+
trackingType: Joi96.string().valid(...assetItemTrackingTypes).required(),
|
|
22534
|
+
purpose: Joi96.string().valid(...assetItemPurposes).required(),
|
|
22535
|
+
itemRefId: Joi96.string().hex().length(24).optional().allow("", null),
|
|
22536
|
+
remarks: Joi96.string().optional().allow("", null),
|
|
22537
|
+
departmentId: Joi96.string().hex().length(24).required(),
|
|
22538
|
+
categoryId: Joi96.string().hex().length(24).optional().allow("", null),
|
|
22539
|
+
subcategoryId: Joi96.string().hex().length(24).optional().allow("", null),
|
|
22540
|
+
categoryPath: Joi96.string().optional().allow("", null),
|
|
22541
|
+
brand: Joi96.string().optional().allow("", null),
|
|
22542
|
+
tags: Joi96.array().items(
|
|
22543
|
+
Joi96.object({
|
|
22544
|
+
id: Joi96.string().required(),
|
|
22545
|
+
name: Joi96.string().required()
|
|
22546
|
+
})
|
|
22547
|
+
).optional().default([]),
|
|
22548
|
+
quantityOnHand: Joi96.number().min(0).optional().default(0)
|
|
22549
|
+
};
|
|
22550
|
+
var schemaAssetItem = Joi96.object({
|
|
22551
|
+
...schemaAssetItemBase,
|
|
22552
|
+
departmentName: Joi96.string().required(),
|
|
22553
|
+
categoryName: Joi96.string().optional().allow("", null),
|
|
22554
|
+
subcategoryName: Joi96.string().optional().allow("", null),
|
|
22555
|
+
status: Joi96.string().valid(...assetItemStatuses).optional().allow("", null)
|
|
22556
|
+
});
|
|
22557
|
+
var schemaAssetItemCreate = Joi96.object({
|
|
22558
|
+
...schemaAssetItemBase,
|
|
22559
|
+
tags: Joi96.array().items(
|
|
22560
|
+
Joi96.object({
|
|
22561
|
+
id: Joi96.string().required(),
|
|
22562
|
+
name: Joi96.string().required()
|
|
22563
|
+
})
|
|
22564
|
+
).optional().default([])
|
|
22565
|
+
}).custom((value, helpers) => {
|
|
22566
|
+
if (value.subcategoryId && !value.categoryId) {
|
|
22567
|
+
return helpers.error("any.custom", {
|
|
22568
|
+
message: "categoryId is required when subcategoryId is provided."
|
|
22569
|
+
});
|
|
22570
|
+
}
|
|
22571
|
+
return value;
|
|
22572
|
+
}, "subcategory-requires-category");
|
|
22573
|
+
var schemaAssetItemUpdate = Joi96.object({
|
|
22574
|
+
name: Joi96.string().optional(),
|
|
22575
|
+
description: Joi96.string().optional().allow("", null),
|
|
22576
|
+
assetCategory: Joi96.string().valid(...assetItemCategories).optional(),
|
|
22577
|
+
assetClass: Joi96.string().valid(...assetItemClasses).optional(),
|
|
22578
|
+
brand: Joi96.string().optional().allow("", null),
|
|
22579
|
+
sku: Joi96.string().optional().allow("", null),
|
|
22580
|
+
price: Joi96.number().min(0).optional().allow("", null),
|
|
22581
|
+
tags: Joi96.array().items(
|
|
22582
|
+
Joi96.object({
|
|
22583
|
+
id: Joi96.string().required(),
|
|
22584
|
+
name: Joi96.string().required()
|
|
22585
|
+
})
|
|
22586
|
+
).optional(),
|
|
22587
|
+
quantityOnHand: Joi96.number().min(0).optional()
|
|
22588
|
+
});
|
|
22589
|
+
function modelAssetItem(data) {
|
|
22590
|
+
const { error } = schemaAssetItem.validate(data);
|
|
22591
|
+
if (error) {
|
|
22592
|
+
throw new BadRequestError108(`Invalid asset item data: ${error.message}`);
|
|
22593
|
+
}
|
|
22594
|
+
try {
|
|
22595
|
+
data.orgId = new ObjectId58(data.orgId);
|
|
22596
|
+
} catch (error2) {
|
|
22597
|
+
throw new BadRequestError108("Invalid orgId format.");
|
|
22598
|
+
}
|
|
22599
|
+
try {
|
|
22600
|
+
data.departmentId = new ObjectId58(data.departmentId);
|
|
22601
|
+
} catch (error2) {
|
|
22602
|
+
throw new BadRequestError108("Invalid departmentId format.");
|
|
22603
|
+
}
|
|
22604
|
+
if (data.categoryId) {
|
|
22605
|
+
try {
|
|
22606
|
+
data.categoryId = new ObjectId58(data.categoryId);
|
|
22607
|
+
} catch (error2) {
|
|
22608
|
+
throw new BadRequestError108("Invalid categoryId format.");
|
|
22609
|
+
}
|
|
22610
|
+
}
|
|
22611
|
+
if (data.subcategoryId) {
|
|
22612
|
+
try {
|
|
22613
|
+
data.subcategoryId = new ObjectId58(data.subcategoryId);
|
|
22614
|
+
} catch (error2) {
|
|
22615
|
+
throw new BadRequestError108("Invalid subcategoryId format.");
|
|
22616
|
+
}
|
|
22617
|
+
}
|
|
22618
|
+
if (data.itemRefId) {
|
|
22619
|
+
try {
|
|
22620
|
+
data.itemRefId = new ObjectId58(data.itemRefId);
|
|
22621
|
+
} catch (error2) {
|
|
22622
|
+
throw new BadRequestError108("Invalid itemRefId format.");
|
|
22623
|
+
}
|
|
22624
|
+
}
|
|
22625
|
+
if (data.purpose === "for-sale" && (data.price === void 0 || data.price === null || data.price === 0)) {
|
|
22626
|
+
throw new BadRequestError108(
|
|
22627
|
+
"Price must be greater than 0 for items marked as 'for-sale'."
|
|
22628
|
+
);
|
|
22629
|
+
}
|
|
22630
|
+
if (data.isPublic && (data.marketplacePrice === void 0 || data.marketplacePrice === null)) {
|
|
22631
|
+
throw new BadRequestError108(
|
|
22632
|
+
"Marketplace price is required for items marked as public."
|
|
22633
|
+
);
|
|
22634
|
+
}
|
|
22635
|
+
return {
|
|
22636
|
+
_id: data._id,
|
|
22637
|
+
orgId: data.orgId,
|
|
22638
|
+
name: data.name,
|
|
22639
|
+
description: data.description ?? "",
|
|
22640
|
+
assetCategory: data.assetCategory,
|
|
22641
|
+
assetClass: data.assetClass,
|
|
22642
|
+
unit: data.unit ?? "",
|
|
22643
|
+
price: data.price ?? 0,
|
|
22644
|
+
marketplacePrice: data.marketplacePrice ?? 0,
|
|
22645
|
+
isPublic: data.isPublic ?? false,
|
|
22646
|
+
sku: data.sku ?? "",
|
|
22647
|
+
brand: data.brand ?? "",
|
|
22648
|
+
trackingType: data.trackingType,
|
|
22649
|
+
purpose: data.purpose,
|
|
22650
|
+
itemRefId: data.itemRefId ?? "",
|
|
22651
|
+
remarks: data.remarks ?? "",
|
|
22652
|
+
departmentId: data.departmentId,
|
|
22653
|
+
departmentName: data.departmentName,
|
|
22654
|
+
categoryId: data.categoryId ?? "",
|
|
22655
|
+
categoryName: data.categoryName ?? "",
|
|
22656
|
+
subcategoryId: data.subcategoryId ?? "",
|
|
22657
|
+
subcategoryName: data.subcategoryName ?? "",
|
|
22658
|
+
categoryPath: data.categoryPath ?? "",
|
|
22659
|
+
tags: data.tags ?? [],
|
|
22660
|
+
quantityOnHand: data.quantityOnHand ?? 0,
|
|
22661
|
+
status: data.status ?? "active",
|
|
22662
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
|
|
22663
|
+
updatedAt: data.updatedAt ?? "",
|
|
22664
|
+
deletedAt: data.deletedAt ?? ""
|
|
22665
|
+
};
|
|
22666
|
+
}
|
|
22667
|
+
|
|
22668
|
+
// src/resources/asset-item/asset.item.repository.ts
|
|
22669
|
+
import {
|
|
22670
|
+
AppError as AppError51,
|
|
22671
|
+
BadRequestError as BadRequestError109,
|
|
22672
|
+
InternalServerError as InternalServerError47,
|
|
22673
|
+
logger as logger54,
|
|
22674
|
+
makeCacheKey as makeCacheKey33,
|
|
22675
|
+
paginate as paginate29,
|
|
22676
|
+
useRepo as useRepo32
|
|
22677
|
+
} from "@goweekdays/utils";
|
|
22678
|
+
import { ObjectId as ObjectId59 } from "mongodb";
|
|
22679
|
+
function useAssetItemRepo() {
|
|
22680
|
+
const namespace_collection = "asset.items";
|
|
22681
|
+
const repo = useRepo32(namespace_collection);
|
|
22682
|
+
async function createIndexes() {
|
|
22683
|
+
try {
|
|
22684
|
+
await repo.collection.createIndexes([
|
|
22685
|
+
{ key: { categoryPath: 1 } },
|
|
22686
|
+
{ key: { departmentId: 1 } },
|
|
22687
|
+
{ key: { categoryId: 1 } },
|
|
22688
|
+
{ key: { subcategoryId: 1 } },
|
|
22689
|
+
{ key: { brand: 1 } },
|
|
22690
|
+
{ key: { "tags.name": 1 } },
|
|
22691
|
+
{ key: { assetCategory: 1 } },
|
|
22692
|
+
{ key: { purpose: 1 } },
|
|
22693
|
+
{ key: { trackingType: 1 } },
|
|
22694
|
+
{ key: { status: 1 } },
|
|
22695
|
+
{ key: { name: "text" } }
|
|
22696
|
+
]);
|
|
22697
|
+
} catch (error) {
|
|
22698
|
+
throw new BadRequestError109("Failed to create asset item indexes.");
|
|
22699
|
+
}
|
|
22700
|
+
}
|
|
22701
|
+
async function add(value, session) {
|
|
22702
|
+
try {
|
|
22703
|
+
value = modelAssetItem(value);
|
|
22704
|
+
const res = await repo.collection.insertOne(value, { session });
|
|
22705
|
+
repo.delCachedData();
|
|
22706
|
+
return res.insertedId;
|
|
22707
|
+
} catch (error) {
|
|
22708
|
+
if (error instanceof AppError51) {
|
|
22709
|
+
throw error;
|
|
22710
|
+
}
|
|
22711
|
+
logger54.log({ level: "error", message: error.message });
|
|
22712
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
22713
|
+
if (isDuplicated) {
|
|
22714
|
+
throw new BadRequestError109("Asset item name already exists.");
|
|
22715
|
+
}
|
|
22716
|
+
throw new InternalServerError47("Failed to create asset item.");
|
|
22717
|
+
}
|
|
22718
|
+
}
|
|
22719
|
+
async function getAll({
|
|
22720
|
+
search = "",
|
|
22721
|
+
page = 1,
|
|
22722
|
+
limit = 10,
|
|
22723
|
+
status: status2 = "active",
|
|
22724
|
+
assetCategory = "",
|
|
22725
|
+
trackingType = "",
|
|
22726
|
+
purpose = "",
|
|
22727
|
+
departmentId = "",
|
|
22728
|
+
categoryId = "",
|
|
22729
|
+
subcategoryId = "",
|
|
22730
|
+
brand = "",
|
|
22731
|
+
tags = []
|
|
22732
|
+
} = {}) {
|
|
22733
|
+
page = page > 0 ? page - 1 : 0;
|
|
22734
|
+
const query = { status: status2 };
|
|
22735
|
+
if (search) {
|
|
22736
|
+
query.$text = { $search: search };
|
|
22737
|
+
}
|
|
22738
|
+
if (assetCategory) {
|
|
22739
|
+
query.assetCategory = assetCategory;
|
|
22740
|
+
}
|
|
22741
|
+
if (trackingType) {
|
|
22742
|
+
query.trackingType = trackingType;
|
|
22743
|
+
}
|
|
22744
|
+
if (purpose) {
|
|
22745
|
+
query.purpose = purpose;
|
|
22746
|
+
}
|
|
22747
|
+
if (departmentId) {
|
|
22748
|
+
query.departmentId = departmentId;
|
|
22749
|
+
}
|
|
22750
|
+
if (categoryId) {
|
|
22751
|
+
query.categoryId = categoryId;
|
|
22752
|
+
}
|
|
22753
|
+
if (subcategoryId) {
|
|
22754
|
+
query.subcategoryId = subcategoryId;
|
|
22755
|
+
}
|
|
22756
|
+
if (brand) {
|
|
22757
|
+
query.brand = brand;
|
|
22758
|
+
}
|
|
22759
|
+
if (tags.length > 0) {
|
|
22760
|
+
query["tags.name"] = { $all: tags };
|
|
22761
|
+
}
|
|
22762
|
+
const cacheKey = makeCacheKey33(namespace_collection, {
|
|
22763
|
+
search,
|
|
22764
|
+
page,
|
|
22765
|
+
limit,
|
|
22766
|
+
status: status2,
|
|
22767
|
+
assetCategory,
|
|
22768
|
+
trackingType,
|
|
22769
|
+
purpose,
|
|
22770
|
+
departmentId,
|
|
22771
|
+
categoryId,
|
|
22772
|
+
subcategoryId,
|
|
22773
|
+
brand,
|
|
22774
|
+
tags: tags.join(","),
|
|
22775
|
+
tag: "getAll"
|
|
22776
|
+
});
|
|
22777
|
+
try {
|
|
22778
|
+
const cached = await repo.getCache(cacheKey);
|
|
22779
|
+
if (cached)
|
|
22780
|
+
return cached;
|
|
22781
|
+
const items = await repo.collection.aggregate([
|
|
22782
|
+
{ $match: query },
|
|
22783
|
+
{ $sort: { _id: -1 } },
|
|
22784
|
+
{ $skip: page * limit },
|
|
22785
|
+
{ $limit: limit }
|
|
22786
|
+
]).toArray();
|
|
22787
|
+
const length = await repo.collection.countDocuments(query);
|
|
22788
|
+
const data = paginate29(items, page, limit, length);
|
|
22789
|
+
repo.setCache(cacheKey, data, 600);
|
|
22790
|
+
return data;
|
|
22791
|
+
} catch (error) {
|
|
22792
|
+
logger54.log({ level: "error", message: `${error}` });
|
|
22793
|
+
throw error;
|
|
22794
|
+
}
|
|
22795
|
+
}
|
|
22796
|
+
async function getById(id) {
|
|
22797
|
+
const cacheKey = makeCacheKey33(namespace_collection, {
|
|
22798
|
+
id,
|
|
22799
|
+
tag: "by-id"
|
|
22800
|
+
});
|
|
22801
|
+
try {
|
|
22802
|
+
const cached = await repo.getCache(cacheKey);
|
|
22803
|
+
if (cached)
|
|
22804
|
+
return cached;
|
|
22805
|
+
const result = await repo.collection.findOne({
|
|
22806
|
+
_id: new ObjectId59(id)
|
|
22807
|
+
});
|
|
22808
|
+
if (!result) {
|
|
22809
|
+
throw new BadRequestError109("Asset item not found.");
|
|
22810
|
+
}
|
|
22811
|
+
repo.setCache(cacheKey, result, 300);
|
|
22812
|
+
return result;
|
|
22813
|
+
} catch (error) {
|
|
22814
|
+
if (error instanceof AppError51)
|
|
22815
|
+
throw error;
|
|
22816
|
+
throw new InternalServerError47("Failed to get asset item.");
|
|
22817
|
+
}
|
|
22818
|
+
}
|
|
22819
|
+
async function updateById(id, value) {
|
|
22820
|
+
try {
|
|
22821
|
+
const result = await repo.collection.findOneAndUpdate(
|
|
22822
|
+
{ _id: new ObjectId59(id) },
|
|
22823
|
+
{ $set: { ...value, updatedAt: /* @__PURE__ */ new Date() } },
|
|
22824
|
+
{ returnDocument: "after" }
|
|
22825
|
+
);
|
|
22826
|
+
if (!result) {
|
|
22827
|
+
throw new BadRequestError109("Asset item not found.");
|
|
22828
|
+
}
|
|
22829
|
+
repo.delCachedData();
|
|
22830
|
+
return result;
|
|
22831
|
+
} catch (error) {
|
|
22832
|
+
if (error instanceof AppError51)
|
|
22833
|
+
throw error;
|
|
22834
|
+
throw new InternalServerError47("Failed to update asset item.");
|
|
22835
|
+
}
|
|
22836
|
+
}
|
|
22837
|
+
async function incrementQuantity(id, amount, session) {
|
|
22838
|
+
try {
|
|
22839
|
+
const result = await repo.collection.findOneAndUpdate(
|
|
22840
|
+
{ _id: new ObjectId59(id) },
|
|
22841
|
+
{
|
|
22842
|
+
$inc: { quantityOnHand: amount },
|
|
22843
|
+
$set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
22844
|
+
},
|
|
22845
|
+
{ returnDocument: "after", session }
|
|
22846
|
+
);
|
|
22847
|
+
if (!result) {
|
|
22848
|
+
throw new BadRequestError109("Asset item not found.");
|
|
22849
|
+
}
|
|
22850
|
+
repo.delCachedData();
|
|
22851
|
+
return result;
|
|
22852
|
+
} catch (error) {
|
|
22853
|
+
if (error instanceof AppError51)
|
|
22854
|
+
throw error;
|
|
22855
|
+
throw new InternalServerError47("Failed to update asset item quantity.");
|
|
22856
|
+
}
|
|
22857
|
+
}
|
|
22858
|
+
async function deleteById(id) {
|
|
22859
|
+
try {
|
|
22860
|
+
const result = await repo.collection.findOneAndUpdate(
|
|
22861
|
+
{ _id: new ObjectId59(id) },
|
|
22862
|
+
{ $set: { deletedAt: /* @__PURE__ */ new Date(), status: "archived" } },
|
|
22863
|
+
{ returnDocument: "after" }
|
|
22864
|
+
);
|
|
22865
|
+
if (!result) {
|
|
22866
|
+
throw new BadRequestError109("Asset item not found.");
|
|
22867
|
+
}
|
|
22868
|
+
repo.delCachedData();
|
|
22869
|
+
return result;
|
|
22870
|
+
} catch (error) {
|
|
22871
|
+
if (error instanceof AppError51)
|
|
22872
|
+
throw error;
|
|
22873
|
+
throw new InternalServerError47("Failed to delete asset item.");
|
|
22874
|
+
}
|
|
22875
|
+
}
|
|
22876
|
+
return {
|
|
22877
|
+
createIndexes,
|
|
22878
|
+
add,
|
|
22879
|
+
getAll,
|
|
22880
|
+
getById,
|
|
22881
|
+
updateById,
|
|
22882
|
+
incrementQuantity,
|
|
22883
|
+
deleteById
|
|
22884
|
+
};
|
|
22885
|
+
}
|
|
22886
|
+
|
|
22887
|
+
// src/resources/asset-item/asset.item.service.ts
|
|
22888
|
+
import {
|
|
22889
|
+
AppError as AppError54,
|
|
22890
|
+
InternalServerError as InternalServerError50,
|
|
22891
|
+
useAtlas as useAtlas17
|
|
22892
|
+
} from "@goweekdays/utils";
|
|
22893
|
+
|
|
22894
|
+
// src/resources/tag/tag.repository.ts
|
|
22895
|
+
import {
|
|
22896
|
+
AppError as AppError52,
|
|
22897
|
+
BadRequestError as BadRequestError110,
|
|
22898
|
+
useRepo as useRepo33,
|
|
22899
|
+
makeCacheKey as makeCacheKey34,
|
|
22900
|
+
paginate as paginate30,
|
|
22901
|
+
logger as logger55,
|
|
22902
|
+
InternalServerError as InternalServerError48
|
|
22903
|
+
} from "@goweekdays/utils";
|
|
22904
|
+
|
|
22905
|
+
// src/resources/tag/tag.model.ts
|
|
22906
|
+
import Joi97 from "joi";
|
|
22907
|
+
import { ObjectId as ObjectId60 } from "mongodb";
|
|
22908
|
+
var schemaTagFields = {
|
|
22909
|
+
name: Joi97.string().required(),
|
|
22910
|
+
type: Joi97.string().valid("public", "private").required(),
|
|
22911
|
+
orgId: Joi97.string().hex().optional().allow("", null),
|
|
22912
|
+
categoryPath: Joi97.string().optional().allow("", null),
|
|
22913
|
+
status: Joi97.string().valid("active", "pending").optional()
|
|
22914
|
+
};
|
|
22915
|
+
var schemaTagCreate = Joi97.object(schemaTagFields);
|
|
22916
|
+
var schemaTagStd = Joi97.object({
|
|
22917
|
+
...schemaTagFields,
|
|
22918
|
+
normalizedName: Joi97.string().required(),
|
|
22919
|
+
usageCount: Joi97.number().integer().min(0).optional()
|
|
22920
|
+
});
|
|
22921
|
+
var schemaTagUpdate = Joi97.object({
|
|
22922
|
+
name: Joi97.string().optional(),
|
|
22923
|
+
type: Joi97.string().valid("public", "private").optional(),
|
|
22924
|
+
orgId: Joi97.string().hex().optional().allow("", null),
|
|
22925
|
+
categoryPath: Joi97.string().optional().allow("", null),
|
|
22926
|
+
status: Joi97.string().valid("active", "pending").optional()
|
|
22927
|
+
}).min(1);
|
|
22928
|
+
function modelTag(data) {
|
|
22929
|
+
const { error } = schemaTagStd.validate(data, { allowUnknown: false });
|
|
22930
|
+
if (error) {
|
|
22931
|
+
throw new Error(`Invalid tag data: ${error.message}`);
|
|
22932
|
+
}
|
|
22933
|
+
if (data.orgId) {
|
|
22934
|
+
try {
|
|
22935
|
+
data.orgId = new ObjectId60(data.orgId);
|
|
22936
|
+
} catch (error2) {
|
|
22937
|
+
throw new Error(`Invalid orgId: ${data.orgId}`);
|
|
22938
|
+
}
|
|
22939
|
+
}
|
|
22940
|
+
return {
|
|
22941
|
+
_id: data._id,
|
|
22942
|
+
name: data.name,
|
|
22943
|
+
normalizedName: data.normalizedName,
|
|
22944
|
+
type: data.type,
|
|
22945
|
+
orgId: data.orgId ?? "",
|
|
22946
|
+
categoryPath: data.categoryPath ?? "",
|
|
22947
|
+
status: data.status ?? "pending",
|
|
22948
|
+
usageCount: data.usageCount ?? 0,
|
|
22949
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
|
|
22950
|
+
updatedAt: data.updatedAt ?? ""
|
|
22951
|
+
};
|
|
22952
|
+
}
|
|
22953
|
+
|
|
22954
|
+
// src/resources/tag/tag.repository.ts
|
|
22955
|
+
import { ObjectId as ObjectId61 } from "mongodb";
|
|
22956
|
+
import Joi98 from "joi";
|
|
22957
|
+
function useTagRepo() {
|
|
22958
|
+
const namespace_collection = "tags";
|
|
22959
|
+
const repo = useRepo33(namespace_collection);
|
|
22960
|
+
async function createIndexes() {
|
|
22961
|
+
try {
|
|
22962
|
+
await repo.collection.createIndexes([
|
|
22963
|
+
{ key: { name: 1 } },
|
|
22964
|
+
{ key: { normalizedName: 1 } },
|
|
22965
|
+
{ key: { type: 1 } },
|
|
22966
|
+
{ key: { orgId: 1 } },
|
|
22967
|
+
{ key: { status: 1 } },
|
|
22968
|
+
{ key: { categoryPath: 1 } },
|
|
22969
|
+
{
|
|
22970
|
+
key: { normalizedName: 1, orgId: 1 },
|
|
22971
|
+
unique: true,
|
|
22972
|
+
name: "unique_normalized_name_per_org"
|
|
22973
|
+
}
|
|
22974
|
+
]);
|
|
22975
|
+
} catch (error) {
|
|
22976
|
+
throw new Error("Failed to create index on tags.");
|
|
22977
|
+
}
|
|
22978
|
+
}
|
|
22979
|
+
async function add(value, session) {
|
|
22980
|
+
try {
|
|
22981
|
+
value = modelTag(value);
|
|
22982
|
+
const res = await repo.collection.insertOne(value, { session });
|
|
22983
|
+
repo.delCachedData();
|
|
22984
|
+
return res.insertedId;
|
|
22985
|
+
} catch (error) {
|
|
22986
|
+
logger55.log({
|
|
22987
|
+
level: "error",
|
|
22988
|
+
message: error.message
|
|
22989
|
+
});
|
|
22990
|
+
const isDuplicate = /normalizedName|unique/i.test(error.message);
|
|
22991
|
+
if (isDuplicate) {
|
|
22992
|
+
throw new BadRequestError110("A tag with this name already exists.");
|
|
22993
|
+
}
|
|
22994
|
+
throw new BadRequestError110(`Failed to create tag: ${error.message}`);
|
|
22995
|
+
}
|
|
22996
|
+
}
|
|
22997
|
+
async function getAll(options) {
|
|
22998
|
+
const validation = Joi98.object({
|
|
22999
|
+
page: Joi98.number().min(1).optional().allow("", null),
|
|
23000
|
+
limit: Joi98.number().min(1).optional().allow("", null),
|
|
23001
|
+
search: Joi98.string().optional().allow("", null),
|
|
23002
|
+
type: Joi98.string().valid("public", "private").optional().allow("", null),
|
|
23003
|
+
orgId: Joi98.string().hex().optional().allow("", null),
|
|
23004
|
+
status: Joi98.string().valid("active", "pending").optional().allow("", null),
|
|
23005
|
+
categoryPath: Joi98.string().optional().allow("", null)
|
|
23006
|
+
});
|
|
23007
|
+
const { error } = validation.validate(options);
|
|
23008
|
+
if (error) {
|
|
23009
|
+
throw new BadRequestError110(error.message);
|
|
23010
|
+
}
|
|
23011
|
+
options.page = options.page && options.page > 0 ? options.page - 1 : 0;
|
|
23012
|
+
options.limit = options.limit ?? 10;
|
|
23013
|
+
const query = {};
|
|
23014
|
+
const cacheKeyOptions = {
|
|
23015
|
+
limit: options.limit,
|
|
23016
|
+
page: options.page
|
|
23017
|
+
};
|
|
23018
|
+
if (options.search) {
|
|
23019
|
+
query.name = { $regex: options.search, $options: "i" };
|
|
23020
|
+
cacheKeyOptions.search = options.search;
|
|
23021
|
+
}
|
|
23022
|
+
if (options.type) {
|
|
23023
|
+
query.type = options.type;
|
|
23024
|
+
cacheKeyOptions.type = options.type;
|
|
23025
|
+
}
|
|
23026
|
+
if (options.orgId) {
|
|
23027
|
+
query.orgId = new ObjectId61(options.orgId);
|
|
23028
|
+
cacheKeyOptions.orgId = options.orgId;
|
|
23029
|
+
}
|
|
23030
|
+
if (options.status) {
|
|
23031
|
+
query.status = options.status;
|
|
23032
|
+
cacheKeyOptions.status = options.status;
|
|
23033
|
+
}
|
|
23034
|
+
if (options.categoryPath) {
|
|
23035
|
+
query.categoryPath = options.categoryPath;
|
|
23036
|
+
cacheKeyOptions.categoryPath = options.categoryPath;
|
|
23037
|
+
}
|
|
23038
|
+
const cacheKey = makeCacheKey34(namespace_collection, cacheKeyOptions);
|
|
23039
|
+
try {
|
|
23040
|
+
const cached = await repo.getCache(cacheKey);
|
|
23041
|
+
if (cached) {
|
|
23042
|
+
return cached;
|
|
23043
|
+
}
|
|
23044
|
+
const items = await repo.collection.aggregate([
|
|
23045
|
+
{ $match: query },
|
|
23046
|
+
{ $sort: { name: 1 } },
|
|
23047
|
+
{ $skip: options.page * options.limit },
|
|
23048
|
+
{ $limit: options.limit },
|
|
23049
|
+
{
|
|
23050
|
+
$project: {
|
|
23051
|
+
_id: 1,
|
|
23052
|
+
name: 1,
|
|
23053
|
+
normalizedName: 1,
|
|
23054
|
+
type: 1,
|
|
23055
|
+
orgId: 1,
|
|
23056
|
+
categoryPath: 1,
|
|
23057
|
+
status: 1,
|
|
23058
|
+
usageCount: 1
|
|
23059
|
+
}
|
|
23060
|
+
}
|
|
23061
|
+
]).toArray();
|
|
23062
|
+
const length = await repo.collection.countDocuments(query);
|
|
23063
|
+
const data = paginate30(items, options.page, options.limit, length);
|
|
23064
|
+
repo.setCache(cacheKey, data, 600).then(() => {
|
|
23065
|
+
logger55.log({
|
|
23066
|
+
level: "info",
|
|
23067
|
+
message: `Cache set for getAll tags: ${cacheKey}`
|
|
23068
|
+
});
|
|
23069
|
+
}).catch((err) => {
|
|
23070
|
+
logger55.log({
|
|
23071
|
+
level: "error",
|
|
23072
|
+
message: `Failed to set cache for getAll tags: ${err.message}`
|
|
23073
|
+
});
|
|
23074
|
+
});
|
|
23075
|
+
return data;
|
|
23076
|
+
} catch (error2) {
|
|
23077
|
+
logger55.log({ level: "error", message: `${error2}` });
|
|
23078
|
+
throw error2;
|
|
23079
|
+
}
|
|
23080
|
+
}
|
|
23081
|
+
async function getById(_id) {
|
|
23082
|
+
try {
|
|
23083
|
+
_id = new ObjectId61(_id);
|
|
23084
|
+
} catch (error) {
|
|
23085
|
+
throw new BadRequestError110("Invalid ID.");
|
|
23086
|
+
}
|
|
23087
|
+
const cacheKey = makeCacheKey34(namespace_collection, { _id: String(_id) });
|
|
23088
|
+
try {
|
|
23089
|
+
const cached = await repo.getCache(cacheKey);
|
|
23090
|
+
if (cached) {
|
|
23091
|
+
return cached;
|
|
23092
|
+
}
|
|
23093
|
+
const result = await repo.collection.findOne({ _id });
|
|
23094
|
+
if (!result) {
|
|
23095
|
+
throw new BadRequestError110("Tag not found.");
|
|
23096
|
+
}
|
|
23097
|
+
repo.setCache(cacheKey, result, 300).then(() => {
|
|
23098
|
+
logger55.log({
|
|
23099
|
+
level: "info",
|
|
23100
|
+
message: `Cache set for tag by id: ${cacheKey}`
|
|
23101
|
+
});
|
|
23102
|
+
}).catch((err) => {
|
|
23103
|
+
logger55.log({
|
|
23104
|
+
level: "error",
|
|
23105
|
+
message: `Failed to set cache for tag by id: ${err.message}`
|
|
23106
|
+
});
|
|
23107
|
+
});
|
|
23108
|
+
return result;
|
|
23109
|
+
} catch (error) {
|
|
23110
|
+
if (error instanceof AppError52) {
|
|
23111
|
+
throw error;
|
|
23112
|
+
} else {
|
|
23113
|
+
throw new InternalServerError48("Failed to get tag.");
|
|
23114
|
+
}
|
|
23115
|
+
}
|
|
23116
|
+
}
|
|
23117
|
+
async function updateById(_id, options, session) {
|
|
23118
|
+
try {
|
|
23119
|
+
_id = new ObjectId61(_id);
|
|
23120
|
+
} catch (error) {
|
|
23121
|
+
throw new BadRequestError110("Invalid Tag ID.");
|
|
23122
|
+
}
|
|
23123
|
+
try {
|
|
23124
|
+
await repo.collection.updateOne(
|
|
23125
|
+
{ _id },
|
|
23126
|
+
{ $set: { ...options, updatedAt: /* @__PURE__ */ new Date() } },
|
|
23127
|
+
{ session }
|
|
23128
|
+
);
|
|
23129
|
+
repo.delCachedData();
|
|
23130
|
+
return "Successfully updated tag.";
|
|
23131
|
+
} catch (error) {
|
|
23132
|
+
const isDuplicate = /normalizedName|unique/i.test(error.message);
|
|
23133
|
+
if (isDuplicate) {
|
|
23134
|
+
throw new BadRequestError110("A tag with this name already exists.");
|
|
23135
|
+
}
|
|
23136
|
+
throw new InternalServerError48("Failed to update tag.");
|
|
23137
|
+
}
|
|
23138
|
+
}
|
|
23139
|
+
async function deleteById(_id, session) {
|
|
23140
|
+
try {
|
|
23141
|
+
_id = new ObjectId61(_id);
|
|
23142
|
+
} catch (error) {
|
|
23143
|
+
throw new BadRequestError110("Invalid ID.");
|
|
23144
|
+
}
|
|
23145
|
+
try {
|
|
23146
|
+
await repo.collection.deleteOne({ _id }, { session });
|
|
23147
|
+
repo.delCachedData();
|
|
23148
|
+
return "Successfully deleted tag.";
|
|
23149
|
+
} catch (error) {
|
|
23150
|
+
throw new InternalServerError48("Failed to delete tag.");
|
|
23151
|
+
}
|
|
23152
|
+
}
|
|
23153
|
+
async function findByNormalizedNameAndOrg(normalizedName, orgId) {
|
|
23154
|
+
try {
|
|
23155
|
+
const query = { normalizedName };
|
|
23156
|
+
query.orgId = new ObjectId61(orgId);
|
|
23157
|
+
return await repo.collection.findOne(query);
|
|
23158
|
+
} catch (error) {
|
|
23159
|
+
if (error instanceof AppError52)
|
|
23160
|
+
throw error;
|
|
23161
|
+
throw new InternalServerError48("Failed to find tag.");
|
|
23162
|
+
}
|
|
23163
|
+
}
|
|
23164
|
+
async function incrementUsageCount(_id, amount = 1, session) {
|
|
23165
|
+
try {
|
|
23166
|
+
_id = new ObjectId61(_id);
|
|
23167
|
+
} catch (error) {
|
|
23168
|
+
throw new BadRequestError110("Invalid ID.");
|
|
23169
|
+
}
|
|
23170
|
+
try {
|
|
23171
|
+
await repo.collection.updateOne(
|
|
23172
|
+
{ _id },
|
|
23173
|
+
{ $inc: { usageCount: amount } },
|
|
23174
|
+
{ session }
|
|
23175
|
+
);
|
|
23176
|
+
repo.delCachedData();
|
|
23177
|
+
} catch (error) {
|
|
23178
|
+
throw new InternalServerError48("Failed to update tag usage count.");
|
|
23179
|
+
}
|
|
23180
|
+
}
|
|
23181
|
+
return {
|
|
23182
|
+
createIndexes,
|
|
23183
|
+
add,
|
|
23184
|
+
getAll,
|
|
23185
|
+
getById,
|
|
23186
|
+
findByNormalizedNameAndOrg,
|
|
23187
|
+
updateById,
|
|
23188
|
+
deleteById,
|
|
23189
|
+
incrementUsageCount
|
|
23190
|
+
};
|
|
23191
|
+
}
|
|
23192
|
+
|
|
23193
|
+
// src/resources/asset-item-category/category.repository.ts
|
|
23194
|
+
import {
|
|
23195
|
+
AppError as AppError53,
|
|
23196
|
+
BadRequestError as BadRequestError112,
|
|
23197
|
+
InternalServerError as InternalServerError49,
|
|
23198
|
+
logger as logger56,
|
|
23199
|
+
makeCacheKey as makeCacheKey35,
|
|
23200
|
+
paginate as paginate31,
|
|
23201
|
+
useRepo as useRepo34
|
|
23202
|
+
} from "@goweekdays/utils";
|
|
23203
|
+
|
|
23204
|
+
// src/resources/asset-item-category/category.model.ts
|
|
23205
|
+
import Joi99 from "joi";
|
|
23206
|
+
import { ObjectId as ObjectId62 } from "mongodb";
|
|
23207
|
+
import { BadRequestError as BadRequestError111 } from "@goweekdays/utils";
|
|
23208
|
+
var categoryLevels = [
|
|
23209
|
+
"department",
|
|
23210
|
+
"category",
|
|
23211
|
+
"subcategory"
|
|
23212
|
+
];
|
|
23213
|
+
var schemaCategoryNodeFields = {
|
|
23214
|
+
orgId: Joi99.string().hex().length(24).optional().allow("", null),
|
|
23215
|
+
level: Joi99.string().valid(...categoryLevels).required(),
|
|
23216
|
+
type: Joi99.string().valid("public", "private").optional().default("public"),
|
|
23217
|
+
name: Joi99.string().required(),
|
|
23218
|
+
displayName: Joi99.string().required(),
|
|
23219
|
+
parentId: Joi99.string().hex().length(24).optional().allow("", null),
|
|
23220
|
+
path: Joi99.string().optional().allow("", null),
|
|
23221
|
+
isActive: Joi99.boolean().optional().default(true)
|
|
23222
|
+
};
|
|
23223
|
+
var schemaCategoryNodeCreate = Joi99.object({
|
|
23224
|
+
displayName: Joi99.string().required(),
|
|
23225
|
+
parentId: Joi99.string().hex().length(24).optional().allow("", null),
|
|
23226
|
+
orgId: Joi99.string().hex().length(24).optional().allow("", null)
|
|
23227
|
+
});
|
|
23228
|
+
var schemaCategoryNodeUpdate = Joi99.object({
|
|
23229
|
+
displayName: Joi99.string().optional()
|
|
23230
|
+
});
|
|
23231
|
+
var schemaCategoryNodeStd = Joi99.object(schemaCategoryNodeFields);
|
|
23232
|
+
var schemaCategoryGetAll = Joi99.object({
|
|
23233
|
+
page: Joi99.number().integer().min(1).default(1),
|
|
23234
|
+
limit: Joi99.number().integer().min(1).max(100).default(20),
|
|
23235
|
+
org: Joi99.string().hex().length(24).optional().allow("", null).default(""),
|
|
23236
|
+
parent: Joi99.string().hex().length(24).optional().allow("", null).default(""),
|
|
23237
|
+
level: Joi99.string().valid(...categoryLevels, "").optional().default(""),
|
|
23238
|
+
type: Joi99.string().valid("public", "private", "").optional().default(""),
|
|
23239
|
+
search: Joi99.string().optional().allow("", null).default(""),
|
|
23240
|
+
status: Joi99.string().optional().allow("", null).default("active")
|
|
23241
|
+
});
|
|
23242
|
+
function normalizeName(displayName) {
|
|
23243
|
+
return displayName.trim().toLowerCase().replace(/\s+/g, "_");
|
|
23244
|
+
}
|
|
23245
|
+
function buildCategoryPath(departmentName, categoryName, subcategoryName) {
|
|
23246
|
+
return [departmentName, categoryName, subcategoryName].filter(Boolean).join(".").replace(/ /g, "_").toLowerCase();
|
|
23247
|
+
}
|
|
23248
|
+
function modelCategoryNode(data) {
|
|
23249
|
+
const { error } = schemaCategoryNodeStd.validate(data);
|
|
23250
|
+
if (error) {
|
|
23251
|
+
throw new BadRequestError111(`Invalid category node data: ${error.message}`);
|
|
23252
|
+
}
|
|
23253
|
+
if (data.orgId) {
|
|
23254
|
+
try {
|
|
23255
|
+
data.orgId = new ObjectId62(data.orgId);
|
|
23256
|
+
} catch {
|
|
23257
|
+
throw new BadRequestError111(`Invalid orgId: ${data.orgId}`);
|
|
23258
|
+
}
|
|
23259
|
+
}
|
|
23260
|
+
if (data.parentId) {
|
|
23261
|
+
try {
|
|
23262
|
+
data.parentId = new ObjectId62(data.parentId);
|
|
23263
|
+
} catch {
|
|
23264
|
+
throw new BadRequestError111(`Invalid parentId: ${data.parentId}`);
|
|
23265
|
+
}
|
|
23266
|
+
}
|
|
23267
|
+
return {
|
|
23268
|
+
_id: data._id,
|
|
23269
|
+
orgId: data.orgId ?? "",
|
|
23270
|
+
level: data.level,
|
|
23271
|
+
type: data.type ?? "public",
|
|
23272
|
+
name: data.name,
|
|
23273
|
+
displayName: data.displayName,
|
|
23274
|
+
parentId: data.parentId ?? "",
|
|
23275
|
+
path: data.path ?? "",
|
|
23276
|
+
status: data.status ?? "active",
|
|
23277
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
|
|
23278
|
+
updatedAt: data.updatedAt ?? ""
|
|
23279
|
+
};
|
|
23280
|
+
}
|
|
23281
|
+
|
|
23282
|
+
// src/resources/asset-item-category/category.repository.ts
|
|
23283
|
+
import { ObjectId as ObjectId63 } from "mongodb";
|
|
23284
|
+
function useCategoryRepo() {
|
|
23285
|
+
const namespace_collection = "categories";
|
|
23286
|
+
const repo = useRepo34(namespace_collection);
|
|
23287
|
+
async function createIndexes() {
|
|
23288
|
+
try {
|
|
23289
|
+
await repo.collection.createIndexes([
|
|
23290
|
+
{ key: { level: 1 } },
|
|
23291
|
+
{ key: { parentId: 1 } },
|
|
23292
|
+
{
|
|
23293
|
+
key: { name: "text" },
|
|
23294
|
+
name: "text_name"
|
|
23295
|
+
},
|
|
23296
|
+
{
|
|
23297
|
+
key: { name: 1, parentId: 1 },
|
|
23298
|
+
unique: true,
|
|
23299
|
+
name: "unique_name_per_parent"
|
|
23300
|
+
},
|
|
23301
|
+
{ key: { path: 1 } },
|
|
23302
|
+
{ key: { isActive: 1 } }
|
|
23303
|
+
]);
|
|
23304
|
+
} catch (error) {
|
|
23305
|
+
throw new InternalServerError49("Failed to create category indexes.");
|
|
23306
|
+
}
|
|
23307
|
+
}
|
|
23308
|
+
async function add(value, session) {
|
|
23309
|
+
try {
|
|
23310
|
+
value = modelCategoryNode(value);
|
|
23311
|
+
const res = await repo.collection.insertOne(value, { session });
|
|
23312
|
+
repo.delCachedData();
|
|
23313
|
+
return res.insertedId;
|
|
23314
|
+
} catch (error) {
|
|
23315
|
+
logger56.log({ level: "error", message: error.message });
|
|
23316
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
23317
|
+
if (isDuplicated) {
|
|
23318
|
+
throw new BadRequestError112(
|
|
23319
|
+
"Category name already exists under this parent."
|
|
23320
|
+
);
|
|
23321
|
+
}
|
|
23322
|
+
throw new InternalServerError49("Failed to create category node.");
|
|
23323
|
+
}
|
|
23324
|
+
}
|
|
23325
|
+
async function getAll({
|
|
23326
|
+
page = 1,
|
|
23327
|
+
limit = 20,
|
|
23328
|
+
org = "",
|
|
23329
|
+
parent = "",
|
|
23330
|
+
level = "",
|
|
23331
|
+
type = "",
|
|
23332
|
+
search = "",
|
|
23333
|
+
status: status2 = "active"
|
|
23334
|
+
} = {}) {
|
|
23335
|
+
page = page > 0 ? page - 1 : 0;
|
|
23336
|
+
const query = { status: status2 };
|
|
23337
|
+
if (org) {
|
|
23338
|
+
try {
|
|
23339
|
+
query.orgId = new ObjectId63(org);
|
|
23340
|
+
} catch (error) {
|
|
23341
|
+
throw new BadRequestError112("Invalid org ID.");
|
|
23342
|
+
}
|
|
23343
|
+
}
|
|
23344
|
+
if (level) {
|
|
23345
|
+
query.level = level;
|
|
23346
|
+
}
|
|
23347
|
+
if (type) {
|
|
23348
|
+
query.type = type;
|
|
23349
|
+
}
|
|
23350
|
+
if (parent) {
|
|
23351
|
+
try {
|
|
23352
|
+
query.parentId = new ObjectId63(parent);
|
|
23353
|
+
} catch (error) {
|
|
23354
|
+
throw new BadRequestError112("Invalid parentId.");
|
|
23355
|
+
}
|
|
23356
|
+
}
|
|
23357
|
+
const cacheKeyOptions = {
|
|
23358
|
+
page,
|
|
23359
|
+
limit,
|
|
23360
|
+
org,
|
|
23361
|
+
parent,
|
|
23362
|
+
level,
|
|
23363
|
+
type,
|
|
23364
|
+
status: status2,
|
|
23365
|
+
tag: "all"
|
|
23366
|
+
};
|
|
23367
|
+
if (search) {
|
|
23368
|
+
query.$text = { $search: `"${search}"` };
|
|
23369
|
+
cacheKeyOptions.search = search;
|
|
23370
|
+
}
|
|
23371
|
+
const cacheKey = makeCacheKey35(namespace_collection, cacheKeyOptions);
|
|
23372
|
+
logger56.log({
|
|
23373
|
+
level: "info",
|
|
23374
|
+
message: `Cache key for getAll categories: ${cacheKey}`
|
|
23375
|
+
});
|
|
23376
|
+
try {
|
|
23377
|
+
const cached = await repo.getCache(cacheKey);
|
|
23378
|
+
if (cached) {
|
|
23379
|
+
logger56.log({
|
|
23380
|
+
level: "info",
|
|
23381
|
+
message: `Cache hit for getAll categories: ${cacheKey}`
|
|
23382
|
+
});
|
|
23383
|
+
return cached;
|
|
23384
|
+
}
|
|
23385
|
+
const items = await repo.collection.aggregate([
|
|
23386
|
+
{ $match: query },
|
|
23387
|
+
{ $skip: page * limit },
|
|
23388
|
+
{ $limit: limit }
|
|
23389
|
+
]).toArray();
|
|
23390
|
+
const length = await repo.collection.countDocuments(query);
|
|
23391
|
+
const data = paginate31(items, page, limit, length);
|
|
23392
|
+
repo.setCache(cacheKey, data, 600).then(() => {
|
|
23393
|
+
logger56.log({
|
|
23394
|
+
level: "info",
|
|
23395
|
+
message: `Cache set for getAll categories: ${cacheKey}`
|
|
23396
|
+
});
|
|
23397
|
+
}).catch((err) => {
|
|
23398
|
+
logger56.log({
|
|
23399
|
+
level: "error",
|
|
23400
|
+
message: `Failed to set cache for getAll categories: ${err.message}`
|
|
23401
|
+
});
|
|
23402
|
+
});
|
|
23403
|
+
return data;
|
|
23404
|
+
} catch (error) {
|
|
23405
|
+
logger56.log({ level: "error", message: `${error}` });
|
|
23406
|
+
throw new InternalServerError49("Failed to get categories.");
|
|
23407
|
+
}
|
|
23408
|
+
}
|
|
23409
|
+
async function getById(_id) {
|
|
23410
|
+
try {
|
|
23411
|
+
_id = new ObjectId63(_id);
|
|
23412
|
+
} catch {
|
|
23413
|
+
throw new BadRequestError112("Invalid category ID.");
|
|
23414
|
+
}
|
|
23415
|
+
const cacheKey = makeCacheKey35(namespace_collection, {
|
|
23416
|
+
_id: String(_id),
|
|
23417
|
+
tag: "by-id"
|
|
23418
|
+
});
|
|
23419
|
+
try {
|
|
23420
|
+
const cached = await repo.getCache(cacheKey);
|
|
23421
|
+
if (cached)
|
|
23422
|
+
return cached;
|
|
23423
|
+
const result = await repo.collection.findOne({ _id });
|
|
23424
|
+
if (!result) {
|
|
23425
|
+
throw new BadRequestError112("Category node not found.");
|
|
23426
|
+
}
|
|
23427
|
+
repo.setCache(cacheKey, result, 300).catch((err) => {
|
|
23428
|
+
logger56.log({
|
|
23429
|
+
level: "error",
|
|
23430
|
+
message: `Failed to set cache for category by id: ${err.message}`
|
|
23431
|
+
});
|
|
23432
|
+
});
|
|
23433
|
+
return result;
|
|
23434
|
+
} catch (error) {
|
|
23435
|
+
if (error instanceof AppError53)
|
|
23436
|
+
throw error;
|
|
23437
|
+
throw new InternalServerError49("Failed to get category node.");
|
|
23438
|
+
}
|
|
23439
|
+
}
|
|
23440
|
+
async function getByNameParent(name, { parentId = "", orgId = "" } = {}) {
|
|
23441
|
+
const query = {
|
|
23442
|
+
$text: { $search: `"${name.trim()}"` }
|
|
23443
|
+
};
|
|
23444
|
+
if (parentId) {
|
|
23445
|
+
try {
|
|
23446
|
+
query.parentId = new ObjectId63(parentId);
|
|
23447
|
+
} catch (error) {
|
|
23448
|
+
throw new BadRequestError112("Invalid parentId.");
|
|
23449
|
+
}
|
|
23450
|
+
}
|
|
23451
|
+
if (orgId) {
|
|
23452
|
+
try {
|
|
23453
|
+
query.orgId = new ObjectId63(orgId);
|
|
23454
|
+
} catch (error) {
|
|
23455
|
+
throw new BadRequestError112("Invalid orgId.");
|
|
23456
|
+
}
|
|
23457
|
+
}
|
|
23458
|
+
try {
|
|
23459
|
+
const cacheKey = makeCacheKey35(namespace_collection, {
|
|
23460
|
+
name,
|
|
23461
|
+
parentId: parentId ?? "null",
|
|
23462
|
+
orgId: orgId ?? "null",
|
|
23463
|
+
tag: "by-name-parent"
|
|
23464
|
+
});
|
|
23465
|
+
logger56.log({
|
|
23466
|
+
level: "info",
|
|
23467
|
+
message: `Cache key for getByNameParentId: ${cacheKey}`
|
|
23468
|
+
});
|
|
23469
|
+
const cached = await repo.getCache(cacheKey);
|
|
23470
|
+
if (cached) {
|
|
23471
|
+
logger56.log({
|
|
23472
|
+
level: "info",
|
|
23473
|
+
message: `Cache hit for getByNameParentId: ${cacheKey}`
|
|
23474
|
+
});
|
|
23475
|
+
return cached;
|
|
23476
|
+
}
|
|
23477
|
+
const result = await repo.collection.findOne(query);
|
|
23478
|
+
if (result) {
|
|
23479
|
+
repo.setCache(cacheKey, result, 300).catch((err) => {
|
|
23480
|
+
logger56.log({
|
|
23481
|
+
level: "error",
|
|
23482
|
+
message: `Failed to set cache for category by name and parentId: ${err.message}`
|
|
23483
|
+
});
|
|
23484
|
+
});
|
|
23485
|
+
}
|
|
23486
|
+
return result;
|
|
23487
|
+
} catch (error) {
|
|
23488
|
+
console.log(error);
|
|
23489
|
+
throw new InternalServerError49("Failed to get category node by name.");
|
|
23490
|
+
}
|
|
23491
|
+
}
|
|
23492
|
+
async function updateById(_id, update, session) {
|
|
23493
|
+
try {
|
|
23494
|
+
_id = new ObjectId63(_id);
|
|
23495
|
+
} catch {
|
|
23496
|
+
throw new BadRequestError112("Invalid category ID.");
|
|
23497
|
+
}
|
|
23498
|
+
try {
|
|
23499
|
+
const res = await repo.collection.updateOne(
|
|
23500
|
+
{ _id },
|
|
23501
|
+
{ $set: update },
|
|
23502
|
+
{ session }
|
|
23503
|
+
);
|
|
23504
|
+
if (res.matchedCount === 0) {
|
|
23505
|
+
throw new BadRequestError112("Category node not found.");
|
|
23506
|
+
}
|
|
23507
|
+
repo.delCachedData();
|
|
23508
|
+
} catch (error) {
|
|
23509
|
+
if (error instanceof AppError53)
|
|
23510
|
+
throw error;
|
|
23511
|
+
logger56.log({ level: "error", message: error.message });
|
|
23512
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
23513
|
+
if (isDuplicated) {
|
|
23514
|
+
throw new BadRequestError112(
|
|
23515
|
+
"Category name already exists under this parent."
|
|
23516
|
+
);
|
|
23517
|
+
}
|
|
23518
|
+
throw new InternalServerError49("Failed to update category node.");
|
|
23519
|
+
}
|
|
23520
|
+
}
|
|
23521
|
+
async function hasChildren(_id) {
|
|
23522
|
+
try {
|
|
23523
|
+
_id = new ObjectId63(_id);
|
|
23524
|
+
} catch {
|
|
23525
|
+
throw new BadRequestError112("Invalid category ID.");
|
|
23526
|
+
}
|
|
23527
|
+
try {
|
|
23528
|
+
const count = await repo.collection.countDocuments({ parentId: _id });
|
|
23529
|
+
return count > 0;
|
|
23530
|
+
} catch (error) {
|
|
23531
|
+
throw new InternalServerError49("Failed to check for child categories.");
|
|
23532
|
+
}
|
|
23533
|
+
}
|
|
23534
|
+
async function softDeleteById(_id, session) {
|
|
23535
|
+
try {
|
|
23536
|
+
_id = new ObjectId63(_id);
|
|
23537
|
+
} catch {
|
|
23538
|
+
throw new BadRequestError112("Invalid category ID.");
|
|
23539
|
+
}
|
|
23540
|
+
try {
|
|
23541
|
+
const res = await repo.collection.updateOne(
|
|
23542
|
+
{ _id },
|
|
23543
|
+
{ $set: { status: "deleted", updatedAt: /* @__PURE__ */ new Date() } },
|
|
23544
|
+
{ session }
|
|
23545
|
+
);
|
|
23546
|
+
if (res.matchedCount === 0) {
|
|
23547
|
+
throw new BadRequestError112("Category node not found.");
|
|
23548
|
+
}
|
|
23549
|
+
repo.delCachedData();
|
|
23550
|
+
} catch (error) {
|
|
23551
|
+
if (error instanceof AppError53)
|
|
23552
|
+
throw error;
|
|
23553
|
+
throw new InternalServerError49("Failed to delete category node.");
|
|
23554
|
+
}
|
|
23555
|
+
}
|
|
23556
|
+
return {
|
|
23557
|
+
createIndexes,
|
|
23558
|
+
add,
|
|
23559
|
+
getAll,
|
|
23560
|
+
getById,
|
|
23561
|
+
getByNameParent,
|
|
23562
|
+
updateById,
|
|
23563
|
+
hasChildren,
|
|
23564
|
+
softDeleteById
|
|
23565
|
+
};
|
|
23566
|
+
}
|
|
23567
|
+
|
|
23568
|
+
// src/resources/asset-item/asset.item.service.ts
|
|
23569
|
+
function normalizeTagName(name) {
|
|
23570
|
+
return name.trim().toLowerCase().replace(/\s+/g, "-");
|
|
23571
|
+
}
|
|
23572
|
+
function useAssetItemService() {
|
|
23573
|
+
const assetItemRepo = useAssetItemRepo();
|
|
23574
|
+
const tagRepo = useTagRepo();
|
|
23575
|
+
const categoryRepo = useCategoryRepo();
|
|
23576
|
+
async function resolveTags(tagNames, orgId, categoryPath, session) {
|
|
23577
|
+
const resolvedTags = [];
|
|
23578
|
+
for (const tag of tagNames) {
|
|
23579
|
+
const normalizedName = normalizeTagName(tag.name);
|
|
23580
|
+
const existing = await tagRepo.findByNormalizedNameAndOrg(
|
|
23581
|
+
normalizedName,
|
|
23582
|
+
orgId
|
|
23583
|
+
);
|
|
23584
|
+
if (existing && existing._id) {
|
|
23585
|
+
await tagRepo.incrementUsageCount(existing._id, 1, session);
|
|
23586
|
+
resolvedTags.push({
|
|
23587
|
+
id: String(existing._id),
|
|
23588
|
+
name: existing.name
|
|
23589
|
+
});
|
|
23590
|
+
} else {
|
|
23591
|
+
const tagData = {
|
|
23592
|
+
name: tag.name.trim(),
|
|
23593
|
+
normalizedName,
|
|
23594
|
+
type: "private",
|
|
23595
|
+
orgId,
|
|
23596
|
+
categoryPath,
|
|
23597
|
+
status: "active",
|
|
23598
|
+
usageCount: 1
|
|
23599
|
+
};
|
|
23600
|
+
const tagId = await tagRepo.add(tagData, session);
|
|
23601
|
+
resolvedTags.push({
|
|
23602
|
+
id: String(tagId),
|
|
23603
|
+
name: tag.name.trim()
|
|
23604
|
+
});
|
|
23605
|
+
}
|
|
23606
|
+
}
|
|
23607
|
+
return resolvedTags;
|
|
23608
|
+
}
|
|
23609
|
+
async function add(value) {
|
|
23610
|
+
const session = useAtlas17.getClient()?.startSession();
|
|
23611
|
+
if (!session) {
|
|
23612
|
+
throw new InternalServerError50(
|
|
23613
|
+
"Unable to start session for asset item service."
|
|
23614
|
+
);
|
|
23615
|
+
}
|
|
23616
|
+
try {
|
|
23617
|
+
await session.startTransaction();
|
|
23618
|
+
const department = await categoryRepo.getById(value.departmentId);
|
|
23619
|
+
value.departmentName = department.displayName;
|
|
23620
|
+
if (value.categoryId) {
|
|
23621
|
+
const category = await categoryRepo.getById(value.categoryId);
|
|
23622
|
+
value.categoryName = category.displayName;
|
|
23623
|
+
}
|
|
23624
|
+
if (value.subcategoryId) {
|
|
23625
|
+
const subcategory = await categoryRepo.getById(value.subcategoryId);
|
|
23626
|
+
value.subcategoryName = subcategory.displayName;
|
|
23627
|
+
}
|
|
23628
|
+
const categoryPath = buildCategoryPath(
|
|
23629
|
+
value.departmentName,
|
|
23630
|
+
value.categoryName ?? "",
|
|
23631
|
+
value.subcategoryName ?? ""
|
|
23632
|
+
);
|
|
23633
|
+
value.categoryPath = categoryPath;
|
|
23634
|
+
const resolvedTags = value.tags.length ? await resolveTags(
|
|
23635
|
+
value.tags,
|
|
23636
|
+
value.orgId,
|
|
23637
|
+
value.categoryPath,
|
|
23638
|
+
session
|
|
23639
|
+
) : [];
|
|
23640
|
+
const assetItem = {
|
|
23641
|
+
...value,
|
|
23642
|
+
tags: resolvedTags
|
|
23643
|
+
};
|
|
23644
|
+
const assetItemId = await assetItemRepo.add(assetItem, session);
|
|
23645
|
+
await session.commitTransaction();
|
|
23646
|
+
return assetItemId;
|
|
23647
|
+
} catch (error) {
|
|
23648
|
+
await session.abortTransaction();
|
|
23649
|
+
if (error instanceof AppError54)
|
|
23650
|
+
throw error;
|
|
23651
|
+
throw new InternalServerError50("Failed to create asset item.");
|
|
23652
|
+
} finally {
|
|
23653
|
+
session.endSession();
|
|
23654
|
+
}
|
|
23655
|
+
}
|
|
23656
|
+
return {
|
|
23657
|
+
add
|
|
23658
|
+
};
|
|
23659
|
+
}
|
|
23660
|
+
|
|
23661
|
+
// src/resources/asset-item/asset.item.controller.ts
|
|
23662
|
+
import Joi100 from "joi";
|
|
23663
|
+
import { BadRequestError as BadRequestError114 } from "@goweekdays/utils";
|
|
23664
|
+
var paginationSchema3 = Joi100.object({
|
|
23665
|
+
page: Joi100.number().min(1).optional().allow("", null),
|
|
23666
|
+
limit: Joi100.number().min(1).optional().allow("", null),
|
|
23667
|
+
search: Joi100.string().optional().allow("", null),
|
|
23668
|
+
status: Joi100.string().valid(...assetItemStatuses).optional().allow("", null),
|
|
23669
|
+
assetCategory: Joi100.string().valid(...assetItemCategories).optional().allow("", null),
|
|
23670
|
+
trackingType: Joi100.string().valid(...assetItemTrackingTypes).optional().allow("", null),
|
|
23671
|
+
purpose: Joi100.string().valid(...assetItemPurposes).optional().allow("", null),
|
|
23672
|
+
departmentId: Joi100.string().optional().allow("", null),
|
|
23673
|
+
categoryId: Joi100.string().optional().allow("", null),
|
|
23674
|
+
subcategoryId: Joi100.string().optional().allow("", null),
|
|
23675
|
+
brand: Joi100.string().optional().allow("", null),
|
|
23676
|
+
tags: Joi100.string().optional().allow("", null)
|
|
23677
|
+
});
|
|
23678
|
+
var idParamSchema2 = Joi100.object({
|
|
23679
|
+
id: Joi100.string().hex().length(24).required()
|
|
23680
|
+
});
|
|
23681
|
+
function useAssetItemController() {
|
|
23682
|
+
const repo = useAssetItemRepo();
|
|
23683
|
+
const service = useAssetItemService();
|
|
23684
|
+
async function add(req, res, next) {
|
|
23685
|
+
try {
|
|
23686
|
+
const { error, value } = schemaAssetItemCreate.validate(req.body);
|
|
23687
|
+
if (error) {
|
|
23688
|
+
next(new BadRequestError114(error.message));
|
|
23689
|
+
return;
|
|
23690
|
+
}
|
|
23691
|
+
const assetItemId = await service.add(value);
|
|
23692
|
+
res.status(201).json({
|
|
23693
|
+
message: "Asset item created successfully.",
|
|
23694
|
+
assetItemId
|
|
23695
|
+
});
|
|
23696
|
+
} catch (error) {
|
|
23697
|
+
next(error);
|
|
23698
|
+
}
|
|
23699
|
+
}
|
|
23700
|
+
async function getAll(req, res, next) {
|
|
23701
|
+
const { error } = paginationSchema3.validate(req.query);
|
|
23702
|
+
if (error) {
|
|
23703
|
+
next(new BadRequestError114(error.message));
|
|
23704
|
+
return;
|
|
23705
|
+
}
|
|
23706
|
+
const page = typeof req.query.page === "string" ? Number(req.query.page) : 1;
|
|
23707
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : 10;
|
|
23708
|
+
const search = req.query.search ?? "";
|
|
23709
|
+
const status2 = req.query.status ?? "active";
|
|
23710
|
+
const assetCategory = req.query.assetCategory ?? "";
|
|
23711
|
+
const trackingType = req.query.trackingType ?? "";
|
|
23712
|
+
const purpose = req.query.purpose ?? "";
|
|
23713
|
+
const departmentId = req.query.departmentId ?? "";
|
|
23714
|
+
const categoryId = req.query.categoryId ?? "";
|
|
23715
|
+
const subcategoryId = req.query.subcategoryId ?? "";
|
|
23716
|
+
const brand = req.query.brand ?? "";
|
|
23717
|
+
const tagsParam = req.query.tags ?? "";
|
|
23718
|
+
const tags = tagsParam ? tagsParam.split(",").filter(Boolean) : [];
|
|
23719
|
+
if (!isFinite(page)) {
|
|
23720
|
+
next(new BadRequestError114("Invalid page number."));
|
|
23721
|
+
return;
|
|
23722
|
+
}
|
|
23723
|
+
if (!isFinite(limit)) {
|
|
23724
|
+
next(new BadRequestError114("Invalid limit number."));
|
|
23725
|
+
return;
|
|
23726
|
+
}
|
|
23727
|
+
try {
|
|
23728
|
+
const results = await repo.getAll({
|
|
23729
|
+
page,
|
|
23730
|
+
limit,
|
|
23731
|
+
search,
|
|
23732
|
+
status: status2,
|
|
23733
|
+
assetCategory,
|
|
23734
|
+
trackingType,
|
|
23735
|
+
purpose,
|
|
23736
|
+
departmentId,
|
|
23737
|
+
categoryId,
|
|
23738
|
+
subcategoryId,
|
|
23739
|
+
brand,
|
|
23740
|
+
tags
|
|
23741
|
+
});
|
|
23742
|
+
res.json(results);
|
|
23743
|
+
} catch (error2) {
|
|
23744
|
+
next(error2);
|
|
23745
|
+
}
|
|
23746
|
+
}
|
|
23747
|
+
async function getById(req, res, next) {
|
|
23748
|
+
const { error } = idParamSchema2.validate(req.params);
|
|
23749
|
+
if (error) {
|
|
23750
|
+
next(new BadRequestError114(error.message));
|
|
23751
|
+
return;
|
|
23752
|
+
}
|
|
23753
|
+
try {
|
|
23754
|
+
const assetItem = await repo.getById(req.params.id);
|
|
23755
|
+
res.json(assetItem);
|
|
23756
|
+
} catch (error2) {
|
|
23757
|
+
next(error2);
|
|
23758
|
+
}
|
|
23759
|
+
}
|
|
23760
|
+
async function updateById(req, res, next) {
|
|
23761
|
+
const { error: paramError } = idParamSchema2.validate(req.params);
|
|
23762
|
+
if (paramError) {
|
|
23763
|
+
next(new BadRequestError114(paramError.message));
|
|
23764
|
+
return;
|
|
23765
|
+
}
|
|
23766
|
+
const { error: bodyError } = schemaAssetItemUpdate.validate(req.body);
|
|
23767
|
+
if (bodyError) {
|
|
23768
|
+
next(new BadRequestError114(bodyError.message));
|
|
23769
|
+
return;
|
|
23770
|
+
}
|
|
23771
|
+
try {
|
|
23772
|
+
const updatedAssetItem = await repo.updateById(req.params.id, req.body);
|
|
23773
|
+
res.json({
|
|
23774
|
+
message: "Asset item updated successfully.",
|
|
23775
|
+
assetItem: updatedAssetItem
|
|
23776
|
+
});
|
|
23777
|
+
} catch (error) {
|
|
23778
|
+
next(error);
|
|
23779
|
+
}
|
|
23780
|
+
}
|
|
23781
|
+
async function deleteById(req, res, next) {
|
|
23782
|
+
const { error } = idParamSchema2.validate(req.params);
|
|
23783
|
+
if (error) {
|
|
23784
|
+
next(new BadRequestError114(error.message));
|
|
23785
|
+
return;
|
|
23786
|
+
}
|
|
23787
|
+
try {
|
|
23788
|
+
await repo.deleteById(req.params.id);
|
|
23789
|
+
res.json({
|
|
23790
|
+
message: "Asset item deleted successfully."
|
|
23791
|
+
});
|
|
23792
|
+
} catch (error2) {
|
|
23793
|
+
next(error2);
|
|
23794
|
+
}
|
|
23795
|
+
}
|
|
23796
|
+
return {
|
|
23797
|
+
add,
|
|
23798
|
+
getAll,
|
|
23799
|
+
getById,
|
|
23800
|
+
updateById,
|
|
23801
|
+
deleteById
|
|
23802
|
+
};
|
|
23803
|
+
}
|
|
23804
|
+
|
|
23805
|
+
// src/resources/asset-item-unit/asset.unit.model.ts
|
|
23806
|
+
import Joi101 from "joi";
|
|
23807
|
+
import { ObjectId as ObjectId64 } from "mongodb";
|
|
23808
|
+
var assetUnitStatuses = [
|
|
23809
|
+
"available",
|
|
23810
|
+
"assigned",
|
|
23811
|
+
"maintenance",
|
|
23812
|
+
"disposed"
|
|
23813
|
+
];
|
|
23814
|
+
var schemaAssetUnitBase = {
|
|
23815
|
+
itemId: Joi101.string().hex().length(24).required(),
|
|
23816
|
+
serialNumber: Joi101.string().optional().allow("", null),
|
|
23817
|
+
plateNumber: Joi101.string().optional().allow("", null),
|
|
23818
|
+
status: Joi101.string().valid(...assetUnitStatuses).required(),
|
|
23819
|
+
locationId: Joi101.string().optional().allow("", null),
|
|
23820
|
+
assignedTo: Joi101.string().optional().allow("", null)
|
|
23821
|
+
};
|
|
23822
|
+
var schemaAssetUnit = Joi101.object(schemaAssetUnitBase);
|
|
23823
|
+
var schemaAssetUnitUpdate = Joi101.object({
|
|
23824
|
+
serialNumber: Joi101.string().optional().allow("", null),
|
|
23825
|
+
plateNumber: Joi101.string().optional().allow("", null),
|
|
23826
|
+
status: Joi101.string().valid(...assetUnitStatuses).optional(),
|
|
23827
|
+
locationId: Joi101.string().optional().allow("", null),
|
|
23828
|
+
assignedTo: Joi101.string().optional().allow("", null)
|
|
23829
|
+
});
|
|
23830
|
+
function modelAssetUnit(data) {
|
|
23831
|
+
const { error } = schemaAssetUnit.validate(data);
|
|
23832
|
+
if (error) {
|
|
23833
|
+
throw new Error(`Invalid asset unit data: ${error.message}`);
|
|
23834
|
+
}
|
|
23835
|
+
if (data.itemId && typeof data.itemId === "string") {
|
|
23836
|
+
try {
|
|
23837
|
+
data.itemId = new ObjectId64(data.itemId);
|
|
23838
|
+
} catch {
|
|
23839
|
+
throw new Error(`Invalid itemId format: ${data.itemId}`);
|
|
23840
|
+
}
|
|
23841
|
+
}
|
|
23842
|
+
return {
|
|
23843
|
+
_id: data._id,
|
|
23844
|
+
itemId: data.itemId,
|
|
23845
|
+
serialNumber: data.serialNumber ?? "",
|
|
23846
|
+
plateNumber: data.plateNumber ?? "",
|
|
23847
|
+
status: data.status,
|
|
23848
|
+
locationId: data.locationId ?? "",
|
|
23849
|
+
assignedTo: data.assignedTo ?? "",
|
|
23850
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
|
|
23851
|
+
updatedAt: data.updatedAt ?? ""
|
|
23852
|
+
};
|
|
23853
|
+
}
|
|
23854
|
+
|
|
23855
|
+
// src/resources/asset-item-unit/asset.unit.repository.ts
|
|
23856
|
+
import {
|
|
23857
|
+
AppError as AppError55,
|
|
23858
|
+
BadRequestError as BadRequestError115,
|
|
23859
|
+
InternalServerError as InternalServerError51,
|
|
23860
|
+
logger as logger57,
|
|
23861
|
+
makeCacheKey as makeCacheKey36,
|
|
23862
|
+
paginate as paginate32,
|
|
23863
|
+
useRepo as useRepo35
|
|
23864
|
+
} from "@goweekdays/utils";
|
|
23865
|
+
import { ObjectId as ObjectId65 } from "mongodb";
|
|
23866
|
+
function useAssetUnitRepo() {
|
|
23867
|
+
const namespace_collection = "asset.units";
|
|
23868
|
+
const repo = useRepo35(namespace_collection);
|
|
23869
|
+
async function createIndexes() {
|
|
23870
|
+
try {
|
|
23871
|
+
await repo.collection.createIndexes([
|
|
23872
|
+
{ key: { itemId: 1 } },
|
|
23873
|
+
{ key: { status: 1 } },
|
|
23874
|
+
{ key: { locationId: 1 } },
|
|
23875
|
+
{ key: { itemId: 1, status: 1 } }
|
|
23876
|
+
]);
|
|
23877
|
+
} catch (error) {
|
|
23878
|
+
throw new BadRequestError115("Failed to create asset unit indexes.");
|
|
23879
|
+
}
|
|
23880
|
+
}
|
|
23881
|
+
async function add(value, session) {
|
|
23882
|
+
try {
|
|
23883
|
+
value = modelAssetUnit(value);
|
|
23884
|
+
const res = await repo.collection.insertOne(value, { session });
|
|
23885
|
+
repo.delCachedData();
|
|
23886
|
+
return res.insertedId;
|
|
23887
|
+
} catch (error) {
|
|
23888
|
+
logger57.log({ level: "error", message: error.message });
|
|
23889
|
+
throw new InternalServerError51("Failed to create asset unit.");
|
|
23890
|
+
}
|
|
23891
|
+
}
|
|
23892
|
+
async function getByItemId({
|
|
23893
|
+
itemId,
|
|
23894
|
+
status: status2 = "",
|
|
23895
|
+
page = 1,
|
|
23896
|
+
limit = 10
|
|
23897
|
+
}) {
|
|
23898
|
+
page = page > 0 ? page - 1 : 0;
|
|
23899
|
+
const query = {
|
|
23900
|
+
itemId: new ObjectId65(itemId)
|
|
23901
|
+
};
|
|
23902
|
+
if (status2) {
|
|
23903
|
+
query.status = status2;
|
|
23904
|
+
}
|
|
23905
|
+
const cacheKey = makeCacheKey36(namespace_collection, {
|
|
23906
|
+
itemId,
|
|
23907
|
+
status: status2,
|
|
23908
|
+
page,
|
|
23909
|
+
limit,
|
|
23910
|
+
tag: "byItemId"
|
|
23911
|
+
});
|
|
23912
|
+
try {
|
|
23913
|
+
const cached = await repo.getCache(cacheKey);
|
|
23914
|
+
if (cached)
|
|
23915
|
+
return cached;
|
|
23916
|
+
const items = await repo.collection.aggregate([
|
|
23917
|
+
{ $match: query },
|
|
23918
|
+
{ $sort: { _id: -1 } },
|
|
23919
|
+
{ $skip: page * limit },
|
|
23920
|
+
{ $limit: limit }
|
|
23921
|
+
]).toArray();
|
|
23922
|
+
const length = await repo.collection.countDocuments(query);
|
|
23923
|
+
const data = paginate32(items, page, limit, length);
|
|
23924
|
+
repo.setCache(cacheKey, data, 600);
|
|
23925
|
+
return data;
|
|
23926
|
+
} catch (error) {
|
|
23927
|
+
logger57.log({ level: "error", message: `${error}` });
|
|
23928
|
+
throw error;
|
|
23929
|
+
}
|
|
23930
|
+
}
|
|
23931
|
+
async function getById(id) {
|
|
23932
|
+
const cacheKey = makeCacheKey36(namespace_collection, {
|
|
23933
|
+
id,
|
|
23934
|
+
tag: "by-id"
|
|
23935
|
+
});
|
|
23936
|
+
try {
|
|
23937
|
+
const cached = await repo.getCache(cacheKey);
|
|
23938
|
+
if (cached)
|
|
23939
|
+
return cached;
|
|
23940
|
+
const result = await repo.collection.findOne({
|
|
23941
|
+
_id: new ObjectId65(id)
|
|
23942
|
+
});
|
|
23943
|
+
if (!result) {
|
|
23944
|
+
throw new BadRequestError115("Asset unit not found.");
|
|
23945
|
+
}
|
|
23946
|
+
repo.setCache(cacheKey, result, 300);
|
|
23947
|
+
return result;
|
|
23948
|
+
} catch (error) {
|
|
23949
|
+
if (error instanceof AppError55)
|
|
23950
|
+
throw error;
|
|
23951
|
+
throw new InternalServerError51("Failed to get asset unit.");
|
|
23952
|
+
}
|
|
23953
|
+
}
|
|
23954
|
+
async function updateById(id, value, session) {
|
|
23955
|
+
try {
|
|
23956
|
+
const result = await repo.collection.findOneAndUpdate(
|
|
23957
|
+
{ _id: new ObjectId65(id) },
|
|
23958
|
+
{ $set: { ...value, updatedAt: /* @__PURE__ */ new Date() } },
|
|
23959
|
+
{ returnDocument: "after", session }
|
|
23960
|
+
);
|
|
23961
|
+
if (!result) {
|
|
23962
|
+
throw new BadRequestError115("Asset unit not found.");
|
|
23963
|
+
}
|
|
23964
|
+
repo.delCachedData();
|
|
23965
|
+
return result;
|
|
23966
|
+
} catch (error) {
|
|
23967
|
+
if (error instanceof AppError55)
|
|
23968
|
+
throw error;
|
|
23969
|
+
throw new InternalServerError51("Failed to update asset unit.");
|
|
23970
|
+
}
|
|
23971
|
+
}
|
|
23972
|
+
async function countByItemIdAndStatus(itemId, status2) {
|
|
23973
|
+
try {
|
|
23974
|
+
return await repo.collection.countDocuments({
|
|
23975
|
+
itemId: new ObjectId65(itemId),
|
|
23976
|
+
status: status2
|
|
23977
|
+
});
|
|
23978
|
+
} catch (error) {
|
|
23979
|
+
throw new InternalServerError51("Failed to count asset units.");
|
|
23980
|
+
}
|
|
23981
|
+
}
|
|
23982
|
+
return {
|
|
23983
|
+
createIndexes,
|
|
23984
|
+
add,
|
|
23985
|
+
getByItemId,
|
|
23986
|
+
getById,
|
|
23987
|
+
updateById,
|
|
23988
|
+
countByItemIdAndStatus
|
|
23989
|
+
};
|
|
23990
|
+
}
|
|
23991
|
+
|
|
23992
|
+
// src/resources/asset-item-unit/asset.unit.controller.ts
|
|
23993
|
+
import Joi102 from "joi";
|
|
23994
|
+
import { BadRequestError as BadRequestError116 } from "@goweekdays/utils";
|
|
23995
|
+
var querySchema = Joi102.object({
|
|
23996
|
+
itemId: Joi102.string().hex().length(24).required(),
|
|
23997
|
+
status: Joi102.string().valid(...assetUnitStatuses).optional().allow("", null),
|
|
23998
|
+
page: Joi102.number().min(1).optional().allow("", null),
|
|
23999
|
+
limit: Joi102.number().min(1).optional().allow("", null)
|
|
24000
|
+
});
|
|
24001
|
+
var idParamSchema3 = Joi102.object({
|
|
24002
|
+
id: Joi102.string().hex().length(24).required()
|
|
24003
|
+
});
|
|
24004
|
+
function useAssetUnitController() {
|
|
24005
|
+
const repo = useAssetUnitRepo();
|
|
24006
|
+
async function add(req, res, next) {
|
|
24007
|
+
try {
|
|
24008
|
+
const { error } = schemaAssetUnit.validate(req.body);
|
|
24009
|
+
if (error) {
|
|
24010
|
+
next(new BadRequestError116(error.message));
|
|
24011
|
+
return;
|
|
24012
|
+
}
|
|
24013
|
+
const unitId = await repo.add(req.body);
|
|
24014
|
+
res.status(201).json({
|
|
24015
|
+
message: "Asset unit created successfully.",
|
|
24016
|
+
unitId
|
|
24017
|
+
});
|
|
24018
|
+
} catch (error) {
|
|
24019
|
+
next(error);
|
|
24020
|
+
}
|
|
24021
|
+
}
|
|
24022
|
+
async function getByItemId(req, res, next) {
|
|
24023
|
+
const { error } = querySchema.validate(req.query);
|
|
24024
|
+
if (error) {
|
|
24025
|
+
next(new BadRequestError116(error.message));
|
|
24026
|
+
return;
|
|
24027
|
+
}
|
|
24028
|
+
const itemId = req.query.itemId;
|
|
24029
|
+
const status2 = req.query.status ?? "";
|
|
24030
|
+
const page = typeof req.query.page === "string" ? Number(req.query.page) : 1;
|
|
24031
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : 10;
|
|
24032
|
+
try {
|
|
24033
|
+
const results = await repo.getByItemId({ itemId, status: status2, page, limit });
|
|
24034
|
+
res.json(results);
|
|
24035
|
+
} catch (error2) {
|
|
24036
|
+
next(error2);
|
|
24037
|
+
}
|
|
24038
|
+
}
|
|
24039
|
+
async function getById(req, res, next) {
|
|
24040
|
+
const { error } = idParamSchema3.validate(req.params);
|
|
24041
|
+
if (error) {
|
|
24042
|
+
next(new BadRequestError116(error.message));
|
|
24043
|
+
return;
|
|
24044
|
+
}
|
|
24045
|
+
try {
|
|
24046
|
+
const unit = await repo.getById(req.params.id);
|
|
24047
|
+
res.json(unit);
|
|
24048
|
+
} catch (error2) {
|
|
24049
|
+
next(error2);
|
|
24050
|
+
}
|
|
24051
|
+
}
|
|
24052
|
+
async function updateById(req, res, next) {
|
|
24053
|
+
const { error: paramError } = idParamSchema3.validate(req.params);
|
|
24054
|
+
if (paramError) {
|
|
24055
|
+
next(new BadRequestError116(paramError.message));
|
|
24056
|
+
return;
|
|
24057
|
+
}
|
|
24058
|
+
const { error: bodyError } = schemaAssetUnitUpdate.validate(req.body);
|
|
24059
|
+
if (bodyError) {
|
|
24060
|
+
next(new BadRequestError116(bodyError.message));
|
|
24061
|
+
return;
|
|
24062
|
+
}
|
|
24063
|
+
try {
|
|
24064
|
+
const updatedUnit = await repo.updateById(req.params.id, req.body);
|
|
24065
|
+
res.json({
|
|
24066
|
+
message: "Asset unit updated successfully.",
|
|
24067
|
+
unit: updatedUnit
|
|
24068
|
+
});
|
|
24069
|
+
} catch (error) {
|
|
24070
|
+
next(error);
|
|
24071
|
+
}
|
|
24072
|
+
}
|
|
24073
|
+
return {
|
|
24074
|
+
add,
|
|
24075
|
+
getByItemId,
|
|
24076
|
+
getById,
|
|
24077
|
+
updateById
|
|
24078
|
+
};
|
|
24079
|
+
}
|
|
24080
|
+
|
|
24081
|
+
// src/resources/asset-item-movement/stock.movement.model.ts
|
|
24082
|
+
import Joi103 from "joi";
|
|
24083
|
+
import { ObjectId as ObjectId66 } from "mongodb";
|
|
24084
|
+
var stockMovementTypes = [
|
|
24085
|
+
"in",
|
|
24086
|
+
"out",
|
|
24087
|
+
"transfer",
|
|
24088
|
+
"adjustment",
|
|
24089
|
+
"conversion"
|
|
24090
|
+
];
|
|
24091
|
+
var stockMovementReferenceTypes = [
|
|
24092
|
+
"purchase",
|
|
24093
|
+
"sale",
|
|
24094
|
+
"transfer",
|
|
24095
|
+
"manual",
|
|
24096
|
+
"conversion"
|
|
24097
|
+
];
|
|
24098
|
+
var schemaStockMovement = Joi103.object({
|
|
24099
|
+
itemId: Joi103.string().hex().length(24).required(),
|
|
24100
|
+
type: Joi103.string().valid(...stockMovementTypes).required(),
|
|
24101
|
+
quantity: Joi103.number().positive().required(),
|
|
24102
|
+
unitCost: Joi103.number().min(0).optional().allow(null),
|
|
24103
|
+
totalCost: Joi103.number().min(0).optional().allow(null),
|
|
24104
|
+
reference: Joi103.object({
|
|
24105
|
+
type: Joi103.string().valid(...stockMovementReferenceTypes).required(),
|
|
24106
|
+
id: Joi103.string().optional().allow("", null)
|
|
24107
|
+
}).optional().allow(null),
|
|
24108
|
+
fromLocationId: Joi103.string().optional().allow("", null),
|
|
24109
|
+
toLocationId: Joi103.string().optional().allow("", null),
|
|
24110
|
+
fromItemId: Joi103.string().hex().length(24).optional().allow("", null),
|
|
24111
|
+
toItemId: Joi103.string().hex().length(24).optional().allow("", null),
|
|
24112
|
+
reason: Joi103.string().optional().allow("", null)
|
|
24113
|
+
});
|
|
24114
|
+
function modelStockMovement(data) {
|
|
24115
|
+
const { error } = schemaStockMovement.validate(data);
|
|
24116
|
+
if (error) {
|
|
24117
|
+
throw new Error(`Invalid stock movement data: ${error.message}`);
|
|
24118
|
+
}
|
|
24119
|
+
if (data.itemId && typeof data.itemId === "string") {
|
|
24120
|
+
try {
|
|
24121
|
+
data.itemId = new ObjectId66(data.itemId);
|
|
24122
|
+
} catch {
|
|
24123
|
+
throw new Error(`Invalid itemId format: ${data.itemId}`);
|
|
24124
|
+
}
|
|
24125
|
+
}
|
|
24126
|
+
return {
|
|
24127
|
+
_id: data._id,
|
|
24128
|
+
itemId: data.itemId,
|
|
24129
|
+
type: data.type,
|
|
24130
|
+
quantity: data.quantity,
|
|
24131
|
+
unitCost: data.unitCost ?? 0,
|
|
24132
|
+
totalCost: data.totalCost ?? 0,
|
|
24133
|
+
reference: data.reference ?? void 0,
|
|
24134
|
+
fromLocationId: data.fromLocationId ?? "",
|
|
24135
|
+
toLocationId: data.toLocationId ?? "",
|
|
24136
|
+
fromItemId: data.fromItemId ?? "",
|
|
24137
|
+
toItemId: data.toItemId ?? "",
|
|
24138
|
+
reason: data.reason ?? "",
|
|
24139
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date()
|
|
24140
|
+
};
|
|
24141
|
+
}
|
|
24142
|
+
|
|
24143
|
+
// src/resources/asset-item-movement/stock.movement.repository.ts
|
|
24144
|
+
import {
|
|
24145
|
+
AppError as AppError56,
|
|
24146
|
+
BadRequestError as BadRequestError117,
|
|
24147
|
+
InternalServerError as InternalServerError52,
|
|
24148
|
+
logger as logger58,
|
|
24149
|
+
makeCacheKey as makeCacheKey37,
|
|
24150
|
+
paginate as paginate33,
|
|
24151
|
+
useRepo as useRepo36
|
|
24152
|
+
} from "@goweekdays/utils";
|
|
24153
|
+
import { ObjectId as ObjectId67 } from "mongodb";
|
|
24154
|
+
function useStockMovementRepo() {
|
|
24155
|
+
const namespace_collection = "stock.movements";
|
|
24156
|
+
const repo = useRepo36(namespace_collection);
|
|
24157
|
+
async function createIndexes() {
|
|
24158
|
+
try {
|
|
24159
|
+
await repo.collection.createIndexes([
|
|
24160
|
+
{ key: { itemId: 1 } },
|
|
24161
|
+
{ key: { createdAt: -1 } },
|
|
24162
|
+
{ key: { itemId: 1, createdAt: -1 } },
|
|
24163
|
+
{ key: { type: 1 } }
|
|
24164
|
+
]);
|
|
24165
|
+
} catch (error) {
|
|
24166
|
+
throw new BadRequestError117("Failed to create stock movement indexes.");
|
|
24167
|
+
}
|
|
24168
|
+
}
|
|
24169
|
+
async function add(value, session) {
|
|
24170
|
+
try {
|
|
24171
|
+
value = modelStockMovement(value);
|
|
24172
|
+
const res = await repo.collection.insertOne(value, { session });
|
|
24173
|
+
repo.delCachedData();
|
|
24174
|
+
return res.insertedId;
|
|
24175
|
+
} catch (error) {
|
|
24176
|
+
logger58.log({ level: "error", message: error.message });
|
|
24177
|
+
throw new InternalServerError52("Failed to create stock movement.");
|
|
24178
|
+
}
|
|
24179
|
+
}
|
|
24180
|
+
async function getByItemId({
|
|
24181
|
+
itemId,
|
|
24182
|
+
type = "",
|
|
24183
|
+
page = 1,
|
|
24184
|
+
limit = 10
|
|
24185
|
+
}) {
|
|
24186
|
+
page = page > 0 ? page - 1 : 0;
|
|
24187
|
+
const query = {
|
|
24188
|
+
itemId: new ObjectId67(itemId)
|
|
24189
|
+
};
|
|
24190
|
+
if (type) {
|
|
24191
|
+
query.type = type;
|
|
24192
|
+
}
|
|
24193
|
+
const cacheKey = makeCacheKey37(namespace_collection, {
|
|
24194
|
+
itemId,
|
|
24195
|
+
type,
|
|
24196
|
+
page,
|
|
24197
|
+
limit,
|
|
24198
|
+
tag: "byItemId"
|
|
24199
|
+
});
|
|
24200
|
+
try {
|
|
24201
|
+
const cached = await repo.getCache(cacheKey);
|
|
24202
|
+
if (cached)
|
|
24203
|
+
return cached;
|
|
24204
|
+
const items = await repo.collection.aggregate([
|
|
24205
|
+
{ $match: query },
|
|
24206
|
+
{ $sort: { createdAt: -1 } },
|
|
24207
|
+
{ $skip: page * limit },
|
|
24208
|
+
{ $limit: limit }
|
|
24209
|
+
]).toArray();
|
|
24210
|
+
const length = await repo.collection.countDocuments(query);
|
|
24211
|
+
const data = paginate33(items, page, limit, length);
|
|
24212
|
+
repo.setCache(cacheKey, data, 600);
|
|
24213
|
+
return data;
|
|
24214
|
+
} catch (error) {
|
|
24215
|
+
logger58.log({ level: "error", message: `${error}` });
|
|
24216
|
+
throw error;
|
|
24217
|
+
}
|
|
24218
|
+
}
|
|
24219
|
+
async function getById(id) {
|
|
24220
|
+
const cacheKey = makeCacheKey37(namespace_collection, {
|
|
24221
|
+
id,
|
|
24222
|
+
tag: "by-id"
|
|
24223
|
+
});
|
|
24224
|
+
try {
|
|
24225
|
+
const cached = await repo.getCache(cacheKey);
|
|
24226
|
+
if (cached)
|
|
24227
|
+
return cached;
|
|
24228
|
+
const result = await repo.collection.findOne({
|
|
24229
|
+
_id: new ObjectId67(id)
|
|
24230
|
+
});
|
|
24231
|
+
if (!result) {
|
|
24232
|
+
throw new BadRequestError117("Stock movement not found.");
|
|
24233
|
+
}
|
|
24234
|
+
repo.setCache(cacheKey, result, 300);
|
|
24235
|
+
return result;
|
|
24236
|
+
} catch (error) {
|
|
24237
|
+
if (error instanceof AppError56)
|
|
24238
|
+
throw error;
|
|
24239
|
+
throw new InternalServerError52("Failed to get stock movement.");
|
|
24240
|
+
}
|
|
24241
|
+
}
|
|
24242
|
+
return {
|
|
24243
|
+
createIndexes,
|
|
24244
|
+
add,
|
|
24245
|
+
getByItemId,
|
|
24246
|
+
getById
|
|
24247
|
+
};
|
|
24248
|
+
}
|
|
24249
|
+
|
|
24250
|
+
// src/resources/asset-item-movement/stock.movement.service.ts
|
|
24251
|
+
import { BadRequestError as BadRequestError118 } from "@goweekdays/utils";
|
|
24252
|
+
import { useAtlas as useAtlas18 } from "@goweekdays/utils";
|
|
24253
|
+
function useStockMovementService() {
|
|
24254
|
+
const movementRepo = useStockMovementRepo();
|
|
24255
|
+
const assetItemRepo = useAssetItemRepo();
|
|
24256
|
+
const assetUnitRepo = useAssetUnitRepo();
|
|
24257
|
+
async function stockIn(value, units) {
|
|
24258
|
+
const item = await assetItemRepo.getById(String(value.itemId));
|
|
24259
|
+
const session = useAtlas18.getClient()?.startSession();
|
|
24260
|
+
if (!session)
|
|
24261
|
+
throw new BadRequestError118("Failed to start session.");
|
|
24262
|
+
try {
|
|
24263
|
+
session.startTransaction();
|
|
24264
|
+
value.type = "in";
|
|
24265
|
+
const movementId = await movementRepo.add(value, session);
|
|
24266
|
+
if (item.trackingType === "individual" && units && units.length > 0) {
|
|
24267
|
+
for (const unit of units) {
|
|
24268
|
+
await assetUnitRepo.add(
|
|
24269
|
+
{
|
|
24270
|
+
itemId: value.itemId,
|
|
24271
|
+
serialNumber: unit.serialNumber,
|
|
24272
|
+
plateNumber: unit.plateNumber,
|
|
24273
|
+
status: "available",
|
|
24274
|
+
locationId: value.toLocationId
|
|
24275
|
+
},
|
|
24276
|
+
session
|
|
24277
|
+
);
|
|
24278
|
+
}
|
|
24279
|
+
}
|
|
24280
|
+
await assetItemRepo.incrementQuantity(
|
|
24281
|
+
String(value.itemId),
|
|
24282
|
+
value.quantity,
|
|
24283
|
+
session
|
|
24284
|
+
);
|
|
24285
|
+
await session.commitTransaction();
|
|
24286
|
+
return movementId;
|
|
24287
|
+
} catch (error) {
|
|
24288
|
+
await session.abortTransaction();
|
|
24289
|
+
throw error;
|
|
24290
|
+
} finally {
|
|
24291
|
+
await session.endSession();
|
|
24292
|
+
}
|
|
24293
|
+
}
|
|
24294
|
+
async function stockOut(value, unitIds) {
|
|
24295
|
+
const item = await assetItemRepo.getById(String(value.itemId));
|
|
24296
|
+
if (item.trackingType === "quantity" && (item.quantityOnHand ?? 0) < value.quantity) {
|
|
24297
|
+
throw new BadRequestError118("Insufficient stock.");
|
|
24298
|
+
}
|
|
24299
|
+
const session = useAtlas18.getClient()?.startSession();
|
|
24300
|
+
if (!session)
|
|
24301
|
+
throw new BadRequestError118("Failed to start session.");
|
|
24302
|
+
try {
|
|
24303
|
+
session.startTransaction();
|
|
24304
|
+
value.type = "out";
|
|
24305
|
+
const movementId = await movementRepo.add(value, session);
|
|
24306
|
+
if (item.trackingType === "individual" && unitIds && unitIds.length > 0) {
|
|
24307
|
+
for (const unitId of unitIds) {
|
|
24308
|
+
await assetUnitRepo.updateById(
|
|
24309
|
+
unitId,
|
|
24310
|
+
{ status: "assigned", assignedTo: value.reference?.id },
|
|
24311
|
+
session
|
|
24312
|
+
);
|
|
24313
|
+
}
|
|
24314
|
+
}
|
|
24315
|
+
await assetItemRepo.incrementQuantity(
|
|
24316
|
+
String(value.itemId),
|
|
24317
|
+
-value.quantity,
|
|
24318
|
+
session
|
|
24319
|
+
);
|
|
24320
|
+
await session.commitTransaction();
|
|
24321
|
+
return movementId;
|
|
24322
|
+
} catch (error) {
|
|
24323
|
+
await session.abortTransaction();
|
|
24324
|
+
throw error;
|
|
24325
|
+
} finally {
|
|
24326
|
+
await session.endSession();
|
|
24327
|
+
}
|
|
24328
|
+
}
|
|
24329
|
+
async function transfer(value, unitIds) {
|
|
24330
|
+
if (!value.fromLocationId || !value.toLocationId) {
|
|
24331
|
+
throw new BadRequestError118(
|
|
24332
|
+
"Transfer requires both fromLocationId and toLocationId."
|
|
24333
|
+
);
|
|
24334
|
+
}
|
|
24335
|
+
const item = await assetItemRepo.getById(String(value.itemId));
|
|
24336
|
+
const session = useAtlas18.getClient()?.startSession();
|
|
24337
|
+
if (!session)
|
|
24338
|
+
throw new BadRequestError118("Failed to start session.");
|
|
24339
|
+
try {
|
|
24340
|
+
session.startTransaction();
|
|
24341
|
+
value.type = "transfer";
|
|
24342
|
+
const movementId = await movementRepo.add(value, session);
|
|
24343
|
+
if (item.trackingType === "individual" && unitIds && unitIds.length > 0) {
|
|
24344
|
+
for (const unitId of unitIds) {
|
|
24345
|
+
await assetUnitRepo.updateById(
|
|
24346
|
+
unitId,
|
|
24347
|
+
{ locationId: value.toLocationId },
|
|
24348
|
+
session
|
|
24349
|
+
);
|
|
24350
|
+
}
|
|
24351
|
+
}
|
|
24352
|
+
await session.commitTransaction();
|
|
24353
|
+
return movementId;
|
|
24354
|
+
} catch (error) {
|
|
24355
|
+
await session.abortTransaction();
|
|
24356
|
+
throw error;
|
|
24357
|
+
} finally {
|
|
24358
|
+
await session.endSession();
|
|
24359
|
+
}
|
|
24360
|
+
}
|
|
24361
|
+
async function adjustment(value) {
|
|
24362
|
+
if (!value.reason) {
|
|
24363
|
+
throw new BadRequestError118("Adjustment requires a reason.");
|
|
24364
|
+
}
|
|
24365
|
+
const session = useAtlas18.getClient()?.startSession();
|
|
24366
|
+
if (!session)
|
|
24367
|
+
throw new BadRequestError118("Failed to start session.");
|
|
24368
|
+
try {
|
|
24369
|
+
session.startTransaction();
|
|
24370
|
+
value.type = "adjustment";
|
|
24371
|
+
const movementId = await movementRepo.add(value, session);
|
|
24372
|
+
await assetItemRepo.incrementQuantity(
|
|
24373
|
+
String(value.itemId),
|
|
24374
|
+
value.quantity,
|
|
24375
|
+
session
|
|
24376
|
+
);
|
|
24377
|
+
await session.commitTransaction();
|
|
24378
|
+
return movementId;
|
|
24379
|
+
} catch (error) {
|
|
24380
|
+
await session.abortTransaction();
|
|
24381
|
+
throw error;
|
|
24382
|
+
} finally {
|
|
24383
|
+
await session.endSession();
|
|
24384
|
+
}
|
|
24385
|
+
}
|
|
24386
|
+
async function conversion(newItem, quantity, unitIds) {
|
|
24387
|
+
const sourceItem = await assetItemRepo.getById(String(newItem.itemRefId));
|
|
24388
|
+
if (!newItem.itemRefId) {
|
|
24389
|
+
throw new BadRequestError118("Conversion requires itemRefId.");
|
|
24390
|
+
}
|
|
24391
|
+
if (sourceItem.trackingType === "quantity" && (sourceItem.quantityOnHand ?? 0) < quantity) {
|
|
24392
|
+
throw new BadRequestError118("Insufficient stock on source item.");
|
|
24393
|
+
}
|
|
24394
|
+
const session = useAtlas18.getClient()?.startSession();
|
|
24395
|
+
if (!session)
|
|
24396
|
+
throw new BadRequestError118("Failed to start session.");
|
|
24397
|
+
try {
|
|
24398
|
+
session.startTransaction();
|
|
24399
|
+
const newItemId = await assetItemRepo.add(newItem, session);
|
|
24400
|
+
const movementData = {
|
|
24401
|
+
itemId: newItem.itemRefId,
|
|
24402
|
+
type: "conversion",
|
|
24403
|
+
quantity,
|
|
24404
|
+
fromItemId: String(newItem.itemRefId),
|
|
24405
|
+
toItemId: String(newItemId),
|
|
24406
|
+
reference: { type: "conversion", id: String(newItemId) }
|
|
24407
|
+
};
|
|
24408
|
+
const movementId = await movementRepo.add(movementData, session);
|
|
24409
|
+
await assetItemRepo.incrementQuantity(
|
|
24410
|
+
String(newItem.itemRefId),
|
|
24411
|
+
-quantity,
|
|
24412
|
+
session
|
|
24413
|
+
);
|
|
24414
|
+
await assetItemRepo.incrementQuantity(
|
|
24415
|
+
String(newItemId),
|
|
24416
|
+
quantity,
|
|
24417
|
+
session
|
|
24418
|
+
);
|
|
24419
|
+
if (sourceItem.trackingType === "individual" && unitIds && unitIds.length > 0) {
|
|
24420
|
+
for (const unitId of unitIds) {
|
|
24421
|
+
await assetUnitRepo.updateById(
|
|
24422
|
+
unitId,
|
|
24423
|
+
{ itemId: String(newItemId) },
|
|
24424
|
+
session
|
|
24425
|
+
);
|
|
24426
|
+
}
|
|
24427
|
+
}
|
|
24428
|
+
await session.commitTransaction();
|
|
24429
|
+
return { movementId, newItemId };
|
|
24430
|
+
} catch (error) {
|
|
24431
|
+
await session.abortTransaction();
|
|
24432
|
+
throw error;
|
|
24433
|
+
} finally {
|
|
24434
|
+
await session.endSession();
|
|
24435
|
+
}
|
|
24436
|
+
}
|
|
24437
|
+
return {
|
|
24438
|
+
stockIn,
|
|
24439
|
+
stockOut,
|
|
24440
|
+
transfer,
|
|
24441
|
+
adjustment,
|
|
24442
|
+
conversion
|
|
24443
|
+
};
|
|
24444
|
+
}
|
|
24445
|
+
|
|
24446
|
+
// src/resources/asset-item-movement/stock.movement.controller.ts
|
|
24447
|
+
import Joi104 from "joi";
|
|
24448
|
+
import { BadRequestError as BadRequestError119 } from "@goweekdays/utils";
|
|
24449
|
+
var querySchema2 = Joi104.object({
|
|
24450
|
+
itemId: Joi104.string().hex().length(24).required(),
|
|
24451
|
+
type: Joi104.string().valid(...stockMovementTypes).optional().allow("", null),
|
|
24452
|
+
page: Joi104.number().min(1).optional().allow("", null),
|
|
24453
|
+
limit: Joi104.number().min(1).optional().allow("", null)
|
|
24454
|
+
});
|
|
24455
|
+
var idParamSchema4 = Joi104.object({
|
|
24456
|
+
id: Joi104.string().hex().length(24).required()
|
|
24457
|
+
});
|
|
24458
|
+
var stockInSchema = Joi104.object({
|
|
24459
|
+
itemId: Joi104.string().hex().length(24).required(),
|
|
24460
|
+
quantity: Joi104.number().positive().required(),
|
|
24461
|
+
unitCost: Joi104.number().min(0).optional().allow(null),
|
|
24462
|
+
totalCost: Joi104.number().min(0).optional().allow(null),
|
|
24463
|
+
reference: Joi104.object({
|
|
24464
|
+
type: Joi104.string().valid("purchase", "sale", "transfer", "manual", "conversion").required(),
|
|
24465
|
+
id: Joi104.string().optional().allow("", null)
|
|
24466
|
+
}).optional().allow(null),
|
|
24467
|
+
toLocationId: Joi104.string().optional().allow("", null),
|
|
24468
|
+
units: Joi104.array().items(
|
|
24469
|
+
Joi104.object({
|
|
24470
|
+
serialNumber: Joi104.string().optional().allow("", null),
|
|
24471
|
+
plateNumber: Joi104.string().optional().allow("", null)
|
|
24472
|
+
})
|
|
24473
|
+
).optional()
|
|
24474
|
+
});
|
|
24475
|
+
var stockOutSchema = Joi104.object({
|
|
24476
|
+
itemId: Joi104.string().hex().length(24).required(),
|
|
24477
|
+
quantity: Joi104.number().positive().required(),
|
|
24478
|
+
unitCost: Joi104.number().min(0).optional().allow(null),
|
|
24479
|
+
totalCost: Joi104.number().min(0).optional().allow(null),
|
|
24480
|
+
reference: Joi104.object({
|
|
24481
|
+
type: Joi104.string().valid("purchase", "sale", "transfer", "manual", "conversion").required(),
|
|
24482
|
+
id: Joi104.string().optional().allow("", null)
|
|
24483
|
+
}).optional().allow(null),
|
|
24484
|
+
fromLocationId: Joi104.string().optional().allow("", null),
|
|
24485
|
+
unitIds: Joi104.array().items(Joi104.string().hex().length(24)).optional()
|
|
24486
|
+
});
|
|
24487
|
+
var transferSchema = Joi104.object({
|
|
24488
|
+
itemId: Joi104.string().hex().length(24).required(),
|
|
24489
|
+
quantity: Joi104.number().positive().required(),
|
|
24490
|
+
fromLocationId: Joi104.string().required(),
|
|
24491
|
+
toLocationId: Joi104.string().required(),
|
|
24492
|
+
reference: Joi104.object({
|
|
24493
|
+
type: Joi104.string().valid("purchase", "sale", "transfer", "manual", "conversion").required(),
|
|
24494
|
+
id: Joi104.string().optional().allow("", null)
|
|
24495
|
+
}).optional().allow(null),
|
|
24496
|
+
unitIds: Joi104.array().items(Joi104.string().hex().length(24)).optional()
|
|
24497
|
+
});
|
|
24498
|
+
var adjustmentSchema = Joi104.object({
|
|
24499
|
+
itemId: Joi104.string().hex().length(24).required(),
|
|
24500
|
+
quantity: Joi104.number().required(),
|
|
24501
|
+
reason: Joi104.string().required(),
|
|
24502
|
+
reference: Joi104.object({
|
|
24503
|
+
type: Joi104.string().valid("purchase", "sale", "transfer", "manual", "conversion").required(),
|
|
24504
|
+
id: Joi104.string().optional().allow("", null)
|
|
24505
|
+
}).optional().allow(null)
|
|
24506
|
+
});
|
|
24507
|
+
var conversionSchema = Joi104.object({
|
|
24508
|
+
quantity: Joi104.number().positive().required(),
|
|
24509
|
+
newItem: Joi104.object({
|
|
24510
|
+
name: Joi104.string().required(),
|
|
24511
|
+
description: Joi104.string().optional().allow("", null),
|
|
24512
|
+
assetCategory: Joi104.string().valid(...assetItemCategories).required(),
|
|
24513
|
+
assetClass: Joi104.string().valid(...assetItemClasses).required(),
|
|
24514
|
+
trackingType: Joi104.string().valid(...assetItemTrackingTypes).required(),
|
|
24515
|
+
purpose: Joi104.string().valid(...assetItemPurposes).required(),
|
|
24516
|
+
itemRefId: Joi104.string().hex().length(24).required(),
|
|
24517
|
+
remarks: Joi104.string().optional().allow("", null),
|
|
24518
|
+
departmentId: Joi104.string().required(),
|
|
24519
|
+
departmentName: Joi104.string().required(),
|
|
24520
|
+
categoryId: Joi104.string().required(),
|
|
24521
|
+
categoryName: Joi104.string().required(),
|
|
24522
|
+
subcategoryId: Joi104.string().required(),
|
|
24523
|
+
subcategoryName: Joi104.string().required(),
|
|
24524
|
+
categoryPath: Joi104.string().required(),
|
|
24525
|
+
brand: Joi104.string().optional().allow("", null),
|
|
24526
|
+
tags: Joi104.array().items(
|
|
24527
|
+
Joi104.object({
|
|
24528
|
+
id: Joi104.string().required(),
|
|
24529
|
+
name: Joi104.string().required()
|
|
24530
|
+
})
|
|
24531
|
+
).optional().default([])
|
|
24532
|
+
}).required(),
|
|
24533
|
+
unitIds: Joi104.array().items(Joi104.string().hex().length(24)).optional()
|
|
24534
|
+
});
|
|
24535
|
+
function useStockMovementController() {
|
|
24536
|
+
const repo = useStockMovementRepo();
|
|
24537
|
+
const service = useStockMovementService();
|
|
24538
|
+
async function stockIn(req, res, next) {
|
|
24539
|
+
try {
|
|
24540
|
+
const { error } = stockInSchema.validate(req.body);
|
|
24541
|
+
if (error) {
|
|
24542
|
+
next(new BadRequestError119(error.message));
|
|
24543
|
+
return;
|
|
24544
|
+
}
|
|
24545
|
+
const { units, ...movement } = req.body;
|
|
24546
|
+
const movementId = await service.stockIn(movement, units);
|
|
24547
|
+
res.status(201).json({
|
|
24548
|
+
message: "Stock in recorded successfully.",
|
|
24549
|
+
movementId
|
|
24550
|
+
});
|
|
24551
|
+
} catch (error) {
|
|
24552
|
+
next(error);
|
|
24553
|
+
}
|
|
24554
|
+
}
|
|
24555
|
+
async function stockOut(req, res, next) {
|
|
24556
|
+
try {
|
|
24557
|
+
const { error } = stockOutSchema.validate(req.body);
|
|
24558
|
+
if (error) {
|
|
24559
|
+
next(new BadRequestError119(error.message));
|
|
24560
|
+
return;
|
|
24561
|
+
}
|
|
24562
|
+
const { unitIds, ...movement } = req.body;
|
|
24563
|
+
const movementId = await service.stockOut(movement, unitIds);
|
|
24564
|
+
res.status(201).json({
|
|
24565
|
+
message: "Stock out recorded successfully.",
|
|
24566
|
+
movementId
|
|
24567
|
+
});
|
|
24568
|
+
} catch (error) {
|
|
24569
|
+
next(error);
|
|
24570
|
+
}
|
|
24571
|
+
}
|
|
24572
|
+
async function transfer(req, res, next) {
|
|
24573
|
+
try {
|
|
24574
|
+
const { error } = transferSchema.validate(req.body);
|
|
24575
|
+
if (error) {
|
|
24576
|
+
next(new BadRequestError119(error.message));
|
|
24577
|
+
return;
|
|
24578
|
+
}
|
|
24579
|
+
const { unitIds, ...movement } = req.body;
|
|
24580
|
+
const movementId = await service.transfer(movement, unitIds);
|
|
24581
|
+
res.status(201).json({
|
|
24582
|
+
message: "Transfer recorded successfully.",
|
|
24583
|
+
movementId
|
|
24584
|
+
});
|
|
24585
|
+
} catch (error) {
|
|
24586
|
+
next(error);
|
|
24587
|
+
}
|
|
24588
|
+
}
|
|
24589
|
+
async function adjustment(req, res, next) {
|
|
24590
|
+
try {
|
|
24591
|
+
const { error } = adjustmentSchema.validate(req.body);
|
|
24592
|
+
if (error) {
|
|
24593
|
+
next(new BadRequestError119(error.message));
|
|
24594
|
+
return;
|
|
24595
|
+
}
|
|
24596
|
+
const movementId = await service.adjustment(req.body);
|
|
24597
|
+
res.status(201).json({
|
|
24598
|
+
message: "Adjustment recorded successfully.",
|
|
24599
|
+
movementId
|
|
24600
|
+
});
|
|
24601
|
+
} catch (error) {
|
|
24602
|
+
next(error);
|
|
24603
|
+
}
|
|
24604
|
+
}
|
|
24605
|
+
async function getByItemId(req, res, next) {
|
|
24606
|
+
const { error } = querySchema2.validate(req.query);
|
|
24607
|
+
if (error) {
|
|
24608
|
+
next(new BadRequestError119(error.message));
|
|
24609
|
+
return;
|
|
24610
|
+
}
|
|
24611
|
+
const itemId = req.query.itemId;
|
|
24612
|
+
const type = req.query.type ?? "";
|
|
24613
|
+
const page = typeof req.query.page === "string" ? Number(req.query.page) : 1;
|
|
24614
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : 10;
|
|
24615
|
+
try {
|
|
24616
|
+
const results = await repo.getByItemId({ itemId, type, page, limit });
|
|
24617
|
+
res.json(results);
|
|
24618
|
+
} catch (error2) {
|
|
24619
|
+
next(error2);
|
|
24620
|
+
}
|
|
24621
|
+
}
|
|
24622
|
+
async function getById(req, res, next) {
|
|
24623
|
+
const { error } = idParamSchema4.validate(req.params);
|
|
24624
|
+
if (error) {
|
|
24625
|
+
next(new BadRequestError119(error.message));
|
|
24626
|
+
return;
|
|
24627
|
+
}
|
|
24628
|
+
try {
|
|
24629
|
+
const movement = await repo.getById(req.params.id);
|
|
24630
|
+
res.json(movement);
|
|
24631
|
+
} catch (error2) {
|
|
24632
|
+
next(error2);
|
|
24633
|
+
}
|
|
24634
|
+
}
|
|
24635
|
+
async function conversion(req, res, next) {
|
|
24636
|
+
try {
|
|
24637
|
+
const { error } = conversionSchema.validate(req.body);
|
|
24638
|
+
if (error) {
|
|
24639
|
+
next(new BadRequestError119(error.message));
|
|
24640
|
+
return;
|
|
24641
|
+
}
|
|
24642
|
+
const { quantity, newItem, unitIds } = req.body;
|
|
24643
|
+
const result = await service.conversion(newItem, quantity, unitIds);
|
|
24644
|
+
res.status(201).json({
|
|
24645
|
+
message: "Conversion recorded successfully.",
|
|
24646
|
+
movementId: result.movementId,
|
|
24647
|
+
newItemId: result.newItemId
|
|
24648
|
+
});
|
|
24649
|
+
} catch (error) {
|
|
24650
|
+
next(error);
|
|
24651
|
+
}
|
|
24652
|
+
}
|
|
24653
|
+
return {
|
|
24654
|
+
stockIn,
|
|
24655
|
+
stockOut,
|
|
24656
|
+
transfer,
|
|
24657
|
+
adjustment,
|
|
24658
|
+
conversion,
|
|
24659
|
+
getByItemId,
|
|
24660
|
+
getById
|
|
24661
|
+
};
|
|
24662
|
+
}
|
|
24663
|
+
|
|
24664
|
+
// src/resources/asset-item-category/category.service.ts
|
|
24665
|
+
import { BadRequestError as BadRequestError120 } from "@goweekdays/utils";
|
|
24666
|
+
function useCategoryService() {
|
|
24667
|
+
const repo = useCategoryRepo();
|
|
24668
|
+
async function add(value) {
|
|
24669
|
+
const { error } = schemaCategoryNodeCreate.validate(value);
|
|
24670
|
+
if (error) {
|
|
24671
|
+
throw new BadRequestError120(error.message);
|
|
24672
|
+
}
|
|
24673
|
+
const name = normalizeName(value.displayName);
|
|
24674
|
+
let level;
|
|
24675
|
+
let path = "";
|
|
24676
|
+
if (!value.parentId) {
|
|
24677
|
+
level = "department";
|
|
24678
|
+
const existing = await repo.getByNameParent(name, { orgId: value.orgId });
|
|
24679
|
+
if (existing) {
|
|
24680
|
+
throw new BadRequestError120(
|
|
24681
|
+
`Department "${value.displayName}" already exists.`
|
|
24682
|
+
);
|
|
24683
|
+
}
|
|
24684
|
+
} else {
|
|
24685
|
+
const parent = await repo.getById(value.parentId);
|
|
24686
|
+
if (parent && parent.level === "department") {
|
|
24687
|
+
level = "category";
|
|
24688
|
+
} else if (parent && parent.level === "category") {
|
|
24689
|
+
level = "subcategory";
|
|
24690
|
+
} else {
|
|
24691
|
+
throw new BadRequestError120(
|
|
24692
|
+
"Cannot create a child under a subcategory. Maximum depth is 3 levels."
|
|
24693
|
+
);
|
|
24694
|
+
}
|
|
24695
|
+
const existing = await repo.getByNameParent(name, {
|
|
24696
|
+
parentId: value.parentId,
|
|
24697
|
+
orgId: value.orgId
|
|
24698
|
+
});
|
|
24699
|
+
if (existing) {
|
|
24700
|
+
throw new BadRequestError120(
|
|
24701
|
+
`"${value.displayName}" already exists under this parent.`
|
|
24702
|
+
);
|
|
24703
|
+
}
|
|
24704
|
+
if (level === "subcategory") {
|
|
24705
|
+
const department = await repo.getById(parent.parentId);
|
|
24706
|
+
path = buildCategoryPath(department.name, parent.name, name);
|
|
24707
|
+
}
|
|
24708
|
+
}
|
|
24709
|
+
const data = {
|
|
24710
|
+
orgId: value.orgId ?? "",
|
|
24711
|
+
level,
|
|
24712
|
+
type: value.orgId ? "private" : "public",
|
|
24713
|
+
name,
|
|
24714
|
+
displayName: value.displayName.trim(),
|
|
24715
|
+
parentId: value.parentId ?? "",
|
|
24716
|
+
path
|
|
24717
|
+
};
|
|
24718
|
+
return await repo.add(data);
|
|
24719
|
+
}
|
|
24720
|
+
async function updateById(_id, value) {
|
|
24721
|
+
const { error } = schemaCategoryNodeUpdate.validate(value);
|
|
24722
|
+
if (error) {
|
|
24723
|
+
throw new BadRequestError120(error.message);
|
|
24724
|
+
}
|
|
24725
|
+
const update = {
|
|
24726
|
+
displayName: "",
|
|
24727
|
+
name: "",
|
|
24728
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
24729
|
+
};
|
|
24730
|
+
if (value.displayName) {
|
|
24731
|
+
update.displayName = value.displayName.trim();
|
|
24732
|
+
update.name = normalizeName(value.displayName);
|
|
24733
|
+
}
|
|
24734
|
+
await repo.updateById(_id, update);
|
|
24735
|
+
}
|
|
24736
|
+
async function deleteById(_id) {
|
|
24737
|
+
const hasChildren = await repo.hasChildren(_id);
|
|
24738
|
+
if (hasChildren) {
|
|
24739
|
+
throw new BadRequestError120(
|
|
24740
|
+
"Cannot delete a category that has child categories. Remove or reassign children first."
|
|
24741
|
+
);
|
|
24742
|
+
}
|
|
24743
|
+
await repo.softDeleteById(_id);
|
|
24744
|
+
}
|
|
24745
|
+
return {
|
|
24746
|
+
add,
|
|
24747
|
+
updateById,
|
|
24748
|
+
deleteById
|
|
24749
|
+
};
|
|
24750
|
+
}
|
|
24751
|
+
|
|
24752
|
+
// src/resources/asset-item-category/category.controller.ts
|
|
24753
|
+
import Joi105 from "joi";
|
|
24754
|
+
import { BadRequestError as BadRequestError121 } from "@goweekdays/utils";
|
|
24755
|
+
function useCategoryController() {
|
|
24756
|
+
const repo = useCategoryRepo();
|
|
24757
|
+
const service = useCategoryService();
|
|
24758
|
+
async function add(req, res, next) {
|
|
24759
|
+
const orgId = req.params.org ?? "";
|
|
24760
|
+
const { error, value } = schemaCategoryNodeCreate.validate({
|
|
24761
|
+
...req.body,
|
|
24762
|
+
orgId
|
|
24763
|
+
});
|
|
24764
|
+
if (error) {
|
|
24765
|
+
next(new BadRequestError121(error.message));
|
|
24766
|
+
return;
|
|
24767
|
+
}
|
|
24768
|
+
try {
|
|
24769
|
+
const id = await service.add(value);
|
|
24770
|
+
res.status(201).json({ message: "Category created successfully.", id });
|
|
24771
|
+
} catch (error2) {
|
|
24772
|
+
next(error2);
|
|
24773
|
+
}
|
|
24774
|
+
}
|
|
24775
|
+
async function getAll(req, res, next) {
|
|
24776
|
+
const org = req.params.org ?? "";
|
|
24777
|
+
const { error, value } = schemaCategoryGetAll.validate({
|
|
24778
|
+
...req.query,
|
|
24779
|
+
org
|
|
24780
|
+
});
|
|
24781
|
+
if (error) {
|
|
24782
|
+
next(new BadRequestError121(error.message));
|
|
24783
|
+
return;
|
|
24784
|
+
}
|
|
24785
|
+
try {
|
|
24786
|
+
const data = await repo.getAll(value);
|
|
24787
|
+
res.json(data);
|
|
24788
|
+
} catch (error2) {
|
|
24789
|
+
next(error2);
|
|
24790
|
+
}
|
|
24791
|
+
}
|
|
24792
|
+
async function getById(req, res, next) {
|
|
24793
|
+
const _id = req.params.id;
|
|
24794
|
+
const { error: errorId } = Joi105.string().hex().length(24).required().validate(_id);
|
|
24795
|
+
if (errorId) {
|
|
24796
|
+
next(new BadRequestError121("Invalid category ID."));
|
|
24797
|
+
return;
|
|
24798
|
+
}
|
|
24799
|
+
try {
|
|
24800
|
+
const category = await repo.getById(_id);
|
|
24801
|
+
if (!category) {
|
|
24802
|
+
next(new BadRequestError121("Category not found."));
|
|
24803
|
+
return;
|
|
24804
|
+
}
|
|
24805
|
+
res.json(category);
|
|
24806
|
+
} catch (error) {
|
|
24807
|
+
next(error);
|
|
24808
|
+
}
|
|
24809
|
+
}
|
|
24810
|
+
async function updateById(req, res, next) {
|
|
24811
|
+
const _id = req.params.id;
|
|
24812
|
+
const { error: errorId } = Joi105.string().hex().length(24).required().validate(_id);
|
|
24813
|
+
if (errorId) {
|
|
24814
|
+
next(new BadRequestError121("Invalid category ID."));
|
|
24815
|
+
return;
|
|
24816
|
+
}
|
|
24817
|
+
const { error, value } = schemaCategoryNodeUpdate.validate(req.body);
|
|
24818
|
+
if (error) {
|
|
24819
|
+
next(new BadRequestError121(error.message));
|
|
24820
|
+
return;
|
|
24821
|
+
}
|
|
24822
|
+
const { displayName } = value;
|
|
24823
|
+
try {
|
|
24824
|
+
await service.updateById(_id, { displayName });
|
|
24825
|
+
res.json({ message: "Category updated successfully." });
|
|
24826
|
+
} catch (error2) {
|
|
24827
|
+
next(error2);
|
|
24828
|
+
}
|
|
24829
|
+
}
|
|
24830
|
+
async function deleteById(req, res, next) {
|
|
24831
|
+
const _id = req.params.id;
|
|
24832
|
+
const { error: errorId } = Joi105.string().hex().length(24).required().validate(_id);
|
|
24833
|
+
if (errorId) {
|
|
24834
|
+
next(new BadRequestError121("Invalid category ID."));
|
|
24835
|
+
return;
|
|
24836
|
+
}
|
|
24837
|
+
try {
|
|
24838
|
+
await service.deleteById(_id);
|
|
24839
|
+
res.json({ message: "Category deleted successfully." });
|
|
24840
|
+
} catch (error) {
|
|
24841
|
+
next(error);
|
|
24842
|
+
}
|
|
24843
|
+
}
|
|
24844
|
+
return {
|
|
24845
|
+
add,
|
|
24846
|
+
getAll,
|
|
24847
|
+
getById,
|
|
24848
|
+
updateById,
|
|
24849
|
+
deleteById
|
|
24850
|
+
};
|
|
24851
|
+
}
|
|
24852
|
+
|
|
24853
|
+
// src/resources/tag/tag.service.ts
|
|
24854
|
+
import { AppError as AppError57, BadRequestError as BadRequestError122 } from "@goweekdays/utils";
|
|
24855
|
+
import Joi106 from "joi";
|
|
24856
|
+
function normalizeTagName2(name) {
|
|
24857
|
+
return name.trim().toLowerCase().replace(/\s+/g, "-");
|
|
24858
|
+
}
|
|
24859
|
+
function useTagService() {
|
|
24860
|
+
const {
|
|
24861
|
+
add: _add,
|
|
24862
|
+
getById: _getById,
|
|
24863
|
+
updateById: _updateById,
|
|
24864
|
+
deleteById: _deleteById
|
|
24865
|
+
} = useTagRepo();
|
|
24866
|
+
async function add(value) {
|
|
24867
|
+
const { error } = schemaTagCreate.validate(value);
|
|
24868
|
+
if (error) {
|
|
24869
|
+
throw new BadRequestError122(error.message);
|
|
24870
|
+
}
|
|
24871
|
+
const normalizedName = normalizeTagName2(value.name);
|
|
24872
|
+
const data = {
|
|
24873
|
+
name: value.name,
|
|
24874
|
+
normalizedName,
|
|
24875
|
+
type: value.type,
|
|
24876
|
+
orgId: value.orgId ?? "",
|
|
24877
|
+
categoryPath: value.categoryPath ?? "",
|
|
24878
|
+
status: "pending",
|
|
24879
|
+
usageCount: 0
|
|
24880
|
+
};
|
|
24881
|
+
try {
|
|
24882
|
+
const id = await _add(data);
|
|
24883
|
+
return id;
|
|
24884
|
+
} catch (error2) {
|
|
24885
|
+
if (error2 instanceof AppError57)
|
|
24886
|
+
throw error2;
|
|
24887
|
+
throw new BadRequestError122("Failed to add tag.");
|
|
24888
|
+
}
|
|
24889
|
+
}
|
|
24890
|
+
async function updateById(id, value) {
|
|
24891
|
+
const { error: errorId } = Joi106.string().hex().required().validate(id);
|
|
24892
|
+
if (errorId) {
|
|
24893
|
+
throw new BadRequestError122("Invalid Tag ID.");
|
|
24894
|
+
}
|
|
24895
|
+
const { error } = schemaTagUpdate.validate(value);
|
|
24896
|
+
if (error) {
|
|
24897
|
+
throw new BadRequestError122(`Invalid tag update data: ${error.message}`);
|
|
24898
|
+
}
|
|
24899
|
+
const existingTag = await _getById(id);
|
|
24900
|
+
if (!existingTag) {
|
|
24901
|
+
throw new BadRequestError122("Tag not found.");
|
|
24902
|
+
}
|
|
24903
|
+
const updatedData = { ...value };
|
|
24904
|
+
if (value.name) {
|
|
24905
|
+
updatedData.normalizedName = normalizeTagName2(value.name);
|
|
24906
|
+
}
|
|
24907
|
+
const message = await _updateById(id, updatedData);
|
|
24908
|
+
return message;
|
|
24909
|
+
}
|
|
24910
|
+
async function deleteById(id) {
|
|
24911
|
+
const { error } = Joi106.string().hex().required().validate(id);
|
|
24912
|
+
if (error) {
|
|
24913
|
+
throw new BadRequestError122("Invalid Tag ID.");
|
|
24914
|
+
}
|
|
24915
|
+
const existingTag = await _getById(id);
|
|
24916
|
+
if (!existingTag) {
|
|
24917
|
+
throw new BadRequestError122("Tag not found.");
|
|
24918
|
+
}
|
|
24919
|
+
const message = await _deleteById(id);
|
|
24920
|
+
return message;
|
|
24921
|
+
}
|
|
24922
|
+
return {
|
|
24923
|
+
add,
|
|
24924
|
+
updateById,
|
|
24925
|
+
deleteById
|
|
24926
|
+
};
|
|
24927
|
+
}
|
|
24928
|
+
|
|
24929
|
+
// src/resources/tag/tag.controller.ts
|
|
24930
|
+
import Joi107 from "joi";
|
|
24931
|
+
import { BadRequestError as BadRequestError123, logger as logger59 } from "@goweekdays/utils";
|
|
24932
|
+
function useTagController() {
|
|
24933
|
+
const { getAll: _getAll, getById: _getById } = useTagRepo();
|
|
24934
|
+
const {
|
|
24935
|
+
add: _add,
|
|
24936
|
+
updateById: _updateById,
|
|
24937
|
+
deleteById: _deleteById
|
|
24938
|
+
} = useTagService();
|
|
24939
|
+
async function add(req, res, next) {
|
|
24940
|
+
const value = req.body;
|
|
24941
|
+
const { error } = schemaTagCreate.validate(value);
|
|
24942
|
+
if (error) {
|
|
24943
|
+
next(new BadRequestError123(error.message));
|
|
24944
|
+
logger59.info(`Controller: ${error.message}`);
|
|
24945
|
+
return;
|
|
24946
|
+
}
|
|
24947
|
+
try {
|
|
24948
|
+
const message = await _add(value);
|
|
24949
|
+
res.json({ message });
|
|
24950
|
+
return;
|
|
24951
|
+
} catch (error2) {
|
|
24952
|
+
next(error2);
|
|
24953
|
+
}
|
|
24954
|
+
}
|
|
24955
|
+
async function getAll(req, res, next) {
|
|
24956
|
+
const query = req.query;
|
|
24957
|
+
const validation = Joi107.object({
|
|
24958
|
+
page: Joi107.number().min(1).optional().allow("", null),
|
|
24959
|
+
limit: Joi107.number().min(1).optional().allow("", null),
|
|
24960
|
+
search: Joi107.string().optional().allow("", null),
|
|
24961
|
+
type: Joi107.string().valid("public", "private").optional().allow("", null),
|
|
24962
|
+
orgId: Joi107.string().hex().optional().allow("", null),
|
|
24963
|
+
status: Joi107.string().valid("active", "pending").optional().allow("", null),
|
|
24964
|
+
categoryPath: Joi107.string().optional().allow("", null)
|
|
24965
|
+
});
|
|
24966
|
+
const { error } = validation.validate(query);
|
|
24967
|
+
if (error) {
|
|
24968
|
+
next(new BadRequestError123(error.message));
|
|
24969
|
+
return;
|
|
24970
|
+
}
|
|
24971
|
+
const page = typeof req.query.page === "string" ? Number(req.query.page) : 1;
|
|
24972
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : 10;
|
|
24973
|
+
const search = req.query.search ?? "";
|
|
24974
|
+
const type = req.query.type ?? "";
|
|
24975
|
+
const orgId = req.query.orgId ?? "";
|
|
24976
|
+
const status2 = req.query.status ?? "";
|
|
24977
|
+
const categoryPath = req.query.categoryPath ?? "";
|
|
24978
|
+
const isPageNumber = isFinite(page);
|
|
24979
|
+
if (!isPageNumber) {
|
|
24980
|
+
next(new BadRequestError123("Invalid page number."));
|
|
24981
|
+
return;
|
|
24982
|
+
}
|
|
24983
|
+
const isLimitNumber = isFinite(limit);
|
|
24984
|
+
if (!isLimitNumber) {
|
|
24985
|
+
next(new BadRequestError123("Invalid limit number."));
|
|
24986
|
+
return;
|
|
24987
|
+
}
|
|
24988
|
+
try {
|
|
24989
|
+
const tags = await _getAll({
|
|
24990
|
+
page,
|
|
24991
|
+
limit,
|
|
24992
|
+
search,
|
|
24993
|
+
type,
|
|
24994
|
+
orgId,
|
|
24995
|
+
status: status2,
|
|
24996
|
+
categoryPath
|
|
24997
|
+
});
|
|
24998
|
+
res.json(tags);
|
|
24999
|
+
return;
|
|
25000
|
+
} catch (error2) {
|
|
25001
|
+
next(error2);
|
|
25002
|
+
}
|
|
25003
|
+
}
|
|
25004
|
+
async function getById(req, res, next) {
|
|
25005
|
+
const id = req.params.id;
|
|
25006
|
+
const validation = Joi107.object({
|
|
25007
|
+
id: Joi107.string().hex().required()
|
|
25008
|
+
});
|
|
25009
|
+
const { error } = validation.validate({ id });
|
|
25010
|
+
if (error) {
|
|
25011
|
+
next(new BadRequestError123(error.message));
|
|
25012
|
+
return;
|
|
25013
|
+
}
|
|
25014
|
+
try {
|
|
25015
|
+
const tag = await _getById(id);
|
|
25016
|
+
res.json(tag);
|
|
25017
|
+
return;
|
|
25018
|
+
} catch (error2) {
|
|
25019
|
+
next(error2);
|
|
25020
|
+
}
|
|
25021
|
+
}
|
|
25022
|
+
async function updateById(req, res, next) {
|
|
25023
|
+
const _id = req.params.id;
|
|
25024
|
+
const { error: errorId } = Joi107.string().hex().required().validate(_id);
|
|
25025
|
+
if (errorId) {
|
|
25026
|
+
next(new BadRequestError123("Invalid Tag ID."));
|
|
25027
|
+
return;
|
|
25028
|
+
}
|
|
25029
|
+
const payload = req.body;
|
|
25030
|
+
const { error } = schemaTagUpdate.validate(payload);
|
|
25031
|
+
if (error) {
|
|
25032
|
+
next(new BadRequestError123(error.message));
|
|
25033
|
+
return;
|
|
25034
|
+
}
|
|
25035
|
+
try {
|
|
25036
|
+
const message = await _updateById(_id, payload);
|
|
25037
|
+
res.json({ message });
|
|
25038
|
+
return;
|
|
25039
|
+
} catch (error2) {
|
|
25040
|
+
next(error2);
|
|
25041
|
+
}
|
|
25042
|
+
}
|
|
25043
|
+
async function deleteById(req, res, next) {
|
|
25044
|
+
const id = req.params.id;
|
|
25045
|
+
if (!id) {
|
|
25046
|
+
next(new BadRequestError123("Tag ID is required."));
|
|
25047
|
+
return;
|
|
25048
|
+
}
|
|
25049
|
+
try {
|
|
25050
|
+
const message = await _deleteById(id);
|
|
25051
|
+
res.json(message);
|
|
25052
|
+
return;
|
|
25053
|
+
} catch (error) {
|
|
25054
|
+
next(error);
|
|
25055
|
+
}
|
|
25056
|
+
}
|
|
25057
|
+
return {
|
|
25058
|
+
add,
|
|
25059
|
+
getAll,
|
|
25060
|
+
getById,
|
|
25061
|
+
updateById,
|
|
25062
|
+
deleteById
|
|
25063
|
+
};
|
|
25064
|
+
}
|
|
22499
25065
|
export {
|
|
22500
25066
|
ACCESS_TOKEN_EXPIRY,
|
|
22501
25067
|
ACCESS_TOKEN_SECRET,
|
|
@@ -22539,6 +25105,14 @@ export {
|
|
|
22539
25105
|
VERIFICATION_USER_INVITE_DURATION,
|
|
22540
25106
|
XENDIT_BASE_URL,
|
|
22541
25107
|
XENDIT_SECRET_KEY,
|
|
25108
|
+
assetItemCategories,
|
|
25109
|
+
assetItemClasses,
|
|
25110
|
+
assetItemPurposes,
|
|
25111
|
+
assetItemStatuses,
|
|
25112
|
+
assetItemTrackingTypes,
|
|
25113
|
+
assetUnitStatuses,
|
|
25114
|
+
buildCategoryPath,
|
|
25115
|
+
categoryLevels,
|
|
22542
25116
|
chartOfAccountControlTypes,
|
|
22543
25117
|
chartOfAccountNormalBalances,
|
|
22544
25118
|
chartOfAccountStatuses,
|
|
@@ -22553,7 +25127,10 @@ export {
|
|
|
22553
25127
|
ledgerBillTypes,
|
|
22554
25128
|
modelAccountBalance,
|
|
22555
25129
|
modelApp,
|
|
25130
|
+
modelAssetItem,
|
|
25131
|
+
modelAssetUnit,
|
|
22556
25132
|
modelBusinessProfile,
|
|
25133
|
+
modelCategoryNode,
|
|
22557
25134
|
modelChartOfAccount,
|
|
22558
25135
|
modelCustomer,
|
|
22559
25136
|
modelJobApplication,
|
|
@@ -22579,14 +25156,22 @@ export {
|
|
|
22579
25156
|
modelPlan,
|
|
22580
25157
|
modelPromo,
|
|
22581
25158
|
modelRole,
|
|
25159
|
+
modelStockMovement,
|
|
22582
25160
|
modelSubscription,
|
|
22583
25161
|
modelSubscriptionTransaction,
|
|
25162
|
+
modelTag,
|
|
22584
25163
|
modelTax,
|
|
22585
25164
|
modelUser,
|
|
22586
25165
|
modelVerification,
|
|
25166
|
+
normalizeName,
|
|
22587
25167
|
schemaAccountBalance,
|
|
22588
25168
|
schemaApp,
|
|
22589
25169
|
schemaAppUpdate,
|
|
25170
|
+
schemaAssetItem,
|
|
25171
|
+
schemaAssetItemCreate,
|
|
25172
|
+
schemaAssetItemUpdate,
|
|
25173
|
+
schemaAssetUnit,
|
|
25174
|
+
schemaAssetUnitUpdate,
|
|
22590
25175
|
schemaAward,
|
|
22591
25176
|
schemaBuilding,
|
|
22592
25177
|
schemaBuildingUnit,
|
|
@@ -22598,6 +25183,10 @@ export {
|
|
|
22598
25183
|
schemaBusinessProfileRegisteredAddress,
|
|
22599
25184
|
schemaBusinessProfileTIN,
|
|
22600
25185
|
schemaBusinessProfileTradeName,
|
|
25186
|
+
schemaCategoryGetAll,
|
|
25187
|
+
schemaCategoryNodeCreate,
|
|
25188
|
+
schemaCategoryNodeStd,
|
|
25189
|
+
schemaCategoryNodeUpdate,
|
|
22601
25190
|
schemaCertification,
|
|
22602
25191
|
schemaChartOfAccountBase,
|
|
22603
25192
|
schemaChartOfAccountStd,
|
|
@@ -22667,6 +25256,7 @@ export {
|
|
|
22667
25256
|
schemaRole,
|
|
22668
25257
|
schemaRoleUpdate,
|
|
22669
25258
|
schemaSkill,
|
|
25259
|
+
schemaStockMovement,
|
|
22670
25260
|
schemaSubscribe,
|
|
22671
25261
|
schemaSubscription,
|
|
22672
25262
|
schemaSubscriptionCompute,
|
|
@@ -22674,6 +25264,9 @@ export {
|
|
|
22674
25264
|
schemaSubscriptionSeats,
|
|
22675
25265
|
schemaSubscriptionTransaction,
|
|
22676
25266
|
schemaSubscriptionUpdate,
|
|
25267
|
+
schemaTagCreate,
|
|
25268
|
+
schemaTagStd,
|
|
25269
|
+
schemaTagUpdate,
|
|
22677
25270
|
schemaTax,
|
|
22678
25271
|
schemaTaxUpdate,
|
|
22679
25272
|
schemaUpdateOptions,
|
|
@@ -22681,6 +25274,8 @@ export {
|
|
|
22681
25274
|
schemaVerification,
|
|
22682
25275
|
schemaVerificationOrgInvite,
|
|
22683
25276
|
schemaWorkExp,
|
|
25277
|
+
stockMovementReferenceTypes,
|
|
25278
|
+
stockMovementTypes,
|
|
22684
25279
|
taxDirections,
|
|
22685
25280
|
taxTypes,
|
|
22686
25281
|
transactionSchema,
|
|
@@ -22688,6 +25283,11 @@ export {
|
|
|
22688
25283
|
useAppController,
|
|
22689
25284
|
useAppRepo,
|
|
22690
25285
|
useAppService,
|
|
25286
|
+
useAssetItemController,
|
|
25287
|
+
useAssetItemRepo,
|
|
25288
|
+
useAssetItemService,
|
|
25289
|
+
useAssetUnitController,
|
|
25290
|
+
useAssetUnitRepo,
|
|
22691
25291
|
useAuthController,
|
|
22692
25292
|
useAuthService,
|
|
22693
25293
|
useBuildingController,
|
|
@@ -22698,6 +25298,9 @@ export {
|
|
|
22698
25298
|
useBuildingUnitService,
|
|
22699
25299
|
useBusinessProfileCtrl,
|
|
22700
25300
|
useBusinessProfileRepo,
|
|
25301
|
+
useCategoryController,
|
|
25302
|
+
useCategoryRepo,
|
|
25303
|
+
useCategoryService,
|
|
22701
25304
|
useChartOfAccountController,
|
|
22702
25305
|
useChartOfAccountRepo,
|
|
22703
25306
|
useCounterModel,
|
|
@@ -22753,11 +25356,17 @@ export {
|
|
|
22753
25356
|
useRoleController,
|
|
22754
25357
|
useRoleRepo,
|
|
22755
25358
|
useRoleService,
|
|
25359
|
+
useStockMovementController,
|
|
25360
|
+
useStockMovementRepo,
|
|
25361
|
+
useStockMovementService,
|
|
22756
25362
|
useSubscriptionController,
|
|
22757
25363
|
useSubscriptionRepo,
|
|
22758
25364
|
useSubscriptionService,
|
|
22759
25365
|
useSubscriptionTransactionController,
|
|
22760
25366
|
useSubscriptionTransactionRepo,
|
|
25367
|
+
useTagController,
|
|
25368
|
+
useTagRepo,
|
|
25369
|
+
useTagService,
|
|
22761
25370
|
useTaxController,
|
|
22762
25371
|
useTaxRepo,
|
|
22763
25372
|
useUserController,
|