@fabriktor/fx 0.0.122 → 0.0.124
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/dist/src/core/multi-create.d.ts +2 -0
- package/dist/src/core/multi-create.js +7 -1
- package/dist/src/core/search.d.ts +1 -0
- package/dist/src/core/search.js +1 -1
- package/dist/src/fx.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/payments/payments.d.ts +9 -0
- package/dist/src/payments/payments.js +12 -1
- package/dist/src/payrolls/payrolls.d.ts +5 -1
- package/dist/src/payrolls/payrolls.js +3 -0
- package/dist/src/payrolls/workspace.d.ts +210 -0
- package/dist/src/payrolls/workspace.js +522 -0
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@ export type MultiCreateStatus = (typeof MultiCreateStatus)[keyof typeof MultiCre
|
|
|
11
11
|
export type MultiCreateCheckpoint = {
|
|
12
12
|
created_id?: string;
|
|
13
13
|
on_created?: (id: string) => void;
|
|
14
|
+
on_rejected?: () => void;
|
|
14
15
|
before_request?: () => void;
|
|
15
16
|
};
|
|
16
17
|
export type MultiCreateAttempt = MultiCreateCheckpoint & {
|
|
@@ -46,6 +47,7 @@ export declare class MultiCreatePool {
|
|
|
46
47
|
export type MultiCreateOptions<Input, Result> = {
|
|
47
48
|
run: (input: Input, attempt: MultiCreateAttempt) => Promise<Result>;
|
|
48
49
|
pool?: MultiCreatePool;
|
|
50
|
+
retry_same_request?: boolean;
|
|
49
51
|
before_request?: () => void;
|
|
50
52
|
on_failed?: (entry: MultiCreateEntry<Input, Result>) => void;
|
|
51
53
|
on_settled?: (outcome: MultiCreateOutcome<Input, Result>) => void;
|
|
@@ -115,10 +115,12 @@ export class MultiCreator {
|
|
|
115
115
|
return this.completion;
|
|
116
116
|
// Explicit user retry is a new creation attempt when no ID was acknowledged.
|
|
117
117
|
// A known ID always resumes retrieval with the original request key.
|
|
118
|
+
if (this.opts.retry_same_request && entry.attempted && input !== undefined)
|
|
119
|
+
throw new Error("A submitted request cannot be edited while retrying.");
|
|
118
120
|
const next = {
|
|
119
121
|
...entry,
|
|
120
122
|
input: input === undefined ? entry.input : structuredClone(input),
|
|
121
|
-
idempotency_key: entry.created_id
|
|
123
|
+
idempotency_key: entry.created_id || (this.opts.retry_same_request && entry.attempted)
|
|
122
124
|
? entry.idempotency_key
|
|
123
125
|
: crypto.randomUUID(),
|
|
124
126
|
status: MultiCreateStatus.Pending,
|
|
@@ -159,6 +161,10 @@ export class MultiCreator {
|
|
|
159
161
|
current = { ...current, created_id: id };
|
|
160
162
|
this.replace(current);
|
|
161
163
|
},
|
|
164
|
+
on_rejected: () => {
|
|
165
|
+
current = { ...current, attempted: false };
|
|
166
|
+
this.replace(current);
|
|
167
|
+
},
|
|
162
168
|
});
|
|
163
169
|
this.replace({
|
|
164
170
|
...current,
|
|
@@ -51,6 +51,7 @@ export declare function runSearch<T>(opts: RunSearchOptions<T>): Promise<SearchP
|
|
|
51
51
|
export declare function normalizeSearchPage(value?: number): number;
|
|
52
52
|
export declare function normalizeSearchPerPage(value?: number): number;
|
|
53
53
|
export declare function normalizeSearchQuery(value?: string): string | undefined;
|
|
54
|
+
export declare function emptySearchPage<T>(page?: number, per_page?: number): SearchPage<T>;
|
|
54
55
|
export type SearchResultWithID = {
|
|
55
56
|
readonly id: string;
|
|
56
57
|
};
|
package/dist/src/core/search.js
CHANGED
|
@@ -201,7 +201,7 @@ function hasMoreSearchResults(opts) {
|
|
|
201
201
|
}
|
|
202
202
|
return opts.result_count >= opts.per_page;
|
|
203
203
|
}
|
|
204
|
-
function emptySearchPage(page, per_page) {
|
|
204
|
+
export function emptySearchPage(page = firstSearchPage, per_page = defaultSearchPerPage) {
|
|
205
205
|
return {
|
|
206
206
|
results: [],
|
|
207
207
|
page,
|
package/dist/src/fx.js
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from "./memos/memos.js";
|
|
|
28
28
|
export * from "./notifications/notifications.js";
|
|
29
29
|
export * from "./payments/payments.js";
|
|
30
30
|
export * from "./payrolls/payrolls.js";
|
|
31
|
+
export * from "./payrolls/workspace.js";
|
|
31
32
|
export * from "./preferences/preferences.js";
|
|
32
33
|
export * from "./privacy/privacy.js";
|
|
33
34
|
export * from "./processes/processes.js";
|
package/dist/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export * from "./memos/memos.js";
|
|
|
28
28
|
export * from "./notifications/notifications.js";
|
|
29
29
|
export * from "./payments/payments.js";
|
|
30
30
|
export * from "./payrolls/payrolls.js";
|
|
31
|
+
export * from "./payrolls/workspace.js";
|
|
31
32
|
export * from "./preferences/preferences.js";
|
|
32
33
|
export * from "./privacy/privacy.js";
|
|
33
34
|
export * from "./processes/processes.js";
|
|
@@ -21,6 +21,13 @@ type PmtPublicSessionInsertOptions = Parameters<PaymentsOperator["insertOnePmtPu
|
|
|
21
21
|
type PmtPublicSessionReplaceOptions = Parameters<PaymentsOperator["replaceOnePmtPublicSession"]>[0];
|
|
22
22
|
type CreditLedgerEntryInsertOptions = Parameters<PaymentsOperator["insertOneCreditLedgerEntry"]>[0];
|
|
23
23
|
type CreditLedgerEntryReplaceOptions = Parameters<PaymentsOperator["replaceOneCreditLedgerEntry"]>[0];
|
|
24
|
+
export type EntityPaymentHistoryOptions = {
|
|
25
|
+
owner_id: string;
|
|
26
|
+
entity_id: string;
|
|
27
|
+
type: Payment["type"];
|
|
28
|
+
recipient_id?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
};
|
|
24
31
|
export type CreatePaymentOptions = PaymentInsertOptions;
|
|
25
32
|
export type ReplacePaymentOptions = PaymentReplaceOptions;
|
|
26
33
|
export type SearchPaymentsOptions = OwnerSearchOptions;
|
|
@@ -105,6 +112,7 @@ export type PaymentsFXOptions = {
|
|
|
105
112
|
payments: PaymentsOperator;
|
|
106
113
|
};
|
|
107
114
|
export interface PaymentsFXOperator {
|
|
115
|
+
entityPaymentHistory(opts: EntityPaymentHistoryOptions): Promise<Payment[]>;
|
|
108
116
|
createPayment(opts: CreatePaymentOptions): Promise<Payment>;
|
|
109
117
|
replacePayment(opts: ReplacePaymentOptions): Promise<Payment>;
|
|
110
118
|
searchPayments(opts: SearchPaymentsOptions): Promise<SearchPaymentsPage>;
|
|
@@ -143,6 +151,7 @@ export declare class PaymentsFX implements PaymentsFXOperator {
|
|
|
143
151
|
private payments;
|
|
144
152
|
private submittedPublicSessionIDs;
|
|
145
153
|
constructor(opts: PaymentsFXOptions);
|
|
154
|
+
entityPaymentHistory(opts: EntityPaymentHistoryOptions): Promise<Payment[]>;
|
|
146
155
|
createPayment(opts: CreatePaymentOptions): Promise<Payment>;
|
|
147
156
|
replacePayment(opts: ReplacePaymentOptions): Promise<Payment>;
|
|
148
157
|
searchPayments(opts: SearchPaymentsOptions): Promise<SearchPaymentsPage>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
4
|
// not use this file except in compliance with the License. You may obtain
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, } from "@fabriktor/schema";
|
|
6
|
+
import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, PaymentKindMain, } from "@fabriktor/schema";
|
|
7
7
|
import { queryParams } from "@fabriktor/client";
|
|
8
8
|
import { errResolveFailed, errValidation } from "../core/err.js";
|
|
9
9
|
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
@@ -101,6 +101,17 @@ export class PaymentsFX {
|
|
|
101
101
|
this.payments = opts.payments;
|
|
102
102
|
this.submittedPublicSessionIDs = new Set();
|
|
103
103
|
}
|
|
104
|
+
async entityPaymentHistory(opts) {
|
|
105
|
+
if (!resolvedID(opts.owner_id) || !resolvedID(opts.entity_id) || !opts.type.trim()) {
|
|
106
|
+
throw errValidation({ field: "entity_id", msg: "Payment history requires an owner, entity, and type." });
|
|
107
|
+
}
|
|
108
|
+
const response = await this.payments.queryPayments({
|
|
109
|
+
signal: opts.signal,
|
|
110
|
+
query: queryParams({ owner_id: opts.owner_id, entity_id: opts.entity_id, type: opts.type, kind: PaymentKindMain, recipient_id: opts.recipient_id }),
|
|
111
|
+
});
|
|
112
|
+
// Preserve the backend's chronological order; do not infer settlement from PSP payment balance flags.
|
|
113
|
+
return response.value ?? [];
|
|
114
|
+
}
|
|
104
115
|
async createPayment(opts) {
|
|
105
116
|
const out = await this.payments.insertOnePayment(opts);
|
|
106
117
|
const id = requiredOperationID(out, msgCreatedPaymentIDMissing);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PayrollsWorkspace } from "./workspace.js";
|
|
2
|
+
import type { BillableOperator, PayrollsOperator } from "@fabriktor/client";
|
|
2
3
|
import type { Payable, Payroll, PayConfig, HourBank, Adjustment, AdjustmentFolder, AdjustmentRecord, AdjustmentAggregate, EmployeePayConfig, PayrollProgram } from "@fabriktor/schema";
|
|
3
4
|
import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
|
|
4
5
|
type PayableInsertOptions = Parameters<PayrollsOperator["insertOnePayable"]>[0];
|
|
@@ -88,8 +89,10 @@ export type SetEmployeePayConfigArchivedResult = Awaited<ReturnType<PayrollsOper
|
|
|
88
89
|
export type SetPayrollProgramEnabledResult = Awaited<ReturnType<PayrollsOperator["replaceOnePayrollProgram"]>>;
|
|
89
90
|
export type PayrollsFXOptions = {
|
|
90
91
|
payrolls: PayrollsOperator;
|
|
92
|
+
billable?: BillableOperator;
|
|
91
93
|
};
|
|
92
94
|
export interface PayrollsFXOperator {
|
|
95
|
+
readonly workspace: PayrollsWorkspace;
|
|
93
96
|
createPayable(opts: CreatePayableOptions): Promise<Payable>;
|
|
94
97
|
replacePayable(opts: ReplacePayableOptions): Promise<Payable>;
|
|
95
98
|
searchPayables(opts: SearchPayablesOptions): Promise<SearchPayablesPage>;
|
|
@@ -127,6 +130,7 @@ export interface PayrollsFXOperator {
|
|
|
127
130
|
setPayrollProgramEnabled(opts: SetPayrollProgramEnabledOptions): Promise<SetPayrollProgramEnabledResult>;
|
|
128
131
|
}
|
|
129
132
|
export declare class PayrollsFX implements PayrollsFXOperator {
|
|
133
|
+
readonly workspace: PayrollsWorkspace;
|
|
130
134
|
private payrolls;
|
|
131
135
|
constructor(opts: PayrollsFXOptions);
|
|
132
136
|
createPayable(opts: CreatePayableOptions): Promise<Payable>;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
4
|
// not use this file except in compliance with the License. You may obtain
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
import { PayrollsWorkspace } from "./workspace.js";
|
|
6
7
|
import { errResolveFailed } from "../core/err.js";
|
|
7
8
|
import { requiredOperationID } from "../core/id.js";
|
|
8
9
|
import { runOwnerSearch, } from "../core/search.js";
|
|
@@ -37,9 +38,11 @@ const resourcePayrollProgram = "payroll_program";
|
|
|
37
38
|
const msgCreatedPayrollProgramIDMissing = "Created payroll program response did not contain an ID.";
|
|
38
39
|
const msgPayrollProgramMissing = "Payroll program could not be resolved after the mutation.";
|
|
39
40
|
export class PayrollsFX {
|
|
41
|
+
workspace;
|
|
40
42
|
payrolls;
|
|
41
43
|
constructor(opts) {
|
|
42
44
|
this.payrolls = opts.payrolls;
|
|
45
|
+
this.workspace = new PayrollsWorkspace(opts.payrolls, opts.billable);
|
|
43
46
|
}
|
|
44
47
|
async createPayable(opts) {
|
|
45
48
|
const out = await this.payrolls.insertOnePayable(opts);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { BillableOperator, PayrollsOperator, QueryOptions } from "@fabriktor/client";
|
|
2
|
+
import { ColPayrollsPayrolls, ColPayrollsHourBanks, ColPayrollsPayables, ColPayrollsAdjustments, ColPayrollsAdjustmentFolders, ColPayrollsPayConfigs, ColPayrollsEmployeePayConfigs, ColPayrollsAdjustmentRecords, ColPayrollsAdjustmentAggregates, ColPayrollsPayrollPrograms, type Payroll, type HourBank, type Payable, type Adjustment, type AdjustmentFolder, type PayConfig, type EmployeePayConfig, type AdjustmentRecord, type AdjustmentAggregate, type PayrollProgram, type InPayroll, type InAdjustment, type InAdjustmentFolder, type InPayConfig, type InEmployeePayConfig, type InPayrollProgram, type InAdjustmentAggregate, type PLPayrollsGeneratePayrollFromEntities, type InDate, type Period } from "@fabriktor/schema";
|
|
3
|
+
import type { EmploymentActor, EmploymentReadContext } from "../contacts/scope.js";
|
|
4
|
+
import { type SearchOptions, type SearchPage } from "../core/search.js";
|
|
5
|
+
import type { MultiCreateAttempt } from "../core/multi-create.js";
|
|
6
|
+
export declare const PayrollResource: {
|
|
7
|
+
readonly Payrolls: "payrolls";
|
|
8
|
+
readonly HourBanks: "hour_banks";
|
|
9
|
+
readonly Payables: "payables";
|
|
10
|
+
readonly Adjustments: "adjustments";
|
|
11
|
+
readonly Folders: "adjustment_folders";
|
|
12
|
+
readonly PayConfigs: "pay_configs";
|
|
13
|
+
readonly EmployeeConfigs: "employee_pay_configs";
|
|
14
|
+
readonly Records: "adjustment_records";
|
|
15
|
+
readonly Aggregates: "adjustment_aggregates";
|
|
16
|
+
readonly Programs: "payroll_programs";
|
|
17
|
+
};
|
|
18
|
+
export type PayrollResource = (typeof PayrollResource)[keyof typeof PayrollResource];
|
|
19
|
+
export type PayrollRecordMap = {
|
|
20
|
+
[ColPayrollsPayrolls]: Payroll;
|
|
21
|
+
[ColPayrollsHourBanks]: HourBank;
|
|
22
|
+
[ColPayrollsPayables]: Payable;
|
|
23
|
+
[ColPayrollsAdjustments]: Adjustment;
|
|
24
|
+
[ColPayrollsAdjustmentFolders]: AdjustmentFolder;
|
|
25
|
+
[ColPayrollsPayConfigs]: PayConfig;
|
|
26
|
+
[ColPayrollsEmployeePayConfigs]: EmployeePayConfig;
|
|
27
|
+
[ColPayrollsAdjustmentRecords]: AdjustmentRecord;
|
|
28
|
+
[ColPayrollsAdjustmentAggregates]: AdjustmentAggregate;
|
|
29
|
+
[ColPayrollsPayrollPrograms]: PayrollProgram;
|
|
30
|
+
};
|
|
31
|
+
export type PayrollRecord = PayrollRecordMap[PayrollResource];
|
|
32
|
+
export type PayrollFilters = {
|
|
33
|
+
employee_id?: string[];
|
|
34
|
+
employee_pay_config_id?: string[];
|
|
35
|
+
pay_config_id?: string[];
|
|
36
|
+
payroll_id?: string[];
|
|
37
|
+
hour_bank_id?: string[];
|
|
38
|
+
folder_id?: string[];
|
|
39
|
+
period_index?: number[];
|
|
40
|
+
period_indexes?: number[];
|
|
41
|
+
currency?: string;
|
|
42
|
+
timezone?: string;
|
|
43
|
+
is_archived?: boolean;
|
|
44
|
+
is_payrolled?: boolean;
|
|
45
|
+
is_banked?: boolean;
|
|
46
|
+
is_problematic?: boolean;
|
|
47
|
+
is_enabled?: boolean;
|
|
48
|
+
is_employer_contribution?: boolean;
|
|
49
|
+
type?: string[];
|
|
50
|
+
name?: string[];
|
|
51
|
+
};
|
|
52
|
+
export declare const payrollPolicies: {
|
|
53
|
+
payrolls: {
|
|
54
|
+
create: true;
|
|
55
|
+
remove: true;
|
|
56
|
+
report: true;
|
|
57
|
+
filters: ("timezone" | "currency" | "employee_id" | "employee_pay_config_id" | "hour_bank_id" | "period_indexes" | "is_banked" | "is_problematic")[];
|
|
58
|
+
employee_read: boolean;
|
|
59
|
+
searchable: boolean;
|
|
60
|
+
archive: boolean;
|
|
61
|
+
};
|
|
62
|
+
hour_banks: {
|
|
63
|
+
searchable: false;
|
|
64
|
+
filters: ("timezone" | "currency" | "employee_id" | "period_indexes" | "is_payrolled")[];
|
|
65
|
+
employee_read: boolean;
|
|
66
|
+
create: boolean;
|
|
67
|
+
archive: boolean;
|
|
68
|
+
remove: boolean;
|
|
69
|
+
report: boolean;
|
|
70
|
+
};
|
|
71
|
+
payables: {
|
|
72
|
+
filters: ("period_index" | "currency" | "employee_id" | "payroll_id" | "is_payrolled")[];
|
|
73
|
+
employee_read: boolean;
|
|
74
|
+
searchable: boolean;
|
|
75
|
+
create: boolean;
|
|
76
|
+
archive: boolean;
|
|
77
|
+
remove: boolean;
|
|
78
|
+
report: boolean;
|
|
79
|
+
};
|
|
80
|
+
adjustments: {
|
|
81
|
+
filters: ("is_archived" | "type" | "currency" | "folder_id" | "name" | "is_employer_contribution")[];
|
|
82
|
+
employee_read: boolean;
|
|
83
|
+
searchable: boolean;
|
|
84
|
+
create: boolean;
|
|
85
|
+
archive: boolean;
|
|
86
|
+
remove: boolean;
|
|
87
|
+
report: boolean;
|
|
88
|
+
};
|
|
89
|
+
adjustment_folders: {
|
|
90
|
+
filters: "is_archived"[];
|
|
91
|
+
employee_read: boolean;
|
|
92
|
+
searchable: boolean;
|
|
93
|
+
create: boolean;
|
|
94
|
+
archive: boolean;
|
|
95
|
+
remove: boolean;
|
|
96
|
+
report: boolean;
|
|
97
|
+
};
|
|
98
|
+
pay_configs: {
|
|
99
|
+
filters: ("is_archived" | "type" | "currency" | "name")[];
|
|
100
|
+
employee_read: boolean;
|
|
101
|
+
searchable: boolean;
|
|
102
|
+
create: boolean;
|
|
103
|
+
archive: boolean;
|
|
104
|
+
remove: boolean;
|
|
105
|
+
report: boolean;
|
|
106
|
+
};
|
|
107
|
+
employee_pay_configs: {
|
|
108
|
+
employee_read: true;
|
|
109
|
+
filters: ("is_archived" | "currency" | "name" | "employee_id" | "pay_config_id")[];
|
|
110
|
+
searchable: boolean;
|
|
111
|
+
create: boolean;
|
|
112
|
+
archive: boolean;
|
|
113
|
+
remove: boolean;
|
|
114
|
+
report: boolean;
|
|
115
|
+
};
|
|
116
|
+
adjustment_records: {
|
|
117
|
+
filters: ("period_index" | "currency" | "employee_id" | "payroll_id")[];
|
|
118
|
+
employee_read: boolean;
|
|
119
|
+
searchable: boolean;
|
|
120
|
+
create: boolean;
|
|
121
|
+
archive: boolean;
|
|
122
|
+
remove: boolean;
|
|
123
|
+
report: boolean;
|
|
124
|
+
};
|
|
125
|
+
adjustment_aggregates: {
|
|
126
|
+
employee_read: false;
|
|
127
|
+
create: true;
|
|
128
|
+
report: true;
|
|
129
|
+
filters: ("currency" | "period_indexes")[];
|
|
130
|
+
searchable: boolean;
|
|
131
|
+
archive: boolean;
|
|
132
|
+
remove: boolean;
|
|
133
|
+
};
|
|
134
|
+
payroll_programs: {
|
|
135
|
+
searchable: false;
|
|
136
|
+
create: true;
|
|
137
|
+
remove: true;
|
|
138
|
+
filters: ("timezone" | "currency" | "employee_id" | "employee_pay_config_id" | "is_enabled")[];
|
|
139
|
+
employee_read: boolean;
|
|
140
|
+
archive: boolean;
|
|
141
|
+
report: boolean;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
export declare const payrollPayTypes: readonly ["per_hour", "fixed"];
|
|
145
|
+
export declare const payrollAdjustmentTypes: readonly ["rate", "per_period", "per_hour"];
|
|
146
|
+
export declare const payrollRecurrenceTypes: readonly ["daily", "weekly", "monthly", "yearly"];
|
|
147
|
+
export declare const payrollPercentageScale = 1000;
|
|
148
|
+
export type PayrollCommand = {
|
|
149
|
+
resource: typeof PayrollResource.Payrolls;
|
|
150
|
+
input: InPayroll;
|
|
151
|
+
} | {
|
|
152
|
+
resource: typeof PayrollResource.HourBanks | typeof PayrollResource.Payables;
|
|
153
|
+
input: PLPayrollsGeneratePayrollFromEntities;
|
|
154
|
+
} | {
|
|
155
|
+
resource: typeof PayrollResource.Adjustments;
|
|
156
|
+
input: InAdjustment;
|
|
157
|
+
} | {
|
|
158
|
+
resource: typeof PayrollResource.Folders;
|
|
159
|
+
input: InAdjustmentFolder;
|
|
160
|
+
} | {
|
|
161
|
+
resource: typeof PayrollResource.PayConfigs;
|
|
162
|
+
input: InPayConfig;
|
|
163
|
+
} | {
|
|
164
|
+
resource: typeof PayrollResource.EmployeeConfigs;
|
|
165
|
+
input: InEmployeePayConfig;
|
|
166
|
+
} | {
|
|
167
|
+
resource: typeof PayrollResource.Aggregates;
|
|
168
|
+
input: InAdjustmentAggregate;
|
|
169
|
+
} | {
|
|
170
|
+
resource: typeof PayrollResource.Programs;
|
|
171
|
+
input: InPayrollProgram;
|
|
172
|
+
};
|
|
173
|
+
export type PayrollBrowseOptions = SearchOptions & {
|
|
174
|
+
resource: PayrollResource;
|
|
175
|
+
context: EmploymentReadContext;
|
|
176
|
+
employer_ids?: string[];
|
|
177
|
+
filters?: PayrollFilters;
|
|
178
|
+
};
|
|
179
|
+
export declare class PayrollWorkflowError extends Error {
|
|
180
|
+
readonly reason: "access" | "reference" | "source" | "invalid" | "changed" | "pending_source";
|
|
181
|
+
constructor(reason: "access" | "reference" | "source" | "invalid" | "changed" | "pending_source");
|
|
182
|
+
}
|
|
183
|
+
export declare function payrollCanManage(actor: EmploymentActor, record?: PayrollRecord | null): boolean;
|
|
184
|
+
export declare function payrollCanRead(actor: EmploymentActor, resource: PayrollResource): boolean;
|
|
185
|
+
export declare function payrollFilterQuery(resource: PayrollResource, filters?: PayrollFilters): QueryOptions;
|
|
186
|
+
export declare class PayrollsWorkspace {
|
|
187
|
+
private readonly payrolls;
|
|
188
|
+
private readonly billable?;
|
|
189
|
+
private readonly ports;
|
|
190
|
+
private readonly submitted;
|
|
191
|
+
private consuming;
|
|
192
|
+
private readonly sourceRequests;
|
|
193
|
+
constructor(payrolls: PayrollsOperator, billable?: BillableOperator | undefined);
|
|
194
|
+
browse(opts: PayrollBrowseOptions): Promise<SearchPage<PayrollRecord>>;
|
|
195
|
+
find<K extends PayrollResource>(resource: K, id: string, signal?: AbortSignal): Promise<PayrollRecordMap[K]>;
|
|
196
|
+
periods(owner_id: string, ranges: readonly {
|
|
197
|
+
start: InDate;
|
|
198
|
+
end: InDate;
|
|
199
|
+
}[]): Promise<Period[]>;
|
|
200
|
+
create(actor: EmploymentActor, command: PayrollCommand, attempt: MultiCreateAttempt): Promise<PayrollRecord>;
|
|
201
|
+
private createChecked;
|
|
202
|
+
private owned;
|
|
203
|
+
private active;
|
|
204
|
+
private adjustments;
|
|
205
|
+
private config;
|
|
206
|
+
private validateCommand;
|
|
207
|
+
setArchived(actor: EmploymentActor, resource: PayrollResource, record: PayrollRecord, value: boolean): Promise<void>;
|
|
208
|
+
setEnabled(actor: EmploymentActor, record: PayrollProgram, is_enabled: boolean): Promise<void>;
|
|
209
|
+
remove(actor: EmploymentActor, resource: PayrollResource, record: PayrollRecord): Promise<void>;
|
|
210
|
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { isHTTPClientErr } from "@fabriktor/client";
|
|
2
|
+
import { ColPayrollsPayrolls, ColPayrollsHourBanks, ColPayrollsPayables, ColPayrollsAdjustments, ColPayrollsAdjustmentFolders, ColPayrollsPayConfigs, ColPayrollsEmployeePayConfigs, ColPayrollsAdjustmentRecords, ColPayrollsAdjustmentAggregates, ColPayrollsPayrollPrograms, UserTypeEmployer, UserTypeEmployee, AdjustmentTypeRate, AdjustmentTypePerHour, AdjustmentTypePerPeriod, PayTypeFixed, PayTypePerHour, RecurrenceTypeDaily, RecurrenceTypeWeekly, RecurrenceTypeMonthly, RecurrenceTypeYearly, } from "@fabriktor/schema";
|
|
3
|
+
import { employmentCanRead, employmentCatalogOwnerIDs, employmentOwnsRecord, } from "../contacts/scope.js";
|
|
4
|
+
import { normalizeIDs, requiredOperationID } from "../core/id.js";
|
|
5
|
+
import { runOwnerScopeQuery } from "../core/query.js";
|
|
6
|
+
import { runOwnerScopeSearch, emptySearchPage, } from "../core/search.js";
|
|
7
|
+
export const PayrollResource = {
|
|
8
|
+
Payrolls: ColPayrollsPayrolls,
|
|
9
|
+
HourBanks: ColPayrollsHourBanks,
|
|
10
|
+
Payables: ColPayrollsPayables,
|
|
11
|
+
Adjustments: ColPayrollsAdjustments,
|
|
12
|
+
Folders: ColPayrollsAdjustmentFolders,
|
|
13
|
+
PayConfigs: ColPayrollsPayConfigs,
|
|
14
|
+
EmployeeConfigs: ColPayrollsEmployeePayConfigs,
|
|
15
|
+
Records: ColPayrollsAdjustmentRecords,
|
|
16
|
+
Aggregates: ColPayrollsAdjustmentAggregates,
|
|
17
|
+
Programs: ColPayrollsPayrollPrograms,
|
|
18
|
+
};
|
|
19
|
+
const generated = {
|
|
20
|
+
employee_read: true,
|
|
21
|
+
searchable: true,
|
|
22
|
+
create: false,
|
|
23
|
+
archive: false,
|
|
24
|
+
remove: false,
|
|
25
|
+
report: false,
|
|
26
|
+
filters: [],
|
|
27
|
+
};
|
|
28
|
+
const catalog = {
|
|
29
|
+
...generated,
|
|
30
|
+
employee_read: false,
|
|
31
|
+
create: true,
|
|
32
|
+
archive: true,
|
|
33
|
+
};
|
|
34
|
+
// The service README and record Rules() define this capability/filter table.
|
|
35
|
+
// A generic CRUD method in the client is not permission to edit an immutable record.
|
|
36
|
+
export const payrollPolicies = {
|
|
37
|
+
[PayrollResource.Payrolls]: {
|
|
38
|
+
...generated,
|
|
39
|
+
create: true,
|
|
40
|
+
remove: true,
|
|
41
|
+
report: true,
|
|
42
|
+
filters: [
|
|
43
|
+
"employee_id",
|
|
44
|
+
"employee_pay_config_id",
|
|
45
|
+
"hour_bank_id",
|
|
46
|
+
"is_banked",
|
|
47
|
+
"is_problematic",
|
|
48
|
+
"period_indexes",
|
|
49
|
+
"currency",
|
|
50
|
+
"timezone",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
[PayrollResource.HourBanks]: {
|
|
54
|
+
...generated,
|
|
55
|
+
searchable: false,
|
|
56
|
+
filters: [
|
|
57
|
+
"employee_id",
|
|
58
|
+
"is_payrolled",
|
|
59
|
+
"period_indexes",
|
|
60
|
+
"currency",
|
|
61
|
+
"timezone",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
[PayrollResource.Payables]: {
|
|
65
|
+
...generated,
|
|
66
|
+
filters: [
|
|
67
|
+
"employee_id",
|
|
68
|
+
"payroll_id",
|
|
69
|
+
"is_payrolled",
|
|
70
|
+
"period_index",
|
|
71
|
+
"currency",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
[PayrollResource.Adjustments]: {
|
|
75
|
+
...catalog,
|
|
76
|
+
filters: [
|
|
77
|
+
"name",
|
|
78
|
+
"type",
|
|
79
|
+
"is_employer_contribution",
|
|
80
|
+
"folder_id",
|
|
81
|
+
"currency",
|
|
82
|
+
"is_archived",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
[PayrollResource.Folders]: { ...catalog, filters: ["is_archived"] },
|
|
86
|
+
[PayrollResource.PayConfigs]: {
|
|
87
|
+
...catalog,
|
|
88
|
+
filters: ["type", "name", "currency", "is_archived"],
|
|
89
|
+
},
|
|
90
|
+
[PayrollResource.EmployeeConfigs]: {
|
|
91
|
+
...catalog,
|
|
92
|
+
employee_read: true,
|
|
93
|
+
filters: [
|
|
94
|
+
"employee_id",
|
|
95
|
+
"pay_config_id",
|
|
96
|
+
"name",
|
|
97
|
+
"currency",
|
|
98
|
+
"is_archived",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
[PayrollResource.Records]: {
|
|
102
|
+
...generated,
|
|
103
|
+
filters: ["employee_id", "payroll_id", "currency", "period_index"],
|
|
104
|
+
},
|
|
105
|
+
[PayrollResource.Aggregates]: {
|
|
106
|
+
...generated,
|
|
107
|
+
employee_read: false,
|
|
108
|
+
create: true,
|
|
109
|
+
report: true,
|
|
110
|
+
filters: ["currency", "period_indexes"],
|
|
111
|
+
},
|
|
112
|
+
[PayrollResource.Programs]: {
|
|
113
|
+
...generated,
|
|
114
|
+
searchable: false,
|
|
115
|
+
create: true,
|
|
116
|
+
remove: true,
|
|
117
|
+
filters: [
|
|
118
|
+
"employee_id",
|
|
119
|
+
"employee_pay_config_id",
|
|
120
|
+
"is_enabled",
|
|
121
|
+
"currency",
|
|
122
|
+
"timezone",
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
export const payrollPayTypes = [PayTypePerHour, PayTypeFixed];
|
|
127
|
+
export const payrollAdjustmentTypes = [
|
|
128
|
+
AdjustmentTypeRate,
|
|
129
|
+
AdjustmentTypePerPeriod,
|
|
130
|
+
AdjustmentTypePerHour,
|
|
131
|
+
];
|
|
132
|
+
export const payrollRecurrenceTypes = [
|
|
133
|
+
RecurrenceTypeDaily,
|
|
134
|
+
RecurrenceTypeWeekly,
|
|
135
|
+
RecurrenceTypeMonthly,
|
|
136
|
+
RecurrenceTypeYearly,
|
|
137
|
+
];
|
|
138
|
+
// record/config.go pctMoney uses basis.X100000. A displayed percentage is 100 times the fraction.
|
|
139
|
+
export const payrollPercentageScale = 1000;
|
|
140
|
+
// Validation and access denials reject these transactional generation requests.
|
|
141
|
+
// A 409 can also come from idempotency middleware, so it cannot release a request.
|
|
142
|
+
const payrollGenerationRejectionStatuses = [400, 403];
|
|
143
|
+
export class PayrollWorkflowError extends Error {
|
|
144
|
+
reason;
|
|
145
|
+
constructor(reason) {
|
|
146
|
+
super(`Payroll workflow: ${reason}`);
|
|
147
|
+
this.reason = reason;
|
|
148
|
+
this.name = "PayrollWorkflowError";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function requirePayroll(value, reason) {
|
|
152
|
+
if (!value)
|
|
153
|
+
throw new PayrollWorkflowError(reason);
|
|
154
|
+
}
|
|
155
|
+
export function payrollCanManage(actor, record) {
|
|
156
|
+
return (employmentCanRead(actor) &&
|
|
157
|
+
actor.active_mode === UserTypeEmployer &&
|
|
158
|
+
(!record || employmentOwnsRecord(actor, record)));
|
|
159
|
+
}
|
|
160
|
+
export function payrollCanRead(actor, resource) {
|
|
161
|
+
return (employmentCanRead(actor) &&
|
|
162
|
+
(actor.active_mode === UserTypeEmployer ||
|
|
163
|
+
payrollPolicies[resource].employee_read));
|
|
164
|
+
}
|
|
165
|
+
export function payrollFilterQuery(resource, filters = {}) {
|
|
166
|
+
const allowed = payrollPolicies[resource].filters;
|
|
167
|
+
return Object.fromEntries(Object.entries(filters).filter(([key, value]) => allowed.includes(key) && value !== undefined));
|
|
168
|
+
}
|
|
169
|
+
export class PayrollsWorkspace {
|
|
170
|
+
payrolls;
|
|
171
|
+
billable;
|
|
172
|
+
ports;
|
|
173
|
+
// Only holds in-flight/ambiguous requests. Successful POSTs are checkpointed then removed.
|
|
174
|
+
submitted = new Map();
|
|
175
|
+
consuming = Promise.resolve();
|
|
176
|
+
sourceRequests = new Map();
|
|
177
|
+
constructor(payrolls, billable) {
|
|
178
|
+
this.payrolls = payrolls;
|
|
179
|
+
this.billable = billable;
|
|
180
|
+
this.ports = {
|
|
181
|
+
[PayrollResource.Payrolls]: {
|
|
182
|
+
query: (o) => payrolls.queryPayrolls(o),
|
|
183
|
+
search: (o) => payrolls.searchManyPayrolls(o),
|
|
184
|
+
find: (o) => payrolls.findOnePayroll(o),
|
|
185
|
+
},
|
|
186
|
+
[PayrollResource.HourBanks]: {
|
|
187
|
+
query: (o) => payrolls.queryHourBanks(o),
|
|
188
|
+
search: (o) => payrolls.searchManyHourBanks(o),
|
|
189
|
+
find: (o) => payrolls.findOneHourBank(o),
|
|
190
|
+
},
|
|
191
|
+
[PayrollResource.Payables]: {
|
|
192
|
+
query: (o) => payrolls.queryPayables(o),
|
|
193
|
+
search: (o) => payrolls.searchManyPayables(o),
|
|
194
|
+
find: (o) => payrolls.findOnePayable(o),
|
|
195
|
+
},
|
|
196
|
+
[PayrollResource.Adjustments]: {
|
|
197
|
+
query: (o) => payrolls.queryAdjustments(o),
|
|
198
|
+
search: (o) => payrolls.searchManyAdjustments(o),
|
|
199
|
+
find: (o) => payrolls.findOneAdjustment(o),
|
|
200
|
+
},
|
|
201
|
+
[PayrollResource.Folders]: {
|
|
202
|
+
query: (o) => payrolls.queryAdjustmentFolders(o),
|
|
203
|
+
search: (o) => payrolls.searchManyAdjustmentFolders(o),
|
|
204
|
+
find: (o) => payrolls.findOneAdjustmentFolder(o),
|
|
205
|
+
},
|
|
206
|
+
[PayrollResource.PayConfigs]: {
|
|
207
|
+
query: (o) => payrolls.queryPayConfigs(o),
|
|
208
|
+
search: (o) => payrolls.searchManyPayConfigs(o),
|
|
209
|
+
find: (o) => payrolls.findOnePayConfig(o),
|
|
210
|
+
},
|
|
211
|
+
[PayrollResource.EmployeeConfigs]: {
|
|
212
|
+
query: (o) => payrolls.queryEmployeePayConfigs(o),
|
|
213
|
+
search: (o) => payrolls.searchManyEmployeePayConfigs(o),
|
|
214
|
+
find: (o) => payrolls.findOneEmployeePayConfig(o),
|
|
215
|
+
},
|
|
216
|
+
[PayrollResource.Records]: {
|
|
217
|
+
query: (o) => payrolls.queryAdjustmentRecords(o),
|
|
218
|
+
search: (o) => payrolls.searchManyAdjustmentRecords(o),
|
|
219
|
+
find: (o) => payrolls.findOneAdjustmentRecord(o),
|
|
220
|
+
},
|
|
221
|
+
[PayrollResource.Aggregates]: {
|
|
222
|
+
query: (o) => payrolls.queryAdjustmentAggregates(o),
|
|
223
|
+
search: (o) => payrolls.searchManyAdjustmentAggregates(o),
|
|
224
|
+
find: (o) => payrolls.findOneAdjustmentAggregate(o),
|
|
225
|
+
},
|
|
226
|
+
[PayrollResource.Programs]: {
|
|
227
|
+
query: (o) => payrolls.queryPayrollPrograms(o),
|
|
228
|
+
search: (o) => payrolls.searchManyPayrollPrograms(o),
|
|
229
|
+
find: (o) => payrolls.findOnePayrollProgram(o),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async browse(opts) {
|
|
234
|
+
if (!payrollCanRead(opts.context, opts.resource))
|
|
235
|
+
return emptySearchPage(opts.page, opts.per_page);
|
|
236
|
+
const owner_ids = employmentCatalogOwnerIDs(opts.context, opts.employer_ids);
|
|
237
|
+
const query = payrollFilterQuery(opts.resource, opts.filters);
|
|
238
|
+
if (opts.context.active_mode === UserTypeEmployee)
|
|
239
|
+
query.employee_id = opts.context.owner_id;
|
|
240
|
+
const port = this.ports[opts.resource];
|
|
241
|
+
if (payrollPolicies[opts.resource].searchable) {
|
|
242
|
+
return runOwnerScopeSearch({
|
|
243
|
+
...opts,
|
|
244
|
+
owner_ids,
|
|
245
|
+
query,
|
|
246
|
+
search: port.search,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
requirePayroll(!opts.q?.trim(), "invalid");
|
|
250
|
+
const results = await runOwnerScopeQuery({
|
|
251
|
+
owner_ids,
|
|
252
|
+
query,
|
|
253
|
+
fetch: port.query,
|
|
254
|
+
signal: opts.signal,
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
...emptySearchPage(opts.page, opts.per_page),
|
|
258
|
+
results,
|
|
259
|
+
total_count: results.length,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async find(resource, id, signal) {
|
|
263
|
+
const value = (await this.ports[resource].find({ id, signal })).value;
|
|
264
|
+
requirePayroll(value, "reference");
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
async periods(owner_id, ranges) {
|
|
268
|
+
requirePayroll(owner_id && this.billable && ranges.length, "invalid");
|
|
269
|
+
const periods = new Map();
|
|
270
|
+
for (const range of ranges) {
|
|
271
|
+
const response = await this.billable.periodsFromDates({
|
|
272
|
+
employer_id: owner_id,
|
|
273
|
+
...range,
|
|
274
|
+
});
|
|
275
|
+
const values = response.value?.periods?.periods;
|
|
276
|
+
requirePayroll(values?.length, "invalid");
|
|
277
|
+
for (const period of values)
|
|
278
|
+
periods.set(period.period_index, period);
|
|
279
|
+
}
|
|
280
|
+
return [...periods.values()].sort((a, b) => a.period_index - b.period_index);
|
|
281
|
+
}
|
|
282
|
+
async create(actor, command, attempt) {
|
|
283
|
+
if (command.resource !== PayrollResource.HourBanks &&
|
|
284
|
+
command.resource !== PayrollResource.Payables)
|
|
285
|
+
return this.createChecked(actor, command, attempt);
|
|
286
|
+
// Consumption transactions must re-read sources after the previous one settles.
|
|
287
|
+
// This also prevents two entries in the shared concurrent creator consuming the same hours.
|
|
288
|
+
const running = this.consuming.then(() => this.createChecked(actor, command, attempt));
|
|
289
|
+
this.consuming = running.catch(() => undefined);
|
|
290
|
+
return running;
|
|
291
|
+
}
|
|
292
|
+
async createChecked(actor, command, attempt) {
|
|
293
|
+
requirePayroll(payrollCanManage(actor), "access");
|
|
294
|
+
const resource = command.resource === PayrollResource.HourBanks ||
|
|
295
|
+
command.resource === PayrollResource.Payables
|
|
296
|
+
? PayrollResource.Payrolls
|
|
297
|
+
: command.resource;
|
|
298
|
+
if (attempt.created_id)
|
|
299
|
+
return this.owned(actor, resource, attempt.created_id, attempt.signal);
|
|
300
|
+
const fingerprint = JSON.stringify({ owner_id: actor.owner_id, command });
|
|
301
|
+
const previous = this.submitted.get(attempt.idempotency_key);
|
|
302
|
+
requirePayroll(!previous || previous === fingerprint, "changed");
|
|
303
|
+
const sourceKeys = command.resource === PayrollResource.HourBanks ||
|
|
304
|
+
command.resource === PayrollResource.Payables
|
|
305
|
+
? command.input.ids.map((id) => `${actor.owner_id}:${command.resource}:${id}`)
|
|
306
|
+
: [];
|
|
307
|
+
for (const key of sourceKeys)
|
|
308
|
+
requirePayroll(!this.sourceRequests.has(key) ||
|
|
309
|
+
this.sourceRequests.get(key) === attempt.idempotency_key, "pending_source");
|
|
310
|
+
if (!previous)
|
|
311
|
+
await this.validateCommand(actor, command, attempt.signal);
|
|
312
|
+
attempt.before_request?.();
|
|
313
|
+
attempt.signal.throwIfAborted();
|
|
314
|
+
this.submitted.set(attempt.idempotency_key, fingerprint);
|
|
315
|
+
for (const key of sourceKeys)
|
|
316
|
+
this.sourceRequests.set(key, attempt.idempotency_key);
|
|
317
|
+
const p = this.payrolls;
|
|
318
|
+
const request = {
|
|
319
|
+
signal: attempt.signal,
|
|
320
|
+
idempotency_key: attempt.idempotency_key,
|
|
321
|
+
};
|
|
322
|
+
const out = await (() => {
|
|
323
|
+
switch (command.resource) {
|
|
324
|
+
case PayrollResource.Payrolls:
|
|
325
|
+
return p.generatePayroll(command.input, request);
|
|
326
|
+
case PayrollResource.HourBanks:
|
|
327
|
+
return p.generatePayrollFromHourBanks(command.input, request);
|
|
328
|
+
case PayrollResource.Payables:
|
|
329
|
+
return p.generatePayrollFromPayables(command.input, request);
|
|
330
|
+
case PayrollResource.Aggregates:
|
|
331
|
+
return p.aggregateAdjustments(command.input, request);
|
|
332
|
+
case PayrollResource.Adjustments:
|
|
333
|
+
return p.insertOneAdjustment({ in: command.input, ...request });
|
|
334
|
+
case PayrollResource.Folders:
|
|
335
|
+
return p.insertOneAdjustmentFolder({ in: command.input, ...request });
|
|
336
|
+
case PayrollResource.PayConfigs:
|
|
337
|
+
return p.insertOnePayConfig({ in: command.input, ...request });
|
|
338
|
+
case PayrollResource.EmployeeConfigs:
|
|
339
|
+
return p.insertOneEmployeePayConfig({
|
|
340
|
+
in: command.input,
|
|
341
|
+
...request,
|
|
342
|
+
});
|
|
343
|
+
case PayrollResource.Programs:
|
|
344
|
+
return p.insertOnePayrollProgram({ in: command.input, ...request });
|
|
345
|
+
}
|
|
346
|
+
})().catch((error) => {
|
|
347
|
+
// Generation from configurations, hour banks and payables returns a payroll.
|
|
348
|
+
// A rejection on retry does not settle an earlier ambiguous submission.
|
|
349
|
+
if (!previous &&
|
|
350
|
+
resource === PayrollResource.Payrolls &&
|
|
351
|
+
isHTTPClientErr(error) &&
|
|
352
|
+
error.status !== undefined &&
|
|
353
|
+
payrollGenerationRejectionStatuses.includes(error.status)) {
|
|
354
|
+
this.submitted.delete(attempt.idempotency_key);
|
|
355
|
+
for (const key of sourceKeys)
|
|
356
|
+
this.sourceRequests.delete(key);
|
|
357
|
+
attempt.on_rejected?.();
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
});
|
|
361
|
+
const id = requiredOperationID(out, "Generated payroll record response did not contain an ID.");
|
|
362
|
+
attempt.on_created?.(id);
|
|
363
|
+
this.submitted.delete(attempt.idempotency_key);
|
|
364
|
+
for (const key of sourceKeys)
|
|
365
|
+
this.sourceRequests.delete(key);
|
|
366
|
+
return this.owned(actor, resource, id, attempt.signal);
|
|
367
|
+
}
|
|
368
|
+
async owned(actor, resource, id, signal) {
|
|
369
|
+
const value = await this.find(resource, id, signal);
|
|
370
|
+
requirePayroll(payrollCanManage(actor, value), "access");
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
async active(actor, resource, id, currency, signal) {
|
|
374
|
+
const value = await this.owned(actor, resource, id, signal);
|
|
375
|
+
requirePayroll(!("is_archived" in value && value.is_archived) &&
|
|
376
|
+
(!currency || !("currency" in value) || value.currency === currency), "reference");
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
async adjustments(actor, ref, currency, signal) {
|
|
380
|
+
for (const [ids, contribution] of [
|
|
381
|
+
[ref.deduction_ids, false],
|
|
382
|
+
[ref.contribution_ids, true],
|
|
383
|
+
]) {
|
|
384
|
+
requirePayroll(normalizeIDs(ids).length === ids.length, "invalid");
|
|
385
|
+
for (const id of ids) {
|
|
386
|
+
const value = await this.active(actor, PayrollResource.Adjustments, id, currency, signal);
|
|
387
|
+
requirePayroll(value.is_employer_contribution === contribution, "reference");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async config(actor, id, currency, signal) {
|
|
392
|
+
const value = await this.active(actor, PayrollResource.EmployeeConfigs, id, currency, signal);
|
|
393
|
+
const pay = await this.active(actor, PayrollResource.PayConfigs, value.pay_config_id, currency, signal);
|
|
394
|
+
await this.adjustments(actor, pay, currency, signal);
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
async validateCommand(actor, command, signal) {
|
|
398
|
+
const v = command.input;
|
|
399
|
+
const integerAmounts = [
|
|
400
|
+
"rate",
|
|
401
|
+
"unit_amount",
|
|
402
|
+
"work_normal_rate",
|
|
403
|
+
"work_overtime_rate",
|
|
404
|
+
"transport_normal_rate",
|
|
405
|
+
"transport_overtime_rate",
|
|
406
|
+
"default_fixed_hours",
|
|
407
|
+
"work_rate",
|
|
408
|
+
"transport_rate",
|
|
409
|
+
];
|
|
410
|
+
for (const [key, value] of Object.entries(v))
|
|
411
|
+
if (integerAmounts.includes(key))
|
|
412
|
+
requirePayroll(typeof value === "number" &&
|
|
413
|
+
Number.isSafeInteger(value) &&
|
|
414
|
+
value >= 0, "invalid");
|
|
415
|
+
if ("sub_contributions" in v)
|
|
416
|
+
requirePayroll(v.sub_contributions.every((rule) => Number.isSafeInteger(rule.rate) && rule.rate >= 0), "invalid");
|
|
417
|
+
if ("periods" in v)
|
|
418
|
+
requirePayroll(v.periods.length &&
|
|
419
|
+
v.periods.every((period) => Number.isSafeInteger(period.period) &&
|
|
420
|
+
period.period > 0 &&
|
|
421
|
+
Number.isSafeInteger(period.period_year) &&
|
|
422
|
+
period.period_year > 0), "invalid");
|
|
423
|
+
if ("currency" in v)
|
|
424
|
+
requirePayroll(v.currency.trim(), "invalid");
|
|
425
|
+
switch (command.resource) {
|
|
426
|
+
case PayrollResource.Payrolls:
|
|
427
|
+
requirePayroll(command.input.periods.length &&
|
|
428
|
+
command.input.timezone === command.input.tz, "invalid");
|
|
429
|
+
await this.config(actor, command.input.employee_pay_config_id, command.input.currency, signal);
|
|
430
|
+
break;
|
|
431
|
+
case PayrollResource.Programs: {
|
|
432
|
+
const input = command.input;
|
|
433
|
+
const config = await this.config(actor, input.employee_pay_config_id, input.currency, signal);
|
|
434
|
+
requirePayroll(input.employee_id === config.employee_id &&
|
|
435
|
+
input.timezone === input.next_payroll_date.tz &&
|
|
436
|
+
Number.isSafeInteger(input.recurrence_rate) &&
|
|
437
|
+
input.recurrence_rate > 0 &&
|
|
438
|
+
payrollRecurrenceTypes.some((x) => x === input.recurrence_type), "invalid");
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case PayrollResource.HourBanks:
|
|
442
|
+
case PayrollResource.Payables: {
|
|
443
|
+
const input = command.input;
|
|
444
|
+
requirePayroll(input.employee_id &&
|
|
445
|
+
input.ids.length &&
|
|
446
|
+
normalizeIDs(input.ids).length === input.ids.length &&
|
|
447
|
+
Number.isSafeInteger(input.period_index), "invalid");
|
|
448
|
+
for (const id of input.ids) {
|
|
449
|
+
const source = await this.owned(actor, command.resource, id, signal);
|
|
450
|
+
requirePayroll(!source.is_payrolled &&
|
|
451
|
+
source.employee_id === input.employee_id &&
|
|
452
|
+
source.currency === input.currency, "source");
|
|
453
|
+
}
|
|
454
|
+
await this.adjustments(actor, input.AdjustmentRef, input.currency, signal);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case PayrollResource.EmployeeConfigs:
|
|
458
|
+
requirePayroll(command.input.employee_id && command.input.name.trim(), "invalid");
|
|
459
|
+
await this.active(actor, PayrollResource.PayConfigs, command.input.pay_config_id, command.input.currency, signal);
|
|
460
|
+
break;
|
|
461
|
+
case PayrollResource.PayConfigs:
|
|
462
|
+
requirePayroll(command.input.name.trim() &&
|
|
463
|
+
payrollPayTypes.some((x) => x === command.input.type) &&
|
|
464
|
+
Number.isSafeInteger(command.input.default_fixed_hours), "invalid");
|
|
465
|
+
await this.adjustments(actor, command.input, command.input.currency, signal);
|
|
466
|
+
break;
|
|
467
|
+
case PayrollResource.Adjustments: {
|
|
468
|
+
const input = command.input;
|
|
469
|
+
requirePayroll(input.name.trim() &&
|
|
470
|
+
payrollAdjustmentTypes.some((x) => x === input.type), "invalid");
|
|
471
|
+
requirePayroll(!input.sub_contributions.length ||
|
|
472
|
+
(!input.is_employer_contribution &&
|
|
473
|
+
!input.is_accrued &&
|
|
474
|
+
input.sub_contributions.every((x) => x.type === AdjustmentTypeRate && !x.is_accrued)), "invalid");
|
|
475
|
+
await this.active(actor, PayrollResource.Folders, input.folder_id, undefined, signal);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case PayrollResource.Folders:
|
|
479
|
+
requirePayroll(command.input.folder_name.trim(), "invalid");
|
|
480
|
+
break;
|
|
481
|
+
case PayrollResource.Aggregates:
|
|
482
|
+
requirePayroll(command.input.employee_ids.length && command.input.periods.length, "invalid");
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async setArchived(actor, resource, record, value) {
|
|
487
|
+
requirePayroll(payrollCanManage(actor, record) && payrollPolicies[resource].archive, "access");
|
|
488
|
+
await this.owned(actor, resource, record.id);
|
|
489
|
+
const opts = { id: record.id, in: { is_archived: value } };
|
|
490
|
+
switch (resource) {
|
|
491
|
+
case PayrollResource.Adjustments:
|
|
492
|
+
await this.payrolls.replaceOneAdjustment(opts);
|
|
493
|
+
break;
|
|
494
|
+
case PayrollResource.Folders:
|
|
495
|
+
await this.payrolls.replaceOneAdjustmentFolder(opts);
|
|
496
|
+
break;
|
|
497
|
+
case PayrollResource.PayConfigs:
|
|
498
|
+
await this.payrolls.replaceOnePayConfig(opts);
|
|
499
|
+
break;
|
|
500
|
+
case PayrollResource.EmployeeConfigs:
|
|
501
|
+
await this.payrolls.replaceOneEmployeePayConfig(opts);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async setEnabled(actor, record, is_enabled) {
|
|
506
|
+
const latest = await this.owned(actor, PayrollResource.Programs, record.id);
|
|
507
|
+
if (is_enabled)
|
|
508
|
+
await this.config(actor, latest.employee_pay_config_id, latest.currency);
|
|
509
|
+
await this.payrolls.replaceOnePayrollProgram({
|
|
510
|
+
id: record.id,
|
|
511
|
+
in: { is_enabled },
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async remove(actor, resource, record) {
|
|
515
|
+
requirePayroll(payrollCanManage(actor, record) && payrollPolicies[resource].remove, "access");
|
|
516
|
+
await this.owned(actor, resource, record.id);
|
|
517
|
+
if (resource === PayrollResource.Payrolls)
|
|
518
|
+
await this.payrolls.deleteOnePayroll({ id: record.id });
|
|
519
|
+
else if (resource === PayrollResource.Programs)
|
|
520
|
+
await this.payrolls.deleteOnePayrollProgram({ id: record.id });
|
|
521
|
+
}
|
|
522
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.124",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.91",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
22
|
"@fabriktor/schema": "0.0.65"
|
|
23
23
|
}
|