law-common 14.0.3-beta.0 → 14.0.3-beta.10

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.
@@ -1,6 +1,7 @@
1
1
  import { TimesheetClassificationStatusEnum } from "../enums/timesheet_classification_status_enum";
2
2
  import { TimesheetStatusEnum } from "../enums/timesheet_status_enum";
3
3
  import { IEntityAuditColumn } from "./entity-audit-columns.interface";
4
+ import { Nullable } from "./entity.utils.interface";
4
5
  export interface ITimesheetEntity extends IEntityAuditColumn {
5
6
  id: number;
6
7
  userId: number;
@@ -15,6 +16,6 @@ export interface ITimesheetEntity extends IEntityAuditColumn {
15
16
  billedDuration?: number;
16
17
  notBilledDuration?: number;
17
18
  classificationStatus: TimesheetClassificationStatusEnum;
18
- classifiedBy?: number;
19
+ classifiedBy?: Nullable<number>;
19
20
  }
20
21
  export type PendingApprovalStatus = TimesheetStatusEnum.CREATE_APPROVAL_PENDING | TimesheetStatusEnum.UPDATE_APPROVAL_PENDING | TimesheetStatusEnum.DELETE_APPROVAL_PENDING | TimesheetStatusEnum.RESOLVED_REJECTED;
@@ -1,4 +1,4 @@
1
- import { ITimesheetUpdateDto, ITimesheetFlowContextData } from "../../api";
1
+ import { ITimesheetFlowContextData, ITimesheetUpdateDto } from "../../api";
2
2
  import { CurrencyEnum } from "../../enums";
3
3
  import { IClassificationWindowConfig, ITimesheetActionItem, SortableTimesheet } from "../../model/entities/interface/timesheet.model.interface";
4
4
  import { TimesheetClassificationStatusEnum } from "../enums/timesheet_classification_status_enum";
@@ -51,11 +51,33 @@ export declare class TimesheetEntityModel extends BaseEntityModel<EntityEnum.TIM
51
51
  get latestStatusFromHistory(): TimesheetStatusEnum | undefined;
52
52
  getDateCodeDay(): number;
53
53
  getClassificationDeadline(config: IClassificationWindowConfig): Date;
54
+ /**
55
+ * IST point-in-time AUTO-CLASSIFY trigger. Returns the classification window
56
+ * whose `endDay + bufferDays + 1 == today` (IST) — i.e. it fires on a SINGLE day
57
+ * per window (the auto-classify cron runs at 01:00 IST each morning).
58
+ * Trailing window (endDay === null) evaluates against the PREVIOUS month's EOM.
59
+ *
60
+ * "today" is `getTodayInIST()` (IST calendar day). Concrete per getTodayInIST()
61
+ * rollover rule: endDay 13 + buffer 3 -> trigger IST day 17; that 01:00 IST IS
62
+ * UTC 19:30 the PREVIOUS day, so a UTC-default container reads getDate() = 16 ->
63
+ * 17 !== 16 -> returns null -> auto-classification silently no-ops.
64
+ */
54
65
  static getTimesheetAutoClassificationDateCodeRange(config: IClassificationWindowConfig): {
55
66
  windowStart: string;
56
67
  windowEnd: string;
57
68
  } | null;
58
- static getActiveClassificationWindowRange(config: IClassificationWindowConfig): {
69
+ /**
70
+ * Resolves the classification window that contains `now` (used by the reminder).
71
+ * `now` defaults to the IST calendar day (getTodayInIST()).
72
+ *
73
+ * Boundary examples per getTodayInIST() rollover rule (IST < 05:30 -> UTC prev day):
74
+ * IST day 1 00:xx -> UTC last day of the PREVIOUS month
75
+ * (a UTC clock would skip the day-1-3 spillover onto the
76
+ * prev-month 21-EOM window);
77
+ * IST day 21 00:30 -> UTC day 20
78
+ * (a UTC clock would miss the current-month 21-EOM tail).
79
+ */
80
+ static getActiveClassificationWindowRange(config: IClassificationWindowConfig, now?: Date): {
59
81
  windowStart: string;
60
82
  windowEnd: string;
61
83
  } | null;
@@ -63,5 +85,12 @@ export declare class TimesheetEntityModel extends BaseEntityModel<EntityEnum.TIM
63
85
  startDay: number;
64
86
  endDay: Nullable<number>;
65
87
  }>;
88
+ /**
89
+ * Fields on a timesheet that, when changed via an UPDATE, reset the timesheet's
90
+ * `classificationStatus` back to `PENDING`. Shared by backend (toUpdateDto) and frontend
91
+ * (edit warning). `billingId` intentionally NOT included — a billing change does not reset
92
+ * classification. Anything not listed here is by definition "non-reset".
93
+ */
94
+ static timesheetClassificationResetFieldsSet(): string[];
66
95
  static sortByProjectAndDesignation(data: SortableTimesheet[], designations: IDesignationEntity[]): SortableTimesheet[];
67
96
  }
