@things-factory/accounting 10.1.140 → 10.1.141
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-server/service/account/account-match.d.ts +32 -0
- package/dist-server/service/account/account-match.js +74 -0
- package/dist-server/service/account/account-match.js.map +1 -0
- package/dist-server/service/account/index.d.ts +1 -1
- package/dist-server/service/accounting-category/index.d.ts +1 -1
- package/dist-server/service/accounting-document/index.d.ts +1 -1
- package/dist-server/service/bank-account/index.d.ts +1 -1
- package/dist-server/service/bank-statement/index.d.ts +1 -1
- package/dist-server/service/financial-institution/index.d.ts +1 -1
- package/dist-server/service/financial-statement/index.d.ts +2 -2
- package/dist-server/service/fiscal-month/index.d.ts +1 -1
- package/dist-server/service/fiscal-quarter/index.d.ts +1 -1
- package/dist-server/service/fiscal-year/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +2 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/payment/document-targets.d.ts +16 -0
- package/dist-server/service/payment/document-targets.js +99 -0
- package/dist-server/service/payment/document-targets.js.map +1 -0
- package/dist-server/service/payment/payment-service.js +3 -0
- package/dist-server/service/payment/payment-service.js.map +1 -1
- package/dist-server/service/payment/payment-targets.d.ts +12 -0
- package/dist-server/service/payment/payment-targets.js +9 -0
- package/dist-server/service/payment/payment-targets.js.map +1 -1
- package/dist-server/service/transaction/transaction-query.js +13 -1
- package/dist-server/service/transaction/transaction-query.js.map +1 -1
- package/dist-server/service/transaction/transaction-service.js +2 -0
- package/dist-server/service/transaction/transaction-service.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -7
- package/translations/en.json +10 -1
- package/translations/ko.json +10 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-targets.js","sourceRoot":"","sources":["../../../server/service/payment/payment-targets.ts"],"names":[],"mappings":";;AAuCA,sDAGC;AAED,0CAEC;AAED,4DAEC;
|
|
1
|
+
{"version":3,"file":"payment-targets.js","sourceRoot":"","sources":["../../../server/service/payment/payment-targets.ts"],"names":[],"mappings":";;AAuCA,sDAGC;AAED,0CAEC;AAWD,kDAEC;AAED,oCAEC;AAED,4DAEC;AA/BD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAA;AAEhD,0GAA0G;AAC1G,SAAgB,qBAAqB,CAAC,UAAkB,EAAE,MAAqB;IAC7E,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,UAAU,uBAAuB,CAAC,CAAA;IAClG,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;AACjC,CAAC;AAED,SAAgB,eAAe,CAAC,UAAkB;IAChD,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;AAChC,CAAC;AASD,MAAM,KAAK,GAAkB,EAAE,CAAA;AAE/B,SAAgB,mBAAmB,CAAC,IAAiB;IACnD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAClB,CAAC;AAED,SAAgB,YAAY;IAC1B,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAgB,wBAAwB;IACtC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;AAC5B,CAAC","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { PaymentPurpose } from './payment'\n\n/**\n * The kinds of thing a transaction can pay, registered by the module that owns them.\n *\n * accounting keeps the general rules - the target kind is known, a transaction pays no more than itself within one\n * purpose. What a target may take (a contract round's capacity, one VAT period per withdrawal) is the owner's rule,\n * run in the same database transaction after the payments are written.\n */\nexport interface PaymentTarget {\n /** What a payment on this kind tells of its transaction. */\n purpose: PaymentPurpose\n /** What people call each target - 「H2026021 3차」 - for the screens that list what a transaction paid. */\n label?: (tx: EntityManager, domain: { id: string }, targetIds: string[]) => Promise<Map<string, string>>\n /**\n * The owner's rules on the payments of its targets, given the targets and transactions a save touched. Throw a\n * `Refusal` to refuse the save.\n */\n validate?: (\n tx: EntityManager,\n domain: { id: string },\n touched: { targetIds: string[]; transactionIds: string[] }\n ) => Promise<void>\n /**\n * What the owner keeps in step with its targets' payments - a round's collected amount and day - run after every\n * kind's rules passed, in the same database transaction.\n */\n onSaved?: (\n tx: EntityManager,\n domain: { id: string },\n touched: { targetIds: string[]; transactionIds: string[] }\n ) => Promise<void>\n}\n\nconst targets = new Map<string, PaymentTarget>()\n\n/** Register a kind of target. A kind is registered once; registering it again is a mistake in the app. */\nexport function registerPaymentTarget(targetType: string, target: PaymentTarget): void {\n if (targets.has(targetType)) throw new Error(`payment target '${targetType}' is registered twice`)\n targets.set(targetType, target)\n}\n\nexport function paymentTargetOf(targetType: string): PaymentTarget | undefined {\n return targets.get(targetType)\n}\n\n/**\n * An application's rule on every payment of a transaction, whatever it paid - run after the target kinds' own, given\n * the transactions a save touched. Throw a `Refusal` to refuse the save. (Office: cash reserved for a refund is not\n * also put on anything else.)\n */\nexport type PaymentRule = (tx: EntityManager, domain: { id: string }, touched: { transactionIds: string[] }) => Promise<void>\n\nconst rules: PaymentRule[] = []\n\nexport function registerPaymentRule(rule: PaymentRule): void {\n rules.push(rule)\n}\n\nexport function paymentRules(): readonly PaymentRule[] {\n return rules\n}\n\nexport function registeredPaymentTargets(): string[] {\n return [...targets.keys()]\n}\n"]}
|
|
@@ -13,6 +13,11 @@ const payment_1 = require("../payment/payment");
|
|
|
13
13
|
const recurring_cash_item_1 = require("../recurring-cash-item/recurring-cash-item");
|
|
14
14
|
const transaction_1 = require("./transaction");
|
|
15
15
|
const transaction_type_1 = require("./transaction-type");
|
|
16
|
+
/**
|
|
17
|
+
* Newest first: the period (a forecast has only that), the day, the time the bank printed, the bank's own order
|
|
18
|
+
* within one second, then when the row was entered.
|
|
19
|
+
*/
|
|
20
|
+
const BOOK_ORDER = ['year', 'month', 'transactionDate', 'transactionAt', 'bankSequence', 'createdAt'];
|
|
16
21
|
let TransactionQuery = class TransactionQuery {
|
|
17
22
|
async transaction(id, context) {
|
|
18
23
|
const { domain } = context.state;
|
|
@@ -29,13 +34,20 @@ let TransactionQuery = class TransactionQuery {
|
|
|
29
34
|
searchables: [
|
|
30
35
|
'description',
|
|
31
36
|
'counterpartyName',
|
|
37
|
+
'counterparty',
|
|
32
38
|
'ledgerCategory',
|
|
39
|
+
'timing',
|
|
33
40
|
'bankDescription',
|
|
34
41
|
'bankCounterpartyName',
|
|
35
42
|
'bankMemo',
|
|
36
43
|
'notes'
|
|
37
|
-
]
|
|
44
|
+
],
|
|
45
|
+
alias: 'transaction'
|
|
38
46
|
});
|
|
47
|
+
/* A book reads newest first - after any order the caller asks (§`BOOK_ORDER`). */
|
|
48
|
+
for (const field of BOOK_ORDER)
|
|
49
|
+
if (!params.sortings?.some(sorting => sorting.name === field))
|
|
50
|
+
queryBuilder.addOrderBy(`transaction.${field}`, 'DESC');
|
|
39
51
|
const [items, total] = await queryBuilder.getManyAndCount();
|
|
40
52
|
return { items, total };
|
|
41
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction-query.js","sourceRoot":"","sources":["../../../server/service/transaction/transaction-query.ts"],"names":[],"mappings":";;;;AAAA,+CAA8F;AAE9F,iDAAuG;AACvG,yDAAgD;AAChD,uDAAuD;AAEvD,gDAA4C;AAC5C,+DAA0D;AAC1D,qEAAgE;AAChE,gDAA4C;AAC5C,oFAA8E;AAC9E,+CAA2C;AAC3C,yDAAoD;
|
|
1
|
+
{"version":3,"file":"transaction-query.js","sourceRoot":"","sources":["../../../server/service/transaction/transaction-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive } from 'type-graphql'\n\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { Counterparty } from '@things-factory/biz-base'\n\nimport { Account } from '../account/account'\nimport { BankAccount } from '../bank-account/bank-account'\nimport { BankStatement } from '../bank-statement/bank-statement'\nimport { Payment } from '../payment/payment'\nimport { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { Transaction } from './transaction'\nimport { TransactionList } from './transaction-type'\n\n/**\n * Newest first: the period (a forecast has only that), the day, the time the bank printed, the bank's own order\n * within one second, then when the row was entered.\n */\nconst BOOK_ORDER = ['year', 'month', 'transactionDate', 'transactionAt', 'bankSequence', 'createdAt'] as const\n\n@Resolver(Transaction)\nexport class TransactionQuery {\n @Directive('@privilege(category: \"accounting\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => Transaction!, { nullable: true, description: 'To fetch a Transaction' })\n async transaction(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Transaction> {\n const { domain } = context.state\n\n return await getRepository(Transaction).findOne({\n where: { domain: { id: domain.id }, id }\n })\n }\n\n @Directive('@privilege(category: \"accounting\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => TransactionList, { description: 'To fetch multiple Transactions' })\n async transactions(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<TransactionList> {\n const { domain } = context.state\n\n const queryBuilder = getQueryBuilderFromListParams({\n domain,\n params,\n repository: await getRepository(Transaction),\n searchables: [\n 'description',\n 'counterpartyName',\n 'counterparty',\n 'ledgerCategory',\n 'timing',\n 'bankDescription',\n 'bankCounterpartyName',\n 'bankMemo',\n 'notes'\n ],\n alias: 'transaction'\n })\n /* A book reads newest first - after any order the caller asks (§`BOOK_ORDER`). */\n for (const field of BOOK_ORDER)\n if (!params.sortings?.some(sorting => sorting.name === field)) queryBuilder.addOrderBy(`transaction.${field}`, 'DESC')\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @FieldResolver(type => BankAccount, { nullable: true })\n async bankAccount(@Root() transaction: Transaction): Promise<BankAccount | null> {\n return transaction.bankAccountId\n ? await getRepository(BankAccount).findOneBy({ id: transaction.bankAccountId })\n : null\n }\n\n @FieldResolver(type => Account, { nullable: true })\n async account(@Root() transaction: Transaction): Promise<Account | null> {\n return transaction.accountId ? await getRepository(Account).findOneBy({ id: transaction.accountId }) : null\n }\n\n @FieldResolver(type => Counterparty, { nullable: true })\n async counterparty(@Root() transaction: Transaction): Promise<Counterparty | null> {\n return transaction.counterpartyId\n ? await getRepository(Counterparty).findOneBy({ id: transaction.counterpartyId })\n : null\n }\n\n @FieldResolver(type => BankStatement, { nullable: true })\n async bankStatement(@Root() transaction: Transaction): Promise<BankStatement | null> {\n return transaction.bankStatementId\n ? await getRepository(BankStatement).findOneBy({ id: transaction.bankStatementId })\n : null\n }\n\n @FieldResolver(type => RecurringCashItem, { nullable: true })\n async recurringItem(@Root() transaction: Transaction): Promise<RecurringCashItem | null> {\n return transaction.recurringItemId\n ? await getRepository(RecurringCashItem).findOneBy({ id: transaction.recurringItemId })\n : null\n }\n\n @FieldResolver(type => [Payment])\n async payments(@Root() transaction: Transaction): Promise<Payment[]> {\n return await getRepository(Payment).find({ where: { transaction: { id: transaction.id } } })\n }\n\n @FieldResolver(type => Domain)\n async domain(@Root() transaction: Transaction): Promise<Domain> {\n return await getRepository(Domain).findOneBy({ id: transaction.domainId })\n }\n\n @FieldResolver(type => User)\n async updater(@Root() transaction: Transaction): Promise<User> {\n return await getRepository(User).findOneBy({ id: transaction.updaterId })\n }\n\n @FieldResolver(type => User)\n async creator(@Root() transaction: Transaction): Promise<User> {\n return await getRepository(User).findOneBy({ id: transaction.creatorId })\n }\n}\n"]}
|
|
@@ -146,6 +146,8 @@ async function saveTransactions(tx, domain, user, inputs) {
|
|
|
146
146
|
});
|
|
147
147
|
for (const [targetType, ofKind] of kinds)
|
|
148
148
|
await (0, payment_targets_1.paymentTargetOf)(targetType)?.validate?.(tx, domain, touchedOf(ofKind));
|
|
149
|
+
for (const rule of (0, payment_targets_1.paymentRules)())
|
|
150
|
+
await rule(tx, domain, { transactionIds: [...new Set(payments.map(one => one.transaction.id))] });
|
|
149
151
|
for (const [targetType, ofKind] of kinds)
|
|
150
152
|
await (0, payment_targets_1.paymentTargetOf)(targetType)?.onSaved?.(tx, domain, touchedOf(ofKind));
|
|
151
153
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction-service.js","sourceRoot":"","sources":["../../../server/service/transaction/transaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { BankAccount } from '../bank-account/bank-account'\nimport { CashFlowType } from '../cash-flow-type'\nimport { Payment } from '../payment/payment'\nimport { checkPaymentsWithin } from '../payment/payment-service'\nimport { paymentTargetOf } from '../payment/payment-targets'\nimport { ForecastStatus, Transaction, TransactionCategory, TransactionSource } from './transaction'\nimport { forecastDay, yearMonthOf } from './transaction-period'\n\n/** A transaction as a module writes it: what a person may write, and the facts a bank statement gives. */\nexport type TransactionInput = Partial<\n Omit<Transaction, 'bankAccount' | 'account' | 'counterparty' | 'recurringItem' | 'bankStatement'>\n> & {\n id?: string\n cuFlag?: string\n bankAccount?: { id: string } | null\n account?: { id: string } | null\n counterparty?: { id: string } | null\n recurringItem?: { id: string } | null\n bankStatement?: { id: string } | null\n}\n\n/** What a bank statement says of a line - changed only by a statement; what a person writes beside them stays open. */\nconst BANK_FACTS = ['transactionDate', 'type', 'amount', 'balance', 'currency', 'bankAccount'] as const\n\n/** A line that came from a bank statement keeps the statement's facts (§`BANK_FACTS`). */\nexport function protectBankFacts(current: Partial<Transaction> & { bankAccountId?: string }, patch: Record<string, any>): void {\n if (current.source !== TransactionSource.BANK_STATEMENT) return\n for (const field of BANK_FACTS) {\n if (!(field in patch)) continue\n const before = field === 'bankAccount' ? current.bankAccountId ?? current.bankAccount?.id : (current as any)[field]\n const after = field === 'bankAccount' ? patch.bankAccount?.id : patch[field]\n const equal = ['amount', 'balance'].includes(field)\n ? (before == null && after == null) || (before != null && after != null && Number(before) === Number(after))\n : before === after\n if (!equal)\n throw new Refusal('bank-facts-locked', {}, 'Bank facts can only be updated from a statement; annotations remain editable')\n }\n}\n\n/** What its payments depend on - they are checked again, and their owners follow, when one of these changes. */\nconst FACTS = ['amount', 'type', 'currency', 'transactionDate'] as const\n\n/**\n * The row a patch writes over the current one: a confirmed transaction has a day and takes its year and month from it;\n * a forecast has a year and a month, and the day its timing names.\n */\nfunction rowOf(input: TransactionInput, current: Transaction | undefined): Record<string, any> {\n const { id, cuFlag, ...fields } = input\n const row: Record<string, any> = { ...fields }\n const merged = { ...current, ...row }\n const category = merged.category ?? TransactionCategory.CONFIRMED\n row.category = category\n\n if (!current && !row.type) throw new Refusal('transaction-type-required', {}, 'type: money in or money out')\n if (row.type !== undefined && !Object.values(CashFlowType).includes(row.type))\n throw new Refusal('transaction-type-required', {}, 'type: money in or money out')\n if (!current && (row.amount === undefined || row.amount === null)) row.amount = 0\n\n if (category === TransactionCategory.CONFIRMED) {\n const period = yearMonthOf(merged.transactionDate)\n if (!period)\n throw new Refusal(\n 'transaction-date-required',\n { value: merged.transactionDate ?? '' },\n 'transactionDate: YYYY-MM-DD'\n )\n Object.assign(row, period, { status: null })\n } else {\n const [year, month] = [Number(merged.year), Number(merged.month)]\n if (!year) throw new Refusal('forecast-year-required', {}, 'year: the year it is planned in')\n if (!(month >= 1 && month <= 12))\n throw new Refusal('month-out-of-range', { month: merged.month ?? '' }, 'month: must be 1 to 12')\n if (!String(merged.description ?? '').trim())\n throw new Refusal('forecast-description-required', {}, 'description: what is planned')\n Object.assign(row, { year, month, transactionDate: forecastDay(year, month, merged.timing) })\n if (!merged.status) row.status = ForecastStatus.PLANNED\n }\n\n if (\n merged.probability !== undefined &&\n merged.probability !== null &&\n !(merged.probability >= 0 && merged.probability <= 100)\n ) {\n throw new Refusal('probability-out-of-range', {}, 'probability: must be 0 to 100')\n }\n\n return row\n}\n\n/** The payments on these transactions, and the transfers into them, by target kind. */\nasync function paymentsOn(tx: EntityManager, domain: { id: string }, ids: string[]): Promise<Payment[]> {\n if (!ids.length) return []\n const repo = tx.getRepository(Payment)\n return [\n ...(await repo.find({\n where: { domain: { id: domain.id }, transaction: { id: In(ids) } },\n relations: ['transaction']\n })),\n ...(await repo.find({\n where: { domain: { id: domain.id }, targetType: 'Transaction', targetId: In(ids) },\n relations: ['transaction']\n }))\n ]\n}\n\n/**\n * Apply rows to a domain's transactions: `+` (or no id) adds, `M` changes, `-` deletes (softly - a financial row\n * that was deleted stays readable). A transaction that pays something, or is paid, is unlinked before it is\n * deleted; when its money changes, its payments are checked again - the general rule and each target kind's own.\n * A line from a bank statement keeps the statement's facts (§`protectBankFacts`).\n */\nexport async function saveTransactions(\n tx: EntityManager,\n domain: { id: string },\n user: any,\n inputs: TransactionInput[]\n): Promise<Transaction[]> {\n const repo = tx.getRepository(Transaction)\n\n const deletes = [...new Set(inputs.filter(one => one.cuFlag === '-' && one.id).map(one => one.id!))]\n if (deletes.length) {\n const found = await repo.find({ where: { domain: { id: domain.id }, id: In(deletes) } })\n if (found.length !== deletes.length)\n throw new Refusal('transaction-not-found', {}, 'a transaction was not found - reload and try again')\n if ((await paymentsOn(tx, domain, deletes)).length)\n throw new Refusal('transaction-has-payments', {}, 'unlink what the transaction paid before deleting it')\n await repo.softRemove(found)\n }\n\n const saved: Transaction[] = []\n const factsChanged: string[] = []\n for (const input of inputs.filter(one => one.cuFlag !== '-')) {\n const isNew = !input.id || input.cuFlag === '+'\n const current = isNew ? undefined : await repo.findOne({ where: { domain: { id: domain.id }, id: input.id } })\n if (!isNew && !current)\n throw new Refusal('transaction-not-found', {}, 'a transaction was not found - reload and try again')\n\n if (current) protectBankFacts(current, input)\n const row = rowOf(input, current)\n if (row.bankAccount?.id && !('currency' in input)) {\n const account = await tx\n .getRepository(BankAccount)\n .findOne({ where: { domain: { id: domain.id }, id: row.bankAccount.id } })\n if (!account)\n throw new Refusal('bank-account-not-found', {}, 'the bank account was not found - reload and try again')\n if (account.currency) row.currency = account.currency\n }\n\n if (row.bankFingerprint) {\n const twin = await repo.findOne({ where: { domain: { id: domain.id }, bankFingerprint: row.bankFingerprint } })\n if (twin && twin.id !== current?.id)\n throw new Refusal(\n 'bank-line-twice',\n { day: twin.transactionDate ?? '' },\n 'this bank line is already in the books'\n )\n }\n\n const written = await repo.save({ ...(current ?? { domain, creator: user }), ...row, updater: user } as Transaction)\n if (current && FACTS.some(field => field in row && row[field] !== (current as any)[field]))\n factsChanged.push(current.id)\n saved.push(written)\n }\n\n if (factsChanged.length) {\n const payments = await paymentsOn(tx, domain, factsChanged)\n await checkPaymentsWithin(\n tx,\n domain,\n payments.map(one => one.transaction.id)\n )\n const kinds = new Map<string, Payment[]>()\n for (const payment of payments) kinds.set(payment.targetType, [...(kinds.get(payment.targetType) ?? []), payment])\n const touchedOf = (ofKind: Payment[]) => ({\n targetIds: [...new Set(ofKind.map(one => one.targetId))],\n transactionIds: [...new Set(ofKind.map(one => one.transaction.id))]\n })\n for (const [targetType, ofKind] of kinds)\n await paymentTargetOf(targetType)?.validate?.(tx, domain, touchedOf(ofKind))\n for (const [targetType, ofKind] of kinds)\n await paymentTargetOf(targetType)?.onSaved?.(tx, domain, touchedOf(ofKind))\n }\n\n return saved\n}\n"]}
|
|
1
|
+
{"version":3,"file":"transaction-service.js","sourceRoot":"","sources":["../../../server/service/transaction/transaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { BankAccount } from '../bank-account/bank-account'\nimport { CashFlowType } from '../cash-flow-type'\nimport { Payment } from '../payment/payment'\nimport { checkPaymentsWithin } from '../payment/payment-service'\nimport { paymentRules, paymentTargetOf } from '../payment/payment-targets'\nimport { ForecastStatus, Transaction, TransactionCategory, TransactionSource } from './transaction'\nimport { forecastDay, yearMonthOf } from './transaction-period'\n\n/** A transaction as a module writes it: what a person may write, and the facts a bank statement gives. */\nexport type TransactionInput = Partial<\n Omit<Transaction, 'bankAccount' | 'account' | 'counterparty' | 'recurringItem' | 'bankStatement'>\n> & {\n id?: string\n cuFlag?: string\n bankAccount?: { id: string } | null\n account?: { id: string } | null\n counterparty?: { id: string } | null\n recurringItem?: { id: string } | null\n bankStatement?: { id: string } | null\n}\n\n/** What a bank statement says of a line - changed only by a statement; what a person writes beside them stays open. */\nconst BANK_FACTS = ['transactionDate', 'type', 'amount', 'balance', 'currency', 'bankAccount'] as const\n\n/** A line that came from a bank statement keeps the statement's facts (§`BANK_FACTS`). */\nexport function protectBankFacts(current: Partial<Transaction> & { bankAccountId?: string }, patch: Record<string, any>): void {\n if (current.source !== TransactionSource.BANK_STATEMENT) return\n for (const field of BANK_FACTS) {\n if (!(field in patch)) continue\n const before = field === 'bankAccount' ? current.bankAccountId ?? current.bankAccount?.id : (current as any)[field]\n const after = field === 'bankAccount' ? patch.bankAccount?.id : patch[field]\n const equal = ['amount', 'balance'].includes(field)\n ? (before == null && after == null) || (before != null && after != null && Number(before) === Number(after))\n : before === after\n if (!equal)\n throw new Refusal('bank-facts-locked', {}, 'Bank facts can only be updated from a statement; annotations remain editable')\n }\n}\n\n/** What its payments depend on - they are checked again, and their owners follow, when one of these changes. */\nconst FACTS = ['amount', 'type', 'currency', 'transactionDate'] as const\n\n/**\n * The row a patch writes over the current one: a confirmed transaction has a day and takes its year and month from it;\n * a forecast has a year and a month, and the day its timing names.\n */\nfunction rowOf(input: TransactionInput, current: Transaction | undefined): Record<string, any> {\n const { id, cuFlag, ...fields } = input\n const row: Record<string, any> = { ...fields }\n const merged = { ...current, ...row }\n const category = merged.category ?? TransactionCategory.CONFIRMED\n row.category = category\n\n if (!current && !row.type) throw new Refusal('transaction-type-required', {}, 'type: money in or money out')\n if (row.type !== undefined && !Object.values(CashFlowType).includes(row.type))\n throw new Refusal('transaction-type-required', {}, 'type: money in or money out')\n if (!current && (row.amount === undefined || row.amount === null)) row.amount = 0\n\n if (category === TransactionCategory.CONFIRMED) {\n const period = yearMonthOf(merged.transactionDate)\n if (!period)\n throw new Refusal(\n 'transaction-date-required',\n { value: merged.transactionDate ?? '' },\n 'transactionDate: YYYY-MM-DD'\n )\n Object.assign(row, period, { status: null })\n } else {\n const [year, month] = [Number(merged.year), Number(merged.month)]\n if (!year) throw new Refusal('forecast-year-required', {}, 'year: the year it is planned in')\n if (!(month >= 1 && month <= 12))\n throw new Refusal('month-out-of-range', { month: merged.month ?? '' }, 'month: must be 1 to 12')\n if (!String(merged.description ?? '').trim())\n throw new Refusal('forecast-description-required', {}, 'description: what is planned')\n Object.assign(row, { year, month, transactionDate: forecastDay(year, month, merged.timing) })\n if (!merged.status) row.status = ForecastStatus.PLANNED\n }\n\n if (\n merged.probability !== undefined &&\n merged.probability !== null &&\n !(merged.probability >= 0 && merged.probability <= 100)\n ) {\n throw new Refusal('probability-out-of-range', {}, 'probability: must be 0 to 100')\n }\n\n return row\n}\n\n/** The payments on these transactions, and the transfers into them, by target kind. */\nasync function paymentsOn(tx: EntityManager, domain: { id: string }, ids: string[]): Promise<Payment[]> {\n if (!ids.length) return []\n const repo = tx.getRepository(Payment)\n return [\n ...(await repo.find({\n where: { domain: { id: domain.id }, transaction: { id: In(ids) } },\n relations: ['transaction']\n })),\n ...(await repo.find({\n where: { domain: { id: domain.id }, targetType: 'Transaction', targetId: In(ids) },\n relations: ['transaction']\n }))\n ]\n}\n\n/**\n * Apply rows to a domain's transactions: `+` (or no id) adds, `M` changes, `-` deletes (softly - a financial row\n * that was deleted stays readable). A transaction that pays something, or is paid, is unlinked before it is\n * deleted; when its money changes, its payments are checked again - the general rule and each target kind's own.\n * A line from a bank statement keeps the statement's facts (§`protectBankFacts`).\n */\nexport async function saveTransactions(\n tx: EntityManager,\n domain: { id: string },\n user: any,\n inputs: TransactionInput[]\n): Promise<Transaction[]> {\n const repo = tx.getRepository(Transaction)\n\n const deletes = [...new Set(inputs.filter(one => one.cuFlag === '-' && one.id).map(one => one.id!))]\n if (deletes.length) {\n const found = await repo.find({ where: { domain: { id: domain.id }, id: In(deletes) } })\n if (found.length !== deletes.length)\n throw new Refusal('transaction-not-found', {}, 'a transaction was not found - reload and try again')\n if ((await paymentsOn(tx, domain, deletes)).length)\n throw new Refusal('transaction-has-payments', {}, 'unlink what the transaction paid before deleting it')\n await repo.softRemove(found)\n }\n\n const saved: Transaction[] = []\n const factsChanged: string[] = []\n for (const input of inputs.filter(one => one.cuFlag !== '-')) {\n const isNew = !input.id || input.cuFlag === '+'\n const current = isNew ? undefined : await repo.findOne({ where: { domain: { id: domain.id }, id: input.id } })\n if (!isNew && !current)\n throw new Refusal('transaction-not-found', {}, 'a transaction was not found - reload and try again')\n\n if (current) protectBankFacts(current, input)\n const row = rowOf(input, current)\n if (row.bankAccount?.id && !('currency' in input)) {\n const account = await tx\n .getRepository(BankAccount)\n .findOne({ where: { domain: { id: domain.id }, id: row.bankAccount.id } })\n if (!account)\n throw new Refusal('bank-account-not-found', {}, 'the bank account was not found - reload and try again')\n if (account.currency) row.currency = account.currency\n }\n\n if (row.bankFingerprint) {\n const twin = await repo.findOne({ where: { domain: { id: domain.id }, bankFingerprint: row.bankFingerprint } })\n if (twin && twin.id !== current?.id)\n throw new Refusal(\n 'bank-line-twice',\n { day: twin.transactionDate ?? '' },\n 'this bank line is already in the books'\n )\n }\n\n const written = await repo.save({ ...(current ?? { domain, creator: user }), ...row, updater: user } as Transaction)\n if (current && FACTS.some(field => field in row && row[field] !== (current as any)[field]))\n factsChanged.push(current.id)\n saved.push(written)\n }\n\n if (factsChanged.length) {\n const payments = await paymentsOn(tx, domain, factsChanged)\n await checkPaymentsWithin(\n tx,\n domain,\n payments.map(one => one.transaction.id)\n )\n const kinds = new Map<string, Payment[]>()\n for (const payment of payments) kinds.set(payment.targetType, [...(kinds.get(payment.targetType) ?? []), payment])\n const touchedOf = (ofKind: Payment[]) => ({\n targetIds: [...new Set(ofKind.map(one => one.targetId))],\n transactionIds: [...new Set(ofKind.map(one => one.transaction.id))]\n })\n for (const [targetType, ofKind] of kinds)\n await paymentTargetOf(targetType)?.validate?.(tx, domain, touchedOf(ofKind))\n for (const rule of paymentRules()) await rule(tx, domain, { transactionIds: [...new Set(payments.map(one => one.transaction.id))] })\n for (const [targetType, ofKind] of kinds)\n await paymentTargetOf(targetType)?.onSaved?.(tx, domain, touchedOf(ofKind))\n }\n\n return saved\n}\n"]}
|