@fabriktor/fx 0.0.120 → 0.0.122

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.
@@ -88,7 +88,9 @@ export type CreateVendorInvoiceOptions = VendorInvoiceInsertOptions & MultiCreat
88
88
  owner_id: string;
89
89
  allow_duplicate?: boolean;
90
90
  };
91
- export type ReplaceVendorInvoiceOptions = VendorInvoiceReplaceOptions;
91
+ export type ReplaceVendorInvoiceOptions = VendorInvoiceReplaceOptions & {
92
+ allow_duplicate?: boolean;
93
+ };
92
94
  export type SearchVendorInvoicesOptions = OwnerSearchOptions & Partial<Pick<VendorInvoice, "is_archived">>;
93
95
  export type SearchVendorInvoicesPage = SearchPage<VendorInvoice>;
94
96
  export type CreateWarehouseOptions = WarehouseInsertOptions & MultiCreateCheckpoint;
@@ -380,8 +380,17 @@ export class BillableFX {
380
380
  return await this.findVendorInvoice(id);
381
381
  }
382
382
  async replaceVendorInvoice(opts) {
383
- await this.billable.replaceOneVendorInvoice(opts);
384
- return await this.findVendorInvoice(opts.id);
383
+ const { allow_duplicate, ...request } = opts;
384
+ if (request.in.number !== undefined) {
385
+ const current = await this.findVendorInvoice(request.id);
386
+ const number = request.in.number.trim();
387
+ if (!allow_duplicate && number !== current.number) {
388
+ await this.checkVendorInvoiceNumber(current.owner_id, number, current.id);
389
+ }
390
+ request.in = { ...request.in, number };
391
+ }
392
+ await this.billable.replaceOneVendorInvoice(request);
393
+ return await this.findVendorInvoice(request.id);
385
394
  }