@@ -84,8 +84,10 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
84
84
  return false;
85
85
  if (actionKey === timesheet_action_enum_1.TimesheetActionEnum.CLASSIFY)
86
86
  return false;
87
- if (actionKey === timesheet_action_enum_1.TimesheetActionEnum.UPDATE && this.classificationStatus && this.classificationStatus !== timesheet_classification_status_enum_1.TimesheetClassificationStatusEnum.PENDING)
88
- return false;
87
+ // LW-842: allow UPDATE regardless of classification status — the backend now resets
88
+ // classificationStatus to PENDING in toUpdateDto when a reset-field changes.
89
+ // if (actionKey === TimesheetActionEnum.UPDATE && this.classificationStatus && this.classificationStatus !== TimesheetClassificationStatusEnum.PENDING)
90
+ // return false;
89
91
  return true;
90
92
  })
91
93
  .map(([actionKey, actionConfig]) => {
@@ -225,10 +227,21 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
225
227
  d.setHours(23, 59, 59, 999);
226
228
  return d;
227
229
  }
230
+ /**
231
+ * IST point-in-time AUTO-CLASSIFY trigger. Returns the classification window
232
+ * whose `endDay + bufferDays + 1 == today` (IST) — i.e. it fires on a SINGLE day
233
+ * per window (the auto-classify cron runs at 01:00 IST each morning).
234
+ * Trailing window (endDay === null) evaluates against the PREVIOUS month's EOM.
235
+ *
236
+ * "today" is `getTodayInIST()` (IST calendar day). Concrete per getTodayInIST()
237
+ * rollover rule: endDay 13 + buffer 3 -> trigger IST day 17; that 01:00 IST IS
238
+ * UTC 19:30 the PREVIOUS day, so a UTC-default container reads getDate() = 16 ->
239
+ * 17 !== 16 -> returns null -> auto-classification silently no-ops.
240
+ */
228
241
  static getTimesheetAutoClassificationDateCodeRange(config) {
229
242
  var _a;
230
243
  const { bufferDays, windows } = config;
231
- const today = new Date();
244
+ const today = (0, utils_1.getTodayInIST)();
232
245
  for (const window of windows) {
233
246
  // Last window (endDay === null) belongs to the previous month
234
247
  const targetDate = window.endDay === null ? (0, date_fns_1.subMonths)(today, 1) : today;
@@ -245,35 +258,54 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
245
258
  }
246
259
  return null;
247
260
  }
248
- static getActiveClassificationWindowRange(config) {
249
- var _a;
250
- const { bufferDays, windows } = config;
251
- const today = new Date();
252
- const day = today.getDate();
253
- const year = today.getFullYear();
254
- const month = today.getMonth();
261
+ /**
262
+ * Resolves the classification window that contains `now` (used by the reminder).
263
+ * `now` defaults to the IST calendar day (getTodayInIST()).
264
+ *
265
+ * Boundary examples per getTodayInIST() rollover rule (IST < 05:30 -> UTC prev day):
266
+ * IST day 1 00:xx -> UTC last day of the PREVIOUS month
267
+ * (a UTC clock would skip the day-1-3 spillover onto the
268
+ * prev-month 21-EOM window);
269
+ * IST day 21 00:30 -> UTC day 20
270
+ * (a UTC clock would miss the current-month 21-EOM tail).
271
+ */
272
+ static getActiveClassificationWindowRange(config, now = (0, utils_1.getTodayInIST)()) {
273
+ const { bufferDays, windows } = config; // bufferDays = 3
274
+ const year = now.getFullYear(); // now = Oct 1 2026 -> year = 2026
275
+ const month = now.getMonth(); // month = 9 (Oct)
276
+ // Spillover check: does `now` still fall within the PREVIOUS month's trailing
277
+ // window (21-EOM) plus its buffer? If so, that window is still pending
278
+ // classification and takes priority over the current month's own windows.
279
+ const lastWindow = windows[windows.length - 1]; // lastWindow = { startDay: 21, endDay: null }
280
+ if ((lastWindow === null || lastWindow === void 0 ? void 0 : lastWindow.endDay) === null) {
281
+ const prevMonthDate = (0, date_fns_1.subMonths)(new Date(year, month, 1), 1); // prevMonthDate = Sep 1 2026
282
+ const prevYear = prevMonthDate.getFullYear(); // prevYear = 2026
283
+ const prevMonth = prevMonthDate.getMonth(); // prevMonth = 8 (Sep)
284
+ const prevMonthEom = (0, date_fns_1.lastDayOfMonth)(prevMonthDate).getDate(); // prevMonthEom = 30
285
+ const prevWindowStartDate = new Date(prevYear, prevMonth, lastWindow.startDay); // Sep 21 2026
286
+ prevWindowStartDate.setHours(0, 0, 0, 0); // Sep 21 2026 00:00:00.000
287
+ const prevWindowEndDate = new Date(prevYear, prevMonth, prevMonthEom); // Sep 30 2026
288
+ prevWindowEndDate.setHours(23, 59, 59, 999); // Sep 30 2026 23:59:59.999
289
+ const prevWindowEndDateWithBuffer = (0, date_fns_1.addDays)(new Date(prevYear, prevMonth, prevMonthEom), bufferDays); // Sep30 + 3 -> Oct 3 2026
290
+ prevWindowEndDateWithBuffer.setHours(23, 59, 59, 999); // Oct 3 2026 23:59:59.999
291
+ if (now.getTime() >= prevWindowStartDate.getTime() && now.getTime() <= prevWindowEndDateWithBuffer.getTime()) {
292
+ // Oct1 >= Sep21 -> true, Oct1 <= Oct3 -> true, so this branch matches and returns here
293
+ return {
294
+ windowStart: (0, date_fns_1.format)(prevWindowStartDate, "yyyyMMdd"), // "20260921"
295
+ windowEnd: (0, date_fns_1.format)(prevWindowEndDate, "yyyyMMdd"), // "20260930"
296
+ };
297
+ }
298
+ }
255
299
  for (const window of windows) {
256
- // last window (21–end) belongs to previous month
257
- const targetDate = window.endDay === null ? (0, date_fns_1.subMonths)(today, 1) : today;
258
- const targetYear = targetDate.getFullYear();
259
- const targetMonth = targetDate.getMonth();
260
- const actualEndDay = (_a = window.endDay) !== null && _a !== void 0 ? _a : (0, date_fns_1.lastDayOfMonth)(targetDate).getDate();
261
- const windowStartDay = window.startDay;
262
- const windowEndDay = actualEndDay;
263
- // check if today falls within this window's active period
264
- // active period = from windowStart until triggerDate (endDay + bufferDays)
265
- const triggerDate = (0, date_fns_1.addDays)(new Date(targetYear, targetMonth, actualEndDay), bufferDays);
266
- const triggerDay = triggerDate.getDate();
267
- const triggerMonth = triggerDate.getMonth();
268
- const triggerYear = triggerDate.getFullYear();
269
- const todayTime = today.getTime();
270
- const windowOpenDate = new Date(targetYear, targetMonth, windowStartDay);
271
- const windowCloseDate = new Date(triggerYear, triggerMonth, triggerDay);
272
- windowCloseDate.setHours(23, 59, 59, 999);
273
- if (todayTime >= windowOpenDate.getTime() && todayTime <= windowCloseDate.getTime()) {
300
+ const windowStartDate = new Date(year, month, window.startDay);
301
+ windowStartDate.setHours(0, 0, 0, 0);
302
+ const windowEndDate = window.endDay === null ? (0, date_fns_1.lastDayOfMonth)(new Date(year, month, 1)) : new Date(year, month, window.endDay);
303
+ windowEndDate.setHours(23, 59, 59, 999);
304
+ if (now.getTime() >= windowStartDate.getTime() && now.getTime() <= windowEndDate.getTime()) {
305
+ // not reached for Oct 1 -- spillover branch already returned above
274
306
  return {
275
- windowStart: (0, date_fns_1.format)(new Date(targetYear, targetMonth, windowStartDay), "yyyyMMdd"),
276
- windowEnd: (0, date_fns_1.format)(new Date(targetYear, targetMonth, windowEndDay), "yyyyMMdd"),
307
+ windowStart: (0, date_fns_1.format)(windowStartDate, "yyyyMMdd"),
308
+ windowEnd: (0, date_fns_1.format)(windowEndDate, "yyyyMMdd"),
277
309
  };
278
310
  }
279
311
  }
@@ -288,6 +320,21 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
288
320
  };
289
321
  });
290
322
  }
