@things-factory/accounting 10.1.154 → 10.1.156
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/activities/activity-expenditure.d.ts +186 -0
- package/dist-server/activities/activity-expenditure.js +122 -0
- package/dist-server/activities/activity-expenditure.js.map +1 -0
- package/dist-server/activities/expenditure-activity-names.d.ts +4 -0
- package/dist-server/activities/expenditure-activity-names.js +13 -0
- package/dist-server/activities/expenditure-activity-names.js.map +1 -0
- package/dist-server/activities/index.d.ts +2 -1
- package/dist-server/activities/index.js +19 -12
- package/dist-server/activities/index.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/account/account-match.d.ts +5 -3
- package/dist-server/service/account/account-match.js +7 -3
- package/dist-server/service/account/account-match.js.map +1 -1
- package/dist-server/service/bank-statement/bank-import.js +9 -3
- package/dist-server/service/bank-statement/bank-import.js.map +1 -1
- package/dist-server/service/bank-statement/bank-source.js +2 -1
- package/dist-server/service/bank-statement/bank-source.js.map +1 -1
- package/dist-server/service/counterparty-account/counterparty-account-query.d.ts +6 -0
- package/dist-server/service/counterparty-account/counterparty-account-query.js +48 -0
- package/dist-server/service/counterparty-account/counterparty-account-query.js.map +1 -0
- package/dist-server/service/counterparty-account/counterparty-account-rules.d.ts +46 -0
- package/dist-server/service/counterparty-account/counterparty-account-rules.js +76 -0
- package/dist-server/service/counterparty-account/counterparty-account-rules.js.map +1 -0
- package/dist-server/service/counterparty-account/counterparty-account-service.d.ts +22 -0
- package/dist-server/service/counterparty-account/counterparty-account-service.js +66 -0
- package/dist-server/service/counterparty-account/counterparty-account-service.js.map +1 -0
- package/dist-server/service/counterparty-account/counterparty-account-type.d.ts +9 -0
- package/dist-server/service/counterparty-account/counterparty-account-type.js +40 -0
- package/dist-server/service/counterparty-account/counterparty-account-type.js.map +1 -0
- package/dist-server/service/counterparty-account/index.d.ts +4 -0
- package/dist-server/service/counterparty-account/index.js +8 -0
- package/dist-server/service/counterparty-account/index.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-payment-state.d.ts +52 -0
- package/dist-server/service/expenditure-request/expenditure-payment-state.js +100 -0
- package/dist-server/service/expenditure-request/expenditure-payment-state.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-item.d.ts +41 -0
- package/dist-server/service/expenditure-request/expenditure-request-item.js +149 -0
- package/dist-server/service/expenditure-request/expenditure-request-item.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-mutation.d.ts +18 -0
- package/dist-server/service/expenditure-request/expenditure-request-mutation.js +140 -0
- package/dist-server/service/expenditure-request/expenditure-request-mutation.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-query.d.ts +46 -0
- package/dist-server/service/expenditure-request/expenditure-request-query.js +365 -0
- package/dist-server/service/expenditure-request/expenditure-request-query.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-rules.d.ts +128 -0
- package/dist-server/service/expenditure-request/expenditure-request-rules.js +183 -0
- package/dist-server/service/expenditure-request/expenditure-request-rules.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-service.d.ts +92 -0
- package/dist-server/service/expenditure-request/expenditure-request-service.js +488 -0
- package/dist-server/service/expenditure-request/expenditure-request-service.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-type.d.ts +85 -0
- package/dist-server/service/expenditure-request/expenditure-request-type.js +312 -0
- package/dist-server/service/expenditure-request/expenditure-request-type.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request.d.ts +67 -0
- package/dist-server/service/expenditure-request/expenditure-request.js +228 -0
- package/dist-server/service/expenditure-request/expenditure-request.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-target.d.ts +1 -0
- package/dist-server/service/expenditure-request/expenditure-target.js +55 -0
- package/dist-server/service/expenditure-request/expenditure-target.js.map +1 -0
- package/dist-server/service/expenditure-request/index.d.ts +8 -0
- package/dist-server/service/expenditure-request/index.js +12 -0
- package/dist-server/service/expenditure-request/index.js.map +1 -0
- package/dist-server/service/index.d.ts +9 -2
- package/dist-server/service/index.js +18 -3
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/translations/en.json +58 -1
- package/translations/es.json +17 -1
- package/translations/ja.json +17 -1
- package/translations/ko.json +58 -1
- package/translations/ms.json +17 -1
- package/translations/zh.json +17 -1
- package/dist-client/activities/activity-expense-edit.d.ts +0 -14
- package/dist-client/activities/activity-expense-edit.js +0 -85
- package/dist-client/activities/activity-expense-edit.js.map +0 -1
- package/dist-client/activities/activity-expense-view.d.ts +0 -14
- package/dist-client/activities/activity-expense-view.js +0 -85
- package/dist-client/activities/activity-expense-view.js.map +0 -1
- package/dist-server/activities/activity-expense.d.ts +0 -58
- package/dist-server/activities/activity-expense.js +0 -150
- package/dist-server/activities/activity-expense.js.map +0 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { ActivityInstance, UpdateActivityInstanceStateAddendum } from '@things-factory/worklist';
|
|
2
|
+
/**
|
|
3
|
+
* What an approver's decision does to the request.
|
|
4
|
+
*
|
|
5
|
+
* worklist calls this on every state change of the instance, in the same transaction, and more than once for one
|
|
6
|
+
* change. `decisionOf` lets a decision land only on the request still waiting on this instance - so a repeat call, or
|
|
7
|
+
* a call about an instance the requester already withdrew and replaced, changes nothing.
|
|
8
|
+
*
|
|
9
|
+
* A rejection does not end the instance; the rejected thread in `causedBy` is how it is known. The person deciding is
|
|
10
|
+
* `context.state.user` - the approver, not the requester.
|
|
11
|
+
*/
|
|
12
|
+
export declare function decideExpenditure(activityInstance: ActivityInstance, addendum: UpdateActivityInstanceStateAddendum, context: ResolverContext): Promise<void>;
|
|
13
|
+
/** 지출결의 - paying a counterparty. */
|
|
14
|
+
export declare const ActivityExpenditureRequest: {
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
guide: string[];
|
|
19
|
+
searchKeys: {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
inputKey: string;
|
|
23
|
+
tKey: string;
|
|
24
|
+
}[];
|
|
25
|
+
decisionKeys: ({
|
|
26
|
+
role: string;
|
|
27
|
+
inputPath: string;
|
|
28
|
+
name: string;
|
|
29
|
+
tKey?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
role: string;
|
|
32
|
+
inputPath: string;
|
|
33
|
+
tKey: string;
|
|
34
|
+
name: string;
|
|
35
|
+
})[];
|
|
36
|
+
release: string;
|
|
37
|
+
provider: string;
|
|
38
|
+
category: string;
|
|
39
|
+
activityType: string;
|
|
40
|
+
priority: number;
|
|
41
|
+
startingType: string;
|
|
42
|
+
multiple: string;
|
|
43
|
+
viewType: string;
|
|
44
|
+
viewSource: string;
|
|
45
|
+
model: {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
tag: string;
|
|
49
|
+
hidden: boolean;
|
|
50
|
+
mandatory: boolean;
|
|
51
|
+
inout: string;
|
|
52
|
+
type: string;
|
|
53
|
+
options: {};
|
|
54
|
+
unit: any;
|
|
55
|
+
quantifier: number[];
|
|
56
|
+
spec: {};
|
|
57
|
+
}[];
|
|
58
|
+
standardTime: number;
|
|
59
|
+
readiness: {
|
|
60
|
+
stage: string;
|
|
61
|
+
tests: string[];
|
|
62
|
+
lastRun: {
|
|
63
|
+
passed: number;
|
|
64
|
+
total: number;
|
|
65
|
+
at: string;
|
|
66
|
+
command: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
callback: (instance: any, addendum: any, context: any) => Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
/** 경비 청구 - paying an employee back what they spent for the company in a month. */
|
|
72
|
+
export declare const ActivityExpenseClaim: {
|
|
73
|
+
name: string;
|
|
74
|
+
label: string;
|
|
75
|
+
description: string;
|
|
76
|
+
guide: string[];
|
|
77
|
+
searchKeys: {
|
|
78
|
+
name: string;
|
|
79
|
+
description: string;
|
|
80
|
+
inputKey: string;
|
|
81
|
+
tKey: string;
|
|
82
|
+
}[];
|
|
83
|
+
decisionKeys: ({
|
|
84
|
+
role: string;
|
|
85
|
+
inputPath: string;
|
|
86
|
+
name: string;
|
|
87
|
+
tKey?: undefined;
|
|
88
|
+
} | {
|
|
89
|
+
role: string;
|
|
90
|
+
inputPath: string;
|
|
91
|
+
tKey: string;
|
|
92
|
+
name: string;
|
|
93
|
+
})[];
|
|
94
|
+
release: string;
|
|
95
|
+
provider: string;
|
|
96
|
+
category: string;
|
|
97
|
+
activityType: string;
|
|
98
|
+
priority: number;
|
|
99
|
+
startingType: string;
|
|
100
|
+
multiple: string;
|
|
101
|
+
viewType: string;
|
|
102
|
+
viewSource: string;
|
|
103
|
+
model: {
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
tag: string;
|
|
107
|
+
hidden: boolean;
|
|
108
|
+
mandatory: boolean;
|
|
109
|
+
inout: string;
|
|
110
|
+
type: string;
|
|
111
|
+
options: {};
|
|
112
|
+
unit: any;
|
|
113
|
+
quantifier: number[];
|
|
114
|
+
spec: {};
|
|
115
|
+
}[];
|
|
116
|
+
standardTime: number;
|
|
117
|
+
readiness: {
|
|
118
|
+
stage: string;
|
|
119
|
+
tests: string[];
|
|
120
|
+
lastRun: {
|
|
121
|
+
passed: number;
|
|
122
|
+
total: number;
|
|
123
|
+
at: string;
|
|
124
|
+
command: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
callback: (instance: any, addendum: any, context: any) => Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
/** The approvals of an expenditure request - an application that uses them installs these into its domains. */
|
|
130
|
+
export declare const EXPENDITURE_ACTIVITIES: {
|
|
131
|
+
name: string;
|
|
132
|
+
label: string;
|
|
133
|
+
description: string;
|
|
134
|
+
guide: string[];
|
|
135
|
+
searchKeys: {
|
|
136
|
+
name: string;
|
|
137
|
+
description: string;
|
|
138
|
+
inputKey: string;
|
|
139
|
+
tKey: string;
|
|
140
|
+
}[];
|
|
141
|
+
decisionKeys: ({
|
|
142
|
+
role: string;
|
|
143
|
+
inputPath: string;
|
|
144
|
+
name: string;
|
|
145
|
+
tKey?: undefined;
|
|
146
|
+
} | {
|
|
147
|
+
role: string;
|
|
148
|
+
inputPath: string;
|
|
149
|
+
tKey: string;
|
|
150
|
+
name: string;
|
|
151
|
+
})[];
|
|
152
|
+
release: string;
|
|
153
|
+
provider: string;
|
|
154
|
+
category: string;
|
|
155
|
+
activityType: string;
|
|
156
|
+
priority: number;
|
|
157
|
+
startingType: string;
|
|
158
|
+
multiple: string;
|
|
159
|
+
viewType: string;
|
|
160
|
+
viewSource: string;
|
|
161
|
+
model: {
|
|
162
|
+
name: string;
|
|
163
|
+
description: string;
|
|
164
|
+
tag: string;
|
|
165
|
+
hidden: boolean;
|
|
166
|
+
mandatory: boolean;
|
|
167
|
+
inout: string;
|
|
168
|
+
type: string;
|
|
169
|
+
options: {};
|
|
170
|
+
unit: any;
|
|
171
|
+
quantifier: number[];
|
|
172
|
+
spec: {};
|
|
173
|
+
}[];
|
|
174
|
+
standardTime: number;
|
|
175
|
+
readiness: {
|
|
176
|
+
stage: string;
|
|
177
|
+
tests: string[];
|
|
178
|
+
lastRun: {
|
|
179
|
+
passed: number;
|
|
180
|
+
total: number;
|
|
181
|
+
at: string;
|
|
182
|
+
command: string;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
callback: (instance: any, addendum: any, context: any) => Promise<void>;
|
|
186
|
+
}[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXPENDITURE_ACTIVITIES = exports.ActivityExpenseClaim = exports.ActivityExpenditureRequest = void 0;
|
|
4
|
+
exports.decideExpenditure = decideExpenditure;
|
|
5
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
|
+
const expenditure_request_1 = require("../service/expenditure-request/expenditure-request");
|
|
7
|
+
const expenditure_request_rules_1 = require("../service/expenditure-request/expenditure-request-rules");
|
|
8
|
+
const expenditure_activity_names_1 = require("./expenditure-activity-names");
|
|
9
|
+
/**
|
|
10
|
+
* What an approver's decision does to the request.
|
|
11
|
+
*
|
|
12
|
+
* worklist calls this on every state change of the instance, in the same transaction, and more than once for one
|
|
13
|
+
* change. `decisionOf` lets a decision land only on the request still waiting on this instance - so a repeat call, or
|
|
14
|
+
* a call about an instance the requester already withdrew and replaced, changes nothing.
|
|
15
|
+
*
|
|
16
|
+
* A rejection does not end the instance; the rejected thread in `causedBy` is how it is known. The person deciding is
|
|
17
|
+
* `context.state.user` - the approver, not the requester.
|
|
18
|
+
*/
|
|
19
|
+
async function decideExpenditure(activityInstance, addendum, context) {
|
|
20
|
+
const { domain, user, tx } = context.state;
|
|
21
|
+
const requestId = activityInstance.input?.requestId;
|
|
22
|
+
if (!requestId)
|
|
23
|
+
return;
|
|
24
|
+
const repo = tx.getRepository(expenditure_request_1.ExpenditureRequest);
|
|
25
|
+
const request = await repo.findOne({ where: { id: requestId, domain: { id: domain.id } } });
|
|
26
|
+
if (!request)
|
|
27
|
+
return;
|
|
28
|
+
const decision = (0, expenditure_request_rules_1.decisionOf)({ id: activityInstance.id, state: String(activityInstance.state) }, (addendum?.causedBy ?? []).map(thread => ({ state: String(thread.state) })), { status: request.status, activityInstanceId: request.activityInstanceId });
|
|
29
|
+
if (!decision)
|
|
30
|
+
return;
|
|
31
|
+
request.status = expenditure_request_1.ExpenditureRequestStatus[decision];
|
|
32
|
+
if (decision !== 'WITHDRAWN') {
|
|
33
|
+
request.decidedAt = new Date();
|
|
34
|
+
request.decidedBy = user;
|
|
35
|
+
}
|
|
36
|
+
await repo.save(request);
|
|
37
|
+
}
|
|
38
|
+
const MODEL = [
|
|
39
|
+
{ name: 'requestId', description: 'The request this approval decides', tag: 'requestId', hidden: true, mandatory: true, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },
|
|
40
|
+
{ name: 'month', description: 'Month, at the top level so the search key can index it', tag: 'month', hidden: true, mandatory: false, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },
|
|
41
|
+
{ name: 'requester', description: 'Who requests', tag: 'requester', hidden: true, mandatory: true, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },
|
|
42
|
+
{ name: 'payee', description: 'Who is paid', tag: 'payee', hidden: true, mandatory: false, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },
|
|
43
|
+
{ name: 'lines', description: 'The lines as the approver reads them', tag: 'lines', hidden: true, mandatory: false, inout: 'in', type: 'object', options: {}, unit: null, quantifier: [1], spec: {} }
|
|
44
|
+
];
|
|
45
|
+
/*
|
|
46
|
+
* What the 업무 카탈로그 shows of how far these forms are (worklist `ActivityReadiness`). `lastRun` is written by
|
|
47
|
+
* hand: change the tests, and change this line with them - the screen shows the day it was measured, so a stale
|
|
48
|
+
* count reads as stale.
|
|
49
|
+
*/
|
|
50
|
+
const READINESS = {
|
|
51
|
+
stage: 'working',
|
|
52
|
+
tests: ['packages/accounting/test/expenditure-requests.test.mjs'],
|
|
53
|
+
lastRun: { passed: 17, total: 17, at: '2026-10-13T13:43:26+09:00', command: 'npm run build:server && node --test test/expenditure-requests.test.mjs (packages/accounting)' }
|
|
54
|
+
};
|
|
55
|
+
const COMMON = {
|
|
56
|
+
release: '1.0.0',
|
|
57
|
+
provider: 'hatiolab.com',
|
|
58
|
+
category: 'accounting',
|
|
59
|
+
activityType: 'user',
|
|
60
|
+
priority: 1,
|
|
61
|
+
startingType: 'post',
|
|
62
|
+
multiple: 'single',
|
|
63
|
+
/* No screen of its own: the generic approval screen draws the decision keys. Set explicitly - leaving it out keeps an old value. */
|
|
64
|
+
viewType: '',
|
|
65
|
+
viewSource: '',
|
|
66
|
+
model: MODEL,
|
|
67
|
+
standardTime: 3 * 24 * 60 * 60,
|
|
68
|
+
readiness: READINESS,
|
|
69
|
+
/* Runs inside worklist's mutation, where no resolver's `SpeakRefusals` reaches. */
|
|
70
|
+
callback: (0, shell_1.speaksRefusals)(decideExpenditure)
|
|
71
|
+
};
|
|
72
|
+
/** 지출결의 - paying a counterparty. */
|
|
73
|
+
exports.ActivityExpenditureRequest = {
|
|
74
|
+
...COMMON,
|
|
75
|
+
name: expenditure_activity_names_1.EXPENDITURE_REQUEST_ACTIVITY,
|
|
76
|
+
label: 'expenditure request',
|
|
77
|
+
description: 'Approval of a payment to a counterparty before the money leaves',
|
|
78
|
+
guide: [
|
|
79
|
+
'someone asks the company to pay a counterparty - an invoice, a service, a deposit - into an account of that counterparty.',
|
|
80
|
+
'approving puts it in the payment queue; finance pays it, and the payment is reconciled with the bank statement.',
|
|
81
|
+
'the account approvers see is the account paid; if it changes after the approval, finance is told before paying.',
|
|
82
|
+
'rejecting returns the request to its writer, who can correct it and submit again.',
|
|
83
|
+
'with no approver named for this domain a request cannot be submitted.'
|
|
84
|
+
],
|
|
85
|
+
searchKeys: [
|
|
86
|
+
{ name: 'payee', description: 'Who is paid', inputKey: 'payee', tKey: 'field.payee' },
|
|
87
|
+
{ name: 'requester', description: 'Who requests', inputKey: 'requester', tKey: 'field.requester' }
|
|
88
|
+
],
|
|
89
|
+
decisionKeys: [
|
|
90
|
+
{ role: 'headline', inputPath: 'headline', name: 'what is to be paid' },
|
|
91
|
+
{ role: 'subject', inputPath: 'payee', tKey: 'field.payee', name: 'who is paid' },
|
|
92
|
+
{ role: 'reason', inputPath: 'lines', tKey: 'field.expenditure-lines', name: 'what it pays for' },
|
|
93
|
+
{ role: 'consequence', inputPath: 'consequence', name: 'what approving does' }
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
/** 경비 청구 - paying an employee back what they spent for the company in a month. */
|
|
97
|
+
exports.ActivityExpenseClaim = {
|
|
98
|
+
...COMMON,
|
|
99
|
+
name: expenditure_activity_names_1.EXPENSE_CLAIM_ACTIVITY,
|
|
100
|
+
label: 'expense claim',
|
|
101
|
+
description: 'Approval of what an employee spent for the company in a month',
|
|
102
|
+
guide: [
|
|
103
|
+
'an employee claims what they spent for the company - meals, travel, supplies.',
|
|
104
|
+
'approving puts it in the payment queue; finance pays the employee back, and the payment is reconciled with the bank statement.',
|
|
105
|
+
'lines paid with the corporate card are reviewed the same way but nothing is paid back for them.',
|
|
106
|
+
'rejecting returns the claim to the employee, who can correct it and submit again.',
|
|
107
|
+
'with no approver named for this domain a claim cannot be submitted.'
|
|
108
|
+
],
|
|
109
|
+
searchKeys: [
|
|
110
|
+
{ name: 'month', description: 'Which month', inputKey: 'month', tKey: 'field.month' },
|
|
111
|
+
{ name: 'requester', description: 'Who claims', inputKey: 'requester', tKey: 'field.requester' }
|
|
112
|
+
],
|
|
113
|
+
decisionKeys: [
|
|
114
|
+
{ role: 'headline', inputPath: 'headline', name: 'what is claimed' },
|
|
115
|
+
{ role: 'subject', inputPath: 'requester', tKey: 'field.requester', name: 'who claims' },
|
|
116
|
+
{ role: 'reason', inputPath: 'lines', tKey: 'field.expenditure-lines', name: 'what was spent, on what' },
|
|
117
|
+
{ role: 'consequence', inputPath: 'consequence', name: 'what approving does' }
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
/** The approvals of an expenditure request - an application that uses them installs these into its domains. */
|
|
121
|
+
exports.EXPENDITURE_ACTIVITIES = [exports.ActivityExpenditureRequest, exports.ActivityExpenseClaim];
|
|
122
|
+
//# sourceMappingURL=activity-expenditure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity-expenditure.js","sourceRoot":"","sources":["../../server/activities/activity-expenditure.ts"],"names":[],"mappings":"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","sourcesContent":["import { ActivityInstance, UpdateActivityInstanceStateAddendum } from '@things-factory/worklist'\nimport { speaksRefusals } from '@things-factory/shell'\n\nimport { ExpenditureRequest, ExpenditureRequestStatus } from '../service/expenditure-request/expenditure-request'\nimport { decisionOf, type RequestStatus } from '../service/expenditure-request/expenditure-request-rules'\nimport { EXPENDITURE_REQUEST_ACTIVITY, EXPENSE_CLAIM_ACTIVITY } from './expenditure-activity-names'\n\n/**\n * What an approver's decision does to the request.\n *\n * worklist calls this on every state change of the instance, in the same transaction, and more than once for one\n * change. `decisionOf` lets a decision land only on the request still waiting on this instance - so a repeat call, or\n * a call about an instance the requester already withdrew and replaced, changes nothing.\n *\n * A rejection does not end the instance; the rejected thread in `causedBy` is how it is known. The person deciding is\n * `context.state.user` - the approver, not the requester.\n */\nexport async function decideExpenditure(activityInstance: ActivityInstance, addendum: UpdateActivityInstanceStateAddendum, context: ResolverContext) {\n const { domain, user, tx } = context.state\n const requestId = (activityInstance.input as any)?.requestId\n if (!requestId) return\n\n const repo = tx.getRepository(ExpenditureRequest)\n const request = await repo.findOne({ where: { id: requestId, domain: { id: domain.id } } as any })\n if (!request) return\n\n const decision = decisionOf(\n { id: activityInstance.id, state: String(activityInstance.state) },\n (addendum?.causedBy ?? []).map(thread => ({ state: String(thread.state) })),\n { status: request.status as RequestStatus, activityInstanceId: request.activityInstanceId }\n )\n if (!decision) return\n\n request.status = ExpenditureRequestStatus[decision]\n if (decision !== 'WITHDRAWN') {\n request.decidedAt = new Date()\n request.decidedBy = user\n }\n await repo.save(request)\n}\n\nconst MODEL = [\n { name: 'requestId', description: 'The request this approval decides', tag: 'requestId', hidden: true, mandatory: true, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },\n { name: 'month', description: 'Month, at the top level so the search key can index it', tag: 'month', hidden: true, mandatory: false, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },\n { name: 'requester', description: 'Who requests', tag: 'requester', hidden: true, mandatory: true, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },\n { name: 'payee', description: 'Who is paid', tag: 'payee', hidden: true, mandatory: false, inout: 'in', type: 'string', options: {}, unit: null, quantifier: [1], spec: {} },\n { name: 'lines', description: 'The lines as the approver reads them', tag: 'lines', hidden: true, mandatory: false, inout: 'in', type: 'object', options: {}, unit: null, quantifier: [1], spec: {} }\n]\n\n/*\n * What the 업무 카탈로그 shows of how far these forms are (worklist `ActivityReadiness`). `lastRun` is written by\n * hand: change the tests, and change this line with them - the screen shows the day it was measured, so a stale\n * count reads as stale.\n */\nconst READINESS = {\n stage: 'working',\n tests: ['packages/accounting/test/expenditure-requests.test.mjs'],\n lastRun: { passed: 17, total: 17, at: '2026-10-13T13:43:26+09:00', command: 'npm run build:server && node --test test/expenditure-requests.test.mjs (packages/accounting)' }\n}\n\nconst COMMON = {\n release: '1.0.0',\n provider: 'hatiolab.com',\n category: 'accounting',\n activityType: 'user',\n priority: 1,\n startingType: 'post',\n multiple: 'single',\n /* No screen of its own: the generic approval screen draws the decision keys. Set explicitly - leaving it out keeps an old value. */\n viewType: '',\n viewSource: '',\n model: MODEL,\n standardTime: 3 * 24 * 60 * 60,\n readiness: READINESS,\n /* Runs inside worklist's mutation, where no resolver's `SpeakRefusals` reaches. */\n callback: speaksRefusals(decideExpenditure)\n}\n\n/** 지출결의 - paying a counterparty. */\nexport const ActivityExpenditureRequest = {\n ...COMMON,\n name: EXPENDITURE_REQUEST_ACTIVITY,\n label: 'expenditure request',\n description: 'Approval of a payment to a counterparty before the money leaves',\n guide: [\n 'someone asks the company to pay a counterparty - an invoice, a service, a deposit - into an account of that counterparty.',\n 'approving puts it in the payment queue; finance pays it, and the payment is reconciled with the bank statement.',\n 'the account approvers see is the account paid; if it changes after the approval, finance is told before paying.',\n 'rejecting returns the request to its writer, who can correct it and submit again.',\n 'with no approver named for this domain a request cannot be submitted.'\n ],\n searchKeys: [\n { name: 'payee', description: 'Who is paid', inputKey: 'payee', tKey: 'field.payee' },\n { name: 'requester', description: 'Who requests', inputKey: 'requester', tKey: 'field.requester' }\n ],\n decisionKeys: [\n { role: 'headline', inputPath: 'headline', name: 'what is to be paid' },\n { role: 'subject', inputPath: 'payee', tKey: 'field.payee', name: 'who is paid' },\n { role: 'reason', inputPath: 'lines', tKey: 'field.expenditure-lines', name: 'what it pays for' },\n { role: 'consequence', inputPath: 'consequence', name: 'what approving does' }\n ]\n}\n\n/** 경비 청구 - paying an employee back what they spent for the company in a month. */\nexport const ActivityExpenseClaim = {\n ...COMMON,\n name: EXPENSE_CLAIM_ACTIVITY,\n label: 'expense claim',\n description: 'Approval of what an employee spent for the company in a month',\n guide: [\n 'an employee claims what they spent for the company - meals, travel, supplies.',\n 'approving puts it in the payment queue; finance pays the employee back, and the payment is reconciled with the bank statement.',\n 'lines paid with the corporate card are reviewed the same way but nothing is paid back for them.',\n 'rejecting returns the claim to the employee, who can correct it and submit again.',\n 'with no approver named for this domain a claim cannot be submitted.'\n ],\n searchKeys: [\n { name: 'month', description: 'Which month', inputKey: 'month', tKey: 'field.month' },\n { name: 'requester', description: 'Who claims', inputKey: 'requester', tKey: 'field.requester' }\n ],\n decisionKeys: [\n { role: 'headline', inputPath: 'headline', name: 'what is claimed' },\n { role: 'subject', inputPath: 'requester', tKey: 'field.requester', name: 'who claims' },\n { role: 'reason', inputPath: 'lines', tKey: 'field.expenditure-lines', name: 'what was spent, on what' },\n { role: 'consequence', inputPath: 'consequence', name: 'what approving does' }\n ]\n}\n\n/** The approvals of an expenditure request - an application that uses them installs these into its domains. */\nexport const EXPENDITURE_ACTIVITIES = [ActivityExpenditureRequest, ActivityExpenseClaim]\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const EXPENDITURE_REQUEST_ACTIVITY = "Expenditure Request";
|
|
2
|
+
export declare const EXPENSE_CLAIM_ACTIVITY = "Expense Claim";
|
|
3
|
+
/** The form a request of a kind is put up on - a counterparty payment and a reimbursement have their own approvers. */
|
|
4
|
+
export declare const activityOfKind: (kind: string) => "Expenditure Request" | "Expense Claim";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.activityOfKind = exports.EXPENSE_CLAIM_ACTIVITY = exports.EXPENDITURE_REQUEST_ACTIVITY = void 0;
|
|
4
|
+
/*
|
|
5
|
+
* The approval forms of an expenditure request (ADR-0135 §5-2) - worklist finds each callback by this name, and the
|
|
6
|
+
* work door is declared under it. Changing one orphans the installed form; add a new name instead.
|
|
7
|
+
*/
|
|
8
|
+
exports.EXPENDITURE_REQUEST_ACTIVITY = 'Expenditure Request';
|
|
9
|
+
exports.EXPENSE_CLAIM_ACTIVITY = 'Expense Claim';
|
|
10
|
+
/** The form a request of a kind is put up on - a counterparty payment and a reimbursement have their own approvers. */
|
|
11
|
+
const activityOfKind = (kind) => (kind === 'VENDOR' ? exports.EXPENDITURE_REQUEST_ACTIVITY : exports.EXPENSE_CLAIM_ACTIVITY);
|
|
12
|
+
exports.activityOfKind = activityOfKind;
|
|
13
|
+
//# sourceMappingURL=expenditure-activity-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expenditure-activity-names.js","sourceRoot":"","sources":["../../server/activities/expenditure-activity-names.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACU,QAAA,4BAA4B,GAAG,qBAAqB,CAAA;AACpD,QAAA,sBAAsB,GAAG,eAAe,CAAA;AAErD,uHAAuH;AAChH,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,oCAA4B,CAAC,CAAC,CAAC,8BAAsB,CAAC,CAAA;AAA9G,QAAA,cAAc,kBAAgG","sourcesContent":["/*\n * The approval forms of an expenditure request (ADR-0135 §5-2) - worklist finds each callback by this name, and the\n * work door is declared under it. Changing one orphans the installed form; add a new name instead.\n */\nexport const EXPENDITURE_REQUEST_ACTIVITY = 'Expenditure Request'\nexport const EXPENSE_CLAIM_ACTIVITY = 'Expense Claim'\n\n/** The form a request of a kind is put up on - a counterparty payment and a reimbursement have their own approvers. */\nexport const activityOfKind = (kind: string) => (kind === 'VENDOR' ? EXPENDITURE_REQUEST_ACTIVITY : EXPENSE_CLAIM_ACTIVITY)\n"]}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './expenditure-activity-names';
|
|
2
|
+
export { EXPENDITURE_ACTIVITIES, decideExpenditure } from './activity-expenditure';
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideExpenditure = exports.EXPENDITURE_ACTIVITIES = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
3
5
|
const worklist_1 = require("@things-factory/worklist");
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const activity_expenditure_1 = require("./activity-expenditure");
|
|
7
|
+
tslib_1.__exportStar(require("./expenditure-activity-names"), exports);
|
|
8
|
+
var activity_expenditure_2 = require("./activity-expenditure");
|
|
9
|
+
Object.defineProperty(exports, "EXPENDITURE_ACTIVITIES", { enumerable: true, get: function () { return activity_expenditure_2.EXPENDITURE_ACTIVITIES; } });
|
|
10
|
+
Object.defineProperty(exports, "decideExpenditure", { enumerable: true, get: function () { return activity_expenditure_2.decideExpenditure; } });
|
|
11
|
+
/*
|
|
12
|
+
* **Importing this file registers the approval templates** - a bootstrap hook would be too late for worklist to find
|
|
13
|
+
* the callbacks. Which domains have them is the application's call: it installs `EXPENDITURE_ACTIVITIES`.
|
|
14
|
+
*/
|
|
15
|
+
activity_expenditure_1.EXPENDITURE_ACTIVITIES.forEach(template => {
|
|
12
16
|
worklist_1.ActivityInstallations.installActivityTemplate(template);
|
|
13
17
|
});
|
|
14
18
|
/*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
* The work doors. A request's whole work is its approval, so with no approver named it is refused (ADR-0077) - a
|
|
20
|
+
* domain names its approvers on the activity screen.
|
|
21
|
+
*/
|
|
22
|
+
(0, worklist_1.registerWorkDoors)({
|
|
23
|
+
module: 'accounting',
|
|
24
|
+
doors: activity_expenditure_1.EXPENDITURE_ACTIVITIES.map(one => ({ name: one.name, kind: 'approval', whenNoLine: 'refuse', because: 'approval-is-the-act' }))
|
|
25
|
+
});
|
|
19
26
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/activities/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/activities/index.ts"],"names":[],"mappings":";;;;AAAA,uDAAmF;AAEnF,iEAA+D;AAE/D,uEAA4C;AAC5C,+DAAkF;AAAzE,8HAAA,sBAAsB,OAAA;AAAE,yHAAA,iBAAiB,OAAA;AAElD;;;GAGG;AACH,6CAAsB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;IACxC,gCAAqB,CAAC,uBAAuB,CAAC,QAAe,CAAC,CAAA;AAChE,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,IAAA,4BAAiB,EAAC;IAChB,MAAM,EAAE,YAAY;IACpB,KAAK,EAAE,6CAAsB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,UAAmB,EAAE,UAAU,EAAE,QAAiB,EAAE,OAAO,EAAE,qBAA8B,EAAE,CAAC,CAAC;CAClK,CAAC,CAAA","sourcesContent":["import { ActivityInstallations, registerWorkDoors } from '@things-factory/worklist'\n\nimport { EXPENDITURE_ACTIVITIES } from './activity-expenditure'\n\nexport * from './expenditure-activity-names'\nexport { EXPENDITURE_ACTIVITIES, decideExpenditure } from './activity-expenditure'\n\n/*\n * **Importing this file registers the approval templates** - a bootstrap hook would be too late for worklist to find\n * the callbacks. Which domains have them is the application's call: it installs `EXPENDITURE_ACTIVITIES`.\n */\nEXPENDITURE_ACTIVITIES.forEach(template => {\n ActivityInstallations.installActivityTemplate(template as any)\n})\n\n/*\n * The work doors. A request's whole work is its approval, so with no approver named it is refused (ADR-0077) - a\n * domain names its approvers on the activity screen.\n */\nregisterWorkDoors({\n module: 'accounting',\n doors: EXPENDITURE_ACTIVITIES.map(one => ({ name: one.name, kind: 'approval' as const, whenNoLine: 'refuse' as const, because: 'approval-is-the-act' as const }))\n})\n"]}
|
package/dist-server/index.d.ts
CHANGED
package/dist-server/index.js
CHANGED
|
@@ -4,5 +4,6 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
tslib_1.__exportStar(require("./migrations"), exports);
|
|
5
5
|
tslib_1.__exportStar(require("./middlewares"), exports);
|
|
6
6
|
tslib_1.__exportStar(require("./service"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./activities"), exports);
|
|
7
8
|
require("./routes");
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist-server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,uDAA4B;AAC5B,wDAA6B;AAC7B,oDAAyB;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,uDAA4B;AAC5B,wDAA6B;AAC7B,oDAAyB;AACzB,uDAA4B;AAE5B,oBAAiB","sourcesContent":["export * from './migrations'\nexport * from './middlewares'\nexport * from './service'\nexport * from './activities'\n\nimport './routes'\n"]}
|
|
@@ -22,19 +22,21 @@ export declare function assignAccounts<T extends Record<string, any>>(tx: Entity
|
|
|
22
22
|
id: string;
|
|
23
23
|
}, inputs: T[]): Promise<T[]>;
|
|
24
24
|
export interface AccountLinking {
|
|
25
|
-
/** Rows given an account, per kind: confirmed transactions, forecasts, recurring items. */
|
|
25
|
+
/** Rows given an account, per kind: confirmed transactions, forecasts, recurring items, expenditure request lines. */
|
|
26
26
|
linked: {
|
|
27
27
|
transactions: number;
|
|
28
28
|
forecasts: number;
|
|
29
29
|
recurring: number;
|
|
30
|
+
expenditureLines: number;
|
|
30
31
|
};
|
|
31
32
|
/** The words of confirmed transactions no account matched. */
|
|
32
33
|
unmatched: string[];
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Give every row without an account the one its words mean: a confirmed transaction by its description then its
|
|
36
|
-
* ledger category, a forecast by its description, a recurring item by its name
|
|
37
|
-
*
|
|
37
|
+
* ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its
|
|
38
|
+
* category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed
|
|
39
|
+
* are left alone.
|
|
38
40
|
*/
|
|
39
41
|
export declare function linkAccounts(tx: EntityManager, domain: {
|
|
40
42
|
id: string;
|
|
@@ -6,6 +6,8 @@ exports.assignAccounts = assignAccounts;
|
|
|
6
6
|
exports.linkAccounts = linkAccounts;
|
|
7
7
|
const typeorm_1 = require("typeorm");
|
|
8
8
|
const account_1 = require("./account");
|
|
9
|
+
const cash_flow_type_1 = require("../cash-flow-type");
|
|
10
|
+
const expenditure_request_item_1 = require("../expenditure-request/expenditure-request-item");
|
|
9
11
|
const recurring_cash_item_1 = require("../recurring-cash-item/recurring-cash-item");
|
|
10
12
|
const transaction_1 = require("../transaction/transaction");
|
|
11
13
|
const key = (text) => String(text ?? '')
|
|
@@ -67,11 +69,12 @@ async function assignAccounts(tx, domain, inputs) {
|
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
69
71
|
* Give every row without an account the one its words mean: a confirmed transaction by its description then its
|
|
70
|
-
* ledger category, a forecast by its description, a recurring item by its name
|
|
71
|
-
*
|
|
72
|
+
* ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its
|
|
73
|
+
* category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed
|
|
74
|
+
* are left alone.
|
|
72
75
|
*/
|
|
73
76
|
async function linkAccounts(tx, domain, match, apply) {
|
|
74
|
-
const report = { linked: { transactions: 0, forecasts: 0, recurring: 0 }, unmatched: [] };
|
|
77
|
+
const report = { linked: { transactions: 0, forecasts: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] };
|
|
75
78
|
const run = async (label, entity, texts, typeOf, scope = {}) => {
|
|
76
79
|
const rows = (await tx.getRepository(entity).find({ where: { domain: { id: domain.id }, ...scope, account: (0, typeorm_1.IsNull)() } }));
|
|
77
80
|
const groups = new Map();
|
|
@@ -94,6 +97,7 @@ async function linkAccounts(tx, domain, match, apply) {
|
|
|
94
97
|
await run('transactions', transaction_1.Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: transaction_1.TransactionCategory.CONFIRMED });
|
|
95
98
|
await run('forecasts', transaction_1.Transaction, row => [row.description], row => row.type, { category: transaction_1.TransactionCategory.FORECAST });
|
|
96
99
|
await run('recurring', recurring_cash_item_1.RecurringCashItem, row => [row.name], row => row.type);
|
|
100
|
+
await run('expenditureLines', expenditure_request_item_1.ExpenditureRequestItem, row => [row.category, row.description], () => cash_flow_type_1.CashFlowType.EXPENSE);
|
|
97
101
|
return report;
|
|
98
102
|
}
|
|
99
103
|
//# sourceMappingURL=account-match.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-match.js","sourceRoot":"","sources":["../../../server/service/account/account-match.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In, IsNull } from 'typeorm'\n\nimport { Account } from './account'\n\nimport { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\n\n/*\n * Which account some words mean. An account is known by its name and its aliases; the longest one contained in the\n * text wins, among accounts of the row's direction. Two different accounts matching equally long is no answer, and\n * nothing matching is no answer - a row nobody can place stays unplaced, to be listed, not filed under a catch-all.\n */\n\nexport interface AccountForMatch {\n id: string\n name: string\n type: string\n aliases?: string | null\n}\n\nconst key = (text: string | null | undefined) =>\n String(text ?? '')\n .normalize('NFC')\n .replace(/[\\s\\-_.·,()]+/g, '')\n .toLowerCase()\n\n/** An account's name and its aliases (one per line, or comma separated). */\nexport function aliasesOf(account: Pick<AccountForMatch, 'name' | 'aliases'>): string[] {\n return [account.name, ...String(account.aliases ?? '').split(/[\\n,]/)].map(one => one.trim()).filter(Boolean)\n}\n\n/**\n * A function telling which account some words mean. `type` limits it to that direction - an expense is never filed\n * under an income account because a word happens to match.\n */\nexport function accountMatcher<T extends AccountForMatch>(accounts: readonly T[]) {\n const words: { word: string; account: T }[] = []\n for (const account of accounts) for (const alias of aliasesOf(account)) if (key(alias).length >= 2) words.push({ word: key(alias), account })\n words.sort((a, b) => b.word.length - a.word.length)\n\n return (texts: readonly (string | null | undefined)[], type?: string | null): T | undefined => {\n for (const text of texts) {\n const hay = key(text)\n if (!hay) continue\n const hits = words.filter(one => (!type || one.account.type === type) && hay.includes(one.word))\n if (!hits.length) continue\n const longest = hits[0].word.length\n const top = new Set(hits.filter(one => one.word.length === longest).map(one => one.account.id))\n return top.size === 1 ? hits[0].account : undefined\n }\n return undefined\n }\n}\n\n/**\n * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a confirmed line by\n * its description then its ledger category, a forecast by its description, among the active accounts of its direction\n * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)\n * keeps what it says; a row whose words mean nothing is saved without one.\n */\nexport async function assignAccounts<T extends Record<string, any>>(tx: EntityManager, domain: { id: string }, inputs: T[]): Promise<T[]> {\n const textsOf = (row: Record<string, any>, category?: string) =>\n category === TransactionCategory.FORECAST ? [row.description] : [row.description, row.ledgerCategory]\n const wants = (row: Record<string, any>) => row.cuFlag !== '-' && !('account' in row) && (row.description || row.ledgerCategory)\n if (!inputs.some(wants)) return inputs\n\n const accounts = await tx.getRepository(Account).find({ where: { domain: { id: domain.id }, active: true } as any })\n const match = accountMatcher(accounts)\n const edited = inputs.filter(row => wants(row) && row.id && row.cuFlag !== '+').map(row => row.id)\n const stored = new Map(\n (edited.length ? await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(edited) } as any, select: ['id', 'type', 'category'] as any }) : []).map(row => [row.id, row])\n )\n return inputs.map(row => {\n if (!wants(row)) return row\n const before = stored.get(row.id)\n const account = match(textsOf(row, row.category ?? before?.category), row.type ?? before?.type)\n return account ? { ...row, account: { id: account.id } } : row\n })\n}\n\nexport interface AccountLinking {\n /** Rows given an account, per kind: confirmed transactions, forecasts, recurring items. */\n linked: { transactions: number; forecasts: number; recurring: number }\n /** The words of confirmed transactions no account matched. */\n unmatched: string[]\n}\n\n/**\n * Give every row without an account the one its words mean: a confirmed transaction by its description then its\n * ledger category, a forecast by its description, a recurring item by its name. `apply` false writes nothing. Rows a\n * person already filed are left alone.\n */\nexport async function linkAccounts(\n tx: EntityManager,\n domain: { id: string },\n match: ReturnType<typeof accountMatcher>,\n apply: boolean\n): Promise<AccountLinking> {\n const report: AccountLinking = { linked: { transactions: 0, forecasts: 0, recurring: 0 }, unmatched: [] }\n\n const run = async <T extends { id: string }>(\n label: keyof AccountLinking['linked'],\n entity: new () => T,\n texts: (row: T) => (string | null | undefined)[],\n typeOf: (row: T) => string | null,\n scope: Record<string, any> = {}\n ) => {\n const rows = (await tx.getRepository(entity as any).find({ where: { domain: { id: domain.id }, ...scope, account: IsNull() } as any })) as T[]\n const groups = new Map<string, string[]>()\n for (const row of rows) {\n const account = match(texts(row), typeOf(row))\n if (!account) {\n if (label === 'transactions') report.unmatched.push(String(texts(row)[0] ?? ''))\n continue\n }\n groups.set(account.id, [...(groups.get(account.id) ?? []), row.id])\n }\n report.linked[label] = [...groups.values()].reduce((sum, ids) => sum + ids.length, 0)\n if (!apply) return\n for (const [id, ids] of groups)\n for (let i = 0; i < ids.length; i += 500) await tx.getRepository(entity as any).update(ids.slice(i, i + 500), { account: { id } } as any)\n }\n\n await run('transactions', Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: TransactionCategory.CONFIRMED })\n await run('forecasts', Transaction, row => [row.description], row => row.type, { category: TransactionCategory.FORECAST })\n await run('recurring', RecurringCashItem, row => [row.name], row => row.type)\n return report\n}\n"]}
|
|
1
|
+
{"version":3,"file":"account-match.js","sourceRoot":"","sources":["../../../server/service/account/account-match.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In, IsNull } from 'typeorm'\n\nimport { Account } from './account'\n\nimport { CashFlowType } from '../cash-flow-type'\nimport { ExpenditureRequestItem } from '../expenditure-request/expenditure-request-item'\nimport { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\n\n/*\n * Which account some words mean. An account is known by its name and its aliases; the longest one contained in the\n * text wins, among accounts of the row's direction. Two different accounts matching equally long is no answer, and\n * nothing matching is no answer - a row nobody can place stays unplaced, to be listed, not filed under a catch-all.\n */\n\nexport interface AccountForMatch {\n id: string\n name: string\n type: string\n aliases?: string | null\n}\n\nconst key = (text: string | null | undefined) =>\n String(text ?? '')\n .normalize('NFC')\n .replace(/[\\s\\-_.·,()]+/g, '')\n .toLowerCase()\n\n/** An account's name and its aliases (one per line, or comma separated). */\nexport function aliasesOf(account: Pick<AccountForMatch, 'name' | 'aliases'>): string[] {\n return [account.name, ...String(account.aliases ?? '').split(/[\\n,]/)].map(one => one.trim()).filter(Boolean)\n}\n\n/**\n * A function telling which account some words mean. `type` limits it to that direction - an expense is never filed\n * under an income account because a word happens to match.\n */\nexport function accountMatcher<T extends AccountForMatch>(accounts: readonly T[]) {\n const words: { word: string; account: T }[] = []\n for (const account of accounts) for (const alias of aliasesOf(account)) if (key(alias).length >= 2) words.push({ word: key(alias), account })\n words.sort((a, b) => b.word.length - a.word.length)\n\n return (texts: readonly (string | null | undefined)[], type?: string | null): T | undefined => {\n for (const text of texts) {\n const hay = key(text)\n if (!hay) continue\n const hits = words.filter(one => (!type || one.account.type === type) && hay.includes(one.word))\n if (!hits.length) continue\n const longest = hits[0].word.length\n const top = new Set(hits.filter(one => one.word.length === longest).map(one => one.account.id))\n return top.size === 1 ? hits[0].account : undefined\n }\n return undefined\n }\n}\n\n/**\n * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a confirmed line by\n * its description then its ledger category, a forecast by its description, among the active accounts of its direction\n * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)\n * keeps what it says; a row whose words mean nothing is saved without one.\n */\nexport async function assignAccounts<T extends Record<string, any>>(tx: EntityManager, domain: { id: string }, inputs: T[]): Promise<T[]> {\n const textsOf = (row: Record<string, any>, category?: string) =>\n category === TransactionCategory.FORECAST ? [row.description] : [row.description, row.ledgerCategory]\n const wants = (row: Record<string, any>) => row.cuFlag !== '-' && !('account' in row) && (row.description || row.ledgerCategory)\n if (!inputs.some(wants)) return inputs\n\n const accounts = await tx.getRepository(Account).find({ where: { domain: { id: domain.id }, active: true } as any })\n const match = accountMatcher(accounts)\n const edited = inputs.filter(row => wants(row) && row.id && row.cuFlag !== '+').map(row => row.id)\n const stored = new Map(\n (edited.length ? await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(edited) } as any, select: ['id', 'type', 'category'] as any }) : []).map(row => [row.id, row])\n )\n return inputs.map(row => {\n if (!wants(row)) return row\n const before = stored.get(row.id)\n const account = match(textsOf(row, row.category ?? before?.category), row.type ?? before?.type)\n return account ? { ...row, account: { id: account.id } } : row\n })\n}\n\nexport interface AccountLinking {\n /** Rows given an account, per kind: confirmed transactions, forecasts, recurring items, expenditure request lines. */\n linked: { transactions: number; forecasts: number; recurring: number; expenditureLines: number }\n /** The words of confirmed transactions no account matched. */\n unmatched: string[]\n}\n\n/**\n * Give every row without an account the one its words mean: a confirmed transaction by its description then its\n * ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its\n * category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed\n * are left alone.\n */\nexport async function linkAccounts(\n tx: EntityManager,\n domain: { id: string },\n match: ReturnType<typeof accountMatcher>,\n apply: boolean\n): Promise<AccountLinking> {\n const report: AccountLinking = { linked: { transactions: 0, forecasts: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] }\n\n const run = async <T extends { id: string }>(\n label: keyof AccountLinking['linked'],\n entity: new () => T,\n texts: (row: T) => (string | null | undefined)[],\n typeOf: (row: T) => string | null,\n scope: Record<string, any> = {}\n ) => {\n const rows = (await tx.getRepository(entity as any).find({ where: { domain: { id: domain.id }, ...scope, account: IsNull() } as any })) as T[]\n const groups = new Map<string, string[]>()\n for (const row of rows) {\n const account = match(texts(row), typeOf(row))\n if (!account) {\n if (label === 'transactions') report.unmatched.push(String(texts(row)[0] ?? ''))\n continue\n }\n groups.set(account.id, [...(groups.get(account.id) ?? []), row.id])\n }\n report.linked[label] = [...groups.values()].reduce((sum, ids) => sum + ids.length, 0)\n if (!apply) return\n for (const [id, ids] of groups)\n for (let i = 0; i < ids.length; i += 500) await tx.getRepository(entity as any).update(ids.slice(i, i + 500), { account: { id } } as any)\n }\n\n await run('transactions', Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: TransactionCategory.CONFIRMED })\n await run('forecasts', Transaction, row => [row.description], row => row.type, { category: TransactionCategory.FORECAST })\n await run('recurring', RecurringCashItem, row => [row.name], row => row.type)\n await run('expenditureLines', ExpenditureRequestItem, row => [row.category, row.description], () => CashFlowType.EXPENSE)\n return report\n}\n"]}
|
|
@@ -4,8 +4,11 @@ exports.BANK_FILE_LIMIT = void 0;
|
|
|
4
4
|
exports.importBankStatement = importBankStatement;
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const typeorm_1 = require("typeorm");
|
|
7
|
+
const counterparty_names_1 = require("@things-factory/biz-base/counterparty-names");
|
|
7
8
|
const shell_1 = require("@things-factory/shell");
|
|
8
9
|
const bank_account_1 = require("../bank-account/bank-account");
|
|
10
|
+
const counterparty_account_rules_1 = require("../counterparty-account/counterparty-account-rules");
|
|
11
|
+
const counterparty_account_service_1 = require("../counterparty-account/counterparty-account-service");
|
|
9
12
|
const transaction_1 = require("../transaction/transaction");
|
|
10
13
|
const transaction_service_1 = require("../transaction/transaction-service");
|
|
11
14
|
const bank_statement_1 = require("./bank-statement");
|
|
@@ -25,9 +28,8 @@ async function importBankStatement(tx, domain, user, file, preview, assignments)
|
|
|
25
28
|
throw new shell_1.Refusal('bank-file-too-large', {}, 'Upload a bank file smaller than 10MB');
|
|
26
29
|
let parsed = (0, bank_parser_1.parseBankWorkbook)(data);
|
|
27
30
|
const accounts = await tx.getRepository(bank_account_1.BankAccount).find({ where: { domain: { id: domain.id }, active: true } });
|
|
28
|
-
const digits = (value) => (value ?? '').replace(/\D/g, '');
|
|
29
31
|
const accountFor = (currency) => {
|
|
30
|
-
const found = accounts.filter(account => account.currency === currency && account.bankName === parsed.bankName &&
|
|
32
|
+
const found = accounts.filter(account => account.currency === currency && account.bankName === parsed.bankName && (0, counterparty_names_1.accountKey)(account.accountNumber) === (0, counterparty_names_1.accountKey)(parsed.accountNumber));
|
|
31
33
|
if (found.length !== 1)
|
|
32
34
|
throw new shell_1.Refusal('bank-account-not-registered', { bank: parsed.bankName, account: parsed.accountNumber, currency }, 'Register the statement account and currency first');
|
|
33
35
|
return found[0];
|
|
@@ -63,6 +65,8 @@ async function importBankStatement(tx, domain, user, file, preview, assignments)
|
|
|
63
65
|
if (!(0, bank_reconcile_1.placeHeldLines)(plan, (assignments ?? []).map(one => ({ fingerprint: one.fingerprint, transactionId: one.transactionId }))))
|
|
64
66
|
throw new shell_1.Refusal('bank-assignment-invalid', {}, 'A held bank line can only be placed on one of its candidates, once');
|
|
65
67
|
const byFingerprint = new Map(plan.map(item => [item.line.fingerprint, item]));
|
|
68
|
+
/* A line paying an account a counterparty registered is that counterparty's (ADR-0134 §2-3). */
|
|
69
|
+
const owners = await (0, counterparty_account_service_1.accountOwners)(tx, domain);
|
|
66
70
|
const result = { added: 0, existing: 0, updated: 0, review: 0, details: '', zeroEvents: 0, filename, accountNumber: parsed.accountNumber, held: [] };
|
|
67
71
|
const details = [];
|
|
68
72
|
const inputs = [];
|
|
@@ -102,6 +106,7 @@ async function importBankStatement(tx, domain, user, file, preview, assignments)
|
|
|
102
106
|
}
|
|
103
107
|
const { raw, row, fingerprint, bankLocalTime, ...facts } = line;
|
|
104
108
|
const matched = item.match ? ledger.find(old => old.id === item.match.id) : undefined;
|
|
109
|
+
const payee = matched?.counterpartyId ? undefined : (0, counterparty_account_rules_1.counterpartyOfAccount)(line.bankCounterpartyAccount, owners);
|
|
105
110
|
inputs.push({
|
|
106
111
|
...(matched
|
|
107
112
|
? {
|
|
@@ -130,7 +135,8 @@ async function importBankStatement(tx, domain, user, file, preview, assignments)
|
|
|
130
135
|
sourceRow: row,
|
|
131
136
|
bankFingerprint: fingerprint,
|
|
132
137
|
description: matched?.description ?? line.bankDescription ?? line.bankKind ?? null,
|
|
133
|
-
counterpartyName: matched?.counterpartyName ?? line.bankCounterpartyName ?? null
|
|
138
|
+
counterpartyName: matched?.counterpartyName ?? line.bankCounterpartyName ?? null,
|
|
139
|
+
...(payee ? { counterparty: { id: payee } } : {})
|
|
134
140
|
});
|
|
135
141
|
}
|
|
136
142
|
if (inputs.length)
|