@hed-hog/finance 0.0.235 → 0.0.237

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.
Files changed (75) hide show
  1. package/dist/dto/create-cost-center.dto.d.ts +4 -0
  2. package/dist/dto/create-cost-center.dto.d.ts.map +1 -0
  3. package/dist/dto/create-cost-center.dto.js +24 -0
  4. package/dist/dto/create-cost-center.dto.js.map +1 -0
  5. package/dist/dto/create-finance-category.dto.d.ts +6 -0
  6. package/dist/dto/create-finance-category.dto.d.ts.map +1 -0
  7. package/dist/dto/create-finance-category.dto.js +37 -0
  8. package/dist/dto/create-finance-category.dto.js.map +1 -0
  9. package/dist/dto/create-period-close.dto.d.ts +7 -0
  10. package/dist/dto/create-period-close.dto.d.ts.map +1 -0
  11. package/dist/dto/create-period-close.dto.js +44 -0
  12. package/dist/dto/create-period-close.dto.js.map +1 -0
  13. package/dist/dto/move-finance-category.dto.d.ts +5 -0
  14. package/dist/dto/move-finance-category.dto.d.ts.map +1 -0
  15. package/dist/dto/move-finance-category.dto.js +32 -0
  16. package/dist/dto/move-finance-category.dto.js.map +1 -0
  17. package/dist/dto/update-cost-center.dto.d.ts +5 -0
  18. package/dist/dto/update-cost-center.dto.d.ts.map +1 -0
  19. package/dist/dto/update-cost-center.dto.js +32 -0
  20. package/dist/dto/update-cost-center.dto.js.map +1 -0
  21. package/dist/dto/update-finance-category.dto.d.ts +7 -0
  22. package/dist/dto/update-finance-category.dto.d.ts.map +1 -0
  23. package/dist/dto/update-finance-category.dto.js +46 -0
  24. package/dist/dto/update-finance-category.dto.js.map +1 -0
  25. package/dist/finance-audit-logs.controller.d.ts +13 -0
  26. package/dist/finance-audit-logs.controller.d.ts.map +1 -0
  27. package/dist/finance-audit-logs.controller.js +54 -0
  28. package/dist/finance-audit-logs.controller.js.map +1 -0
  29. package/dist/finance-categories.controller.d.ts +42 -0
  30. package/dist/finance-categories.controller.d.ts.map +1 -0
  31. package/dist/finance-categories.controller.js +84 -0
  32. package/dist/finance-categories.controller.js.map +1 -0
  33. package/dist/finance-cost-centers.controller.d.ts +32 -0
  34. package/dist/finance-cost-centers.controller.d.ts.map +1 -0
  35. package/dist/finance-cost-centers.controller.js +72 -0
  36. package/dist/finance-cost-centers.controller.js.map +1 -0
  37. package/dist/finance-period-close.controller.d.ts +27 -0
  38. package/dist/finance-period-close.controller.d.ts.map +1 -0
  39. package/dist/finance-period-close.controller.js +64 -0
  40. package/dist/finance-period-close.controller.js.map +1 -0
  41. package/dist/finance.module.d.ts.map +1 -1
  42. package/dist/finance.module.js +8 -0
  43. package/dist/finance.module.js.map +1 -1
  44. package/dist/finance.service.d.ts +111 -0
  45. package/dist/finance.service.d.ts.map +1 -1
  46. package/dist/finance.service.js +446 -17
  47. package/dist/finance.service.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -0
  51. package/dist/index.js.map +1 -1
  52. package/hedhog/data/route.yaml +108 -0
  53. package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +627 -0
  54. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +865 -883
  55. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +838 -861
  56. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +309 -0
  57. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +725 -0
  58. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +378 -0
  59. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +502 -0
  60. package/hedhog/frontend/messages/en.json +225 -0
  61. package/hedhog/frontend/messages/pt.json +225 -0
  62. package/package.json +5 -5
  63. package/src/dto/create-cost-center.dto.ts +9 -0
  64. package/src/dto/create-finance-category.dto.ts +21 -0
  65. package/src/dto/create-period-close.dto.ts +34 -0
  66. package/src/dto/move-finance-category.dto.ts +18 -0
  67. package/src/dto/update-cost-center.dto.ts +17 -0
  68. package/src/dto/update-finance-category.dto.ts +30 -0
  69. package/src/finance-audit-logs.controller.ts +30 -0
  70. package/src/finance-categories.controller.ts +52 -0
  71. package/src/finance-cost-centers.controller.ts +43 -0
  72. package/src/finance-period-close.controller.ts +34 -0
  73. package/src/finance.module.ts +8 -0
  74. package/src/finance.service.ts +578 -9
  75. package/src/index.ts +4 -0
