@things-factory/accounting 10.1.155 → 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 +7 -6
- 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,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The rules of an expenditure request (ADR-0135) - who may change it when, what it pays, what a decision means, and
|
|
4
|
+
* the monthly summary.
|
|
5
|
+
*
|
|
6
|
+
* ── The life of a request ──────────────────────────────────────────────────────────────────────
|
|
7
|
+
* DRAFT ──submit──▶ SUBMITTED ──approved──▶ APPROVED ──bank lines pay it──▶ (paid, then reconciled)
|
|
8
|
+
* ▲ │ │
|
|
9
|
+
* │ │ └─rejected──▶ REJECTED ──edit & submit again──▶ SUBMITTED
|
|
10
|
+
* └────withdraw──────┘ (WITHDRAWN, editable again)
|
|
11
|
+
*
|
|
12
|
+
* Only the requester edits, and only while nobody is judging it (DRAFT, REJECTED, WITHDRAWN). What approvers saw is
|
|
13
|
+
* what is paid: once submitted the lines and the payee's account are frozen.
|
|
14
|
+
*
|
|
15
|
+
* Paid and reconciled are not states stored here; they are read from the payments (§`paymentStateFrom`).
|
|
16
|
+
*
|
|
17
|
+
* Pure.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.isPayable = exports.monthOf = exports.EDITABLE = void 0;
|
|
21
|
+
exports.dayOf = dayOf;
|
|
22
|
+
exports.canEdit = canEdit;
|
|
23
|
+
exports.totalsOf = totalsOf;
|
|
24
|
+
exports.submitRefusal = submitRefusal;
|
|
25
|
+
exports.decisionOf = decisionOf;
|
|
26
|
+
exports.requestForLine = requestForLine;
|
|
27
|
+
exports.payeeAccountDrift = payeeAccountDrift;
|
|
28
|
+
exports.monthlySummary = monthlySummary;
|
|
29
|
+
exports.statusCounts = statusCounts;
|
|
30
|
+
const shell_1 = require("@things-factory/shell");
|
|
31
|
+
exports.EDITABLE = ['DRAFT', 'REJECTED', 'WITHDRAWN'];
|
|
32
|
+
/** A day as people write it - 2026-09-03, 2026.9.3, 2026/9/3 - in YYYY-MM-DD, or undefined when it is not one. */
|
|
33
|
+
function dayOf(value) {
|
|
34
|
+
const match = String(value ?? '').trim().match(/^(\d{4})[-./](\d{1,2})[-./](\d{1,2})\.?$/);
|
|
35
|
+
if (!match)
|
|
36
|
+
return undefined;
|
|
37
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
38
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
39
|
+
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
|
|
40
|
+
return undefined;
|
|
41
|
+
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
42
|
+
}
|
|
43
|
+
/** A month as YYYY-MM, or undefined. */
|
|
44
|
+
const monthOf = (value) => (/^\d{4}-(0[1-9]|1[0-2])$/.test(String(value ?? '')) ? String(value) : undefined);
|
|
45
|
+
exports.monthOf = monthOf;
|
|
46
|
+
function canEdit(status) {
|
|
47
|
+
return exports.EDITABLE.includes(status);
|
|
48
|
+
}
|
|
49
|
+
/** A line the company pays - everything but what the corporate card already paid. */
|
|
50
|
+
const isPayable = (line) => line.paymentMethod !== 'CORPORATE_CARD';
|
|
51
|
+
exports.isPayable = isPayable;
|
|
52
|
+
/**
|
|
53
|
+
* What a request totals, and what of it the company is to pay. **The one place this is decided** - the request's
|
|
54
|
+
* stored totals, the payment queue and every reader take it from here.
|
|
55
|
+
*/
|
|
56
|
+
function totalsOf(lines) {
|
|
57
|
+
let total = 0;
|
|
58
|
+
let payable = 0;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
total += line.amount || 0;
|
|
61
|
+
if ((0, exports.isPayable)(line))
|
|
62
|
+
payable += line.amount || 0;
|
|
63
|
+
}
|
|
64
|
+
return { total, payable, count: lines.length };
|
|
65
|
+
}
|
|
66
|
+
/** Why a request cannot be submitted, or undefined when it can. */
|
|
67
|
+
function submitRefusal(head, lines) {
|
|
68
|
+
const { kind, status, month } = head;
|
|
69
|
+
if (!canEdit(status))
|
|
70
|
+
return new shell_1.Refusal('expenditure-submitted-already', { status: status.toLowerCase() }, `a ${status.toLowerCase()} request cannot be submitted again`);
|
|
71
|
+
if (kind === 'VENDOR') {
|
|
72
|
+
if (!head.counterpartyId)
|
|
73
|
+
return new shell_1.Refusal('expenditure-payee-missing', {}, 'choose who is paid');
|
|
74
|
+
if (!head.payeeAccountId)
|
|
75
|
+
return new shell_1.Refusal('expenditure-payee-account-missing', {}, "choose the counterparty's account to pay into");
|
|
76
|
+
if (!head.dueDate)
|
|
77
|
+
return new shell_1.Refusal('expenditure-due-date-missing', {}, 'set the day it is to be paid');
|
|
78
|
+
}
|
|
79
|
+
if (!lines.length)
|
|
80
|
+
return new shell_1.Refusal('expenditure-no-lines', {}, 'the request has no lines');
|
|
81
|
+
if (lines.some(line => !(line.amount > 0)))
|
|
82
|
+
return new shell_1.Refusal('expenditure-line-amount-missing', {}, 'every line needs an amount above zero');
|
|
83
|
+
if (lines.some(line => !line.spentDate))
|
|
84
|
+
return new shell_1.Refusal('expenditure-line-day-missing', {}, 'every line needs its day');
|
|
85
|
+
if (lines.some(line => !line.category && !line.accountId))
|
|
86
|
+
return new shell_1.Refusal('expenditure-line-category-missing', {}, 'every line needs a category or an account');
|
|
87
|
+
if (month && lines.some(line => line.spentDate && line.spentDate.slice(0, 7) > month))
|
|
88
|
+
return new shell_1.Refusal('expenditure-line-after-month', { month }, 'a line is dated after the month the request is for');
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What an approval outcome does to the request.
|
|
93
|
+
*
|
|
94
|
+
* worklist calls the activity's callback on every state change, and more than once for the same one. A decision lands
|
|
95
|
+
* only on the request **still waiting on that very instance** - once it has moved on (decided, or withdrawn and
|
|
96
|
+
* submitted again as a new instance) a late call changes nothing.
|
|
97
|
+
*/
|
|
98
|
+
function decisionOf(instance, causedBy, request) {
|
|
99
|
+
if (request.status !== 'SUBMITTED' || request.activityInstanceId !== instance.id)
|
|
100
|
+
return undefined;
|
|
101
|
+
const state = String(instance.state).toLowerCase();
|
|
102
|
+
if (state === 'ended')
|
|
103
|
+
return 'APPROVED';
|
|
104
|
+
if (state === 'aborted')
|
|
105
|
+
return 'WITHDRAWN';
|
|
106
|
+
/* A rejection does not end the instance - the rejected thread is the only sign of it. */
|
|
107
|
+
if (causedBy.some(one => String(one.state).toLowerCase() === 'rejected'))
|
|
108
|
+
return 'REJECTED';
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Which reimbursement a line added on the go joins - **the requester's editable one of that month.**
|
|
113
|
+
*
|
|
114
|
+
* The phone flow has no "request" step: a person snaps a receipt, types the amount and is done. The line goes to the
|
|
115
|
+
* month's draft; with none, to one sent back (rejected or withdrawn) so it can go up again with the rest; with none of
|
|
116
|
+
* those either, a new draft is started (undefined here). A request already with approvers or decided is never added
|
|
117
|
+
* to - what they saw stays what it was.
|
|
118
|
+
*/
|
|
119
|
+
function requestForLine(requests, month) {
|
|
120
|
+
const order = ['DRAFT', 'REJECTED', 'WITHDRAWN'];
|
|
121
|
+
const candidates = requests
|
|
122
|
+
.filter(one => one.month === month && order.includes(one.status))
|
|
123
|
+
.sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status) || String(b.updatedAt ?? '').localeCompare(String(a.updatedAt ?? '')));
|
|
124
|
+
return candidates[0]?.id;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether the account approvers saw is still the one on file - `gone` when the counterparty's account was deleted
|
|
128
|
+
* (deleting a counterparty deletes its accounts), `changed` when its number or holder is no longer what was written at
|
|
129
|
+
* submission, or the account was moved to another counterparty. Finance is told before paying.
|
|
130
|
+
*/
|
|
131
|
+
function payeeAccountDrift(approved, current, keyOf) {
|
|
132
|
+
if (!current || current.deletedAt)
|
|
133
|
+
return 'gone';
|
|
134
|
+
if ((current.counterpartyId ?? null) !== (approved.counterpartyId ?? null))
|
|
135
|
+
return 'changed';
|
|
136
|
+
if (keyOf(current.accountNumber) !== keyOf(approved.payeeAccountNumber))
|
|
137
|
+
return 'changed';
|
|
138
|
+
if ((current.holderName ?? '').trim() !== (approved.payeeHolderName ?? '').trim())
|
|
139
|
+
return 'changed';
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The monthly summary - per month, requester and category.
|
|
144
|
+
*
|
|
145
|
+
* Only approved requests count; a draft or a rejected one is not spending the company agreed to. The caller shows the
|
|
146
|
+
* others as counts by status.
|
|
147
|
+
*/
|
|
148
|
+
function monthlySummary(requests) {
|
|
149
|
+
const rows = new Map();
|
|
150
|
+
for (const request of requests) {
|
|
151
|
+
if (request.status !== 'APPROVED' || !request.month)
|
|
152
|
+
continue;
|
|
153
|
+
for (const item of request.items) {
|
|
154
|
+
const category = item.category || '기타';
|
|
155
|
+
const key = `${request.month}|${request.requesterId}|${category}`;
|
|
156
|
+
const row = rows.get(key) ?? {
|
|
157
|
+
month: request.month,
|
|
158
|
+
requesterId: request.requesterId,
|
|
159
|
+
requesterName: request.requesterName ?? null,
|
|
160
|
+
category,
|
|
161
|
+
total: 0,
|
|
162
|
+
payable: 0,
|
|
163
|
+
count: 0
|
|
164
|
+
};
|
|
165
|
+
row.total += item.amount || 0;
|
|
166
|
+
if ((0, exports.isPayable)(item))
|
|
167
|
+
row.payable += item.amount || 0;
|
|
168
|
+
row.count++;
|
|
169
|
+
rows.set(key, row);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [...rows.values()].sort((a, b) => b.month.localeCompare(a.month) ||
|
|
173
|
+
String(a.requesterName ?? '').localeCompare(String(b.requesterName ?? '')) ||
|
|
174
|
+
a.category.localeCompare(b.category));
|
|
175
|
+
}
|
|
176
|
+
/** Requests per status - so drafts and pending ones are seen, not summed. */
|
|
177
|
+
function statusCounts(requests) {
|
|
178
|
+
const counts = { DRAFT: 0, SUBMITTED: 0, APPROVED: 0, REJECTED: 0, WITHDRAWN: 0 };
|
|
179
|
+
for (const request of requests)
|
|
180
|
+
counts[request.status]++;
|
|
181
|
+
return counts;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=expenditure-request-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expenditure-request-rules.js","sourceRoot":"","sources":["../../../server/service/expenditure-request/expenditure-request-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The rules of an expenditure request (ADR-0135) - who may change it when, what it pays, what a decision means, and\n * the monthly summary.\n *\n * ── The life of a request ──────────────────────────────────────────────────────────────────────\n * DRAFT ──submit──▶ SUBMITTED ──approved──▶ APPROVED ──bank lines pay it──▶ (paid, then reconciled)\n * ▲ │ │\n * │ │ └─rejected──▶ REJECTED ──edit & submit again──▶ SUBMITTED\n * └────withdraw──────┘ (WITHDRAWN, editable again)\n *\n * Only the requester edits, and only while nobody is judging it (DRAFT, REJECTED, WITHDRAWN). What approvers saw is\n * what is paid: once submitted the lines and the payee's account are frozen.\n *\n * Paid and reconciled are not states stored here; they are read from the payments (§`paymentStateFrom`).\n *\n * Pure.\n */\n\nimport { Refusal } from '@things-factory/shell'\n\nexport type RequestStatus = 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'WITHDRAWN'\nexport type RequestKind = 'VENDOR' | 'REIMBURSEMENT'\n\nexport const EDITABLE: readonly RequestStatus[] = ['DRAFT', 'REJECTED', 'WITHDRAWN']\n\nexport interface RequestLine {\n amount: number\n vatAmount?: number | null\n paymentMethod?: string | null\n category?: string | null\n accountId?: string | null\n spentDate?: string | null\n taxInvoiceId?: string | null\n}\n\n/** A day as people write it - 2026-09-03, 2026.9.3, 2026/9/3 - in YYYY-MM-DD, or undefined when it is not one. */\nexport function dayOf(value: unknown): string | undefined {\n const match = String(value ?? '').trim().match(/^(\\d{4})[-./](\\d{1,2})[-./](\\d{1,2})\\.?$/)\n if (!match) return undefined\n const [year, month, day] = match.slice(1).map(Number)\n const date = new Date(Date.UTC(year, month - 1, day))\n if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return undefined\n return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`\n}\n\n/** A month as YYYY-MM, or undefined. */\nexport const monthOf = (value: unknown): string | undefined => (/^\\d{4}-(0[1-9]|1[0-2])$/.test(String(value ?? '')) ? String(value) : undefined)\n\nexport function canEdit(status: RequestStatus): boolean {\n return EDITABLE.includes(status)\n}\n\n/** A line the company pays - everything but what the corporate card already paid. */\nexport const isPayable = (line: Pick<RequestLine, 'paymentMethod'>) => line.paymentMethod !== 'CORPORATE_CARD'\n\n/**\n * What a request totals, and what of it the company is to pay. **The one place this is decided** - the request's\n * stored totals, the payment queue and every reader take it from here.\n */\nexport function totalsOf(lines: readonly RequestLine[]): { total: number; payable: number; count: number } {\n let total = 0\n let payable = 0\n for (const line of lines) {\n total += line.amount || 0\n if (isPayable(line)) payable += line.amount || 0\n }\n return { total, payable, count: lines.length }\n}\n\nexport interface SubmittedHead {\n kind: RequestKind\n status: RequestStatus\n month?: string | null\n dueDate?: string | null\n counterpartyId?: string | null\n payeeAccountId?: string | null\n}\n\n/** Why a request cannot be submitted, or undefined when it can. */\nexport function submitRefusal(head: SubmittedHead, lines: readonly RequestLine[]): Refusal | undefined {\n const { kind, status, month } = head\n if (!canEdit(status))\n return new Refusal('expenditure-submitted-already', { status: status.toLowerCase() }, `a ${status.toLowerCase()} request cannot be submitted again`)\n if (kind === 'VENDOR') {\n if (!head.counterpartyId) return new Refusal('expenditure-payee-missing', {}, 'choose who is paid')\n if (!head.payeeAccountId) return new Refusal('expenditure-payee-account-missing', {}, \"choose the counterparty's account to pay into\")\n if (!head.dueDate) return new Refusal('expenditure-due-date-missing', {}, 'set the day it is to be paid')\n }\n if (!lines.length) return new Refusal('expenditure-no-lines', {}, 'the request has no lines')\n if (lines.some(line => !(line.amount > 0))) return new Refusal('expenditure-line-amount-missing', {}, 'every line needs an amount above zero')\n if (lines.some(line => !line.spentDate)) return new Refusal('expenditure-line-day-missing', {}, 'every line needs its day')\n if (lines.some(line => !line.category && !line.accountId))\n return new Refusal('expenditure-line-category-missing', {}, 'every line needs a category or an account')\n if (month && lines.some(line => line.spentDate && line.spentDate.slice(0, 7) > month))\n return new Refusal('expenditure-line-after-month', { month }, 'a line is dated after the month the request is for')\n return undefined\n}\n\n/**\n * What an approval outcome does to the request.\n *\n * worklist calls the activity's callback on every state change, and more than once for the same one. A decision lands\n * only on the request **still waiting on that very instance** - once it has moved on (decided, or withdrawn and\n * submitted again as a new instance) a late call changes nothing.\n */\nexport function decisionOf(\n instance: { id: string; state: string },\n causedBy: readonly { state?: string }[],\n request: { status: RequestStatus; activityInstanceId?: string | null }\n): 'APPROVED' | 'REJECTED' | 'WITHDRAWN' | undefined {\n if (request.status !== 'SUBMITTED' || request.activityInstanceId !== instance.id) return undefined\n\n const state = String(instance.state).toLowerCase()\n if (state === 'ended') return 'APPROVED'\n if (state === 'aborted') return 'WITHDRAWN'\n /* A rejection does not end the instance - the rejected thread is the only sign of it. */\n if (causedBy.some(one => String(one.state).toLowerCase() === 'rejected')) return 'REJECTED'\n\n return undefined\n}\n\n/**\n * Which reimbursement a line added on the go joins - **the requester's editable one of that month.**\n *\n * The phone flow has no \"request\" step: a person snaps a receipt, types the amount and is done. The line goes to the\n * month's draft; with none, to one sent back (rejected or withdrawn) so it can go up again with the rest; with none of\n * those either, a new draft is started (undefined here). A request already with approvers or decided is never added\n * to - what they saw stays what it was.\n */\nexport function requestForLine(\n requests: readonly { id: string; status: RequestStatus; month?: string | null; updatedAt?: Date | string | null }[],\n month: string\n): string | undefined {\n const order: RequestStatus[] = ['DRAFT', 'REJECTED', 'WITHDRAWN']\n const candidates = requests\n .filter(one => one.month === month && order.includes(one.status))\n .sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status) || String(b.updatedAt ?? '').localeCompare(String(a.updatedAt ?? '')))\n return candidates[0]?.id\n}\n\n/**\n * Whether the account approvers saw is still the one on file - `gone` when the counterparty's account was deleted\n * (deleting a counterparty deletes its accounts), `changed` when its number or holder is no longer what was written at\n * submission, or the account was moved to another counterparty. Finance is told before paying.\n */\nexport function payeeAccountDrift(\n approved: { counterpartyId?: string | null; payeeAccountNumber?: string | null; payeeHolderName?: string | null },\n current: { counterpartyId?: string | null; accountNumber?: string | null; holderName?: string | null; deletedAt?: Date | string | null } | null | undefined,\n keyOf: (value: unknown) => string\n): 'gone' | 'changed' | undefined {\n if (!current || current.deletedAt) return 'gone'\n if ((current.counterpartyId ?? null) !== (approved.counterpartyId ?? null)) return 'changed'\n if (keyOf(current.accountNumber) !== keyOf(approved.payeeAccountNumber)) return 'changed'\n if ((current.holderName ?? '').trim() !== (approved.payeeHolderName ?? '').trim()) return 'changed'\n return undefined\n}\n\nexport interface SummaryRequest {\n month?: string | null\n requesterId: string\n requesterName?: string | null\n status: RequestStatus\n items: RequestLine[]\n}\n\nexport interface MonthSummaryRow {\n month: string\n requesterId: string\n requesterName: string | null\n category: string\n total: number\n payable: number\n count: number\n}\n\n/**\n * The monthly summary - per month, requester and category.\n *\n * Only approved requests count; a draft or a rejected one is not spending the company agreed to. The caller shows the\n * others as counts by status.\n */\nexport function monthlySummary(requests: readonly SummaryRequest[]): MonthSummaryRow[] {\n const rows = new Map<string, MonthSummaryRow>()\n\n for (const request of requests) {\n if (request.status !== 'APPROVED' || !request.month) continue\n for (const item of request.items) {\n const category = item.category || '기타'\n const key = `${request.month}|${request.requesterId}|${category}`\n const row = rows.get(key) ?? {\n month: request.month,\n requesterId: request.requesterId,\n requesterName: request.requesterName ?? null,\n category,\n total: 0,\n payable: 0,\n count: 0\n }\n row.total += item.amount || 0\n if (isPayable(item)) row.payable += item.amount || 0\n row.count++\n rows.set(key, row)\n }\n }\n\n return [...rows.values()].sort(\n (a, b) =>\n b.month.localeCompare(a.month) ||\n String(a.requesterName ?? '').localeCompare(String(b.requesterName ?? '')) ||\n a.category.localeCompare(b.category)\n )\n}\n\n/** Requests per status - so drafts and pending ones are seen, not summed. */\nexport function statusCounts(requests: readonly { status: RequestStatus }[]): Record<RequestStatus, number> {\n const counts: Record<RequestStatus, number> = { DRAFT: 0, SUBMITTED: 0, APPROVED: 0, REJECTED: 0, WITHDRAWN: 0 }\n for (const request of requests) counts[request.status]++\n return counts\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Refusal } from '@things-factory/shell';
|
|
3
|
+
import { ExpenditureRequest } from './expenditure-request';
|
|
4
|
+
import { ExpenditureRequestItem } from './expenditure-request-item';
|
|
5
|
+
import { type PaymentState } from './expenditure-payment-state';
|
|
6
|
+
export declare const RECEIPT_REF_TYPE = "ExpenditureRequestItem";
|
|
7
|
+
export interface RequestHead {
|
|
8
|
+
id?: string;
|
|
9
|
+
kind?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
month?: string;
|
|
12
|
+
dueDate?: string;
|
|
13
|
+
counterparty?: {
|
|
14
|
+
id: string;
|
|
15
|
+
} | null;
|
|
16
|
+
payeeAccount?: {
|
|
17
|
+
id: string;
|
|
18
|
+
} | null;
|
|
19
|
+
notes?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ItemPatch {
|
|
22
|
+
id?: string;
|
|
23
|
+
cuFlag?: string;
|
|
24
|
+
spentDate?: string;
|
|
25
|
+
category?: string;
|
|
26
|
+
account?: {
|
|
27
|
+
id: string;
|
|
28
|
+
} | null;
|
|
29
|
+
merchant?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
amount?: number;
|
|
32
|
+
vatAmount?: number;
|
|
33
|
+
paymentMethod?: string;
|
|
34
|
+
taxInvoice?: {
|
|
35
|
+
id: string;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
/** A request somebody is judging (or that is decided) - it changes only after it is withdrawn. */
|
|
39
|
+
export declare const lockedRequest: (status: string) => Refusal;
|
|
40
|
+
/** The request of this domain, refusing one that is not the caller's own. */
|
|
41
|
+
export declare function ownRequest(tx: EntityManager, id: string, domain: {
|
|
42
|
+
id: string;
|
|
43
|
+
}, user: {
|
|
44
|
+
id: string;
|
|
45
|
+
}): Promise<ExpenditureRequest>;
|
|
46
|
+
/**
|
|
47
|
+
* Save a request and its lines in one go - the head, then the lines. A new request belongs to the caller; an existing
|
|
48
|
+
* one only to its requester, and only while it is editable.
|
|
49
|
+
*/
|
|
50
|
+
export declare function saveRequest(tx: EntityManager, domain: any, user: any, head: RequestHead, items: ItemPatch[]): Promise<ExpenditureRequest>;
|
|
51
|
+
/** Put a request up for approval. The approval carries what the approver needs to judge it. */
|
|
52
|
+
export declare function submitRequest(tx: EntityManager, domain: any, user: any, id: string, context: ResolverContext): Promise<ExpenditureRequest>;
|
|
53
|
+
/** Take a request back from its approvers. worklist's abort does not call the activity back, so the request is set here. */
|
|
54
|
+
export declare function withdrawRequest(tx: EntityManager, domain: any, user: any, id: string, context: ResolverContext): Promise<ExpenditureRequest>;
|
|
55
|
+
/** Delete one of the caller's requests that nobody is judging - with its lines. */
|
|
56
|
+
export declare function deleteRequest(tx: EntityManager, domain: any, user: any, id: string): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Add one line from the phone. It joins the caller's editable reimbursement of that month (§`requestForLine`), or
|
|
59
|
+
* starts one. The receipt photo is attached to the line.
|
|
60
|
+
*/
|
|
61
|
+
export declare function addLine(tx: EntityManager, domain: any, user: any, line: ItemPatch & {
|
|
62
|
+
month?: string;
|
|
63
|
+
receipt?: any;
|
|
64
|
+
}, context: ResolverContext): Promise<ExpenditureRequest>;
|
|
65
|
+
/** Put a receipt on a line - replacing the one it had. Only the requester, only while editable. */
|
|
66
|
+
export declare function replaceReceipt(tx: EntityManager, domain: any, user: any, itemId: string, file: any, context: ResolverContext): Promise<ExpenditureRequestItem>;
|
|
67
|
+
/** Approved requests with their lines and how far each is paid - what the payment queue and payments are made of. */
|
|
68
|
+
export declare function approvedWithStates(tx: EntityManager, domain: {
|
|
69
|
+
id: string;
|
|
70
|
+
}, ids?: string[]): Promise<{
|
|
71
|
+
requests: ExpenditureRequest[];
|
|
72
|
+
states: Map<string, PaymentState>;
|
|
73
|
+
}>;
|
|
74
|
+
/** Whether the account approvers saw is still the one on file (§`payeeAccountDrift`); `missing` when none was written. */
|
|
75
|
+
export declare function driftOf(tx: EntityManager, request: ExpenditureRequest): Promise<string | undefined>;
|
|
76
|
+
export interface PaymentRecord {
|
|
77
|
+
paidDate: string;
|
|
78
|
+
bankAccount?: {
|
|
79
|
+
id: string;
|
|
80
|
+
} | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pay approved requests - finance transferred the money at the bank and records it. Each request gets a withdrawal of
|
|
84
|
+
* what it still asks, written as a person's line; the next bank statement import matches it, and then the payment is
|
|
85
|
+
* reconciled (§`bank-reconcile`).
|
|
86
|
+
*/
|
|
87
|
+
export declare function payRequests(tx: EntityManager, domain: any, user: any, ids: string[], record: PaymentRecord): Promise<ExpenditureRequest[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Pay a request with a withdrawal already in the books - a bank statement brought it in first. The part put on the
|
|
90
|
+
* request is what it still asks unless a smaller amount is given.
|
|
91
|
+
*/
|
|
92
|
+
export declare function payWithLine(tx: EntityManager, domain: any, user: any, requestId: string, transactionId: string, amount?: number | null): Promise<ExpenditureRequest>;
|