@fin.cx/skr 3.0.0 → 3.2.0
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/skr.api.d.ts +34 -0
- package/dist_ts/skr.api.js +59 -1
- package/dist_ts/skr.opos.d.ts +80 -0
- package/dist_ts/skr.opos.js +182 -0
- package/dist_ts/skr.ustva.d.ts +31 -0
- package/dist_ts/skr.ustva.js +99 -0
- package/package.json +2 -1
- package/readme.plan.md +60 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +13 -0
- package/ts/skr.api.ts +96 -0
- package/ts/skr.opos.ts +258 -0
- package/ts/skr.ustva.ts +151 -0
package/ts/skr.opos.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPOS (Offene-Posten-Buchhaltung): open-item accounting over the v2 ledger.
|
|
3
|
+
*
|
|
4
|
+
* Open items are DERIVED from the hashed journal — no separate bookkeeping
|
|
5
|
+
* state that could drift. A line on a personal account (debtors 10000-69999,
|
|
6
|
+
* creditors 70000-99999) belongs to the open item keyed by
|
|
7
|
+
* (account, belegfeld1); the item is open while its signed sum is non-zero.
|
|
8
|
+
* Settling an item is ordinary posting (core.bookBankPayment + JournalPoster),
|
|
9
|
+
* so Festschreibung and the audit chain apply unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Only dunning metadata lives outside the ledger (OposMeta collection) —
|
|
12
|
+
* process state, not bookkeeping.
|
|
13
|
+
*/
|
|
14
|
+
import * as core from './core/index.js';
|
|
15
|
+
import { getDbSync } from './skr.database.js';
|
|
16
|
+
import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
|
|
17
|
+
import type { TSKRType } from './skr.types.js';
|
|
18
|
+
|
|
19
|
+
export const OPOS_META_COLLECTION = 'OposMeta';
|
|
20
|
+
|
|
21
|
+
export type TOposKind = 'debtor' | 'creditor';
|
|
22
|
+
|
|
23
|
+
export interface IOpenItem {
|
|
24
|
+
accountNumber: string;
|
|
25
|
+
kind: TOposKind;
|
|
26
|
+
/** belegfeld1 of the originating lines (usually the invoice number) */
|
|
27
|
+
reference: string;
|
|
28
|
+
/** original claim (sum of the claim-side lines) */
|
|
29
|
+
invoiceCents: number;
|
|
30
|
+
/** settled so far (sum of the settlement-side lines) */
|
|
31
|
+
clearedCents: number;
|
|
32
|
+
/** invoiceCents - clearedCents; > 0 means open */
|
|
33
|
+
openCents: number;
|
|
34
|
+
firstDate: string;
|
|
35
|
+
lastDate: string;
|
|
36
|
+
entryIds: string[];
|
|
37
|
+
dunningLevel: number;
|
|
38
|
+
dueDate?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface IAgingBucket {
|
|
42
|
+
label: string;
|
|
43
|
+
maxAgeDays: number | null;
|
|
44
|
+
items: IOpenItem[];
|
|
45
|
+
totalCents: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface IControlTotals {
|
|
49
|
+
debtorsCents: number;
|
|
50
|
+
creditorsCents: number;
|
|
51
|
+
debtorAccounts: number;
|
|
52
|
+
creditorAccounts: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kindOf(accountNumber: string, policy: core.IAccountPolicy): TOposKind | null {
|
|
56
|
+
if (core.isDebtorAccount(policy, accountNumber)) return 'debtor';
|
|
57
|
+
if (core.isCreditorAccount(policy, accountNumber)) return 'creditor';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ILineRow {
|
|
62
|
+
entryId: string;
|
|
63
|
+
accountNumber: string;
|
|
64
|
+
belegfeld1?: string;
|
|
65
|
+
side: 'debit' | 'credit';
|
|
66
|
+
amountCents: number;
|
|
67
|
+
date: Date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function loadPersonalLines(
|
|
71
|
+
skrType: TSKRType,
|
|
72
|
+
policy: core.IAccountPolicy,
|
|
73
|
+
accountNumber?: string,
|
|
74
|
+
asOf?: Date,
|
|
75
|
+
): Promise<ILineRow[]> {
|
|
76
|
+
const match: Record<string, unknown> = {
|
|
77
|
+
skrType,
|
|
78
|
+
schemaVersion: 2,
|
|
79
|
+
status: { $in: ['posted', 'finalized'] },
|
|
80
|
+
};
|
|
81
|
+
if (asOf) match.date = { $lte: asOf };
|
|
82
|
+
if (accountNumber) match['lines.accountNumber'] = accountNumber;
|
|
83
|
+
|
|
84
|
+
const docs = await getDbSync()
|
|
85
|
+
.mongoDb.collection(JOURNAL_ENTRY_COLLECTION)
|
|
86
|
+
.find(match)
|
|
87
|
+
.sort({ date: 1, sequenceNumber: 1 })
|
|
88
|
+
.toArray();
|
|
89
|
+
|
|
90
|
+
const rows: ILineRow[] = [];
|
|
91
|
+
for (const doc of docs) {
|
|
92
|
+
for (const line of (doc.lines ?? []) as Array<Record<string, unknown>>) {
|
|
93
|
+
const account = line.accountNumber as string;
|
|
94
|
+
if (!kindOf(account, policy)) continue;
|
|
95
|
+
if (accountNumber && account !== accountNumber) continue;
|
|
96
|
+
rows.push({
|
|
97
|
+
entryId: doc.id as string,
|
|
98
|
+
accountNumber: account,
|
|
99
|
+
belegfeld1: (line.belegfeld1 as string) || (doc.reference as string) || '',
|
|
100
|
+
side: line.side as 'debit' | 'credit',
|
|
101
|
+
amountCents: (line.amountCents as number) ?? 0,
|
|
102
|
+
date: doc.date as Date,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Derive open items. Debtors: claims are debits, settlements credits;
|
|
111
|
+
* creditors inverse. Items with openCents === 0 are settled and omitted
|
|
112
|
+
* unless includeSettled is set.
|
|
113
|
+
*/
|
|
114
|
+
export async function listOpenItems(
|
|
115
|
+
skrType: TSKRType,
|
|
116
|
+
policy: core.IAccountPolicy,
|
|
117
|
+
options?: {
|
|
118
|
+
kind?: TOposKind;
|
|
119
|
+
accountNumber?: string;
|
|
120
|
+
asOf?: Date;
|
|
121
|
+
includeSettled?: boolean;
|
|
122
|
+
},
|
|
123
|
+
): Promise<IOpenItem[]> {
|
|
124
|
+
const rows = await loadPersonalLines(skrType, policy, options?.accountNumber, options?.asOf);
|
|
125
|
+
const groups = new Map<string, IOpenItem>();
|
|
126
|
+
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const kind = kindOf(row.accountNumber, policy)!;
|
|
129
|
+
if (options?.kind && kind !== options.kind) continue;
|
|
130
|
+
const key = `${row.accountNumber}:${row.belegfeld1}`;
|
|
131
|
+
let item = groups.get(key);
|
|
132
|
+
if (!item) {
|
|
133
|
+
item = {
|
|
134
|
+
accountNumber: row.accountNumber,
|
|
135
|
+
kind,
|
|
136
|
+
reference: row.belegfeld1 ?? '',
|
|
137
|
+
invoiceCents: 0,
|
|
138
|
+
clearedCents: 0,
|
|
139
|
+
openCents: 0,
|
|
140
|
+
firstDate: row.date.toISOString().slice(0, 10),
|
|
141
|
+
lastDate: row.date.toISOString().slice(0, 10),
|
|
142
|
+
entryIds: [],
|
|
143
|
+
dunningLevel: 0,
|
|
144
|
+
};
|
|
145
|
+
groups.set(key, item);
|
|
146
|
+
}
|
|
147
|
+
const isClaim = (kind === 'debtor') === (row.side === 'debit');
|
|
148
|
+
if (isClaim) item.invoiceCents += row.amountCents;
|
|
149
|
+
else item.clearedCents += row.amountCents;
|
|
150
|
+
item.lastDate = row.date.toISOString().slice(0, 10);
|
|
151
|
+
if (!item.entryIds.includes(row.entryId)) item.entryIds.push(row.entryId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const meta = await getDbSync().mongoDb.collection(OPOS_META_COLLECTION).find({ skrType }).toArray();
|
|
155
|
+
const metaByKey = new Map(meta.map((m) => [`${m.accountNumber}:${m.reference}`, m]));
|
|
156
|
+
|
|
157
|
+
const items: IOpenItem[] = [];
|
|
158
|
+
for (const item of groups.values()) {
|
|
159
|
+
item.openCents = item.invoiceCents - item.clearedCents;
|
|
160
|
+
const extra = metaByKey.get(`${item.accountNumber}:${item.reference}`);
|
|
161
|
+
if (extra) {
|
|
162
|
+
item.dunningLevel = (extra.dunningLevel as number) ?? 0;
|
|
163
|
+
item.dueDate = extra.dueDate as string | undefined;
|
|
164
|
+
}
|
|
165
|
+
if (item.openCents !== 0 || options?.includeSettled) items.push(item);
|
|
166
|
+
}
|
|
167
|
+
return items.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber) || a.firstDate.localeCompare(b.firstDate));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Aging list relative to asOf, bucketed by days since dueDate (falling back
|
|
172
|
+
* to the item's first posting date).
|
|
173
|
+
*/
|
|
174
|
+
export async function getAging(
|
|
175
|
+
skrType: TSKRType,
|
|
176
|
+
policy: core.IAccountPolicy,
|
|
177
|
+
kind: TOposKind,
|
|
178
|
+
asOf: Date,
|
|
179
|
+
bucketDays: number[] = [30, 60, 90],
|
|
180
|
+
): Promise<IAgingBucket[]> {
|
|
181
|
+
const items = await listOpenItems(skrType, policy, { kind, asOf });
|
|
182
|
+
const buckets: IAgingBucket[] = [
|
|
183
|
+
...bucketDays.map((days, index) => ({
|
|
184
|
+
label: index === 0 ? `0-${days}` : `${bucketDays[index - 1] + 1}-${days}`,
|
|
185
|
+
maxAgeDays: days,
|
|
186
|
+
items: [] as IOpenItem[],
|
|
187
|
+
totalCents: 0,
|
|
188
|
+
})),
|
|
189
|
+
{ label: `> ${bucketDays[bucketDays.length - 1]}`, maxAgeDays: null, items: [], totalCents: 0 },
|
|
190
|
+
];
|
|
191
|
+
for (const item of items) {
|
|
192
|
+
const anchor = item.dueDate ?? item.firstDate;
|
|
193
|
+
const ageDays = Math.max(0, Math.floor((asOf.getTime() - new Date(anchor).getTime()) / 86_400_000));
|
|
194
|
+
const bucket =
|
|
195
|
+
buckets.find((candidate) => candidate.maxAgeDays !== null && ageDays <= candidate.maxAgeDays) ??
|
|
196
|
+
buckets[buckets.length - 1];
|
|
197
|
+
bucket.items.push(item);
|
|
198
|
+
bucket.totalCents += item.openCents;
|
|
199
|
+
}
|
|
200
|
+
return buckets;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Subledger control totals: the aggregated personal-account balances that a
|
|
205
|
+
* balance sheet reports as Forderungen (B.II.1) / Verbindlichkeiten (C.4)
|
|
206
|
+
* aus Lieferungen und Leistungen. Control accounts themselves are blocked for
|
|
207
|
+
* posting, so these aggregates ARE the control balances.
|
|
208
|
+
*/
|
|
209
|
+
export async function getControlTotals(
|
|
210
|
+
skrType: TSKRType,
|
|
211
|
+
policy: core.IAccountPolicy,
|
|
212
|
+
asOf?: Date,
|
|
213
|
+
): Promise<IControlTotals> {
|
|
214
|
+
const rows = await loadPersonalLines(skrType, policy, undefined, asOf);
|
|
215
|
+
let debtorsCents = 0;
|
|
216
|
+
let creditorsCents = 0;
|
|
217
|
+
const debtorAccounts = new Set<string>();
|
|
218
|
+
const creditorAccounts = new Set<string>();
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
const signed = row.side === 'debit' ? row.amountCents : -row.amountCents;
|
|
221
|
+
if (kindOf(row.accountNumber, policy) === 'debtor') {
|
|
222
|
+
debtorsCents += signed;
|
|
223
|
+
debtorAccounts.add(row.accountNumber);
|
|
224
|
+
} else {
|
|
225
|
+
creditorsCents -= signed; // creditors carry credit balances positive
|
|
226
|
+
creditorAccounts.add(row.accountNumber);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
debtorsCents,
|
|
231
|
+
creditorsCents,
|
|
232
|
+
debtorAccounts: debtorAccounts.size,
|
|
233
|
+
creditorAccounts: creditorAccounts.size,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Dunning metadata (process state, not bookkeeping — letters are out of
|
|
239
|
+
* scope; the level/due-date state machine is in).
|
|
240
|
+
*/
|
|
241
|
+
export async function setOposMeta(params: {
|
|
242
|
+
skrType: TSKRType;
|
|
243
|
+
accountNumber: string;
|
|
244
|
+
reference: string;
|
|
245
|
+
dunningLevel?: number;
|
|
246
|
+
dueDate?: string;
|
|
247
|
+
}): Promise<void> {
|
|
248
|
+
const update: Record<string, unknown> = { updatedAt: new Date() };
|
|
249
|
+
if (params.dunningLevel !== undefined) update.dunningLevel = params.dunningLevel;
|
|
250
|
+
if (params.dueDate !== undefined) update.dueDate = params.dueDate;
|
|
251
|
+
await getDbSync()
|
|
252
|
+
.mongoDb.collection(OPOS_META_COLLECTION)
|
|
253
|
+
.updateOne(
|
|
254
|
+
{ skrType: params.skrType, accountNumber: params.accountNumber, reference: params.reference },
|
|
255
|
+
{ $set: update },
|
|
256
|
+
{ upsert: true },
|
|
257
|
+
);
|
|
258
|
+
}
|
package/ts/skr.ustva.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UStVA period extraction: one period's account movements plus the EXACT
|
|
3
|
+
* scenario Bemessungsgrundlagen (igE / §13b) taken from taxScenario-tagged
|
|
4
|
+
* journal entries. The result feeds @fin.cx/tax `computeUstva()` directly —
|
|
5
|
+
* movements as `IAccountMovementInput[]`, scenarioBases as
|
|
6
|
+
* `IScenarioBaseInput`.
|
|
7
|
+
*
|
|
8
|
+
* Base extraction per tagged entry: sum of (debit - credit) over lines that
|
|
9
|
+
* sit neither on a policy VAT account nor on a personal account (>= 10000).
|
|
10
|
+
* For the recipe shapes that is exactly the net expense line; Stornos flip
|
|
11
|
+
* sides and cancel. Every extracted base is cross-checked against the
|
|
12
|
+
* entry's own input-VAT line — disagreement lands in `warnings`.
|
|
13
|
+
*/
|
|
14
|
+
import { vatFromNetCents } from '@fin.cx/calculation';
|
|
15
|
+
import { getDbSync } from './skr.database.js';
|
|
16
|
+
import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
|
|
17
|
+
import { aggregateTrialBalance } from './skr.balances.js';
|
|
18
|
+
import type { IAccountPolicy } from './core/index.js';
|
|
19
|
+
import type { TSKRType } from './skr.types.js';
|
|
20
|
+
|
|
21
|
+
export interface IUstvaMovement {
|
|
22
|
+
accountNumber: string;
|
|
23
|
+
/** debitCents - creditCents over the period */
|
|
24
|
+
signedCents: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IUstvaScenarioBases {
|
|
28
|
+
/** igE 19 % net bases (Kz 89) */
|
|
29
|
+
intraEuAcquisitionStandardCents: number;
|
|
30
|
+
/** igE 7 % net bases (Kz 93) */
|
|
31
|
+
intraEuAcquisitionReducedCents: number;
|
|
32
|
+
/**
|
|
33
|
+
* §13b bases (Kz 46). The scenario tag does not distinguish EU services
|
|
34
|
+
* from other §13b cases — everything lands here; reclassify to Kz 84
|
|
35
|
+
* manually if a non-EU-services §13b case ever occurs.
|
|
36
|
+
*/
|
|
37
|
+
reverseChargeServicesEuCents: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IUstvaPeriodData {
|
|
41
|
+
movements: IUstvaMovement[];
|
|
42
|
+
scenarioBases: IUstvaScenarioBases;
|
|
43
|
+
/** tagged igE/§13b entries examined */
|
|
44
|
+
taggedEntryCount: number;
|
|
45
|
+
/** extraction anomalies — review before filing */
|
|
46
|
+
warnings: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IRawEntry {
|
|
50
|
+
sequenceNumber?: number;
|
|
51
|
+
journalNumber?: string;
|
|
52
|
+
taxScenario?: string;
|
|
53
|
+
lines: Array<{ accountNumber: string; side?: string; amountCents?: number }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPersonalAccount(accountNumber: string): boolean {
|
|
57
|
+
const numeric = Number.parseInt(accountNumber, 10);
|
|
58
|
+
return numeric >= 10000 && numeric <= 99999;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getUstvaPeriodData(
|
|
62
|
+
skrType: TSKRType,
|
|
63
|
+
policy: IAccountPolicy,
|
|
64
|
+
options: { dateFrom: Date; dateTo: Date },
|
|
65
|
+
): Promise<IUstvaPeriodData> {
|
|
66
|
+
const trialBalance = await aggregateTrialBalance(skrType, options);
|
|
67
|
+
const movements: IUstvaMovement[] = trialBalance.accounts
|
|
68
|
+
.filter((account) => account.balanceCents !== 0)
|
|
69
|
+
.map((account) => ({
|
|
70
|
+
accountNumber: account.accountNumber,
|
|
71
|
+
signedCents: account.balanceCents,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const vatAccounts = new Set(Object.values(policy.vat));
|
|
75
|
+
const warnings: string[] = [];
|
|
76
|
+
const scenarioBases: IUstvaScenarioBases = {
|
|
77
|
+
intraEuAcquisitionStandardCents: 0,
|
|
78
|
+
intraEuAcquisitionReducedCents: 0,
|
|
79
|
+
reverseChargeServicesEuCents: 0,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
|
|
83
|
+
const tagged = (await collection
|
|
84
|
+
.find(
|
|
85
|
+
{
|
|
86
|
+
schemaVersion: 2,
|
|
87
|
+
skrType,
|
|
88
|
+
status: { $in: ['posted', 'finalized'] },
|
|
89
|
+
taxScenario: { $in: ['intra_eu_acquisition', 'reverse_charge_13b'] },
|
|
90
|
+
date: { $gte: options.dateFrom, $lte: options.dateTo },
|
|
91
|
+
},
|
|
92
|
+
{ projection: { sequenceNumber: 1, journalNumber: 1, taxScenario: 1, lines: 1 } },
|
|
93
|
+
)
|
|
94
|
+
.toArray()) as unknown as IRawEntry[];
|
|
95
|
+
|
|
96
|
+
for (const entry of tagged) {
|
|
97
|
+
const label = entry.journalNumber ?? `seq ${entry.sequenceNumber}`;
|
|
98
|
+
const inputVatAccount =
|
|
99
|
+
entry.taxScenario === 'intra_eu_acquisition'
|
|
100
|
+
? policy.vat.intraEuAcqInput
|
|
101
|
+
: policy.vat.reverseChargeInput;
|
|
102
|
+
|
|
103
|
+
let baseCents = 0;
|
|
104
|
+
let inputVatCents = 0;
|
|
105
|
+
for (const line of entry.lines) {
|
|
106
|
+
const amount = line.amountCents ?? 0;
|
|
107
|
+
const signed = line.side === 'debit' ? amount : -amount;
|
|
108
|
+
if (line.accountNumber === inputVatAccount) {
|
|
109
|
+
inputVatCents += signed;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (vatAccounts.has(line.accountNumber)) continue;
|
|
113
|
+
if (isPersonalAccount(line.accountNumber)) continue;
|
|
114
|
+
baseCents += signed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (baseCents === 0 && inputVatCents !== 0) {
|
|
118
|
+
warnings.push(
|
|
119
|
+
`entry ${label}: no base extractable (counterparty not a personal account?) — VAT line is ${inputVatCents} cents`,
|
|
120
|
+
);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (baseCents === 0) continue;
|
|
124
|
+
|
|
125
|
+
// rate detection from the entry's own VAT line (19 % vs 7 %)
|
|
126
|
+
const absBase = Math.abs(baseCents);
|
|
127
|
+
const absVat = Math.abs(inputVatCents);
|
|
128
|
+
const reduced = Math.abs(absVat * 100 - 7 * absBase) < Math.abs(absVat * 100 - 19 * absBase);
|
|
129
|
+
const rate = reduced ? 7 : 19;
|
|
130
|
+
const expectedVat = vatFromNetCents(baseCents, rate);
|
|
131
|
+
if (Math.abs(inputVatCents - expectedVat) > 2) {
|
|
132
|
+
warnings.push(
|
|
133
|
+
`entry ${label}: base ${baseCents} cents at ${rate} % expects VAT ${expectedVat}, ledger line says ${inputVatCents}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (entry.taxScenario === 'intra_eu_acquisition') {
|
|
138
|
+
if (reduced) scenarioBases.intraEuAcquisitionReducedCents += baseCents;
|
|
139
|
+
else scenarioBases.intraEuAcquisitionStandardCents += baseCents;
|
|
140
|
+
} else {
|
|
141
|
+
scenarioBases.reverseChargeServicesEuCents += baseCents;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
movements,
|
|
147
|
+
scenarioBases,
|
|
148
|
+
taggedEntryCount: tagged.length,
|
|
149
|
+
warnings,
|
|
150
|
+
};
|
|
151
|
+
}
|