erp_be/src/modules/assets/assets.service.js

764 lines
29 KiB
JavaScript

const prisma = require('../../config/prisma');
const ApiError = require('../../utils/ApiError');
const auditLog = require('../../utils/auditLog');
const { getPagination, isDropdownCall } = require('../../utils/pagination');
const { nextDocumentNumber } = require('../../utils/generateCode');
const { rowsToCsv } = require('../../utils/csv');
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
const { assertAnyLocation } = require('../../utils/locations');
const repository = require('./assets.repository');
const {
DEPRECIATION_METHOD_OPTIONS,
resolveDepreciationRate,
calculateDepreciation,
} = require('./assets.depreciation');
const { assertReference, toDateOnly, normalizeChecklistTemplate, presentChecklistTemplate } = require('./assets.helpers');
const { sanitizeAttachment } = require('./assets.attachments.service');
const assetInclude = {
item_categories: {
select: {
id: true,
code: true,
name: true,
code_prefix: true,
default_useful_life_years: true,
default_depreciation_method: true,
},
},
item_subcategories: { select: { id: true, code: true, name: true, item_category_id: true } },
location: { select: { id: true, code: true, name: true, type: true } },
departments: { select: { id: true, name: true } },
users_assets_assigned_to_user_idTousers: {
select: { id: true, full_name: true, employee_code: true },
},
users_assets_maintenance_incharge_idTousers: {
select: { id: true, full_name: true, employee_code: true },
},
asset_maintenance_logs: {
where: { deleted_at: null },
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
take: 1,
select: { id: true, performed_date: true, next_due_date: true },
},
vendors_assets_vendor_idTovendors: { select: { id: true, vendor_code: true, vendor_name: true } },
purchase_orders: { select: { id: true, po_number: true } },
grn: { select: { id: true, grn_number: true } },
users_assets_created_byTousers: { select: { id: true, full_name: true } },
};
const assetDetailInclude = {
...assetInclude,
users_assets_updated_byTousers: { select: { id: true, full_name: true } },
asset_attachments: {
orderBy: { created_at: 'desc' },
include: {
users: { select: { id: true, full_name: true, employee_code: true } },
},
},
};
const transferInclude = {
from_location: { select: { id: true, code: true, name: true, type: true } },
to_location: { select: { id: true, code: true, name: true, type: true } },
departments_asset_transfers_from_department_idTodepartments: { select: { id: true, name: true } },
departments_asset_transfers_to_department_idTodepartments: { select: { id: true, name: true } },
users_asset_transfers_from_user_idTousers: { select: { id: true, full_name: true } },
users_asset_transfers_to_user_idTousers: { select: { id: true, full_name: true } },
users_asset_transfers_transferred_byTousers: { select: { id: true, full_name: true } },
};
const assetSeriesCode = (categoryCode) => `ASSET_${categoryCode}`;
const startOfUtcDay = (value) => {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return null;
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
};
const addDays = (value, days) => {
const base = startOfUtcDay(value);
if (!base || days === null || days === undefined) return null;
base.setUTCDate(base.getUTCDate() + Number(days));
return base;
};
// Derive maintenance due state from the configured frequency, the latest log and
// the asset's usage start date (commencement_date, falling back to purchase_date).
const buildMaintenanceSummary = (rest, logs) => {
const frequency =
rest.maintenance_frequency_in_days !== null && rest.maintenance_frequency_in_days !== undefined
? Number(rest.maintenance_frequency_in_days)
: null;
const lastLog = Array.isArray(logs) && logs.length ? logs[0] : null;
const lastMaintenanceDate = lastLog?.performed_date ?? null;
const baseline = lastMaintenanceDate || rest.commencement_date || rest.purchase_date || null;
let nextDueDate = lastLog?.next_due_date ?? null;
if (!nextDueDate && frequency && baseline) {
nextDueDate = addDays(baseline, frequency);
}
const today = startOfUtcDay(new Date());
const isDue = Boolean(frequency && nextDueDate && new Date(nextDueDate) <= today);
const daysUntilDue =
nextDueDate && today
? Math.round((new Date(nextDueDate).getTime() - today.getTime()) / 86400000)
: null;
return {
frequency_in_days: frequency,
checklist: presentChecklistTemplate(rest.maintenance_checklist_json),
last_maintenance_date: lastMaintenanceDate,
next_due_date: nextDueDate,
is_due: isDue,
days_until_due: daysUntilDue,
};
};
const sanitizeAsset = (asset) => {
if (!asset) return null;
const {
item_categories,
item_subcategories,
location,
departments,
users_assets_assigned_to_user_idTousers,
users_assets_maintenance_incharge_idTousers,
asset_maintenance_logs,
vendors_assets_vendor_idTovendors,
purchase_orders,
grn,
users_assets_created_byTousers,
users_assets_updated_byTousers,
asset_attachments,
...rest
} = asset;
const purchaseCost =
rest.purchase_cost !== null && rest.purchase_cost !== undefined
? Number(rest.purchase_cost)
: 0;
const salvageValue =
rest.salvage_value !== null && rest.salvage_value !== undefined
? Number(rest.salvage_value)
: 0;
const depreciationRate =
rest.depreciation_rate !== null && rest.depreciation_rate !== undefined
? Number(rest.depreciation_rate)
: null;
const depreciation = calculateDepreciation({
depreciation_method: rest.depreciation_method,
depreciation_rate: depreciationRate,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
useful_life_years: rest.useful_life_years,
commencement_date: rest.commencement_date,
purchase_date: rest.purchase_date,
});
return {
...rest,
maintenance_checklist_json: presentChecklistTemplate(rest.maintenance_checklist_json),
purchase_cost: purchaseCost,
salvage_value: salvageValue,
depreciation_rate: depreciationRate,
// Current written-down / book value after depreciation (same as depreciation.book_value)
current_value: depreciation.book_value,
item_category: item_categories || null,
item_subcategory: item_subcategories || null,
location: location || null,
department: departments || null,
assigned_to_user: users_assets_assigned_to_user_idTousers || null,
maintenance_incharge_user: users_assets_maintenance_incharge_idTousers || null,
maintenance: buildMaintenanceSummary(rest, asset_maintenance_logs),
vendor: vendors_assets_vendor_idTovendors || null,
purchase_order: purchase_orders || null,
grn: grn || null,
created_by_user: users_assets_created_byTousers || null,
updated_by_user: users_assets_updated_byTousers || null,
attachments: (asset_attachments || []).map(sanitizeAttachment),
depreciation: {
...depreciation,
current_value: depreciation.book_value,
},
item_categories: undefined,
item_subcategories: undefined,
departments: undefined,
users_assets_assigned_to_user_idTousers: undefined,
users_assets_maintenance_incharge_idTousers: undefined,
asset_maintenance_logs: undefined,
vendors_assets_vendor_idTovendors: undefined,
purchase_orders: undefined,
users_assets_created_byTousers: undefined,
users_assets_updated_byTousers: undefined,
asset_attachments: undefined,
};
};
const sanitizeTransfer = (row) => {
if (!row) return null;
const {
from_location,
to_location,
departments_asset_transfers_from_department_idTodepartments,
departments_asset_transfers_to_department_idTodepartments,
users_asset_transfers_from_user_idTousers,
users_asset_transfers_to_user_idTousers,
users_asset_transfers_transferred_byTousers,
...rest
} = row;
return {
...rest,
from_location: from_location || null,
to_location: to_location || null,
from_department: departments_asset_transfers_from_department_idTodepartments || null,
to_department: departments_asset_transfers_to_department_idTodepartments || null,
from_user: users_asset_transfers_from_user_idTousers || null,
to_user: users_asset_transfers_to_user_idTousers || null,
transferred_by_user: users_asset_transfers_transferred_byTousers || null,
};
};
const assertDisposalFields = (status, disposalDate) => {
if (DISPOSAL_STATUSES.includes(status) && !disposalDate) {
throw new ApiError(422, 'disposal_date is required when status is DISPOSED or SCRAPPED');
}
};
const assertItemSubcategory = async (subcategoryId, categoryId) => {
const subcategory = await assertReference(
'item_subcategories',
subcategoryId,
'item_subcategory_id'
);
if (subcategory.item_category_id.toString() !== BigInt(categoryId).toString()) {
throw new ApiError(422, 'item_subcategory_id does not belong to the selected item_category_id');
}
return subcategory;
};
const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
const category = await assertReference(
'item_categories',
payload.item_category_id,
'item_category_id'
);
if (isCreate && !payload.item_subcategory_id) {
throw new ApiError(422, 'item_subcategory_id is required');
}
if (payload.item_subcategory_id) {
await assertItemSubcategory(payload.item_subcategory_id, payload.item_category_id);
}
await assertAnyLocation(payload.location_id, 'location_id');
if (payload.department_id)
await assertReference('departments', payload.department_id, 'department_id');
if (payload.assigned_to_user_id)
await assertReference('users', payload.assigned_to_user_id, 'assigned_to_user_id');
if (payload.maintenance_incharge_user_id)
await assertReference(
'users',
payload.maintenance_incharge_user_id,
'maintenance_incharge_user_id'
);
let checklistJson = null;
if (payload.maintenance_checklist_json !== undefined) {
if (payload.maintenance_checklist_json === null) {
checklistJson = null;
} else {
checklistJson = normalizeChecklistTemplate(payload.maintenance_checklist_json);
}
}
if (payload.vendor_id) await assertReference('vendors', payload.vendor_id, 'vendor_id');
if (payload.po_id) await assertReference('purchase_orders', payload.po_id, 'po_id');
if (payload.grn_id) await assertReference('grn', payload.grn_id, 'grn_id');
if (payload.grn_item_id)
await assertReference('grn_items', payload.grn_item_id, 'grn_item_id', {
requireActive: false,
});
const status = payload.status ?? 'IN_USE';
const disposalDate = payload.disposal_date ? toDateOnly(payload.disposal_date) : null;
assertDisposalFields(status, disposalDate);
const purchaseCost = payload.purchase_cost ?? 0;
const salvageValue = payload.salvage_value ?? 0;
const usefulLifeYears =
payload.useful_life_years !== undefined && payload.useful_life_years !== null
? payload.useful_life_years
: category.default_useful_life_years;
const depreciationMethod =
payload.depreciation_method || category.default_depreciation_method || null;
if (depreciationMethod === 'OTHER' && (payload.depreciation_rate === undefined || payload.depreciation_rate === null || payload.depreciation_rate === '')) {
throw new ApiError(422, 'depreciation_rate is required when depreciation_method is OTHER');
}
const depreciationRate = resolveDepreciationRate({
method: depreciationMethod,
depreciation_rate: payload.depreciation_rate,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
useful_life_years: usefulLifeYears,
});
return {
asset_name: payload.asset_name,
item_category_id: BigInt(payload.item_category_id),
item_subcategory_id: payload.item_subcategory_id
? BigInt(payload.item_subcategory_id)
: null,
brand_model: payload.brand_model || null,
manufacturer: payload.manufacturer || null,
serial_number: payload.serial_number || null,
part_number: payload.part_number || null,
location_id: BigInt(payload.location_id),
department_id: payload.department_id ? BigInt(payload.department_id) : null,
location_detail: payload.location_detail || null,
assigned_to_user_id: payload.assigned_to_user_id ? BigInt(payload.assigned_to_user_id) : null,
maintenance_incharge_user_id: payload.maintenance_incharge_user_id
? BigInt(payload.maintenance_incharge_user_id)
: null,
maintenance_frequency_in_days:
payload.maintenance_frequency_in_days !== undefined &&
payload.maintenance_frequency_in_days !== null &&
payload.maintenance_frequency_in_days !== ''
? Number(payload.maintenance_frequency_in_days)
: null,
maintenance_checklist_json:
payload.maintenance_checklist_json !== undefined ? checklistJson : null,
vendor_id: payload.vendor_id ? BigInt(payload.vendor_id) : null,
po_id: payload.po_id ? BigInt(payload.po_id) : null,
grn_id: payload.grn_id ? BigInt(payload.grn_id) : null,
grn_item_id: payload.grn_item_id ? BigInt(payload.grn_item_id) : null,
commencement_date: payload.commencement_date ? toDateOnly(payload.commencement_date) : null,
purchase_date: payload.purchase_date ? toDateOnly(payload.purchase_date) : null,
purchase_cost: purchaseCost,
useful_life_years: usefulLifeYears,
depreciation_method: depreciationMethod,
depreciation_rate: depreciationRate,
salvage_value: salvageValue,
warranty_expiry_date: payload.warranty_expiry_date
? toDateOnly(payload.warranty_expiry_date)
: null,
condition: payload.condition ?? (isCreate ? 'NEW' : undefined),
status,
qr_code_value: payload.qr_code_value || null,
disposal_date: disposalDate,
disposal_reason: payload.disposal_reason || null,
disposal_value: payload.disposal_value ?? null,
remarks: payload.remarks || null,
is_active: payload.is_active ?? (isCreate ? true : undefined),
category,
};
};
const getAssetOrThrow = async (id) => {
const asset = await prisma.assets.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: assetDetailInclude,
});
if (!asset) throw new ApiError(404, 'Asset not found');
return asset;
};
const createAsset = async (payload, userId, requestId) => {
const normalized = await normalizeAssetPayload(payload, { isCreate: true });
const { category, ...data } = normalized;
void category;
const assetCode = await nextDocumentNumber(assetSeriesCode(normalized.category.code));
const created = await prisma.assets.create({
data: {
...data,
asset_code: assetCode,
created_by: userId ? BigInt(userId) : null,
updated_by: userId ? BigInt(userId) : null,
},
include: assetDetailInclude,
});
await auditLog({
tableName: 'assets',
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: sanitizeAsset(created),
userId,
requestId,
});
return sanitizeAsset(created);
};
const buildAssetsWhere = (query) => ({
deleted_at: null,
...(query.status ? { status: query.status } : {}),
...(query.condition ? { condition: query.condition } : {}),
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
...(query.item_subcategory_id
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
: {}),
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
...(query.maintenance_incharge_user_id
? { maintenance_incharge_user_id: BigInt(query.maintenance_incharge_user_id) }
: {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.search
? {
OR: [
{ asset_code: { contains: query.search, mode: 'insensitive' } },
{ asset_name: { contains: query.search, mode: 'insensitive' } },
{ serial_number: { contains: query.search, mode: 'insensitive' } },
{ qr_code_value: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
});
const listAssets = async (query) => {
const where = buildAssetsWhere(query);
if (isDropdownCall(query)) {
where.is_active = true;
const rows = await prisma.assets.findMany({
where,
include: assetInclude,
orderBy: { created_at: 'desc' },
});
let data = rows.map(sanitizeAsset);
if (query.due_only === true) {
data = data.filter((row) => row.maintenance?.is_due === true);
}
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
}
if (query.due_only === true) {
const rows = await prisma.assets.findMany({
where,
include: assetInclude,
orderBy: { created_at: 'desc' },
});
const due = rows.map(sanitizeAsset).filter((row) => row.maintenance?.is_due === true);
const { page, limit, skip } = getPagination(query);
return {
data: due.slice(skip, skip + limit),
meta: { page, limit, total: due.length },
};
}
const { page, limit, skip } = getPagination(query);
const [rows, total] = await Promise.all([
prisma.assets.findMany({
where,
include: assetInclude,
orderBy: { created_at: 'desc' },
skip,
take: limit,
}),
prisma.assets.count({ where }),
]);
return { data: rows.map(sanitizeAsset), meta: { page, limit, total } };
};
const exportAssets = async (query) => {
const rows = await prisma.assets.findMany({
where: buildAssetsWhere(query),
include: assetInclude,
orderBy: { created_at: 'desc' },
});
return rowsToCsv(
[
{ key: 'asset_code', header: 'Asset Code' },
{ key: 'asset_name', header: 'Asset Name' },
{ key: (row) => row.item_category?.name || '', header: 'Category' },
{ key: (row) => row.item_subcategory?.name || '', header: 'Subcategory' },
{ key: 'serial_number', header: 'Serial Number' },
{ key: 'condition', header: 'Condition' },
{ key: 'status', header: 'Status' },
{ key: (row) => row.location?.name || '', header: 'Location' },
{ key: (row) => row.department?.name || '', header: 'Department' },
{ key: (row) => row.assigned_to_user?.full_name || '', header: 'Assigned To' },
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor' },
{ key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' },
{ key: (row) => row.grn?.grn_number || '', header: 'GRN Number' },
{ key: 'commencement_date', header: 'Commencement Date', type: 'date' },
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
{ key: 'purchase_cost', header: 'Purchase Cost' },
{ key: 'current_value', header: 'Current Value' },
{ key: (row) => row.depreciation?.accumulated_depreciation ?? '', header: 'Accumulated Depreciation' },
{ key: (row) => row.depreciation?.annual_depreciation ?? '', header: 'Annual Depreciation' },
{ key: (row) => row.depreciation?.depreciation_method || '', header: 'Depreciation Method' },
{ key: (row) => row.depreciation?.depreciation_rate ?? '', header: 'Depreciation Rate %' },
{ key: 'warranty_expiry_date', header: 'Warranty Expiry', type: 'date' },
{
key: (row) => row.maintenance_incharge_user?.full_name || '',
header: 'Maintenance Incharge',
},
{ key: (row) => row.maintenance?.frequency_in_days ?? '', header: 'Maintenance Freq (days)' },
{ key: (row) => row.maintenance?.next_due_date || '', header: 'Maintenance Next Due', type: 'date' },
{ key: 'is_active', header: 'Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
],
rows.map(sanitizeAsset)
);
};
const getAssetById = async (id) => sanitizeAsset(await getAssetOrThrow(id));
const updateAsset = async (id, payload, userId, requestId) => {
const existing = await getAssetOrThrow(id);
const merged = {
asset_name: payload.asset_name ?? existing.asset_name,
item_category_id: payload.item_category_id ?? existing.item_category_id,
item_subcategory_id:
payload.item_subcategory_id !== undefined
? payload.item_subcategory_id
: existing.item_subcategory_id,
brand_model: payload.brand_model !== undefined ? payload.brand_model : existing.brand_model,
manufacturer: payload.manufacturer !== undefined ? payload.manufacturer : existing.manufacturer,
serial_number:
payload.serial_number !== undefined ? payload.serial_number : existing.serial_number,
part_number: payload.part_number !== undefined ? payload.part_number : existing.part_number,
location_id: payload.location_id ?? existing.location_id,
department_id:
payload.department_id !== undefined ? payload.department_id : existing.department_id,
location_detail:
payload.location_detail !== undefined ? payload.location_detail : existing.location_detail,
assigned_to_user_id:
payload.assigned_to_user_id !== undefined
? payload.assigned_to_user_id
: existing.assigned_to_user_id,
maintenance_incharge_user_id:
payload.maintenance_incharge_user_id !== undefined
? payload.maintenance_incharge_user_id
: existing.maintenance_incharge_user_id,
maintenance_frequency_in_days:
payload.maintenance_frequency_in_days !== undefined
? payload.maintenance_frequency_in_days
: existing.maintenance_frequency_in_days,
maintenance_checklist_json:
payload.maintenance_checklist_json !== undefined
? payload.maintenance_checklist_json
: existing.maintenance_checklist_json,
vendor_id: payload.vendor_id !== undefined ? payload.vendor_id : existing.vendor_id,
po_id: payload.po_id !== undefined ? payload.po_id : existing.po_id,
grn_id: payload.grn_id !== undefined ? payload.grn_id : existing.grn_id,
grn_item_id: payload.grn_item_id !== undefined ? payload.grn_item_id : existing.grn_item_id,
commencement_date:
payload.commencement_date !== undefined
? payload.commencement_date
: existing.commencement_date,
purchase_date:
payload.purchase_date !== undefined ? payload.purchase_date : existing.purchase_date,
purchase_cost: payload.purchase_cost ?? Number(existing.purchase_cost),
useful_life_years:
payload.useful_life_years !== undefined
? payload.useful_life_years
: existing.useful_life_years,
depreciation_method:
payload.depreciation_method !== undefined
? payload.depreciation_method
: existing.depreciation_method,
depreciation_rate:
payload.depreciation_rate !== undefined
? payload.depreciation_rate
: existing.depreciation_rate !== null && existing.depreciation_rate !== undefined
? Number(existing.depreciation_rate)
: null,
salvage_value: payload.salvage_value ?? Number(existing.salvage_value ?? 0),
warranty_expiry_date:
payload.warranty_expiry_date !== undefined
? payload.warranty_expiry_date
: existing.warranty_expiry_date,
condition: payload.condition ?? existing.condition,
status: payload.status ?? existing.status,
qr_code_value:
payload.qr_code_value !== undefined ? payload.qr_code_value : existing.qr_code_value,
disposal_date:
payload.disposal_date !== undefined ? payload.disposal_date : existing.disposal_date,
disposal_reason:
payload.disposal_reason !== undefined ? payload.disposal_reason : existing.disposal_reason,
disposal_value:
payload.disposal_value !== undefined ? payload.disposal_value : existing.disposal_value,
remarks: payload.remarks !== undefined ? payload.remarks : existing.remarks,
is_active: payload.is_active !== undefined ? payload.is_active : existing.is_active,
};
const normalized = await normalizeAssetPayload(merged);
const { category, ...data } = normalized;
void category;
const updated = await prisma.assets.update({
where: { id: BigInt(id) },
data: {
...data,
updated_by: userId ? BigInt(userId) : null,
},
include: assetDetailInclude,
});
await auditLog({
tableName: 'assets',
recordId: id,
action: 'UPDATE',
oldValue: sanitizeAsset(existing),
newValue: sanitizeAsset(updated),
userId,
requestId,
});
return sanitizeAsset(updated);
};
const deleteAsset = async (id, userId, requestId) => {
const existing = await getAssetOrThrow(id);
if (DISPOSAL_STATUSES.includes(existing.status)) {
throw new ApiError(409, 'Disposed or scrapped assets cannot be deleted');
}
const deleted = await prisma.assets.update({
where: { id: BigInt(id) },
data: {
deleted_at: new Date(),
is_active: false,
updated_by: userId ? BigInt(userId) : null,
},
});
await auditLog({
tableName: 'assets',
recordId: id,
action: 'DELETE',
oldValue: sanitizeAsset(existing),
newValue: deleted,
userId,
requestId,
});
};
const transferAsset = async (id, payload, userId, requestId) => {
const existing = await getAssetOrThrow(id);
if (DISPOSAL_STATUSES.includes(existing.status)) {
throw new ApiError(409, 'Cannot transfer disposed or scrapped assets');
}
if (payload.to_location_id) await assertAnyLocation(payload.to_location_id, 'to_location_id');
if (payload.to_department_id) {
await assertReference('departments', payload.to_department_id, 'to_department_id');
}
if (payload.to_user_id) await assertReference('users', payload.to_user_id, 'to_user_id');
const transfer = {
transfer_date: toDateOnly(payload.transfer_date),
from_location_id: existing.location_id,
to_location_id: payload.to_location_id ? BigInt(payload.to_location_id) : null,
from_department_id: existing.department_id,
to_department_id: payload.to_department_id ? BigInt(payload.to_department_id) : null,
from_user_id: existing.assigned_to_user_id,
to_user_id: payload.to_user_id ? BigInt(payload.to_user_id) : null,
reason: payload.reason || null,
};
const updates = {
...(payload.to_location_id ? { location_id: BigInt(payload.to_location_id) } : {}),
...(payload.to_department_id !== undefined
? { department_id: payload.to_department_id ? BigInt(payload.to_department_id) : null }
: {}),
...(payload.to_user_id !== undefined
? { assigned_to_user_id: payload.to_user_id ? BigInt(payload.to_user_id) : null }
: {}),
};
const { transferRow } = await repository.transferAsset({
assetId: id,
transfer,
updates,
userId,
});
const detail = await prisma.asset_transfers.findFirst({
where: { id: transferRow.id },
include: transferInclude,
});
const updatedAsset = await getAssetOrThrow(id);
await auditLog({
tableName: 'asset_transfers',
recordId: transferRow.id,
action: 'TRANSFER',
oldValue: sanitizeAsset(existing),
newValue: { transfer: sanitizeTransfer(detail), asset: sanitizeAsset(updatedAsset) },
userId,
requestId,
});
return {
asset: sanitizeAsset(updatedAsset),
transfer: sanitizeTransfer(detail),
};
};
const getTransferHistory = async (id) => {
await getAssetOrThrow(id);
const rows = await prisma.asset_transfers.findMany({
where: { asset_id: BigInt(id) },
include: transferInclude,
orderBy: [{ transfer_date: 'desc' }, { created_at: 'desc' }],
});
return rows.map(sanitizeTransfer);
};
const listDepreciationMethods = () => DEPRECIATION_METHOD_OPTIONS;
const listContractTypes = () => getAssetDropdownOptions().contract_types;
const listVisitTypes = () => getAssetDropdownOptions().visit_types;
const listPolicyTypes = () => getAssetDropdownOptions().policy_types;
const listAssetConditions = () => getAssetDropdownOptions().asset_conditions;
const listAssetStatuses = () => getAssetDropdownOptions().asset_statuses;
const listPaymentFrequencies = () => getAssetDropdownOptions().payment_frequencies;
const listServiceFrequencies = () => getAssetDropdownOptions().service_frequencies;
const listVisitStatuses = () => getAssetDropdownOptions().visit_statuses;
const listVisitConditionsAfter = () => getAssetDropdownOptions().visit_conditions_after;
const listAssetOptions = () => getAssetDropdownOptions();
const previewDepreciation = (payload) => {
if (payload.depreciation_method === 'OTHER' && (payload.depreciation_rate === undefined || payload.depreciation_rate === null)) {
throw new ApiError(422, 'depreciation_rate is required when depreciation_method is OTHER');
}
return calculateDepreciation(payload);
};
module.exports = {
createAsset,
listAssets,
exportAssets,
getAssetById,
updateAsset,
deleteAsset,
transferAsset,
getTransferHistory,
listDepreciationMethods,
listContractTypes,
listVisitTypes,
listPolicyTypes,
listAssetConditions,
listAssetStatuses,
listPaymentFrequencies,
listServiceFrequencies,
listVisitStatuses,
listVisitConditionsAfter,
listAssetOptions,
previewDepreciation,
sanitizeAsset,
};