323
+ /**
324
+ * Fields on a timesheet that, when changed via an UPDATE, reset the timesheet's
325
+ * `classificationStatus` back to `PENDING`. Shared by backend (toUpdateDto) and frontend
326
+ * (edit warning). `billingId` intentionally NOT included — a billing change does not reset
327
+ * classification. Anything not listed here is by definition "non-reset".
328
+ */
329
+ static timesheetClassificationResetFieldsSet() {
330
+ return [
331
+ "userId",
332
+ "projectId",
333
+ "dateCode",
334
+ "totalDuration",
335
+ // "task", "description",
336
+ ];
337
+ }
291
338
  static sortByProjectAndDesignation(data, designations) {
292
339
  const priorityMap = new Map();
293
340
  designations.forEach((d) => {
@@ -1,3 +1,22 @@
1
1
  export declare function compareDates(recordDate: string, buffer: number): boolean;
2
2
  export declare function epochToDateStringFormat(epoch: number, format?: string): string;
3
3
  export declare function convertDateCodeToFormat(dateString: string, toFormat: string): string;
4
+ /**
5
+ * Current calendar date resolved in Asia/Kolkata (IST).
6
+ *
7
+ * TIMEZONE RULE: IST = UTC + 5:30 (fixed offset, no DST).
8
+ * Converting IST -> UTC: subtract 5:30.
9
+ * - If IST time < 05:30 -> UTC date is the PREVIOUS calendar day.
10
+ * - If IST time >= 05:30 -> UTC date is the SAME calendar day.
11
+ *
12
+ * Example: IST 01:00 -> UTC 19:30 (prev day).
13
+ *
14
+ * Why this matters: the production container (node:20.10-alpine) sets no TZ,
15
+ * so it defaults to UTC. A bare `new Date().getDate()` at 01:00 IST would read
16
+ * UTC's date (still "yesterday"), causing classification jobs to target the
17
+ * wrong day or silently miss their trigger window.
18
+ *
19
+ * This function returns a Date whose local getFullYear()/getMonth()/getDate()
20
+ * always equal the IST calendar date, regardless of container timezone.
21
+ */
22
+ export declare function getTodayInIST(): Date;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compareDates = compareDates;
4
4
  exports.epochToDateStringFormat = epochToDateStringFormat;
5
5
  exports.convertDateCodeToFormat = convertDateCodeToFormat;
6
+ exports.getTodayInIST = getTodayInIST;
6
7
  const date_fns_1 = require("date-fns");
7
8
  const date_code_model_util_1 = require("./models/date-code.model.util");
8
9
  function compareDates(recordDate, buffer) {
@@ -16,3 +17,31 @@ function convertDateCodeToFormat(dateString, toFormat) {
16
17
  const dateObj = new date_code_model_util_1.DateCodeModel(dateString);
17
18
  return dateObj.toStringFormat(toFormat);
18
19
  }
20
+ /**
21
+ * Current calendar date resolved in Asia/Kolkata (IST).
22
+ *
23
+ * TIMEZONE RULE: IST = UTC + 5:30 (fixed offset, no DST).
24
+ * Converting IST -> UTC: subtract 5:30.
25
+ * - If IST time < 05:30 -> UTC date is the PREVIOUS calendar day.
26
+ * - If IST time >= 05:30 -> UTC date is the SAME calendar day.
27
+ *
28
+ * Example: IST 01:00 -> UTC 19:30 (prev day).
29
+ *
30
+ * Why this matters: the production container (node:20.10-alpine) sets no TZ,
31
+ * so it defaults to UTC. A bare `new Date().getDate()` at 01:00 IST would read
32
+ * UTC's date (still "yesterday"), causing classification jobs to target the
33
+ * wrong day or silently miss their trigger window.
34
+ *
35
+ * This function returns a Date whose local getFullYear()/getMonth()/getDate()
36
+ * always equal the IST calendar date, regardless of container timezone.
37
+ */
38
+ function getTodayInIST() {
39
+ const parts = new Intl.DateTimeFormat("en-GB", {
40
+ timeZone: "Asia/Kolkata",
41
+ year: "numeric",
42
+ month: "numeric",
43
+ day: "numeric",
44
+ }).formatToParts(new Date());
45
+ const get = (type) => { var _a, _b; return Number((_b = (_a = parts.find((part) => part.type === type)) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : 0); };
46
+ return new Date(get("year"), get("month") - 1, get("day"));
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "law-common",
3
- "version": "14.0.3-beta.0",
3
+ "version": "14.0.3-beta.10",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -35,7 +35,8 @@
35
35
  "script:publish:install:patch": "node scripts/publish-and-install.js patch",
36
36
  "script:publish:install:minor": "node scripts/publish-and-install.js minor",
37
37
  "script:publish:install:major": "node scripts/publish-and-install.js major",
38
- "build:link": "bash scripts/build-link.sh"
38
+ "build:link": "bash scripts/build-link.sh",
39
+ "script:deploy:prod": "bash scripts/deploy-prod.sh"
39
40
  },
40
41
  "keywords": [],
41
42
  "author": "",