@things-factory/accounting 10.1.159 → 10.1.162
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/service/expenditure-request/expenditure-payment-state.js +3 -2
- package/dist-server/service/expenditure-request/expenditure-payment-state.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-request-service.js +2 -1
- package/dist-server/service/expenditure-request/expenditure-request-service.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-target.js +10 -3
- package/dist-server/service/expenditure-request/expenditure-target.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -0
- package/dist-server/service/index.js +1 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/payment/document-targets.d.ts +0 -2
- package/dist-server/service/payment/document-targets.js +16 -28
- package/dist-server/service/payment/document-targets.js.map +1 -1
- package/dist-server/service/payment/payment-currency.d.ts +11 -0
- package/dist-server/service/payment/payment-currency.js +57 -0
- package/dist-server/service/payment/payment-currency.js.map +1 -0
- package/dist-server/service/payment/payment-service.d.ts +3 -2
- package/dist-server/service/payment/payment-service.js +5 -2
- package/dist-server/service/payment/payment-service.js.map +1 -1
- package/dist-server/service/payment/payment-targets.d.ts +7 -0
- package/dist-server/service/payment/payment-targets.js +2 -0
- package/dist-server/service/payment/payment-targets.js.map +1 -1
- package/dist-server/service/payment/transfer-target.js +8 -2
- package/dist-server/service/payment/transfer-target.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 +2 -2
- package/translations/en.json +5 -5
- package/translations/es.json +4 -4
- package/translations/ko.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.TAX_INVOICE_TARGET = exports.INVOICE_TARGET = void 0;
|
|
4
4
|
exports.documentPaymentsOf = documentPaymentsOf;
|
|
5
5
|
const typeorm_1 = require("typeorm");
|
|
6
6
|
const invoice_1 = require("@things-factory/invoice");
|
|
@@ -8,18 +8,17 @@ const shell_1 = require("@things-factory/shell");
|
|
|
8
8
|
const tax_invoice_1 = require("@things-factory/tax-invoice");
|
|
9
9
|
const transaction_1 = require("../transaction/transaction");
|
|
10
10
|
const payment_1 = require("./payment");
|
|
11
|
+
const payment_currency_1 = require("./payment-currency");
|
|
11
12
|
const payment_targets_1 = require("./payment-targets");
|
|
12
13
|
/*
|
|
13
14
|
* The bank lines that paid a commercial invoice or a tax invoice (ADR-0122). Both documents evidence the money - a
|
|
14
15
|
* contract round may settle it too - so they are EVIDENCE, each kind on its own: a local-L/C export's zero-rated tax
|
|
15
16
|
* invoice and its commercial invoice each evidence the whole deposit. The rules are the same in every application,
|
|
16
|
-
* so accounting registers both kinds; the document modules do not know accounting.
|
|
17
|
+
* so accounting registers both kinds; the document modules do not know accounting. A payment from a line in another
|
|
18
|
+
* currency says what it paid in the document's (§`checkPaymentCurrencies`).
|
|
17
19
|
*/
|
|
18
20
|
exports.INVOICE_TARGET = 'Invoice';
|
|
19
21
|
exports.TAX_INVOICE_TARGET = 'TaxInvoice';
|
|
20
|
-
/** What a payment pays of its document, in the document's currency. */
|
|
21
|
-
const paidOfDocument = (payment) => Number(payment.targetAmount ?? payment.amount);
|
|
22
|
-
exports.paidOfDocument = paidOfDocument;
|
|
23
22
|
/** The payments on some documents of a kind, or of some bank lines, with their line. */
|
|
24
23
|
async function documentPaymentsOf(tx, domain, targetType, of, options = {}) {
|
|
25
24
|
return tx.getRepository(payment_1.Payment).find({
|
|
@@ -33,14 +32,9 @@ async function documentPaymentsOf(tx, domain, targetType, of, options = {}) {
|
|
|
33
32
|
relations: ['transaction', ...(options.relations ?? [])]
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
|
-
const fits = (amount, currency) => {
|
|
37
|
-
const scale = 10 ** (0, invoice_1.currencyDigits)(currency);
|
|
38
|
-
return Math.abs(amount * scale - Math.round(amount * scale)) <= 0.00001;
|
|
39
|
-
};
|
|
40
35
|
/**
|
|
41
36
|
* A document is paid by bank lines on its side - money in for a sale, out for a purchase, the other way for a
|
|
42
|
-
* negative one - and by no more than its total, in its own currency.
|
|
43
|
-
* the document (targetAmount) and on what basis (notes); a line in the same currency says nothing more.
|
|
37
|
+
* negative one - and by no more than its total, in its own currency.
|
|
44
38
|
*/
|
|
45
39
|
async function checkDocuments(tx, domain, targetType, documents, codes) {
|
|
46
40
|
const payments = await documentPaymentsOf(tx, domain, targetType, { documentIds: documents.map(one => one.id) });
|
|
@@ -51,26 +45,17 @@ async function checkDocuments(tx, domain, targetType, documents, codes) {
|
|
|
51
45
|
const line = one.transaction;
|
|
52
46
|
if (line.category !== transaction_1.TransactionCategory.CONFIRMED || line.type !== (incoming ? 'INCOME' : 'EXPENSE'))
|
|
53
47
|
throw new shell_1.Refusal(codes.side, {}, 'choose a deposit for a sale, a withdrawal for a purchase; the other way for a negative document');
|
|
54
|
-
const lineCurrency = line.currency ?? 'KRW';
|
|
55
|
-
if (lineCurrency === document.currency) {
|
|
56
|
-
if (one.targetAmount !== null && one.targetAmount !== undefined)
|
|
57
|
-
throw new shell_1.Refusal('document-payment-same-currency', {}, 'a line in the document currency says its amount once');
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
if (one.targetAmount === null || one.targetAmount === undefined)
|
|
61
|
-
throw new shell_1.Refusal('document-payment-amount-required', { currency: document.currency }, 'record what the line paid in the document currency');
|
|
62
|
-
if (!one.notes?.trim())
|
|
63
|
-
throw new shell_1.Refusal('document-payment-basis-required', {}, 'record the exchange rate or settlement basis in notes');
|
|
64
|
-
}
|
|
65
|
-
if (!fits(Number(one.amount), lineCurrency) || !fits((0, exports.paidOfDocument)(one), document.currency))
|
|
66
|
-
throw new shell_1.Refusal('document-payment-precision', {}, 'an amount has more decimals than its currency');
|
|
67
48
|
}
|
|
68
|
-
if (ofDocument.reduce((sum, one) => sum + (0,
|
|
49
|
+
if (ofDocument.reduce((sum, one) => sum + (0, payment_currency_1.paidOfTarget)(one), 0) > Math.abs(document.total) + 0.00001)
|
|
69
50
|
throw new shell_1.Refusal(codes.over, {}, 'the payments are more than the document');
|
|
70
51
|
}
|
|
71
52
|
}
|
|
72
53
|
(0, payment_targets_1.registerPaymentTarget)(exports.INVOICE_TARGET, {
|
|
73
54
|
purpose: payment_1.PaymentPurpose.EVIDENCE,
|
|
55
|
+
async currencyOf(tx, domain, ids) {
|
|
56
|
+
const invoices = await tx.getRepository(invoice_1.Invoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) }, withDeleted: true });
|
|
57
|
+
return new Map(invoices.map(one => [one.id, one.currency]));
|
|
58
|
+
},
|
|
74
59
|
async label(tx, domain, ids) {
|
|
75
60
|
const invoices = await tx.getRepository(invoice_1.Invoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } });
|
|
76
61
|
return new Map(invoices.map(one => [one.id, [one.number, one.direction === 'SALES' ? one.buyerName : one.supplierName].filter(Boolean).join(' ')]));
|
|
@@ -79,21 +64,24 @@ async function checkDocuments(tx, domain, targetType, documents, codes) {
|
|
|
79
64
|
const invoices = await tx.getRepository(invoice_1.Invoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
80
65
|
if (invoices.length !== new Set(targetIds).size)
|
|
81
66
|
throw new shell_1.Refusal('invoice-not-found', {}, 'the invoice was not found - reload and try again');
|
|
82
|
-
await checkDocuments(tx, domain, exports.INVOICE_TARGET, invoices.map(one => ({ id: one.id,
|
|
67
|
+
await checkDocuments(tx, domain, exports.INVOICE_TARGET, invoices.map(one => ({ id: one.id, total: Number(one.totalAmount), sale: one.direction === 'SALES' })), { side: 'invoice-payment-side', over: 'invoice-payment-over' });
|
|
83
68
|
}
|
|
84
69
|
});
|
|
85
70
|
(0, payment_targets_1.registerPaymentTarget)(exports.TAX_INVOICE_TARGET, {
|
|
86
71
|
purpose: payment_1.PaymentPurpose.EVIDENCE,
|
|
72
|
+
/* A tax invoice is in won. */
|
|
73
|
+
async currencyOf(tx, domain, ids) {
|
|
74
|
+
return new Map(ids.map(id => [id, 'KRW']));
|
|
75
|
+
},
|
|
87
76
|
async label(tx, domain, ids) {
|
|
88
77
|
const invoices = await tx.getRepository(tax_invoice_1.TaxInvoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } });
|
|
89
78
|
return new Map(invoices.map(one => [one.id, [one.issueDate, one.category === tax_invoice_1.Category.EXPORT ? one.buyerBusinessName : one.supplierBusinessName, one.approvalNo].filter(Boolean).join(' ')]));
|
|
90
79
|
},
|
|
91
|
-
/* A tax invoice is in won. */
|
|
92
80
|
async validate(tx, domain, { targetIds }) {
|
|
93
81
|
const invoices = await tx.getRepository(tax_invoice_1.TaxInvoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
94
82
|
if (invoices.length !== new Set(targetIds).size)
|
|
95
83
|
throw new shell_1.Refusal('tax-invoice-not-found', {}, 'the tax invoice was not found - reload and try again');
|
|
96
|
-
await checkDocuments(tx, domain, exports.TAX_INVOICE_TARGET, invoices.map(one => ({ id: one.id,
|
|
84
|
+
await checkDocuments(tx, domain, exports.TAX_INVOICE_TARGET, invoices.map(one => ({ id: one.id, total: Number(one.totalAmount), sale: one.category === tax_invoice_1.Category.EXPORT })), { side: 'tax-invoice-payment-side', over: 'tax-invoice-payment-over' });
|
|
97
85
|
}
|
|
98
86
|
});
|
|
99
87
|
//# sourceMappingURL=document-targets.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-targets.js","sourceRoot":"","sources":["../../../server/service/payment/document-targets.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { currencyDigits, Invoice } from '@things-factory/invoice'\nimport { Refusal } from '@things-factory/shell'\nimport { Category, TaxInvoice } from '@things-factory/tax-invoice'\n\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\nimport { Payment, PaymentPurpose } from './payment'\nimport { registerPaymentTarget } from './payment-targets'\n\n/*\n * The bank lines that paid a commercial invoice or a tax invoice (ADR-0122). Both documents evidence the money - a\n * contract round may settle it too - so they are EVIDENCE, each kind on its own: a local-L/C export's zero-rated tax\n * invoice and its commercial invoice each evidence the whole deposit. The rules are the same in every application,\n * so accounting registers both kinds; the document modules do not know accounting.\n */\n\nexport const INVOICE_TARGET = 'Invoice'\nexport const TAX_INVOICE_TARGET = 'TaxInvoice'\n\n/** What a payment pays of its document, in the document's currency. */\nexport const paidOfDocument = (payment: Payment) => Number(payment.targetAmount ?? payment.amount)\n\n/** The payments on some documents of a kind, or of some bank lines, with their line. */\nexport async function documentPaymentsOf(\n tx: EntityManager,\n domain: { id: string },\n targetType: string,\n of: { documentIds?: string[]; transactionIds?: string[] },\n options: { withDeleted?: boolean; relations?: string[] } = {}\n): Promise<Payment[]> {\n return tx.getRepository(Payment).find({\n where: {\n domain: { id: domain.id },\n targetType,\n ...(of.documentIds ? { targetId: In(of.documentIds) } : {}),\n ...(of.transactionIds ? { transaction: { id: In(of.transactionIds) } } : {})\n } as any,\n withDeleted: options.withDeleted,\n relations: ['transaction', ...(options.relations ?? [])]\n })\n}\n\nconst fits = (amount: number, currency: string) => {\n const scale = 10 ** currencyDigits(currency)\n return Math.abs(amount * scale - Math.round(amount * scale)) <= 0.00001\n}\n\n/** A document as its payments are held to it: its currency, its signed total, and whether its money comes in. */\ninterface PaidDocument {\n id: string\n currency: string\n total: number\n /** A sale's money comes in - a negative (corrective) one's goes back out. */\n sale: boolean\n}\n\n/**\n * A document is paid by bank lines on its side - money in for a sale, out for a purchase, the other way for a\n * negative one - and by no more than its total, in its own currency. A line in another currency says what it paid of\n * the document (targetAmount) and on what basis (notes); a line in the same currency says nothing more.\n */\nasync function checkDocuments(tx: EntityManager, domain: { id: string }, targetType: string, documents: PaidDocument[], codes: { side: string; over: string }) {\n const payments = await documentPaymentsOf(tx, domain, targetType, { documentIds: documents.map(one => one.id) })\n for (const document of documents) {\n const ofDocument = payments.filter(one => one.targetId === document.id)\n const incoming = document.sale === document.total >= 0\n for (const one of ofDocument) {\n const line: Transaction = one.transaction\n if (line.category !== TransactionCategory.CONFIRMED || line.type !== (incoming ? 'INCOME' : 'EXPENSE'))\n throw new Refusal(codes.side, {}, 'choose a deposit for a sale, a withdrawal for a purchase; the other way for a negative document')\n const lineCurrency = line.currency ?? 'KRW'\n if (lineCurrency === document.currency) {\n if (one.targetAmount !== null && one.targetAmount !== undefined)\n throw new Refusal('document-payment-same-currency', {}, 'a line in the document currency says its amount once')\n } else {\n if (one.targetAmount === null || one.targetAmount === undefined)\n throw new Refusal('document-payment-amount-required', { currency: document.currency }, 'record what the line paid in the document currency')\n if (!one.notes?.trim()) throw new Refusal('document-payment-basis-required', {}, 'record the exchange rate or settlement basis in notes')\n }\n if (!fits(Number(one.amount), lineCurrency) || !fits(paidOfDocument(one), document.currency))\n throw new Refusal('document-payment-precision', {}, 'an amount has more decimals than its currency')\n }\n if (ofDocument.reduce((sum, one) => sum + paidOfDocument(one), 0) > Math.abs(document.total) + 0.00001)\n throw new Refusal(codes.over, {}, 'the payments are more than the document')\n }\n}\n\nregisterPaymentTarget(INVOICE_TARGET, {\n purpose: PaymentPurpose.EVIDENCE,\n\n async label(tx, domain, ids) {\n const invoices = await tx.getRepository(Invoice).find({ where: { domain: { id: domain.id }, id: In(ids) } as any })\n return new Map(invoices.map(one => [one.id, [one.number, one.direction === 'SALES' ? one.buyerName : one.supplierName].filter(Boolean).join(' ')]))\n },\n\n async validate(tx, domain, { targetIds }) {\n const invoices = await tx.getRepository(Invoice).find({ where: { domain: { id: domain.id }, id: In(targetIds) } as any })\n if (invoices.length !== new Set(targetIds).size) throw new Refusal('invoice-not-found', {}, 'the invoice was not found - reload and try again')\n await checkDocuments(\n tx,\n domain,\n INVOICE_TARGET,\n invoices.map(one => ({ id: one.id, currency: one.currency, total: Number(one.totalAmount), sale: one.direction === 'SALES' })),\n { side: 'invoice-payment-side', over: 'invoice-payment-over' }\n )\n }\n})\n\nregisterPaymentTarget(TAX_INVOICE_TARGET, {\n purpose: PaymentPurpose.EVIDENCE,\n\n async label(tx, domain, ids) {\n const invoices = await tx.getRepository(TaxInvoice).find({ where: { domain: { id: domain.id }, id: In(ids) } as any })\n return new Map(\n invoices.map(one => [one.id, [one.issueDate, one.category === Category.EXPORT ? one.buyerBusinessName : one.supplierBusinessName, one.approvalNo].filter(Boolean).join(' ')])\n )\n },\n\n /* A tax invoice is in won. */\n async validate(tx, domain, { targetIds }) {\n const invoices = await tx.getRepository(TaxInvoice).find({ where: { domain: { id: domain.id }, id: In(targetIds) } as any })\n if (invoices.length !== new Set(targetIds).size) throw new Refusal('tax-invoice-not-found', {}, 'the tax invoice was not found - reload and try again')\n await checkDocuments(\n tx,\n domain,\n TAX_INVOICE_TARGET,\n invoices.map(one => ({ id: one.id, currency: 'KRW', total: Number(one.totalAmount), sale: one.category === Category.EXPORT })),\n { side: 'tax-invoice-payment-side', over: 'tax-invoice-payment-over' }\n )\n }\n})\n"]}
|
|
1
|
+
{"version":3,"file":"document-targets.js","sourceRoot":"","sources":["../../../server/service/payment/document-targets.ts"],"names":[],"mappings":";;;AAuBA,gDAiBC;AAxCD,qCAA2C;AAE3C,qDAAiD;AACjD,iDAA+C;AAC/C,6DAAkE;AAElE,4DAA6E;AAC7E,uCAAmD;AACnD,yDAAiD;AACjD,uDAAyD;AAEzD;;;;;;GAMG;AAEU,QAAA,cAAc,GAAG,SAAS,CAAA;AAC1B,QAAA,kBAAkB,GAAG,YAAY,CAAA;AAE9C,wFAAwF;AACjF,KAAK,UAAU,kBAAkB,CACtC,EAAiB,EACjB,MAAsB,EACtB,UAAkB,EAClB,EAAyD,EACzD,UAA2D,EAAE;IAE7D,OAAO,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,IAAI,CAAC;QACpC,KAAK,EAAE;YACL,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;YACzB,UAAU;YACV,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAA,YAAE,EAAC,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,GAAG,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE;QACR,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,SAAS,EAAE,CAAC,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;KACzD,CAAC,CAAA;AACJ,CAAC;AAUD;;;GAGG;AACH,KAAK,UAAU,cAAc,CAAC,EAAiB,EAAE,MAAsB,EAAE,UAAkB,EAAE,SAAyB,EAAE,KAAqC;IAC3J,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IAChH,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC,CAAA;QACvE,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAA;QACtD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAgB,GAAG,CAAC,WAAW,CAAA;YACzC,IAAI,IAAI,CAAC,QAAQ,KAAK,iCAAmB,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;gBACpG,MAAM,IAAI,eAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,iGAAiG,CAAC,CAAA;QACxI,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAA,+BAAY,EAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,OAAO;YAClG,MAAM,IAAI,eAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,yCAAyC,CAAC,CAAA;IAChF,CAAC;AACH,CAAC;AAED,IAAA,uCAAqB,EAAC,sBAAc,EAAE;IACpC,OAAO,EAAE,wBAAc,CAAC,QAAQ;IAEhC,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG;QAC9B,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,GAAG,CAAC,EAAS,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;QACtI,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC7D,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG;QACzB,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,GAAG,CAAC,EAAS,EAAE,CAAC,CAAA;QACnH,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACrJ,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE;QACtC,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAS,EAAE,CAAC,CAAA;QACzH,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI;YAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,EAAE,kDAAkD,CAAC,CAAA;QAC/I,MAAM,cAAc,CAClB,EAAE,EACF,MAAM,EACN,sBAAc,EACd,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC,CAAC,EACtG,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAC/D,CAAA;IACH,CAAC;CACF,CAAC,CAAA;AAEF,IAAA,uCAAqB,EAAC,0BAAkB,EAAE;IACxC,OAAO,EAAE,wBAAc,CAAC,QAAQ;IAEhC,8BAA8B;IAC9B,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG;QAC9B,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG;QACzB,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,wBAAU,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,GAAG,CAAC,EAAS,EAAE,CAAC,CAAA;QACtH,OAAO,IAAI,GAAG,CACZ,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,QAAQ,KAAK,sBAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAC9K,CAAA;IACH,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE;QACtC,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,wBAAU,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAS,EAAE,CAAC,CAAA;QAC5H,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI;YAAE,MAAM,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,EAAE,sDAAsD,CAAC,CAAA;QACvJ,MAAM,cAAc,CAClB,EAAE,EACF,MAAM,EACN,0BAAkB,EAClB,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,KAAK,sBAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAC7G,EAAE,IAAI,EAAE,0BAA0B,EAAE,IAAI,EAAE,0BAA0B,EAAE,CACvE,CAAA;IACH,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Invoice } from '@things-factory/invoice'\nimport { Refusal } from '@things-factory/shell'\nimport { Category, TaxInvoice } from '@things-factory/tax-invoice'\n\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\nimport { Payment, PaymentPurpose } from './payment'\nimport { paidOfTarget } from './payment-currency'\nimport { registerPaymentTarget } from './payment-targets'\n\n/*\n * The bank lines that paid a commercial invoice or a tax invoice (ADR-0122). Both documents evidence the money - a\n * contract round may settle it too - so they are EVIDENCE, each kind on its own: a local-L/C export's zero-rated tax\n * invoice and its commercial invoice each evidence the whole deposit. The rules are the same in every application,\n * so accounting registers both kinds; the document modules do not know accounting. A payment from a line in another\n * currency says what it paid in the document's (§`checkPaymentCurrencies`).\n */\n\nexport const INVOICE_TARGET = 'Invoice'\nexport const TAX_INVOICE_TARGET = 'TaxInvoice'\n\n/** The payments on some documents of a kind, or of some bank lines, with their line. */\nexport async function documentPaymentsOf(\n tx: EntityManager,\n domain: { id: string },\n targetType: string,\n of: { documentIds?: string[]; transactionIds?: string[] },\n options: { withDeleted?: boolean; relations?: string[] } = {}\n): Promise<Payment[]> {\n return tx.getRepository(Payment).find({\n where: {\n domain: { id: domain.id },\n targetType,\n ...(of.documentIds ? { targetId: In(of.documentIds) } : {}),\n ...(of.transactionIds ? { transaction: { id: In(of.transactionIds) } } : {})\n } as any,\n withDeleted: options.withDeleted,\n relations: ['transaction', ...(options.relations ?? [])]\n })\n}\n\n/** A document as its payments are held to it: its signed total, and whether its money comes in. */\ninterface PaidDocument {\n id: string\n total: number\n /** A sale's money comes in - a negative (corrective) one's goes back out. */\n sale: boolean\n}\n\n/**\n * A document is paid by bank lines on its side - money in for a sale, out for a purchase, the other way for a\n * negative one - and by no more than its total, in its own currency.\n */\nasync function checkDocuments(tx: EntityManager, domain: { id: string }, targetType: string, documents: PaidDocument[], codes: { side: string; over: string }) {\n const payments = await documentPaymentsOf(tx, domain, targetType, { documentIds: documents.map(one => one.id) })\n for (const document of documents) {\n const ofDocument = payments.filter(one => one.targetId === document.id)\n const incoming = document.sale === document.total >= 0\n for (const one of ofDocument) {\n const line: Transaction = one.transaction\n if (line.category !== TransactionCategory.CONFIRMED || line.type !== (incoming ? 'INCOME' : 'EXPENSE'))\n throw new Refusal(codes.side, {}, 'choose a deposit for a sale, a withdrawal for a purchase; the other way for a negative document')\n }\n if (ofDocument.reduce((sum, one) => sum + paidOfTarget(one), 0) > Math.abs(document.total) + 0.00001)\n throw new Refusal(codes.over, {}, 'the payments are more than the document')\n }\n}\n\nregisterPaymentTarget(INVOICE_TARGET, {\n purpose: PaymentPurpose.EVIDENCE,\n\n async currencyOf(tx, domain, ids) {\n const invoices = await tx.getRepository(Invoice).find({ where: { domain: { id: domain.id }, id: In(ids) } as any, withDeleted: true })\n return new Map(invoices.map(one => [one.id, one.currency]))\n },\n\n async label(tx, domain, ids) {\n const invoices = await tx.getRepository(Invoice).find({ where: { domain: { id: domain.id }, id: In(ids) } as any })\n return new Map(invoices.map(one => [one.id, [one.number, one.direction === 'SALES' ? one.buyerName : one.supplierName].filter(Boolean).join(' ')]))\n },\n\n async validate(tx, domain, { targetIds }) {\n const invoices = await tx.getRepository(Invoice).find({ where: { domain: { id: domain.id }, id: In(targetIds) } as any })\n if (invoices.length !== new Set(targetIds).size) throw new Refusal('invoice-not-found', {}, 'the invoice was not found - reload and try again')\n await checkDocuments(\n tx,\n domain,\n INVOICE_TARGET,\n invoices.map(one => ({ id: one.id, total: Number(one.totalAmount), sale: one.direction === 'SALES' })),\n { side: 'invoice-payment-side', over: 'invoice-payment-over' }\n )\n }\n})\n\nregisterPaymentTarget(TAX_INVOICE_TARGET, {\n purpose: PaymentPurpose.EVIDENCE,\n\n /* A tax invoice is in won. */\n async currencyOf(tx, domain, ids) {\n return new Map(ids.map(id => [id, 'KRW']))\n },\n\n async label(tx, domain, ids) {\n const invoices = await tx.getRepository(TaxInvoice).find({ where: { domain: { id: domain.id }, id: In(ids) } as any })\n return new Map(\n invoices.map(one => [one.id, [one.issueDate, one.category === Category.EXPORT ? one.buyerBusinessName : one.supplierBusinessName, one.approvalNo].filter(Boolean).join(' ')])\n )\n },\n\n async validate(tx, domain, { targetIds }) {\n const invoices = await tx.getRepository(TaxInvoice).find({ where: { domain: { id: domain.id }, id: In(targetIds) } as any })\n if (invoices.length !== new Set(targetIds).size) throw new Refusal('tax-invoice-not-found', {}, 'the tax invoice was not found - reload and try again')\n await checkDocuments(\n tx,\n domain,\n TAX_INVOICE_TARGET,\n invoices.map(one => ({ id: one.id, total: Number(one.totalAmount), sale: one.category === Category.EXPORT })),\n { side: 'tax-invoice-payment-side', over: 'tax-invoice-payment-over' }\n )\n }\n})\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Payment } from './payment';
|
|
3
|
+
/** What a payment paid of its target, in the target's currency - the one reader of it. */
|
|
4
|
+
export declare const paidOfTarget: (payment: Pick<Payment, "amount" | "targetAmount">) => number;
|
|
5
|
+
/**
|
|
6
|
+
* The payments of some transactions against the currencies of what they paid. A target its kind does not know is left
|
|
7
|
+
* to the kind's own rules, which refuse it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkPaymentCurrencies(tx: EntityManager, domain: {
|
|
10
|
+
id: string;
|
|
11
|
+
}, transactionIds: string[]): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paidOfTarget = void 0;
|
|
4
|
+
exports.checkPaymentCurrencies = checkPaymentCurrencies;
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const invoice_1 = require("@things-factory/invoice");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const payment_1 = require("./payment");
|
|
9
|
+
const payment_targets_1 = require("./payment-targets");
|
|
10
|
+
/*
|
|
11
|
+
* A payment and the currencies it is told in. Its amount is in its transaction's currency. A target is kept in its
|
|
12
|
+
* own currency, which its kind declares (§`PaymentTarget.currencyOf`); a payment on a target in another currency says
|
|
13
|
+
* what it paid of the target in the target's currency (`targetAmount`) and on what basis (notes). One rule for every
|
|
14
|
+
* kind of target - a document, an expenditure request, a contract round, the other line of a transfer.
|
|
15
|
+
*/
|
|
16
|
+
/** What a payment paid of its target, in the target's currency - the one reader of it. */
|
|
17
|
+
const paidOfTarget = (payment) => Number(payment.targetAmount ?? payment.amount);
|
|
18
|
+
exports.paidOfTarget = paidOfTarget;
|
|
19
|
+
const fits = (amount, currency) => {
|
|
20
|
+
const scale = 10 ** (0, invoice_1.currencyDigits)(currency);
|
|
21
|
+
return Math.abs(amount * scale - Math.round(amount * scale)) <= 0.00001;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The payments of some transactions against the currencies of what they paid. A target its kind does not know is left
|
|
25
|
+
* to the kind's own rules, which refuse it.
|
|
26
|
+
*/
|
|
27
|
+
async function checkPaymentCurrencies(tx, domain, transactionIds) {
|
|
28
|
+
const ids = [...new Set(transactionIds)];
|
|
29
|
+
if (!ids.length)
|
|
30
|
+
return;
|
|
31
|
+
const payments = await tx.getRepository(payment_1.Payment).find({ where: { domain: { id: domain.id }, transaction: { id: (0, typeorm_1.In)(ids) } }, relations: ['transaction'] });
|
|
32
|
+
const kinds = new Map();
|
|
33
|
+
for (const one of payments)
|
|
34
|
+
kinds.set(one.targetType, [...(kinds.get(one.targetType) ?? []), one]);
|
|
35
|
+
for (const [targetType, ofKind] of kinds) {
|
|
36
|
+
const currencies = await (0, payment_targets_1.paymentTargetOf)(targetType).currencyOf(tx, domain, [...new Set(ofKind.map(one => one.targetId))]);
|
|
37
|
+
for (const one of ofKind) {
|
|
38
|
+
const targetCurrency = currencies.get(one.targetId);
|
|
39
|
+
if (!targetCurrency)
|
|
40
|
+
continue;
|
|
41
|
+
const lineCurrency = one.transaction.currency ?? 'KRW';
|
|
42
|
+
if (lineCurrency === targetCurrency) {
|
|
43
|
+
if (one.targetAmount !== null && one.targetAmount !== undefined)
|
|
44
|
+
throw new shell_1.Refusal('payment-same-currency', { currency: targetCurrency }, 'a line in the currency of what it paid says its amount once');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
if (one.targetAmount === null || one.targetAmount === undefined)
|
|
48
|
+
throw new shell_1.Refusal('payment-target-amount-required', { lineCurrency, currency: targetCurrency }, `the line is in ${lineCurrency} - record what it paid in ${targetCurrency}`);
|
|
49
|
+
if (!one.notes?.trim())
|
|
50
|
+
throw new shell_1.Refusal('payment-basis-required', {}, 'record the exchange rate or settlement basis in notes');
|
|
51
|
+
}
|
|
52
|
+
if (!fits(Number(one.amount), lineCurrency) || !fits((0, exports.paidOfTarget)(one), targetCurrency))
|
|
53
|
+
throw new shell_1.Refusal('payment-precision', {}, 'an amount has more decimals than its currency');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=payment-currency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-currency.js","sourceRoot":"","sources":["../../../server/service/payment/payment-currency.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { currencyDigits } from '@things-factory/invoice'\nimport { Refusal } from '@things-factory/shell'\n\nimport { Payment } from './payment'\nimport { paymentTargetOf } from './payment-targets'\n\n/*\n * A payment and the currencies it is told in. Its amount is in its transaction's currency. A target is kept in its\n * own currency, which its kind declares (§`PaymentTarget.currencyOf`); a payment on a target in another currency says\n * what it paid of the target in the target's currency (`targetAmount`) and on what basis (notes). One rule for every\n * kind of target - a document, an expenditure request, a contract round, the other line of a transfer.\n */\n\n/** What a payment paid of its target, in the target's currency - the one reader of it. */\nexport const paidOfTarget = (payment: Pick<Payment, 'amount' | 'targetAmount'>) => Number(payment.targetAmount ?? payment.amount)\n\nconst fits = (amount: number, currency: string) => {\n const scale = 10 ** currencyDigits(currency)\n return Math.abs(amount * scale - Math.round(amount * scale)) <= 0.00001\n}\n\n/**\n * The payments of some transactions against the currencies of what they paid. A target its kind does not know is left\n * to the kind's own rules, which refuse it.\n */\nexport async function checkPaymentCurrencies(tx: EntityManager, domain: { id: string }, transactionIds: string[]): Promise<void> {\n const ids = [...new Set(transactionIds)]\n if (!ids.length) return\n\n const payments = await tx.getRepository(Payment).find({ where: { domain: { id: domain.id }, transaction: { id: In(ids) } } as any, relations: ['transaction'] })\n const kinds = new Map<string, Payment[]>()\n for (const one of payments) kinds.set(one.targetType, [...(kinds.get(one.targetType) ?? []), one])\n\n for (const [targetType, ofKind] of kinds) {\n const currencies = await paymentTargetOf(targetType)!.currencyOf(tx, domain, [...new Set(ofKind.map(one => one.targetId))])\n for (const one of ofKind) {\n const targetCurrency = currencies.get(one.targetId)\n if (!targetCurrency) continue\n const lineCurrency = one.transaction.currency ?? 'KRW'\n if (lineCurrency === targetCurrency) {\n if (one.targetAmount !== null && one.targetAmount !== undefined)\n throw new Refusal('payment-same-currency', { currency: targetCurrency }, 'a line in the currency of what it paid says its amount once')\n } else {\n if (one.targetAmount === null || one.targetAmount === undefined)\n throw new Refusal(\n 'payment-target-amount-required',\n { lineCurrency, currency: targetCurrency },\n `the line is in ${lineCurrency} - record what it paid in ${targetCurrency}`\n )\n if (!one.notes?.trim()) throw new Refusal('payment-basis-required', {}, 'record the exchange rate or settlement basis in notes')\n }\n if (!fits(Number(one.amount), lineCurrency) || !fits(paidOfTarget(one), targetCurrency))\n throw new Refusal('payment-precision', {}, 'an amount has more decimals than its currency')\n }\n }\n}\n"]}
|
|
@@ -10,8 +10,9 @@ export declare function checkPaymentsWithin(tx: EntityManager, domain: {
|
|
|
10
10
|
id: string;
|
|
11
11
|
}, transactionIds: string[]): Promise<void>;
|
|
12
12
|
/**
|
|
13
|
-
* Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general
|
|
14
|
-
* (§`checkPaymentsWithin`) and each touched kind's own rules run, in the same database
|
|
13
|
+
* Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general rules
|
|
14
|
+
* (§`checkPaymentsWithin`, §`checkPaymentCurrencies`) and each touched kind's own rules run, in the same database
|
|
15
|
+
* transaction.
|
|
15
16
|
*/
|
|
16
17
|
export declare function savePayments(tx: EntityManager, domain: {
|
|
17
18
|
id: string;
|
|
@@ -6,6 +6,7 @@ const typeorm_1 = require("typeorm");
|
|
|
6
6
|
const shell_1 = require("@things-factory/shell");
|
|
7
7
|
const transaction_1 = require("../transaction/transaction");
|
|
8
8
|
const payment_1 = require("./payment");
|
|
9
|
+
const payment_currency_1 = require("./payment-currency");
|
|
9
10
|
const payment_targets_1 = require("./payment-targets");
|
|
10
11
|
/** Amounts are decimals; a sum within half a cent of the transaction is the transaction. */
|
|
11
12
|
const CENT = 0.005;
|
|
@@ -83,8 +84,9 @@ function rowOf(patch, isNew) {
|
|
|
83
84
|
return row;
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
|
-
* Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general
|
|
87
|
-
* (§`checkPaymentsWithin`) and each touched kind's own rules run, in the same database
|
|
87
|
+
* Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general rules
|
|
88
|
+
* (§`checkPaymentsWithin`, §`checkPaymentCurrencies`) and each touched kind's own rules run, in the same database
|
|
89
|
+
* transaction.
|
|
88
90
|
*/
|
|
89
91
|
async function savePayments(tx, domain, user, patches) {
|
|
90
92
|
const repo = tx.getRepository(payment_1.Payment);
|
|
@@ -138,6 +140,7 @@ async function savePayments(tx, domain, user, patches) {
|
|
|
138
140
|
saved.push(written);
|
|
139
141
|
}
|
|
140
142
|
await checkPaymentsWithin(tx, domain, [...touched.values()].flatMap(kind => [...kind.transactionIds]));
|
|
143
|
+
await (0, payment_currency_1.checkPaymentCurrencies)(tx, domain, [...touched.values()].flatMap(kind => [...kind.transactionIds]));
|
|
141
144
|
const touchedOf = (kind) => ({
|
|
142
145
|
targetIds: [...kind.targetIds],
|
|
143
146
|
transactionIds: [...kind.transactionIds]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-service.js","sourceRoot":"","sources":["../../../server/service/payment/payment-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { Transaction } from '../transaction/transaction'\nimport { Payment, PaymentMethod, PaymentPurpose } from './payment'\nimport { paymentRules, paymentTargetOf } from './payment-targets'\nimport { PaymentPatch } from './payment-type'\n\n/** Amounts are decimals; a sum within half a cent of the transaction is the transaction. */\nconst CENT = 0.005\n\n/**\n * Within one purpose a transaction pays no more than itself - and for evidence, within each kind of document: the\n * same money is evidenced in full by a tax invoice and by a commercial invoice alike, each telling it its own way.\n * Run after its payments or its amount change.\n */\nexport async function checkPaymentsWithin(\n tx: EntityManager,\n domain: { id: string },\n transactionIds: string[]\n): Promise<void> {\n const ids = [...new Set(transactionIds)]\n if (!ids.length) return\n\n const transactions = await tx\n .getRepository(Transaction)\n .find({ where: { domain: { id: domain.id }, id: In(ids) }, select: ['id', 'amount'] })\n const payments = await tx\n .getRepository(Payment)\n .find({ where: { domain: { id: domain.id }, transaction: { id: In(ids) } }, relations: ['transaction'] })\n const paid = new Map<string, number>()\n for (const payment of payments) {\n const layer = payment.purpose === PaymentPurpose.EVIDENCE ? `${payment.purpose}:${payment.targetType}` : payment.purpose\n const key = `${payment.transaction.id}|${layer}`\n paid.set(key, (paid.get(key) ?? 0) + Number(payment.amount))\n }\n for (const transaction of transactions) {\n for (const [key, sum] of paid) {\n const [id, purpose] = key.split('|')\n if (id !== transaction.id) continue\n const amount = Math.abs(Number(transaction.amount))\n if (sum > amount + CENT) {\n throw new Refusal(\n 'payment-over-transaction',\n { purpose, paid: sum, amount },\n `${purpose}: ${sum} put on a transaction of ${amount}`\n )\n }\n }\n }\n}\n\n/** A patch as the row it writes: the target kind known, the amount more than zero, the purpose the kind's. */\nfunction rowOf(patch: PaymentPatch, isNew: boolean): Partial<Payment> & { transactionId?: string } {\n const row: Record<string, any> = {}\n const transactionId = patch.transaction?.id\n if (isNew && !transactionId)\n throw new Refusal('payment-transaction-required', {}, 'a payment needs the transaction it is a part of')\n if (transactionId) row.transactionId = transactionId\n\n if (patch.targetType !== undefined || isNew) {\n const target = paymentTargetOf(String(patch.targetType ?? ''))\n if (!target)\n throw new Refusal(\n 'payment-target-unknown',\n { targetType: patch.targetType },\n `'${patch.targetType}' is not a kind of target any module registered`\n )\n row.targetType = patch.targetType\n row.purpose = target.purpose\n }\n if (patch.targetId !== undefined || isNew) {\n if (!patch.targetId) throw new Refusal('payment-target-required', {}, 'a payment needs what it paid')\n row.targetId = patch.targetId\n }\n if (patch.amount !== undefined || isNew) {\n const amount = Number(patch.amount)\n if (!(amount > 0)) throw new Refusal('payment-amount-not-positive', {}, 'amount: must be more than zero')\n row.amount = amount\n }\n if (patch.targetAmount !== undefined) {\n const targetAmount = patch.targetAmount === null ? null : Number(patch.targetAmount)\n if (targetAmount !== null && !(targetAmount > 0))\n throw new Refusal('payment-amount-not-positive', {}, 'targetAmount: must be more than zero')\n row.targetAmount = targetAmount\n }\n if (patch.method !== undefined) row.method = patch.method ?? PaymentMethod.PERSON\n if (patch.basis !== undefined) row.basis = patch.basis || null\n if (patch.notes !== undefined) row.notes = patch.notes || null\n return row\n}\n\n/**\n * Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general rule\n * (§`checkPaymentsWithin`) and each touched kind's own rules run, in the same database transaction.\n */\nexport async function savePayments(\n tx: EntityManager,\n domain: { id: string },\n user: any,\n patches: PaymentPatch[]\n): Promise<Payment[]> {\n const repo = tx.getRepository(Payment)\n const transactions = tx.getRepository(Transaction)\n const touched = new Map<string, { targetIds: Set<string>; transactionIds: Set<string> }>()\n const touch = (payment: { targetType: string; targetId: string; transactionId?: string }) => {\n const kind = touched.get(payment.targetType) ?? { targetIds: new Set<string>(), transactionIds: new Set<string>() }\n kind.targetIds.add(payment.targetId)\n if (payment.transactionId) kind.transactionIds.add(payment.transactionId)\n touched.set(payment.targetType, kind)\n }\n\n const deletes = patches.filter(one => one.cuFlag === '-' && one.id).map(one => one.id!)\n if (deletes.length) {\n const owned = await repo.find({ where: { domain: { id: domain.id }, id: In(deletes) }, relations: ['transaction'] })\n if (owned.length !== new Set(deletes).size)\n throw new Refusal('payment-not-found', {}, 'a payment was not found - reload and try again')\n for (const one of owned) touch({ ...one, transactionId: one.transaction.id })\n await repo.softDelete({ id: In(deletes) })\n }\n\n const saved: Payment[] = []\n for (const patch of patches.filter(one => one.cuFlag !== '-')) {\n const isNew = !patch.id || patch.cuFlag === '+'\n const { transactionId, ...row } = rowOf(patch, isNew)\n const current = isNew\n ? undefined\n : await repo.findOne({ where: { domain: { id: domain.id }, id: patch.id }, relations: ['transaction'] })\n if (!isNew && !current) throw new Refusal('payment-not-found', {}, 'a payment was not found - reload and try again')\n\n const transactionOf = transactionId ?? current!.transaction.id\n const transaction = await transactions.findOne({ where: { domain: { id: domain.id }, id: transactionOf } })\n if (!transaction)\n throw new Refusal('payment-transaction-not-found', {}, 'the transaction was not found - reload and try again')\n\n const targetType = row.targetType ?? current!.targetType\n const targetId = row.targetId ?? current!.targetId\n const twin = await repo.findOne({\n where: { domain: { id: domain.id }, transaction: { id: transaction.id }, targetType, targetId }\n })\n if (twin && twin.id !== current?.id)\n throw new Refusal(\n 'payment-twice',\n { targetType, targetId },\n 'the transaction already pays this - change that payment instead'\n )\n\n if (current) touch({ ...current, transactionId: current.transaction.id })\n const written = await repo.save({\n ...(current ?? { domain, creator: user }),\n ...row,\n transaction,\n updater: user\n } as Payment)\n touch({ targetType: written.targetType, targetId: written.targetId, transactionId: transaction.id })\n saved.push(written)\n }\n\n await checkPaymentsWithin(\n tx,\n domain,\n [...touched.values()].flatMap(kind => [...kind.transactionIds])\n )\n const touchedOf = (kind: { targetIds: Set<string>; transactionIds: Set<string> }) => ({\n targetIds: [...kind.targetIds],\n transactionIds: [...kind.transactionIds]\n })\n for (const [targetType, kind] of touched) await paymentTargetOf(targetType)?.validate?.(tx, domain, touchedOf(kind))\n const transactionIds = [...new Set([...touched.values()].flatMap(kind => [...kind.transactionIds]))]\n for (const rule of paymentRules()) await rule(tx, domain, { transactionIds })\n for (const [targetType, kind] of touched) await paymentTargetOf(targetType)?.onSaved?.(tx, domain, touchedOf(kind))\n\n return saved\n}\n"]}
|
|
1
|
+
{"version":3,"file":"payment-service.js","sourceRoot":"","sources":["../../../server/service/payment/payment-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { Transaction } from '../transaction/transaction'\nimport { Payment, PaymentMethod, PaymentPurpose } from './payment'\nimport { checkPaymentCurrencies } from './payment-currency'\nimport { paymentRules, paymentTargetOf } from './payment-targets'\nimport { PaymentPatch } from './payment-type'\n\n/** Amounts are decimals; a sum within half a cent of the transaction is the transaction. */\nconst CENT = 0.005\n\n/**\n * Within one purpose a transaction pays no more than itself - and for evidence, within each kind of document: the\n * same money is evidenced in full by a tax invoice and by a commercial invoice alike, each telling it its own way.\n * Run after its payments or its amount change.\n */\nexport async function checkPaymentsWithin(\n tx: EntityManager,\n domain: { id: string },\n transactionIds: string[]\n): Promise<void> {\n const ids = [...new Set(transactionIds)]\n if (!ids.length) return\n\n const transactions = await tx\n .getRepository(Transaction)\n .find({ where: { domain: { id: domain.id }, id: In(ids) }, select: ['id', 'amount'] })\n const payments = await tx\n .getRepository(Payment)\n .find({ where: { domain: { id: domain.id }, transaction: { id: In(ids) } }, relations: ['transaction'] })\n const paid = new Map<string, number>()\n for (const payment of payments) {\n const layer = payment.purpose === PaymentPurpose.EVIDENCE ? `${payment.purpose}:${payment.targetType}` : payment.purpose\n const key = `${payment.transaction.id}|${layer}`\n paid.set(key, (paid.get(key) ?? 0) + Number(payment.amount))\n }\n for (const transaction of transactions) {\n for (const [key, sum] of paid) {\n const [id, purpose] = key.split('|')\n if (id !== transaction.id) continue\n const amount = Math.abs(Number(transaction.amount))\n if (sum > amount + CENT) {\n throw new Refusal(\n 'payment-over-transaction',\n { purpose, paid: sum, amount },\n `${purpose}: ${sum} put on a transaction of ${amount}`\n )\n }\n }\n }\n}\n\n/** A patch as the row it writes: the target kind known, the amount more than zero, the purpose the kind's. */\nfunction rowOf(patch: PaymentPatch, isNew: boolean): Partial<Payment> & { transactionId?: string } {\n const row: Record<string, any> = {}\n const transactionId = patch.transaction?.id\n if (isNew && !transactionId)\n throw new Refusal('payment-transaction-required', {}, 'a payment needs the transaction it is a part of')\n if (transactionId) row.transactionId = transactionId\n\n if (patch.targetType !== undefined || isNew) {\n const target = paymentTargetOf(String(patch.targetType ?? ''))\n if (!target)\n throw new Refusal(\n 'payment-target-unknown',\n { targetType: patch.targetType },\n `'${patch.targetType}' is not a kind of target any module registered`\n )\n row.targetType = patch.targetType\n row.purpose = target.purpose\n }\n if (patch.targetId !== undefined || isNew) {\n if (!patch.targetId) throw new Refusal('payment-target-required', {}, 'a payment needs what it paid')\n row.targetId = patch.targetId\n }\n if (patch.amount !== undefined || isNew) {\n const amount = Number(patch.amount)\n if (!(amount > 0)) throw new Refusal('payment-amount-not-positive', {}, 'amount: must be more than zero')\n row.amount = amount\n }\n if (patch.targetAmount !== undefined) {\n const targetAmount = patch.targetAmount === null ? null : Number(patch.targetAmount)\n if (targetAmount !== null && !(targetAmount > 0))\n throw new Refusal('payment-amount-not-positive', {}, 'targetAmount: must be more than zero')\n row.targetAmount = targetAmount\n }\n if (patch.method !== undefined) row.method = patch.method ?? PaymentMethod.PERSON\n if (patch.basis !== undefined) row.basis = patch.basis || null\n if (patch.notes !== undefined) row.notes = patch.notes || null\n return row\n}\n\n/**\n * Apply rows to a domain's payments: `+` (or no id) adds, `M` changes, `-` deletes (softly). Then the general rules\n * (§`checkPaymentsWithin`, §`checkPaymentCurrencies`) and each touched kind's own rules run, in the same database\n * transaction.\n */\nexport async function savePayments(\n tx: EntityManager,\n domain: { id: string },\n user: any,\n patches: PaymentPatch[]\n): Promise<Payment[]> {\n const repo = tx.getRepository(Payment)\n const transactions = tx.getRepository(Transaction)\n const touched = new Map<string, { targetIds: Set<string>; transactionIds: Set<string> }>()\n const touch = (payment: { targetType: string; targetId: string; transactionId?: string }) => {\n const kind = touched.get(payment.targetType) ?? { targetIds: new Set<string>(), transactionIds: new Set<string>() }\n kind.targetIds.add(payment.targetId)\n if (payment.transactionId) kind.transactionIds.add(payment.transactionId)\n touched.set(payment.targetType, kind)\n }\n\n const deletes = patches.filter(one => one.cuFlag === '-' && one.id).map(one => one.id!)\n if (deletes.length) {\n const owned = await repo.find({ where: { domain: { id: domain.id }, id: In(deletes) }, relations: ['transaction'] })\n if (owned.length !== new Set(deletes).size)\n throw new Refusal('payment-not-found', {}, 'a payment was not found - reload and try again')\n for (const one of owned) touch({ ...one, transactionId: one.transaction.id })\n await repo.softDelete({ id: In(deletes) })\n }\n\n const saved: Payment[] = []\n for (const patch of patches.filter(one => one.cuFlag !== '-')) {\n const isNew = !patch.id || patch.cuFlag === '+'\n const { transactionId, ...row } = rowOf(patch, isNew)\n const current = isNew\n ? undefined\n : await repo.findOne({ where: { domain: { id: domain.id }, id: patch.id }, relations: ['transaction'] })\n if (!isNew && !current) throw new Refusal('payment-not-found', {}, 'a payment was not found - reload and try again')\n\n const transactionOf = transactionId ?? current!.transaction.id\n const transaction = await transactions.findOne({ where: { domain: { id: domain.id }, id: transactionOf } })\n if (!transaction)\n throw new Refusal('payment-transaction-not-found', {}, 'the transaction was not found - reload and try again')\n\n const targetType = row.targetType ?? current!.targetType\n const targetId = row.targetId ?? current!.targetId\n const twin = await repo.findOne({\n where: { domain: { id: domain.id }, transaction: { id: transaction.id }, targetType, targetId }\n })\n if (twin && twin.id !== current?.id)\n throw new Refusal(\n 'payment-twice',\n { targetType, targetId },\n 'the transaction already pays this - change that payment instead'\n )\n\n if (current) touch({ ...current, transactionId: current.transaction.id })\n const written = await repo.save({\n ...(current ?? { domain, creator: user }),\n ...row,\n transaction,\n updater: user\n } as Payment)\n touch({ targetType: written.targetType, targetId: written.targetId, transactionId: transaction.id })\n saved.push(written)\n }\n\n await checkPaymentsWithin(\n tx,\n domain,\n [...touched.values()].flatMap(kind => [...kind.transactionIds])\n )\n await checkPaymentCurrencies(\n tx,\n domain,\n [...touched.values()].flatMap(kind => [...kind.transactionIds])\n )\n const touchedOf = (kind: { targetIds: Set<string>; transactionIds: Set<string> }) => ({\n targetIds: [...kind.targetIds],\n transactionIds: [...kind.transactionIds]\n })\n for (const [targetType, kind] of touched) await paymentTargetOf(targetType)?.validate?.(tx, domain, touchedOf(kind))\n const transactionIds = [...new Set([...touched.values()].flatMap(kind => [...kind.transactionIds]))]\n for (const rule of paymentRules()) await rule(tx, domain, { transactionIds })\n for (const [targetType, kind] of touched) await paymentTargetOf(targetType)?.onSaved?.(tx, domain, touchedOf(kind))\n\n return saved\n}\n"]}
|
|
@@ -10,6 +10,13 @@ import { PaymentPurpose } from './payment';
|
|
|
10
10
|
export interface PaymentTarget {
|
|
11
11
|
/** What a payment on this kind tells of its transaction. */
|
|
12
12
|
purpose: PaymentPurpose;
|
|
13
|
+
/**
|
|
14
|
+
* The currency each target is kept in. A payment from a line in another currency says what it paid of the target in
|
|
15
|
+
* this currency (§`checkPaymentCurrencies`). Every kind declares it - none is taken to be in won.
|
|
16
|
+
*/
|
|
17
|
+
currencyOf: (tx: EntityManager, domain: {
|
|
18
|
+
id: string;
|
|
19
|
+
}, targetIds: string[]) => Promise<Map<string, string>>;
|
|
13
20
|
/** What people call each target - 「H2026021 3차」 - for the screens that list what a transaction paid. */
|
|
14
21
|
label?: (tx: EntityManager, domain: {
|
|
15
22
|
id: string;
|
|
@@ -10,6 +10,8 @@ const targets = new Map();
|
|
|
10
10
|
function registerPaymentTarget(targetType, target) {
|
|
11
11
|
if (targets.has(targetType))
|
|
12
12
|
throw new Error(`payment target '${targetType}' is registered twice`);
|
|
13
|
+
if (typeof target.currencyOf !== 'function')
|
|
14
|
+
throw new Error(`payment target '${targetType}' does not say the currency of its targets`);
|
|
13
15
|
targets.set(targetType, target);
|
|
14
16
|
}
|
|
15
17
|
function paymentTargetOf(targetType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-targets.js","sourceRoot":"","sources":["../../../server/service/payment/payment-targets.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"payment-targets.js","sourceRoot":"","sources":["../../../server/service/payment/payment-targets.ts"],"names":[],"mappings":";;AA4CA,sDAIC;AAED,0CAEC;AAWD,kDAEC;AAED,oCAEC;AAED,4DAEC;AAhCD,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,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,UAAU,4CAA4C,CAAC,CAAA;IACvI,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 /**\n * The currency each target is kept in. A payment from a line in another currency says what it paid of the target in\n * this currency (§`checkPaymentCurrencies`). Every kind declares it - none is taken to be in won.\n */\n currencyOf: (tx: EntityManager, domain: { id: string }, targetIds: string[]) => Promise<Map<string, string>>\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 if (typeof target.currencyOf !== 'function') throw new Error(`payment target '${targetType}' does not say the currency of its targets`)\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"]}
|
|
@@ -5,13 +5,19 @@ const shell_1 = require("@things-factory/shell");
|
|
|
5
5
|
const cash_flow_type_1 = require("../cash-flow-type");
|
|
6
6
|
const transaction_1 = require("../transaction/transaction");
|
|
7
7
|
const payment_1 = require("./payment");
|
|
8
|
+
const payment_currency_1 = require("./payment-currency");
|
|
8
9
|
const payment_targets_1 = require("./payment-targets");
|
|
9
10
|
/**
|
|
10
11
|
* A move between our own accounts is two bank lines: the withdrawal pays the deposit. The payment sits on the
|
|
11
|
-
* withdrawal and names the deposit; a deposit takes no more than itself from the withdrawals that name it.
|
|
12
|
+
* withdrawal and names the deposit; a deposit takes no more than itself from the withdrawals that name it. A
|
|
13
|
+
* withdrawal in another currency - an exchange - says what it put into the deposit in the deposit's currency.
|
|
12
14
|
*/
|
|
13
15
|
(0, payment_targets_1.registerPaymentTarget)('Transaction', {
|
|
14
16
|
purpose: payment_1.PaymentPurpose.TRANSFER,
|
|
17
|
+
async currencyOf(tx, domain, targetIds) {
|
|
18
|
+
const lines = await tx.getRepository(transaction_1.Transaction).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
19
|
+
return new Map(lines.map(line => [line.id, line.currency ?? 'KRW']));
|
|
20
|
+
},
|
|
15
21
|
async label(tx, domain, targetIds) {
|
|
16
22
|
const lines = await tx
|
|
17
23
|
.getRepository(transaction_1.Transaction)
|
|
@@ -34,7 +40,7 @@ const payment_targets_1 = require("./payment-targets");
|
|
|
34
40
|
for (const deposit of deposits) {
|
|
35
41
|
const taken = payments
|
|
36
42
|
.filter(one => one.targetId === deposit.id)
|
|
37
|
-
.reduce((sum, one) => sum +
|
|
43
|
+
.reduce((sum, one) => sum + (0, payment_currency_1.paidOfTarget)(one), 0);
|
|
38
44
|
if (taken > Math.abs(Number(deposit.amount)) + 0.005)
|
|
39
45
|
throw new shell_1.Refusal('transfer-over-deposit', { taken, amount: deposit.amount }, `${taken} moved into a deposit of ${deposit.amount}`);
|
|
40
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfer-target.js","sourceRoot":"","sources":["../../../server/service/payment/transfer-target.ts"],"names":[],"mappings":";;AAAA,qCAA4B;AAE5B,iDAA+C;AAE/C,sDAAgD;AAChD,4DAAwD;AACxD,uCAAmD;AACnD,uDAAyD;AAEzD
|
|
1
|
+
{"version":3,"file":"transfer-target.js","sourceRoot":"","sources":["../../../server/service/payment/transfer-target.ts"],"names":[],"mappings":";;AAAA,qCAA4B;AAE5B,iDAA+C;AAE/C,sDAAgD;AAChD,4DAAwD;AACxD,uCAAmD;AACnD,yDAAiD;AACjD,uDAAyD;AAEzD;;;;GAIG;AACH,IAAA,uCAAqB,EAAC,aAAa,EAAE;IACnC,OAAO,EAAE,wBAAc,CAAC,QAAQ;IAChC,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS;QACpC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,yBAAW,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAA;QACnH,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,CAAA;IACtE,CAAC;IACD,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS;QAC/B,MAAM,KAAK,GAAG,MAAM,EAAE;aACnB,aAAa,CAAC,yBAAW,CAAC;aAC1B,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAChG,OAAO,IAAI,GAAG,CACZ,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CACvG,CAAA;IACH,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE;QACtD,MAAM,IAAI,GAAG,EAAE,CAAC,aAAa,CAAC,yBAAW,CAAC,CAAA;QAC1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAA;QAC7F,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI;YAC7C,MAAM,IAAI,eAAO,CAAC,2BAA2B,EAAE,EAAE,EAAE,8CAA8C,CAAC,CAAA;QACpG,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,6BAAY,CAAC,MAAM,CAAC;YAC1D,MAAM,IAAI,eAAO,CAAC,6BAA6B,EAAE,EAAE,EAAE,yDAAyD,CAAC,CAAA;QAEjH,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,cAAc,CAAC,EAAE,EAAE,CAAC,CAAA;QACrG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,6BAAY,CAAC,OAAO,CAAC;YAC9D,MAAM,IAAI,eAAO,CAAC,8BAA8B,EAAE,EAAE,EAAE,qCAAqC,CAAC,CAAA;QAE9F,MAAM,QAAQ,GAAG,MAAM,EAAE;aACtB,aAAa,CAAC,iBAAO,CAAC;aACtB,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAA;QACrG,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,QAAQ;iBACnB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;iBAC1C,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAA,+BAAY,EAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACnD,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK;gBAClD,MAAM,IAAI,eAAO,CACf,uBAAuB,EACvB,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EACjC,GAAG,KAAK,4BAA4B,OAAO,CAAC,MAAM,EAAE,CACrD,CAAA;QACL,CAAC;IACH,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { CashFlowType } from '../cash-flow-type'\nimport { Transaction } from '../transaction/transaction'\nimport { Payment, PaymentPurpose } from './payment'\nimport { paidOfTarget } from './payment-currency'\nimport { registerPaymentTarget } from './payment-targets'\n\n/**\n * A move between our own accounts is two bank lines: the withdrawal pays the deposit. The payment sits on the\n * withdrawal and names the deposit; a deposit takes no more than itself from the withdrawals that name it. A\n * withdrawal in another currency - an exchange - says what it put into the deposit in the deposit's currency.\n */\nregisterPaymentTarget('Transaction', {\n purpose: PaymentPurpose.TRANSFER,\n async currencyOf(tx, domain, targetIds) {\n const lines = await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(targetIds) } })\n return new Map(lines.map(line => [line.id, line.currency ?? 'KRW']))\n },\n async label(tx, domain, targetIds) {\n const lines = await tx\n .getRepository(Transaction)\n .find({ where: { domain: { id: domain.id }, id: In(targetIds) }, relations: ['bankAccount'] })\n return new Map(\n lines.map(line => [line.id, [line.transactionDate, line.bankAccount?.name].filter(Boolean).join(' ')])\n )\n },\n async validate(tx, domain, { targetIds, transactionIds }) {\n const repo = tx.getRepository(Transaction)\n const deposits = await repo.find({ where: { domain: { id: domain.id }, id: In(targetIds) } })\n if (deposits.length !== new Set(targetIds).size)\n throw new Refusal('transfer-target-not-found', {}, 'the other line of the transfer was not found')\n if (deposits.some(line => line.type !== CashFlowType.INCOME))\n throw new Refusal('transfer-target-not-deposit', {}, 'a withdrawal pays a deposit - the other line is not one')\n\n const withdrawals = await repo.find({ where: { domain: { id: domain.id }, id: In(transactionIds) } })\n if (withdrawals.some(line => line.type !== CashFlowType.EXPENSE))\n throw new Refusal('transfer-from-not-withdrawal', {}, 'a transfer is put on its withdrawal')\n\n const payments = await tx\n .getRepository(Payment)\n .find({ where: { domain: { id: domain.id }, targetType: 'Transaction', targetId: In(targetIds) } })\n for (const deposit of deposits) {\n const taken = payments\n .filter(one => one.targetId === deposit.id)\n .reduce((sum, one) => sum + paidOfTarget(one), 0)\n if (taken > Math.abs(Number(deposit.amount)) + 0.005)\n throw new Refusal(\n 'transfer-over-deposit',\n { taken, amount: deposit.amount },\n `${taken} moved into a deposit of ${deposit.amount}`\n )\n }\n }\n})\n"]}
|
|
@@ -11,6 +11,7 @@ const bank_statement_1 = require("../bank-statement/bank-statement");
|
|
|
11
11
|
const recurring_cash_item_1 = require("../recurring-cash-item/recurring-cash-item");
|
|
12
12
|
const cash_flow_type_1 = require("../cash-flow-type");
|
|
13
13
|
const payment_1 = require("../payment/payment");
|
|
14
|
+
const payment_currency_1 = require("../payment/payment-currency");
|
|
14
15
|
const payment_service_1 = require("../payment/payment-service");
|
|
15
16
|
const payment_targets_1 = require("../payment/payment-targets");
|
|
16
17
|
const transaction_1 = require("./transaction");
|
|
@@ -155,6 +156,7 @@ async function saveTransactions(tx, domain, user, inputs) {
|
|
|
155
156
|
if (factsChanged.length) {
|
|
156
157
|
const payments = await paymentsOn(tx, domain, factsChanged);
|
|
157
158
|
await (0, payment_service_1.checkPaymentsWithin)(tx, domain, payments.map(one => one.transaction.id));
|
|
159
|
+
await (0, payment_currency_1.checkPaymentCurrencies)(tx, domain, payments.map(one => one.transaction.id));
|
|
158
160
|
const kinds = new Map();
|
|
159
161
|
for (const payment of payments)
|
|
160
162
|
kinds.set(payment.targetType, [...(kinds.get(payment.targetType) ?? []), payment]);
|
|
@@ -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 { 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 { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\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/** The rows a transaction names besides its bank account, each of the caller's domain. */\nconst REFERENCES: [string, () => any][] = [\n ['account', () => Account],\n ['counterparty', () => Counterparty],\n ['recurringItem', () => RecurringCashItem],\n ['bankStatement', () => BankStatement]\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 /* An emptied currency is won, the column's own default - a row is never without one. */\n if ('currency' in row && !row.currency) row.currency = 'KRW'\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 /* What a row names is the caller's own - never another domain's account, counterparty, item or statement. */\n for (const [field, entity] of REFERENCES)\n if (row[field]?.id && !(await tx.getRepository(entity()).findOne({ where: { domain: { id: domain.id }, id: row[field].id } as any, withDeleted: true })))\n throw new Refusal('transaction-reference-not-found', { field }, `${field} was not found - reload and try again`)\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"]}
|
|
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 { 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 { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { CashFlowType } from '../cash-flow-type'\nimport { Payment } from '../payment/payment'\nimport { checkPaymentCurrencies } from '../payment/payment-currency'\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/** The rows a transaction names besides its bank account, each of the caller's domain. */\nconst REFERENCES: [string, () => any][] = [\n ['account', () => Account],\n ['counterparty', () => Counterparty],\n ['recurringItem', () => RecurringCashItem],\n ['bankStatement', () => BankStatement]\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 /* An emptied currency is won, the column's own default - a row is never without one. */\n if ('currency' in row && !row.currency) row.currency = 'KRW'\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 /* What a row names is the caller's own - never another domain's account, counterparty, item or statement. */\n for (const [field, entity] of REFERENCES)\n if (row[field]?.id && !(await tx.getRepository(entity()).findOne({ where: { domain: { id: domain.id }, id: row[field].id } as any, withDeleted: true })))\n throw new Refusal('transaction-reference-not-found', { field }, `${field} was not found - reload and try again`)\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 await checkPaymentCurrencies(\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"]}
|