@@ -9,8 +9,14 @@ import {
9
9
  NotFoundException
10
10
  } from '@nestjs/common';
11
11
  import { CreateBankAccountDto } from './dto/create-bank-account.dto';
12
+ import { CreateCostCenterDto } from './dto/create-cost-center.dto';
13
+ import { CreateFinanceCategoryDto } from './dto/create-finance-category.dto';
12
14
  import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
15
+ import { CreatePeriodCloseDto } from './dto/create-period-close.dto';
16
+ import { MoveFinanceCategoryDto } from './dto/move-finance-category.dto';
13
17
  import { UpdateBankAccountDto } from './dto/update-bank-account.dto';
18
+ import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
19
+ import { UpdateFinanceCategoryDto } from './dto/update-finance-category.dto';
14
20
 
15
21
  type TitleType = 'payable' | 'receivable';
16
22
 
@@ -797,6 +803,221 @@ export class FinanceService {
797
803
  return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
798
804
  }
799
805
 
806
+ async listCostCenters() {
807
+ const costCenters = await this.prisma.cost_center.findMany({
808
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
809
+ });
810
+
811
+ return costCenters.map((costCenter) => this.mapCostCenterToFront(costCenter));
812
+ }
813
+
814
+ async listAuditLogs(
815
+ paginationParams,
816
+ filters?: {
817
+ search?: string;
818
+ action?: string;
819
+ entity_table?: string;
820
+ actor_user_id?: string;
821
+ from?: string;
822
+ to?: string;
823
+ },
824
+ ) {
825
+ const actorUserId = filters?.actor_user_id
826
+ ? Number.parseInt(filters.actor_user_id, 10)
827
+ : undefined;
828
+
829
+ const fromDate = filters?.from ? new Date(filters.from) : undefined;
830
+ const toDate = filters?.to ? new Date(filters.to) : undefined;
831
+
832
+ const where: any = {
833
+ ...(filters?.action ? { action: filters.action } : {}),
834
+ ...(filters?.entity_table ? { entity_table: filters.entity_table } : {}),
835
+ ...(Number.isNaN(actorUserId) || !actorUserId
836
+ ? {}
837
+ : { actor_user_id: actorUserId }),
838
+ ...((fromDate || toDate) &&
839
+ !(fromDate && Number.isNaN(fromDate.getTime())) &&
840
+ !(toDate && Number.isNaN(toDate.getTime()))
841
+ ? {
842
+ created_at: {
843
+ ...(fromDate ? { gte: fromDate } : {}),
844
+ ...(toDate ? { lte: toDate } : {}),
845
+ },
846
+ }
847
+ : {}),
848
+ };
849
+
850
+ const search = filters?.search?.trim();
851
+ if (search) {
852
+ where.OR = [
853
+ { action: { contains: search, mode: 'insensitive' } },
854
+ { entity_table: { contains: search, mode: 'insensitive' } },
855
+ { entity_id: { contains: search, mode: 'insensitive' } },
856
+ { summary: { contains: search, mode: 'insensitive' } },
857
+ { ip_address: { contains: search, mode: 'insensitive' } },
858
+ ];
859
+ }
860
+
861
+ const paginated = await this.paginationService.paginatePrismaModel(
862
+ this.prisma.audit_log,
863
+ {
864
+ page: paginationParams?.page,
865
+ pageSize: paginationParams?.pageSize,
866
+ sortField: paginationParams?.sortField || 'created_at',
867
+ sortOrder: paginationParams?.sortOrder || 'desc',
868
+ validSortFields: [
869
+ 'id',
870
+ 'created_at',
871
+ 'action',
872
+ 'entity_table',
873
+ 'entity_id',
874
+ ],
875
+ where,
876
+ include: {
877
+ user: {
878
+ select: {
879
+ id: true,
880
+ name: true,
881
+ },
882
+ },
883
+ },
884
+ },
885
+ );
886
+
887
+ return {
888
+ ...paginated,
889
+ data: (paginated.data || []).map((log) => ({
890
+ id: String(log.id),
891
+ actorUserId: log.actor_user_id ? String(log.actor_user_id) : null,
892
+ actorName: log.user?.name || '-',
893
+ actorEmail: '',
894
+ action: log.action,
895
+ entityTable: log.entity_table,
896
+ entityId: log.entity_id,
897
+ summary: log.summary || '',
898
+ ipAddress: log.ip_address || '',
899
+ beforeData: log.before_data || '',
900
+ afterData: log.after_data || '',
901
+ createdAt: log.created_at?.toISOString?.() || null,
902
+ })),
903
+ };
904
+ }
905
+
906
+ async listFinanceCategories() {
907
+ const categories = await this.prisma.finance_category.findMany({
908
+ orderBy: [{ parent_id: 'asc' }, { updated_at: 'asc' }, { name: 'asc' }],
909
+ });
910
+
911
+ return categories.map((category) => this.mapFinanceCategoryToFront(category));
912
+ }
913
+
914
+ async listPeriodClose(
915
+ paginationParams: PaginationDTO,
916
+ filters?: {
917
+ search?: string;
918
+ status?: string;
919
+ user?: string;
920
+ from?: string;
921
+ to?: string;
922
+ },
923
+ ) {
924
+ const fromDate = filters?.from ? new Date(filters.from) : undefined;
925
+ const toDate = filters?.to ? new Date(filters.to) : undefined;
926
+ const userNumericId = filters?.user
927
+ ? Number.parseInt(filters.user, 10)
928
+ : undefined;
929
+
930
+ const andConditions: any[] = [];
931
+
932
+ if (filters?.status && filters.status !== 'all') {
933
+ andConditions.push({ status: filters.status });
934
+ }
935
+
936
+ if (
937
+ (fromDate && !Number.isNaN(fromDate.getTime())) ||
938
+ (toDate && !Number.isNaN(toDate.getTime()))
939
+ ) {
940
+ andConditions.push({
941
+ period_start: {
942
+ ...(fromDate && !Number.isNaN(fromDate.getTime())
943
+ ? { gte: fromDate }
944
+ : {}),
945
+ },
946
+ });
947
+
948
+ andConditions.push({
949
+ period_end: {
950
+ ...(toDate && !Number.isNaN(toDate.getTime()) ? { lte: toDate } : {}),
951
+ },
952
+ });
953
+ }
954
+
955
+ const search = filters?.search?.trim();
956
+ if (search) {
957
+ andConditions.push({
958
+ OR: [
959
+ { notes: { contains: search, mode: 'insensitive' } },
960
+ { user: { is: { name: { contains: search, mode: 'insensitive' } } } },
961
+ {
962
+ user: { is: { email: { contains: search, mode: 'insensitive' } } },
963
+ },
964
+ ],
965
+ });
966
+ }
967
+
968
+ if (filters?.user?.trim()) {
969
+ andConditions.push({
970
+ OR: [
971
+ ...(Number.isNaN(userNumericId) || !userNumericId
972
+ ? []
973
+ : [{ closed_by_user_id: userNumericId }]),
974
+ { user: { is: { name: { contains: filters.user, mode: 'insensitive' } } } },
975
+ {
976
+ user: {
977
+ is: { email: { contains: filters.user, mode: 'insensitive' } },
978
+ },
979
+ },
980
+ ],
981
+ });
982
+ }
983
+
984
+ const where = andConditions.length > 0 ? { AND: andConditions } : {};
985
+
986
+ const paginated = await this.paginationService.paginatePrismaModel(
987
+ this.prisma.period_close,
988
+ {
989
+ page: paginationParams?.page,
990
+ pageSize: paginationParams?.pageSize,
991
+ sortField: paginationParams?.sortField || 'period_start',
992
+ sortOrder: paginationParams?.sortOrder || 'desc',
993
+ validSortFields: [
994
+ 'id',
995
+ 'period_start',
996
+ 'period_end',
997
+ 'status',
998
+ 'closed_at',
999
+ 'created_at',
1000
+ ],
1001
+ where,
1002
+ include: {
1003
+ user: {
1004
+ select: {
1005
+ id: true,
1006
+ name: true,
1007
+ },
1008
+ },
1009
+ },
1010
+ },
1011
+ );
1012
+
1013
+ return {
1014
+ ...paginated,
1015
+ data: (paginated.data || []).map((period) =>
1016
+ this.mapPeriodCloseToFront(period),
1017
+ ),
1018
+ };
1019
+ }
1020
+
800
1021
  async listBankStatements(bankAccountId?: number) {
801
1022
  const statements = await this.prisma.bank_statement_line.findMany({
802
1023
  where: {
@@ -879,6 +1100,89 @@ export class FinanceService {
879
1100
  return this.mapBankAccountToFront(account);
880
1101
  }
881
1102
 
1103
+ async createCostCenter(data: CreateCostCenterDto) {
1104
+ const code = await this.generateCostCenterCode(data.name);
1105
+
1106
+ const created = await this.prisma.cost_center.create({
1107
+ data: {
1108
+ code,
1109
+ name: data.name.trim(),
1110
+ status: 'active',
1111
+ },
1112
+ });
1113
+
1114
+ return this.mapCostCenterToFront(created);
1115
+ }
1116
+
1117
+ async createFinanceCategory(data: CreateFinanceCategoryDto) {
1118
+ if (data.parent_id) {
1119
+ await this.ensureFinanceCategoryExists(data.parent_id);
1120
+ }
1121
+
1122
+ const created = await this.prisma.finance_category.create({
1123
+ data: {
1124
+ code: await this.generateFinanceCategoryCode(),
1125
+ name: data.name.trim(),
1126
+ kind: this.mapCategoryKindFromPt(data.kind),
1127
+ status: 'active',
1128
+ parent_id: data.parent_id || null,
1129
+ },
1130
+ });
1131
+
1132
+ return this.mapFinanceCategoryToFront(created);
1133
+ }
1134
+
1135
+ async createPeriodClose(data: CreatePeriodCloseDto, userId?: number) {
1136
+ const periodStart = new Date(data.period_start);
1137
+ const periodEnd = new Date(data.period_end);
1138
+
1139
+ if (
1140
+ Number.isNaN(periodStart.getTime()) ||
1141
+ Number.isNaN(periodEnd.getTime())
1142
+ ) {
1143
+ throw new BadRequestException('Invalid period dates');
1144
+ }
1145
+
1146
+ if (periodStart > periodEnd) {
1147
+ throw new BadRequestException('period_start must be before period_end');
1148
+ }
1149
+
1150
+ const overlapped = await this.prisma.period_close.findFirst({
1151
+ where: {
1152
+ period_start: { lte: periodEnd },
1153
+ period_end: { gte: periodStart },
1154
+ },
1155
+ select: { id: true },
1156
+ });
1157
+
1158
+ if (overlapped) {
1159
+ throw new BadRequestException('There is already a period in this range');
1160
+ }
1161
+
1162
+ const status = data.status || 'closed';
1163
+
1164
+ const created = await this.prisma.period_close.create({
1165
+ data: {
1166
+ period_start: periodStart,
1167
+ period_end: periodEnd,
1168
+ status,
1169
+ closed_at: status === 'closed' ? new Date() : null,
1170
+ closed_by_user_id: status === 'closed' ? userId || null : null,
1171
+ notes: data.notes?.trim() || null,
1172
+ },
1173
+ include: {
1174
+ user: {
1175
+ select: {
1176
+ id: true,
1177
+ name: true,
1178
+ },
1179
+ },
1180
+ },
1181
+ });
1182
+
1183
+ return this.mapPeriodCloseToFront(created);
1184
+ }
1185
+
882
1186
  async updateBankAccount(id: number, data: UpdateBankAccountDto) {
883
1187
  const current = await this.prisma.bank_account.findUnique({
884
1188
  where: { id },
@@ -912,6 +1216,104 @@ export class FinanceService {
912
1216
  return this.mapBankAccountToFront(updated);
913
1217
  }
914
1218
 
1219
+ async updateCostCenter(id: number, data: UpdateCostCenterDto) {
1220
+ const current = await this.prisma.cost_center.findUnique({
1221
+ where: { id },
1222
+ select: { id: true },
1223
+ });
1224
+
1225
+ if (!current) {
1226
+ throw new NotFoundException('Cost center not found');
1227
+ }
1228
+
1229
+ const updated = await this.prisma.cost_center.update({
1230
+ where: { id },
1231
+ data: {
1232
+ name: data.name?.trim(),
1233
+ status: data.status,
1234
+ },
1235
+ });
1236
+
1237
+ return this.mapCostCenterToFront(updated);
1238
+ }
1239
+
1240
+ async updateFinanceCategory(id: number, data: UpdateFinanceCategoryDto) {
1241
+ const current = await this.prisma.finance_category.findUnique({
1242
+ where: { id },
1243
+ select: { id: true },
1244
+ });
1245
+
1246
+ if (!current) {
1247
+ throw new NotFoundException('Finance category not found');
1248
+ }
1249
+
1250
+ if (data.parent_id) {
1251
+ await this.ensureFinanceCategoryExists(data.parent_id);
1252
+ await this.ensureNoFinanceCategoryCycle(id, data.parent_id);
1253
+ }
1254
+
1255
+ const updated = await this.prisma.finance_category.update({
1256
+ where: { id },
1257
+ data: {
1258
+ name: data.name?.trim(),
1259
+ kind: data.kind ? this.mapCategoryKindFromPt(data.kind) : undefined,
1260
+ parent_id: data.parent_id,
1261
+ status: data.status,
1262
+ },
1263
+ });
1264
+
1265
+ return this.mapFinanceCategoryToFront(updated);
1266
+ }
1267
+
1268
+ async moveFinanceCategory(id: number, data: MoveFinanceCategoryDto) {
1269
+ const current = await this.prisma.finance_category.findUnique({
1270
+ where: { id },
1271
+ select: { id: true, parent_id: true },
1272
+ });
1273
+
1274
+ if (!current) {
1275
+ throw new NotFoundException('Finance category not found');
1276
+ }
1277
+
1278
+ const targetParentId = data.parent_id || null;
1279
+
1280
+ if (targetParentId) {
1281
+ await this.ensureFinanceCategoryExists(targetParentId);
1282
+ await this.ensureNoFinanceCategoryCycle(id, targetParentId);
1283
+ }
1284
+
1285
+ await this.prisma.finance_category.update({
1286
+ where: { id },
1287
+ data: { parent_id: targetParentId },
1288
+ });
1289
+
1290
+ const siblings = await this.prisma.finance_category.findMany({
1291
+ where: { parent_id: targetParentId },
1292
+ select: { id: true },
1293
+ orderBy: [{ updated_at: 'asc' }, { name: 'asc' }],
1294
+ });
1295
+
1296
+ const siblingIds = siblings.map((item) => item.id).filter((item) => item !== id);
1297
+ const targetPosition = Math.max(
1298
+ 0,
1299
+ Math.min(data.position ?? siblingIds.length, siblingIds.length),
1300
+ );
1301
+ siblingIds.splice(targetPosition, 0, id);
1302
+
1303
+ await this.prisma.$transaction(
1304
+ siblingIds.map((categoryId) =>
1305
+ this.prisma.finance_category.update({
1306
+ where: { id: categoryId },
1307
+ data: {
1308
+ parent_id: targetParentId,
1309
+ },
1310
+ }),
1311
+ ),
1312
+ );
1313
+
1314
+ return { success: true };
1315
+ }
1316
+
915
1317
  async deleteBankAccount(id: number) {
916
1318
  const current = await this.prisma.bank_account.findUnique({
917
1319
  where: { id },
@@ -932,6 +1334,46 @@ export class FinanceService {
932
1334
  return { success: true };
933
1335
  }
934
1336
 
1337
+ async deleteCostCenter(id: number) {
1338
+ const current = await this.prisma.cost_center.findUnique({
1339
+ where: { id },
1340
+ select: { id: true },
1341
+ });
1342
+
1343
+ if (!current) {
1344
+ throw new NotFoundException('Cost center not found');
1345
+ }
1346
+
1347
+ await this.prisma.cost_center.update({
1348
+ where: { id },
1349
+ data: {
1350
+ status: 'inactive',
1351
+ },
1352
+ });
1353
+
1354
+ return { success: true };
1355
+ }
1356
+
1357
+ async deleteFinanceCategory(id: number) {
1358
+ const current = await this.prisma.finance_category.findUnique({
1359
+ where: { id },
1360
+ select: { id: true },
1361
+ });
1362
+
1363
+ if (!current) {
1364
+ throw new NotFoundException('Finance category not found');
1365
+ }
1366
+
1367
+ await this.prisma.finance_category.update({
1368
+ where: { id },
1369
+ data: {
1370
+ status: 'inactive',
1371
+ },
1372
+ });
1373
+
1374
+ return { success: true };
1375
+ }
1376
+
935
1377
  private async listTitles(
936
1378
  titleType: TitleType,
937
1379
  paginationParams: PaginationDTO,
@@ -1096,14 +1538,6 @@ export class FinanceService {
1096
1538
 
1097
1539
  private async loadPeople() {
1098
1540
  const people = await this.prisma.person.findMany({
1099
- include: {
1100
- document: {
1101
- select: {
1102
- value: true,
1103
- },
1104
- take: 1,
1105
- },
1106
- },
1107
1541
  orderBy: { name: 'asc' },
1108
1542
  });
1109
1543
 
@@ -1111,7 +1545,7 @@ export class FinanceService {
1111
1545
  id: String(person.id),
1112
1546
  nome: person.name,
1113
1547
  tipo: 'ambos',
1114
- documento: person.document[0]?.value || '',
1548
+ documento: '',
1115
1549
  }));
1116
1550
  }
1117
1551
 
@@ -1394,6 +1828,76 @@ export class FinanceService {
1394
1828
  };
1395
1829
  }
1396
1830
 
1831
+ private mapCostCenterToFront(costCenter: any) {
1832
+ return {
1833
+ id: String(costCenter.id),
1834
+ codigo: costCenter.code,
1835
+ nome: costCenter.name,
1836
+ status: costCenter.status,
1837
+ ativo: costCenter.status === 'active',
1838
+ };
1839
+ }
1840
+
1841
+ private mapCategoryKindToPt(kind?: string | null) {
1842
+ const kindMap = {
1843
+ revenue: 'receita',
1844
+ expense: 'despesa',
1845
+ transfer: 'transferencia',
1846
+ adjustment: 'ajuste',
1847
+ other: 'outro',
1848
+ };
1849
+
1850
+ return kindMap[kind] || 'outro';
1851
+ }
1852
+
1853
+ private mapCategoryKindFromPt(kind?: string | null) {
1854
+ const normalized = (kind || '').toLowerCase();
1855
+ const kindMap = {
1856
+ receita: 'revenue',
1857
+ revenue: 'revenue',
1858
+ despesa: 'expense',
1859
+ expense: 'expense',
1860
+ transferencia: 'transfer',
1861
+ transferência: 'transfer',
1862
+ transfer: 'transfer',
1863
+ ajuste: 'adjustment',
1864
+ adjustment: 'adjustment',
1865
+ outro: 'other',
1866
+ other: 'other',
1867
+ };
1868
+
1869
+ return kindMap[normalized] || 'other';
1870
+ }
1871
+
1872
+ private mapFinanceCategoryToFront(category: any) {
1873
+ return {
1874
+ id: String(category.id),
1875
+ codigo: category.code,
1876
+ nome: category.name,
1877
+ parentId: category.parent_id ? String(category.parent_id) : null,
1878
+ natureza: this.mapCategoryKindToPt(category.kind),
1879
+ status: category.status,
1880
+ ativo: category.status === 'active',
1881
+ };
1882
+ }
1883
+
1884
+ private mapPeriodCloseToFront(period: any) {
1885
+ return {
1886
+ id: String(period.id),
1887
+ periodStart: period.period_start?.toISOString?.() || null,
1888
+ periodEnd: period.period_end?.toISOString?.() || null,
1889
+ status: period.status,
1890
+ closedAt: period.closed_at?.toISOString?.() || null,
1891
+ closedByUserId: period.closed_by_user_id
1892
+ ? String(period.closed_by_user_id)
1893
+ : null,
1894
+ closedByName: period.user?.name || '-',
1895
+ closedByEmail: '',
1896
+ notes: period.notes || '',
1897
+ createdAt: period.created_at?.toISOString?.() || null,
1898
+ };
1899
+ }
1900
+
1397
1901
  private mapStatementStatusToPt(status?: string | null) {
1398
1902
  const statusMap = {
1399
1903
  pending: 'pendente',
@@ -1419,6 +1923,71 @@ export class FinanceService {
1419
1923
  return `${bankPrefix || 'ACC'}-${accountSuffix}`;
1420
1924
  }
1421
1925
 
1926
+ private async generateCostCenterCode(name?: string) {
1927
+ const sanitizedName = (name || 'CENTRO')
1928
+ .trim()
1929
+ .replace(/\s+/g, '-')
1930
+ .replace(/[^A-Za-z0-9-]/g, '')
1931
+ .toUpperCase();
1932
+
1933
+ const basePrefix = sanitizedName.slice(0, 8) || 'CENTRO';
1934
+
1935
+ const lastCostCenter = await this.prisma.cost_center.findFirst({
1936
+ orderBy: { id: 'desc' },
1937
+ select: { id: true },
1938
+ });
1939
+
1940
+ const suffix = String((lastCostCenter?.id || 0) + 1).padStart(4, '0');
1941
+
1942
+ return `${basePrefix}-${suffix}`;
1943
+ }
1944
+
1945
+ private async generateFinanceCategoryCode() {
1946
+ const lastCategory = await this.prisma.finance_category.findFirst({
1947
+ orderBy: { id: 'desc' },
1948
+ select: { id: true },
1949
+ });
1950
+
1951
+ const suffix = String((lastCategory?.id || 0) + 1).padStart(4, '0');
1952
+
1953
+ return `CAT-${suffix}`;
1954
+ }
1955
+
1956
+ private async ensureFinanceCategoryExists(id: number) {
1957
+ const category = await this.prisma.finance_category.findUnique({
1958
+ where: { id },
1959
+ select: { id: true },
1960
+ });
1961
+
1962
+ if (!category) {
1963
+ throw new NotFoundException('Finance category not found');
1964
+ }
1965
+ }
1966
+
1967
+ private async ensureNoFinanceCategoryCycle(
1968
+ categoryId: number,
1969
+ targetParentId: number,
1970
+ ) {
1971
+ if (categoryId === targetParentId) {
1972
+ throw new BadRequestException('Category cannot be parent of itself');
1973
+ }
1974
+
1975
+ let currentParentId: number | null = targetParentId;
1976
+
1977
+ while (currentParentId) {
1978
+ if (currentParentId === categoryId) {
1979
+ throw new BadRequestException('Invalid parent category hierarchy');
1980
+ }
1981
+
1982
+ const parent = await this.prisma.finance_category.findUnique({
1983
+ where: { id: currentParentId },
1984
+ select: { parent_id: true },
1985
+ });
1986
+
1987
+ currentParentId = parent?.parent_id || null;
1988
+ }
1989
+ }
1990
+
1422
1991
  private toCents(value: number) {
1423
1992
  return Math.round(value * 100);
1424
1993
  }
package/src/index.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  export * from './dto/create-financial-title.dto';
2
+ export * from './finance-audit-logs.controller';
2
3
  export * from './finance-bank-accounts.controller';
4
+ export * from './finance-categories.controller';
5
+ export * from './finance-cost-centers.controller';
3
6
  export * from './finance-data.controller';
4
7
  export * from './finance-installments.controller';
8
+ export * from './finance-period-close.controller';
5
9
  export * from './finance-statements.controller';
6
10
  export * from './finance.module';
7
11
  export * from './finance.service';