386
395
  async searchVendorInvoices(opts) {
387
396
  return await runOwnerSearch({
@@ -479,14 +488,14 @@ export class BillableFX {
479
488
  const id = requiredOperationID(out, msgCreatedBillIDMissing);
480
489
  return await this.findBill(id);
481
490
  }
482
- async checkVendorInvoiceNumber(owner_id, number) {
491
+ async checkVendorInvoiceNumber(owner_id, number, excluding_id) {
483
492
  await runPreActionCheck({
484
493
  reason: BillablePreActionReason.VendorInvoiceNumberExists,
485
494
  allowed: async () => {
486
495
  const out = await this.billable.queryVendorInvoices({
487
496
  query: withOwnerQuery(owner_id, queryParams({ number })),
488
497
  });
489
- return (out.value?.length ?? 0) === 0;
498
+ return !(out.value ?? []).some((record) => record.id !== excluding_id);
490
499
  },
491
500
  });
492
501
  }
@@ -13,9 +13,16 @@ export type QueryTimesheetSyncDestinationsOptions = {
13
13
  source: WeekSheet;
14
14
  signal?: AbortSignal;
15
15
  };
16
+ export type QueryTimesheetSyncSourcesOptions = Pick<TimesheetsReadOptions, "context" | "owner_ids" | "period_indexes" | "signal">;
17
+ export type TimesheetSyncTarget = {
18
+ owner_id: string;
19
+ week_sheet?: WeekSheet;
20
+ };
16
21
  type TimesheetRecord = WeekSheet | Summary;
17
22
  export declare function timesheetCanReadResource(context: TimesheetsActor): boolean;
18
23
  export declare function timesheetCanCreateWeek(context: TimesheetsActor): boolean;
24
+ export declare function timesheetCanManageSync(context: TimesheetsActor): boolean;
25
+ export declare function timesheetCanSyncRecord(context: TimesheetsActor, record: WeekSheet | null | undefined): boolean;
19
26
  export declare function timesheetCanReadRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
20
27
  export declare function timesheetCanWriteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
21
28
  export declare function timesheetCanDeleteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
@@ -1,13 +1,20 @@
1
- import { PunchStatusWorking, UserTypeEmployee, } from "@fabriktor/schema";
1
+ import { PunchStatusWorking, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
2
2
  import { employmentActivityOwnerIDs, employmentCanRead, employmentEmployeeIDs, employmentEmployerIDs, employmentManagesRecord, employmentOwnsRecord, } from "../contacts/scope.js";
3
3
  import { restrictOwnerIDs } from "../core/scope.js";
4
- import { timesheetCanApprove, timesheetCanSync, timesheetHasWorkingPunches, timesheetIsMutable, } from "./workspace.js";
4
+ import { timesheetCanApprove, timesheetCanSync, timesheetCanSyncSource, timesheetHasWorkingPunches, timesheetIsMutable, } from "./workspace.js";
5
5
  export function timesheetCanReadResource(context) {
6
6
  return employmentCanRead(context);
7
7
  }
8
8
  export function timesheetCanCreateWeek(context) {
9
9
  return timesheetCanReadResource(context) && context.active_mode === UserTypeEmployee;
10
10
  }
11
+ export function timesheetCanManageSync(context) {
12
+ return timesheetCanReadResource(context) && context.active_mode === UserTypeEmployer;
13
+ }
14
+ export function timesheetCanSyncRecord(context, record) {
15
+ return record != null && timesheetCanManageSync(context) &&
16
+ employmentManagesRecord(context, record) && timesheetCanSyncSource(record);
17
+ }
11
18
  export function timesheetCanReadRecord(context, record) {
12
19
  return timesheetCanReadResource(context) && record != null && (employmentOwnsRecord(context, record) || employmentManagesRecord(context, record));
13
20
  }
@@ -30,7 +37,7 @@ export function timesheetCanEditDay(context, record, week_day) {
30
37
  day.punches.every((punch) => punch.punch_status !== PunchStatusWorking);
31
38
  }
32
39
  export function timesheetCanSyncDestination(context, source, destination) {
33
- return timesheetCanApproveRecord(context, source) &&
40
+ return timesheetCanSyncRecord(context, source) &&
34
41
  timesheetCanWriteRecord(context, destination) && timesheetCanSync(source, destination);
35
42
  }
36
43
  export function timesheetEmployeeScopeIDs(context) {
@@ -1,7 +1,7 @@
1
1
  import { type Date as SchemaDate, type OperationResult, type Summary, type WeekSheet } from "@fabriktor/schema";
2
2
  import type { BillableOperator, TimesheetsOperator } from "@fabriktor/client";
3
3
  import type { ContactsFXOperator } from "../contacts/contacts.js";
4
- import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type TimesheetsReadContext, type TimesheetsReadOptions } from "./access.js";
4
+ import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type QueryTimesheetSyncSourcesOptions, type TimesheetsReadContext, type TimesheetsReadOptions, type TimesheetSyncTarget } from "./access.js";
5
5
  import { TimesheetsWorkspace } from "./workspace.js";
6
6
  type PunchPayload = Parameters<TimesheetsOperator["punch"]>[0];
7
7
  type WeekSheetReplaceOptions = Parameters<TimesheetsOperator["replaceOneWeekSheet"]>[0];
@@ -57,7 +57,9 @@ export type PunchControls = {
57
57
  };
58
58
  export type SyncWeekOptions = {
59
59
  from: WeekSheet;
60
- to: WeekSheet[];
60
+ to?: WeekSheet[];
61
+ to_owner_ids?: string[];
62
+ actor?: LoadTimesheetsReadContextOptions;
61
63
  };
62
64
  export type SyncDaysOptions = SyncWeekOptions & {
63
65
  days: number[];
@@ -72,6 +74,8 @@ export interface TimesheetsFXOperator {
72
74
  queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
73
75
  queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
74
76
  querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
77
+ querySyncSources(opts: QueryTimesheetSyncSourcesOptions): Promise<WeekSheet[]>;
78
+ querySyncTargets(opts: QueryTimesheetSyncDestinationsOptions): Promise<TimesheetSyncTarget[]>;
75
79
  replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
76
80
  replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
77
81
  punchControls(opts: PunchControlsOptions): PunchControls;
@@ -96,6 +100,8 @@ export declare class TimesheetsFX implements TimesheetsFXOperator {
96
100
  queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
97
101
  queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
98
102
  querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
103
+ querySyncSources(opts: QueryTimesheetSyncSourcesOptions): Promise<WeekSheet[]>;
104
+ querySyncTargets(opts: QueryTimesheetSyncDestinationsOptions): Promise<TimesheetSyncTarget[]>;
99
105
  replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
100
106
  replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
101
107
  punchControls(opts: PunchControlsOptions): PunchControls;
@@ -9,8 +9,8 @@ import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
9
9
  import { normalizeIDs } from "../core/id.js";
10
10
  import { loadEmploymentReadContext } from "../contacts/scope.js";
11
11
  import { PreActionBlocked } from "../core/pre-action.js";
12
- import { timesheetCanApproveRecord, timesheetCanSyncDestination, timesheetReadFilters, } from "./access.js";
13
- import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetCanSync, timesheetIsMutable, } from "./workspace.js";
12
+ import { timesheetCanSyncRecord, timesheetCanSyncDestination, timesheetEmployeeScopeIDs, timesheetReadFilters, } from "./access.js";
13
+ import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetCanSync, timesheetCanSyncSource, timesheetIsMutable, } from "./workspace.js";
14
14
  const allWeekDays = [0, 1, 2, 3, 4, 5, 6];
15
15
  const minuteStep = 15;
16
16
  const msgPunchInProgress = "A punch action is already in progress.";
@@ -55,7 +55,7 @@ export class TimesheetsFX {
55
55
  const source = await this.workspace.week(opts.source.id, opts.signal);
56
56
  if (source.updated_at !== opts.source.updated_at)
57
57
  throw new PreActionBlocked(TimesheetsBlock.Changed);
58
- if (!timesheetCanApproveRecord(opts.context, source))
58
+ if (!timesheetCanSyncRecord(opts.context, source))
59
59
  throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
60
60
  const weeks = await this.queryReadableWeekSheets({
61
61
  context: opts.context,
@@ -66,6 +66,41 @@ export class TimesheetsFX {
66
66
  });
67
67
  return weeks.filter((week) => timesheetCanSyncDestination(opts.context, source, week));
68
68
  }
69
+ async querySyncSources(opts) {
70
+ // Full weeks are needed for eligibility; never use cleaned day/punch results.
71
+ const weeks = await this.queryReadableWeekSheets({
72
+ context: opts.context,
73
+ owner_ids: opts.owner_ids,
74
+ period_indexes: opts.period_indexes,
75
+ signal: opts.signal,
76
+ });
77
+ return weeks.filter((week) => timesheetCanSyncRecord(opts.context, week));
78
+ }
79
+ async querySyncTargets(opts) {
80
+ const source = await this.workspace.week(opts.source.id, opts.signal);
81
+ if (source.updated_at !== opts.source.updated_at)
82
+ throw new PreActionBlocked(TimesheetsBlock.Changed);
83
+ if (!timesheetCanSyncRecord(opts.context, source))
84
+ throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
85
+ // Include locked records: hiding them would mistake them for missing weeks.
86
+ const weeks = await this.queryReadableWeekSheets({
87
+ context: opts.context,
88
+ employer_ids: [source.employer_id],
89
+ period_indexes: [source.period_index],
90
+ signal: opts.signal,
91
+ });
92
+ const targets = [];
93
+ for (const owner_id of timesheetEmployeeScopeIDs(opts.context)) {
94
+ if (owner_id === source.owner_id)
95
+ continue;
96
+ const matches = weeks.filter((week) => week.owner_id === owner_id);
97
+ if (matches.length === 0)
98
+ targets.push({ owner_id });
99
+ else if (matches.length === 1 && timesheetCanSyncDestination(opts.context, source, matches[0]))
100
+ targets.push({ owner_id, week_sheet: matches[0] });
101
+ }
102
+ return targets;
103
+ }
69
104
  async replaceWeekSheet(opts) {
70
105
  await this.timesheets.replaceOneWeekSheet(opts);
71
106
  return await this.findWeekSheet(opts.id);
@@ -212,8 +247,7 @@ export class TimesheetsFX {
212
247
  }
213
248
  async syncWeek(opts) {
214
249
  await this.syncSheets({
215
- from: opts.from,
216
- to: opts.to,
250
+ ...opts,
217
251
  days: [...allWeekDays],
218
252
  });
219
253
  }
@@ -226,8 +260,7 @@ export class TimesheetsFX {
226
260
  });
227
261
  }
228
262
  await this.syncSheets({
229
- from: opts.from,
230
- to: opts.to,
263
+ ...opts,
231
264
  days,
232
265
  });
233
266
  }
@@ -308,18 +341,36 @@ export class TimesheetsFX {
308
341
  const source = await this.findWeekSheet(opts.from.id);
309
342
  if (source.updated_at !== opts.from.updated_at)
310
343
  throw new PreActionBlocked(TimesheetsBlock.Changed);
311
- if (!timesheetCanApprove(source))
344
+ if (!timesheetCanSyncSource(source))
312
345
  throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
313
346
  opts = { ...opts, from: source };
314
347
  const from_id = requiredID(opts.from.id, "from", msgWeekSheetIDRequired);
315
- if (opts.to.length === 0) {
348
+ const owner_ids = normalizeIDs(opts.to_owner_ids ?? []);
349
+ if (!opts.to?.length && owner_ids.length === 0) {
316
350
  throw errRequired({
317
351
  field: "to",
318
352
  msg: msgDestinationWeekSheetsRequired,
319
353
  });
320
354
  }
355
+ if (owner_ids.length > 0) {
356
+ if (!opts.actor)
357
+ throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
358
+ const context = await this.loadReadContext(opts.actor);
359
+ const targets = await this.querySyncTargets({ context, source });
360
+ if (owner_ids.some((id) => !targets.some((target) => target.owner_id === id)))
361
+ throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
362
+ // Refetch full records immediately before submission. The service checks
363
+ // again inside its transaction and creates missing destinations there.
364
+ for (const target of targets) {
365
+ if (!owner_ids.includes(target.owner_id) || !target.week_sheet)
366
+ continue;
367
+ const fresh = await this.findWeekSheet(target.week_sheet.id);
368
+ if (!timesheetCanSync(source, fresh))
369
+ throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
370
+ }
371
+ }
321
372
  const to_ids = [];
322
- for (const expected of opts.to) {
373
+ for (const expected of opts.to ?? []) {
323
374
  const week_sheet = await this.findWeekSheet(expected.id);
324
375
  if (week_sheet.updated_at !== expected.updated_at)
325
376
  throw new PreActionBlocked(TimesheetsBlock.Changed);
@@ -337,6 +388,7 @@ export class TimesheetsFX {
337
388
  const input = {
338
389
  from_id,
339
390
  to_ids: normalizeIDs(to_ids),
391
+ to_owner_ids: owner_ids,
340
392
  days: normalizeDays(opts.days),
341
393
  };
342
394
  await this.timesheets.sync(input);
@@ -50,6 +50,7 @@ export declare function timesheetPunches(sheet: WeekSheet): Punch[];
50
50
  export declare function timesheetHasWorkingPunches(sheet: WeekSheet): boolean;
51
51
  export declare function timesheetIsMutable(sheet: WeekSheet): boolean;
52
52
  export declare function timesheetCanApprove(sheet: WeekSheet): boolean;
53
+ export declare function timesheetCanSyncSource(sheet: WeekSheet): boolean;
53
54
  export declare function timesheetCanSync(source: WeekSheet, destination: WeekSheet): boolean;
54
55
  export declare function timesheetStateKey(state: TimesheetPunchState): string;
55
56
  export declare function timesheetWeekDay(date: InDate): number;
@@ -32,12 +32,18 @@ export function timesheetCanApprove(sheet) {
32
32
  punches.length > 0 &&
33
33
  punches.every((p) => p.punch_status === PunchStatusPending));
34
34
  }
35
+ export function timesheetCanSyncSource(sheet) {
36
+ const punches = timesheetPunches(sheet);
37
+ return punches.length > 0 && punches.every((punch) => punch.punch_status === PunchStatusPending || punch.punch_status === PunchStatusApproved);
38
+ }
35
39
  export function timesheetCanSync(source, destination) {
36
- return timesheetCanApprove(source) && timesheetIsMutable(destination) &&
37
- !timesheetHasWorkingPunches(destination) &&
40
+ return timesheetCanSyncSource(source) && !destination.is_archived &&
41
+ timesheetPunches(destination).every((punch) => punch.punch_status === PunchStatusPending) &&
38
42
  destination.employer_id === source.employer_id &&
39
43
  destination.id !== source.id &&
40
44
  destination.period_index === source.period_index &&
45
+ destination.period === source.period &&
46
+ destination.period_year === source.period_year &&
41
47
  destination.timezone === source.timezone;
42
48
  }
43
49
  export function timesheetStateKey(state) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.120",
3
+ "version": "0.0.122",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,8 +17,8 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.88",
20
+ "@fabriktor/client": "0.0.90",
21
21
  "@fabriktor/fx": "0.0.94",
22
- "@fabriktor/schema": "0.0.64"
22
+ "@fabriktor/schema": "0.0.65"
23
23
  }
24
24
  }