@things-factory/accounting 10.1.233 → 10.1.234
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/account/account-mutation.js +7 -53
- package/dist-server/service/account/account-mutation.js.map +1 -1
- package/dist-server/service/account/account-service.d.ts +15 -0
- package/dist-server/service/account/account-service.js +40 -0
- package/dist-server/service/account/account-service.js.map +1 -1
- package/dist-server/service/bank-account/bank-account-history.d.ts +7 -0
- package/dist-server/service/bank-account/bank-account-history.js +29 -0
- package/dist-server/service/bank-account/bank-account-history.js.map +1 -1
- package/dist-server/service/bank-account/bank-account-mutation.js +4 -4
- package/dist-server/service/bank-account/bank-account-mutation.js.map +1 -1
- package/dist-server/service/bank-account/bank-account-query.d.ts +3 -0
- package/dist-server/service/bank-account/bank-account-query.js +23 -0
- package/dist-server/service/bank-account/bank-account-query.js.map +1 -1
- package/dist-server/service/bank-account/bank-account-service.d.ts +27 -0
- package/dist-server/service/bank-account/bank-account-service.js +55 -0
- package/dist-server/service/bank-account/bank-account-service.js.map +1 -1
- package/dist-server/service/bank-account/bank-account-type.d.ts +9 -0
- package/dist-server/service/bank-account/bank-account-type.js +33 -0
- package/dist-server/service/bank-account/bank-account-type.js.map +1 -1
- package/dist-server/service/bank-account/bank-account.d.ts +7 -0
- package/dist-server/service/bank-account/bank-account.js +29 -0
- package/dist-server/service/bank-account/bank-account.js.map +1 -1
- package/dist-server/service/bank-account/card-query.d.ts +24 -0
- package/dist-server/service/bank-account/card-query.js +140 -0
- package/dist-server/service/bank-account/card-query.js.map +1 -0
- package/dist-server/service/bank-account/card-rules.d.ts +21 -0
- package/dist-server/service/bank-account/card-rules.js +30 -0
- package/dist-server/service/bank-account/card-rules.js.map +1 -0
- package/dist-server/service/bank-account/card-summary.d.ts +29 -0
- package/dist-server/service/bank-account/card-summary.js +42 -0
- package/dist-server/service/bank-account/card-summary.js.map +1 -0
- package/dist-server/service/bank-account/index.d.ts +2 -1
- package/dist-server/service/bank-account/index.js +2 -1
- package/dist-server/service/bank-account/index.js.map +1 -1
- package/dist-server/service/expenditure-request/card-charge-query.d.ts +10 -0
- package/dist-server/service/expenditure-request/card-charge-query.js +54 -0
- package/dist-server/service/expenditure-request/card-charge-query.js.map +1 -0
- package/dist-server/service/expenditure-request/expenditure-request-item.d.ts +4 -0
- package/dist-server/service/expenditure-request/expenditure-request-item.js +19 -1
- package/dist-server/service/expenditure-request/expenditure-request-item.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-request-mutation.js +2 -1
- package/dist-server/service/expenditure-request/expenditure-request-mutation.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-request-rules.d.ts +26 -2
- package/dist-server/service/expenditure-request/expenditure-request-rules.js +19 -2
- package/dist-server/service/expenditure-request/expenditure-request-rules.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-request-service.d.ts +6 -0
- package/dist-server/service/expenditure-request/expenditure-request-service.js +35 -1
- package/dist-server/service/expenditure-request/expenditure-request-service.js.map +1 -1
- package/dist-server/service/expenditure-request/expenditure-request-type.d.ts +2 -0
- package/dist-server/service/expenditure-request/expenditure-request-type.js +8 -0
- package/dist-server/service/expenditure-request/expenditure-request-type.js.map +1 -1
- package/dist-server/service/expenditure-request/index.d.ts +2 -1
- package/dist-server/service/expenditure-request/index.js +2 -1
- package/dist-server/service/expenditure-request/index.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -1
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/payment/card-bill-target.d.ts +2 -0
- package/dist-server/service/payment/card-bill-target.js +37 -0
- package/dist-server/service/payment/card-bill-target.js.map +1 -0
- package/dist-server/service/payment/index.d.ts +1 -0
- package/dist-server/service/payment/index.js +1 -0
- package/dist-server/service/payment/index.js.map +1 -1
- package/dist-server/service/recurring-cash-item/recurring-cash-item-query.d.ts +2 -0
- package/dist-server/service/recurring-cash-item/recurring-cash-item-query.js +14 -0
- package/dist-server/service/recurring-cash-item/recurring-cash-item-query.js.map +1 -1
- package/dist-server/service/recurring-cash-item/recurring-cash-item-service.d.ts +5 -0
- package/dist-server/service/recurring-cash-item/recurring-cash-item-service.js +22 -1
- package/dist-server/service/recurring-cash-item/recurring-cash-item-service.js.map +1 -1
- package/dist-server/service/recurring-cash-item/recurring-schedule.d.ts +5 -1
- package/dist-server/service/recurring-cash-item/recurring-schedule.js +10 -2
- package/dist-server/service/recurring-cash-item/recurring-schedule.js.map +1 -1
- package/dist-server/service/transaction/transaction-query.d.ts +5 -0
- package/dist-server/service/transaction/transaction-query.js +3 -3
- package/dist-server/service/transaction/transaction-query.js.map +1 -1
- package/dist-server/service/transaction/transaction-service.d.ts +5 -1
- package/dist-server/service/transaction/transaction-service.js +15 -10
- package/dist-server/service/transaction/transaction-service.js.map +1 -1
- package/dist-server/service/transaction/transaction.d.ts +5 -3
- package/dist-server/service/transaction/transaction.js +6 -4
- package/dist-server/service/transaction/transaction.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -10
- package/translations/en.json +30 -1
- package/translations/es.json +30 -1
- package/translations/ja.json +30 -1
- package/translations/ko.json +30 -1
- package/translations/ms.json +30 -1
- package/translations/zh.json +30 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CARD_BILL_TARGET = void 0;
|
|
4
|
+
const typeorm_1 = require("typeorm");
|
|
5
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
|
+
const bank_account_1 = require("../bank-account/bank-account");
|
|
7
|
+
const cash_flow_type_1 = require("../cash-flow-type");
|
|
8
|
+
const transaction_1 = require("../transaction/transaction");
|
|
9
|
+
const payment_1 = require("./payment");
|
|
10
|
+
const payment_targets_1 = require("./payment-targets");
|
|
11
|
+
/** What a card's bill pays: the card - its charges, spent before and paid now (ADR-0178 결정 4). */
|
|
12
|
+
exports.CARD_BILL_TARGET = 'BankAccount';
|
|
13
|
+
/**
|
|
14
|
+
* A card's bill is a bank line - a withdrawal from a bank account - that pays the card. The charges on the card are
|
|
15
|
+
* lines of their own (`TransactionCategory.CARD`); the bill only says which card it paid, so what the card still owes is
|
|
16
|
+
* its charges less its bills.
|
|
17
|
+
*/
|
|
18
|
+
(0, payment_targets_1.registerPaymentTarget)(exports.CARD_BILL_TARGET, {
|
|
19
|
+
purpose: payment_1.PaymentPurpose.SETTLEMENT,
|
|
20
|
+
async currencyOf(tx, domain, targetIds) {
|
|
21
|
+
const cards = await tx.getRepository(bank_account_1.BankAccount).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
22
|
+
return new Map(cards.map(card => [card.id, card.currency ?? 'KRW']));
|
|
23
|
+
},
|
|
24
|
+
async label(tx, domain, targetIds) {
|
|
25
|
+
const cards = await tx.getRepository(bank_account_1.BankAccount).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
26
|
+
return new Map(cards.map(card => [card.id, card.name]));
|
|
27
|
+
},
|
|
28
|
+
async validate(tx, domain, { targetIds, transactionIds }) {
|
|
29
|
+
const cards = await tx.getRepository(bank_account_1.BankAccount).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
30
|
+
if (cards.length !== new Set(targetIds).size || cards.some(card => card.accountType !== bank_account_1.AccountType.CARD))
|
|
31
|
+
throw new shell_1.Refusal('card-bill-not-card', {}, 'a bill pays one of the cards');
|
|
32
|
+
const bills = await tx.getRepository(transaction_1.Transaction).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(transactionIds) } });
|
|
33
|
+
if (bills.some(line => line.category !== transaction_1.TransactionCategory.CONFIRMED || line.type !== cash_flow_type_1.CashFlowType.EXPENSE))
|
|
34
|
+
throw new shell_1.Refusal('card-bill-not-withdrawal', {}, "a card's bill is a withdrawal from a bank account");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=card-bill-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-bill-target.js","sourceRoot":"","sources":["../../../server/service/payment/card-bill-target.ts"],"names":[],"mappings":";;;AAAA,qCAA4B;AAE5B,iDAA+C;AAE/C,+DAAuE;AACvE,sDAAgD;AAChD,4DAA6E;AAC7E,uCAA0C;AAC1C,uDAAyD;AAEzD,kGAAkG;AACrF,QAAA,gBAAgB,GAAG,aAAa,CAAA;AAE7C;;;;GAIG;AACH,IAAA,uCAAqB,EAAC,wBAAgB,EAAE;IACtC,OAAO,EAAE,wBAAc,CAAC,UAAU;IAClC,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS;QACpC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,0BAAW,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,CAAC,aAAa,CAAC,0BAAW,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,IAAI,CAAC,CAAC,CAAC,CAAA;IACzD,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE;QACtD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,0BAAW,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,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,0BAAW,CAAC,IAAI,CAAC;YACvG,MAAM,IAAI,eAAO,CAAC,oBAAoB,EAAE,EAAE,EAAE,8BAA8B,CAAC,CAAA;QAC7E,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,cAAc,CAAC,EAAE,EAAE,CAAC,CAAA;QACxH,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,iCAAmB,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,6BAAY,CAAC,OAAO,CAAC;YAC3G,MAAM,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,mDAAmD,CAAC,CAAA;IAC1G,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { In } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { AccountType, BankAccount } from '../bank-account/bank-account'\nimport { CashFlowType } from '../cash-flow-type'\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\nimport { PaymentPurpose } from './payment'\nimport { registerPaymentTarget } from './payment-targets'\n\n/** What a card's bill pays: the card - its charges, spent before and paid now (ADR-0178 결정 4). */\nexport const CARD_BILL_TARGET = 'BankAccount'\n\n/**\n * A card's bill is a bank line - a withdrawal from a bank account - that pays the card. The charges on the card are\n * lines of their own (`TransactionCategory.CARD`); the bill only says which card it paid, so what the card still owes is\n * its charges less its bills.\n */\nregisterPaymentTarget(CARD_BILL_TARGET, {\n purpose: PaymentPurpose.SETTLEMENT,\n async currencyOf(tx, domain, targetIds) {\n const cards = await tx.getRepository(BankAccount).find({ where: { domain: { id: domain.id }, id: In(targetIds) } })\n return new Map(cards.map(card => [card.id, card.currency ?? 'KRW']))\n },\n async label(tx, domain, targetIds) {\n const cards = await tx.getRepository(BankAccount).find({ where: { domain: { id: domain.id }, id: In(targetIds) } })\n return new Map(cards.map(card => [card.id, card.name]))\n },\n async validate(tx, domain, { targetIds, transactionIds }) {\n const cards = await tx.getRepository(BankAccount).find({ where: { domain: { id: domain.id }, id: In(targetIds) } })\n if (cards.length !== new Set(targetIds).size || cards.some(card => card.accountType !== AccountType.CARD))\n throw new Refusal('card-bill-not-card', {}, 'a bill pays one of the cards')\n const bills = await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(transactionIds) } })\n if (bills.some(line => line.category !== TransactionCategory.CONFIRMED || line.type !== CashFlowType.EXPENSE))\n throw new Refusal('card-bill-not-withdrawal', {}, \"a card's bill is a withdrawal from a bank account\")\n }\n})\n"]}
|
|
@@ -3,6 +3,7 @@ import { PaymentHistoryEntitySubscriber } from './event-subscriber';
|
|
|
3
3
|
import { PaymentQuery } from './payment-query';
|
|
4
4
|
import { PaymentMutation } from './payment-mutation';
|
|
5
5
|
import './transfer-target';
|
|
6
|
+
import './card-bill-target';
|
|
6
7
|
export declare const entities: (typeof Payment)[];
|
|
7
8
|
export declare const resolvers: (typeof PaymentQuery | typeof PaymentMutation)[];
|
|
8
9
|
export declare const subscribers: (typeof PaymentHistoryEntitySubscriber)[];
|
|
@@ -7,6 +7,7 @@ const event_subscriber_1 = require("./event-subscriber");
|
|
|
7
7
|
const payment_query_1 = require("./payment-query");
|
|
8
8
|
const payment_mutation_1 = require("./payment-mutation");
|
|
9
9
|
require("./transfer-target");
|
|
10
|
+
require("./card-bill-target");
|
|
10
11
|
const shell_1 = require("@things-factory/shell");
|
|
11
12
|
/* A payment names what it paid by kind and id (§`registerPaymentTarget`); what it pays is tied to it (§`refuseTiedRows`). */
|
|
12
13
|
(0, shell_1.registerKindTie)(payment_1.Payment, { kind: 'targetType', id: 'targetId' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/payment/index.ts"],"names":[],"mappings":";;;AAAA,uCAAmC;AACnC,uDAAkD;AAClD,yDAAmE;AACnE,mDAA8C;AAC9C,yDAAoD;AACpD,6BAA0B;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/payment/index.ts"],"names":[],"mappings":";;;AAAA,uCAAmC;AACnC,uDAAkD;AAClD,yDAAmE;AACnE,mDAA8C;AAC9C,yDAAoD;AACpD,6BAA0B;AAC1B,8BAA2B;AAE3B,iDAAuD;AAEvD,6HAA6H;AAC7H,IAAA,uBAAe,EAAC,iBAAO,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;AAEnD,QAAA,QAAQ,GAAG,CAAC,iBAAO,EAAE,gCAAc,CAAC,CAAA;AACpC,QAAA,SAAS,GAAG,CAAC,4BAAY,EAAE,kCAAe,CAAC,CAAA;AAC3C,QAAA,WAAW,GAAG,CAAC,iDAA8B,CAAC,CAAA","sourcesContent":["import { Payment } from './payment'\nimport { PaymentHistory } from './payment-history'\nimport { PaymentHistoryEntitySubscriber } from './event-subscriber'\nimport { PaymentQuery } from './payment-query'\nimport { PaymentMutation } from './payment-mutation'\nimport './transfer-target'\nimport './card-bill-target'\n\nimport { registerKindTie } from '@things-factory/shell'\n\n/* A payment names what it paid by kind and id (§`registerPaymentTarget`); what it pays is tied to it (§`refuseTiedRows`). */\nregisterKindTie(Payment, { kind: 'targetType', id: 'targetId' })\n\nexport const entities = [Payment, PaymentHistory]\nexport const resolvers = [PaymentQuery, PaymentMutation]\nexport const subscribers = [PaymentHistoryEntitySubscriber]\n"]}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Domain, ListParam } from '@things-factory/shell';
|
|
2
2
|
import { User } from '@things-factory/auth-base';
|
|
3
3
|
import { Counterparty } from '@things-factory/biz-base';
|
|
4
|
+
import { FactChange } from '@things-factory/fact-change';
|
|
4
5
|
import { Account } from '../account/account';
|
|
5
6
|
import { RecurringCashItem } from './recurring-cash-item';
|
|
6
7
|
import { RecurringCashItemList } from './recurring-cash-item-type';
|
|
7
8
|
export declare class RecurringCashItemQuery {
|
|
8
9
|
recurringCashItem(id: string, context: ResolverContext): Promise<RecurringCashItem | null>;
|
|
9
10
|
recurringCashItems(params: ListParam, context: ResolverContext): Promise<RecurringCashItemList>;
|
|
11
|
+
recurringCashItemChanges(id: string, context: ResolverContext): Promise<FactChange[]>;
|
|
10
12
|
account(item: RecurringCashItem): Promise<Account | null>;
|
|
11
13
|
counterparty(item: RecurringCashItem): Promise<Counterparty | null>;
|
|
12
14
|
domain(item: RecurringCashItem): Promise<Domain>;
|
|
@@ -6,9 +6,11 @@ const type_graphql_1 = require("type-graphql");
|
|
|
6
6
|
const shell_1 = require("@things-factory/shell");
|
|
7
7
|
const auth_base_1 = require("@things-factory/auth-base");
|
|
8
8
|
const biz_base_1 = require("@things-factory/biz-base");
|
|
9
|
+
const fact_change_1 = require("@things-factory/fact-change");
|
|
9
10
|
const account_1 = require("../account/account");
|
|
10
11
|
const recurring_cash_item_1 = require("./recurring-cash-item");
|
|
11
12
|
const recurring_cash_item_type_1 = require("./recurring-cash-item-type");
|
|
13
|
+
const recurring_cash_item_service_1 = require("./recurring-cash-item-service");
|
|
12
14
|
let RecurringCashItemQuery = class RecurringCashItemQuery {
|
|
13
15
|
async recurringCashItem(id, context) {
|
|
14
16
|
const { domain } = context.state;
|
|
@@ -25,6 +27,9 @@ let RecurringCashItemQuery = class RecurringCashItemQuery {
|
|
|
25
27
|
const [items, total] = await queryBuilder.getManyAndCount();
|
|
26
28
|
return { items, total };
|
|
27
29
|
}
|
|
30
|
+
async recurringCashItemChanges(id, context) {
|
|
31
|
+
return (0, recurring_cash_item_service_1.recurringCashItemChanges)((0, shell_1.getRepository)(recurring_cash_item_1.RecurringCashItem).manager, context.state.domain, id);
|
|
32
|
+
}
|
|
28
33
|
async account(item) {
|
|
29
34
|
return item.accountId ? await (0, shell_1.getRepository)(account_1.Account).findOneBy({ id: item.accountId }) : null;
|
|
30
35
|
}
|
|
@@ -60,6 +65,15 @@ tslib_1.__decorate([
|
|
|
60
65
|
tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object]),
|
|
61
66
|
tslib_1.__metadata("design:returntype", Promise)
|
|
62
67
|
], RecurringCashItemQuery.prototype, "recurringCashItems", null);
|
|
68
|
+
tslib_1.__decorate([
|
|
69
|
+
(0, type_graphql_1.Directive)('@privilege(category: "accounting", privilege: "query", domainOwnerGranted: true)'),
|
|
70
|
+
(0, type_graphql_1.Query)(returns => [fact_change_1.FactChange], { description: 'What moved on a recurring item, field by field, newest first (its 360)' }),
|
|
71
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
|
|
72
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
73
|
+
tslib_1.__metadata("design:type", Function),
|
|
74
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
75
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
76
|
+
], RecurringCashItemQuery.prototype, "recurringCashItemChanges", null);
|
|
63
77
|
tslib_1.__decorate([
|
|
64
78
|
(0, type_graphql_1.FieldResolver)(type => account_1.Account, { nullable: true }),
|
|
65
79
|
tslib_1.__param(0, (0, type_graphql_1.Root)()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recurring-cash-item-query.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-cash-item-query.ts"],"names":[],"mappings":";;;;AAAA,+CAA8F;AAE9F,iDAAuG;AACvG,yDAAgD;AAChD,uDAAuD;
|
|
1
|
+
{"version":3,"file":"recurring-cash-item-query.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-cash-item-query.ts"],"names":[],"mappings":";;;;AAAA,+CAA8F;AAE9F,iDAAuG;AACvG,yDAAgD;AAChD,uDAAuD;AACvD,6DAAwD;AAExD,gDAA4C;AAC5C,+DAAyD;AACzD,yEAAkE;AAClE,+EAAwE;AAGjE,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAG3B,AAAN,KAAK,CAAC,iBAAiB,CAAY,EAAU,EAAS,OAAwB;QAC5E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,MAAM,IAAA,qBAAa,EAAC,uCAAiB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;IACrG,CAAC;IAIK,AAAN,KAAK,CAAC,kBAAkB,CACG,MAAiB,EACnC,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAChC,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,MAAM;YACN,MAAM;YACN,UAAU,EAAE,IAAA,qBAAa,EAAC,uCAAiB,CAAC;YAC5C,WAAW,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;SAC/B,CAAC,CAAA;QACF,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAE3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAIK,AAAN,KAAK,CAAC,wBAAwB,CAAY,EAAU,EAAS,OAAwB;QACnF,OAAO,IAAA,sDAAwB,EAAC,IAAA,qBAAa,EAAC,uCAAiB,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACrG,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,IAAuB;QAC3C,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,iBAAO,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/F,CAAC;IAGK,AAAN,KAAK,CAAC,YAAY,CAAS,IAAuB;QAChD,OAAO,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,uBAAY,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9G,CAAC;IAGK,AAAN,KAAK,CAAC,MAAM,CAAS,IAAuB;QAC1C,OAAO,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IACrE,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,IAAuB;QAC3C,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC5F,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,IAAuB;QAC3C,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC5F,CAAC;CACF,CAAA;AAzDY,wDAAsB;AAG3B;IAFL,IAAA,wBAAS,EAAC,kFAAkF,CAAC;IAC7F,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,uCAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;IAC5E,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;+DAIpD;AAIK;IAFL,IAAA,wBAAS,EAAC,kFAAkF,CAAC;IAC7F,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,gDAAqB,EAAE,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;IAE9F,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IACvB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAD2B,iBAAS;;gEAa3C;AAIK;IAFL,IAAA,wBAAS,EAAC,kFAAkF,CAAC;IAC7F,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,wBAAU,CAAC,EAAE,EAAE,WAAW,EAAE,wEAAwE,EAAE,CAAC;IAC1F,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;sEAE3D;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACpC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,uCAAiB;;qDAE5C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,uBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACpC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,uCAAiB;;0DAEjD;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IAChB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,uCAAiB;;oDAE3C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,uCAAiB;;qDAE5C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,uCAAiB;;qDAE5C;iCAxDU,sBAAsB;IADlC,IAAA,uBAAQ,EAAC,uCAAiB,CAAC;GACf,sBAAsB,CAyDlC","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'\nimport { FactChange } from '@things-factory/fact-change'\n\nimport { Account } from '../account/account'\nimport { RecurringCashItem } from './recurring-cash-item'\nimport { RecurringCashItemList } from './recurring-cash-item-type'\nimport { recurringCashItemChanges } from './recurring-cash-item-service'\n\n@Resolver(RecurringCashItem)\nexport class RecurringCashItemQuery {\n @Directive('@privilege(category: \"accounting\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => RecurringCashItem, { nullable: true, description: 'To fetch a RecurringCashItem' })\n async recurringCashItem(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<RecurringCashItem | null> {\n const { domain } = context.state\n\n return await getRepository(RecurringCashItem).findOne({ where: { domain: { id: domain.id }, id } })\n }\n\n @Directive('@privilege(category: \"accounting\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => RecurringCashItemList, { description: 'To fetch multiple RecurringCashItems' })\n async recurringCashItems(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<RecurringCashItemList> {\n const { domain } = context.state\n const queryBuilder = getQueryBuilderFromListParams({\n domain,\n params,\n repository: getRepository(RecurringCashItem),\n searchables: ['name', 'notes']\n })\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Directive('@privilege(category: \"accounting\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => [FactChange], { description: 'What moved on a recurring item, field by field, newest first (its 360)' })\n async recurringCashItemChanges(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<FactChange[]> {\n return recurringCashItemChanges(getRepository(RecurringCashItem).manager, context.state.domain, id)\n }\n\n @FieldResolver(type => Account, { nullable: true })\n async account(@Root() item: RecurringCashItem): Promise<Account | null> {\n return item.accountId ? await getRepository(Account).findOneBy({ id: item.accountId }) : null\n }\n\n @FieldResolver(type => Counterparty, { nullable: true })\n async counterparty(@Root() item: RecurringCashItem): Promise<Counterparty | null> {\n return item.counterpartyId ? await getRepository(Counterparty).findOneBy({ id: item.counterpartyId }) : null\n }\n\n @FieldResolver(type => Domain)\n async domain(@Root() item: RecurringCashItem): Promise<Domain> {\n return await getRepository(Domain).findOneBy({ id: item.domainId })\n }\n\n @FieldResolver(type => User, { nullable: true })\n async creator(@Root() item: RecurringCashItem): Promise<User | null> {\n return item.creatorId ? await getRepository(User).findOneBy({ id: item.creatorId }) : null\n }\n\n @FieldResolver(type => User, { nullable: true })\n async updater(@Root() item: RecurringCashItem): Promise<User | null> {\n return item.updaterId ? await getRepository(User).findOneBy({ id: item.updaterId }) : null\n }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EntityManager } from 'typeorm';
|
|
2
2
|
import { RecurringCashItem } from './recurring-cash-item';
|
|
3
|
+
export declare const RECURRING_CASH_ITEM_FACT = "recurring-cash-item";
|
|
3
4
|
export type RecurringCashItemInput = Partial<Omit<RecurringCashItem, 'account' | 'counterparty'>> & {
|
|
4
5
|
id?: string;
|
|
5
6
|
cuFlag?: string;
|
|
@@ -17,3 +18,7 @@ export type RecurringCashItemInput = Partial<Omit<RecurringCashItem, 'account' |
|
|
|
17
18
|
export declare function saveRecurringCashItems(tx: EntityManager, domain: {
|
|
18
19
|
id: string;
|
|
19
20
|
}, user: any, inputs: RecurringCashItemInput[]): Promise<RecurringCashItem[]>;
|
|
21
|
+
/** What moved on a recurring item, newest first. */
|
|
22
|
+
export declare function recurringCashItemChanges(tx: EntityManager, domain: {
|
|
23
|
+
id: string;
|
|
24
|
+
}, id: string): Promise<import("@things-factory/fact-change").FactChange[]>;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RECURRING_CASH_ITEM_FACT = void 0;
|
|
3
4
|
exports.saveRecurringCashItems = saveRecurringCashItems;
|
|
5
|
+
exports.recurringCashItemChanges = recurringCashItemChanges;
|
|
4
6
|
const typeorm_1 = require("typeorm");
|
|
7
|
+
const fact_change_1 = require("@things-factory/fact-change");
|
|
5
8
|
const shell_1 = require("@things-factory/shell");
|
|
6
9
|
const recurring_cash_item_1 = require("./recurring-cash-item");
|
|
7
10
|
const MONTH = /^\d{4}-(0[1-9]|1[0-2])$/;
|
|
11
|
+
/* What moved on a recurring item, field by field - kept by the fact-change module for its 360 (ADR-0177 decision 11). */
|
|
12
|
+
exports.RECURRING_CASH_ITEM_FACT = 'recurring-cash-item';
|
|
13
|
+
const TRACKED = ['name', 'accountId', 'type', 'amount', 'currency', 'dayOfMonth', 'lastDayOfMonth', 'startMonth', 'endMonth', 'counterpartyId', 'active'];
|
|
14
|
+
const asIds = (row) => ({
|
|
15
|
+
...row,
|
|
16
|
+
...('account' in row ? { accountId: row.account?.id ?? null } : {}),
|
|
17
|
+
...('counterparty' in row ? { counterpartyId: row.counterparty?.id ?? null } : {})
|
|
18
|
+
});
|
|
8
19
|
function rowOf(input, current) {
|
|
9
20
|
const { id, cuFlag, ...row } = input;
|
|
10
21
|
const merged = { ...current, ...row };
|
|
@@ -47,12 +58,22 @@ async function saveRecurringCashItems(tx, domain, user, inputs) {
|
|
|
47
58
|
const current = isNew ? undefined : await repo.findOne({ where: { domain: { id: domain.id }, id: input.id } });
|
|
48
59
|
if (!isNew && !current)
|
|
49
60
|
throw new shell_1.Refusal('recurring-not-found', {}, 'a recurring item was not found - reload and try again');
|
|
61
|
+
const row = rowOf(input, current);
|
|
62
|
+
if (current)
|
|
63
|
+
await (0, fact_change_1.recordFactChanges)(tx, domain, { factKind: exports.RECURRING_CASH_ITEM_FACT, factName: current.id, changes: (0, fact_change_1.fieldChanges)(current, { ...current, ...asIds(row) }, TRACKED) }, { user, source: 'accounting' });
|
|
50
64
|
saved.push(await repo.save({
|
|
51
65
|
...(current ?? { domain, creator: user }),
|
|
52
|
-
...
|
|
66
|
+
...row,
|
|
53
67
|
updater: user
|
|
54
68
|
}));
|
|
55
69
|
}
|
|
56
70
|
return saved;
|
|
57
71
|
}
|
|
72
|
+
/** What moved on a recurring item, newest first. */
|
|
73
|
+
async function recurringCashItemChanges(tx, domain, id) {
|
|
74
|
+
const item = await tx.getRepository(recurring_cash_item_1.RecurringCashItem).findOne({ where: { domain: { id: domain.id }, id }, withDeleted: true });
|
|
75
|
+
if (!item)
|
|
76
|
+
throw new shell_1.Refusal('recurring-not-found', {}, 'a recurring item was not found - reload and try again');
|
|
77
|
+
return (0, fact_change_1.readFactChanges)(tx, domain, exports.RECURRING_CASH_ITEM_FACT, item.id);
|
|
78
|
+
}
|
|
58
79
|
//# sourceMappingURL=recurring-cash-item-service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recurring-cash-item-service.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-cash-item-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recurring-cash-item-service.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-cash-item-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { fieldChanges, readFactChanges, recordFactChanges } from '@things-factory/fact-change'\nimport { Refusal, refuseTiedRows } from '@things-factory/shell'\n\nimport { RecurringCashItem } from './recurring-cash-item'\n\nconst MONTH = /^\\d{4}-(0[1-9]|1[0-2])$/\n\n/* What moved on a recurring item, field by field - kept by the fact-change module for its 360 (ADR-0177 decision 11). */\nexport const RECURRING_CASH_ITEM_FACT = 'recurring-cash-item'\nconst TRACKED = ['name', 'accountId', 'type', 'amount', 'currency', 'dayOfMonth', 'lastDayOfMonth', 'startMonth', 'endMonth', 'counterpartyId', 'active'] as const\nconst asIds = (row: Record<string, any>) => ({\n ...row,\n ...('account' in row ? { accountId: row.account?.id ?? null } : {}),\n ...('counterparty' in row ? { counterpartyId: row.counterparty?.id ?? null } : {})\n})\n\nexport type RecurringCashItemInput = Partial<Omit<RecurringCashItem, 'account' | 'counterparty'>> & {\n id?: string\n cuFlag?: string\n account?: { id: string } | null\n counterparty?: { id: string } | null\n}\n\nfunction rowOf(input: RecurringCashItemInput, current: RecurringCashItem | undefined): Record<string, any> {\n const { id, cuFlag, ...row } = input as Record<string, any>\n const merged = { ...current, ...row }\n if (!String(merged.name ?? '').trim()) throw new Refusal('recurring-name-required', {}, 'name: what it is')\n if (!merged.type) throw new Refusal('transaction-type-required', {}, 'type: money in or money out')\n if (!MONTH.test(String(merged.startMonth ?? '')))\n throw new Refusal('recurring-start-required', {}, 'startMonth: YYYY-MM')\n if (merged.endMonth && !MONTH.test(merged.endMonth))\n throw new Refusal('recurring-end-not-a-month', {}, 'endMonth: YYYY-MM')\n if (merged.endMonth && merged.endMonth < merged.startMonth)\n throw new Refusal('recurring-end-before-start', {}, 'endMonth: before startMonth')\n if (\n merged.dayOfMonth !== null &&\n merged.dayOfMonth !== undefined &&\n !(merged.dayOfMonth >= 1 && merged.dayOfMonth <= 31)\n ) {\n throw new Refusal('day-of-month-out-of-range', {}, 'dayOfMonth: must be 1 to 31')\n }\n if (!current && (row.amount === undefined || row.amount === null))\n throw new Refusal('recurring-amount-required', {}, 'amount: every month')\n return row\n}\n\n/**\n * Apply rows to a domain's recurring items: `+` (or no id) adds, `M` changes, `-` deletes (softly). An item another row\n * still names is not deleted (§`refuseTiedRows`).\n */\nexport async function saveRecurringCashItems(\n tx: EntityManager,\n domain: { id: string },\n user: any,\n inputs: RecurringCashItemInput[]\n): Promise<RecurringCashItem[]> {\n const repo = tx.getRepository(RecurringCashItem)\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('recurring-not-found', {}, 'a recurring item was not found - reload and try again')\n await refuseTiedRows(tx, RecurringCashItem, deletes)\n await repo.softRemove(found)\n }\n\n const saved: RecurringCashItem[] = []\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('recurring-not-found', {}, 'a recurring item was not found - reload and try again')\n const row = rowOf(input, current)\n if (current)\n await recordFactChanges(tx, domain as any, { factKind: RECURRING_CASH_ITEM_FACT, factName: current.id, changes: fieldChanges(current as any, { ...current, ...asIds(row) }, TRACKED as any) }, { user, source: 'accounting' })\n saved.push(\n await repo.save({\n ...(current ?? { domain, creator: user }),\n ...row,\n updater: user\n } as RecurringCashItem)\n )\n }\n return saved\n}\n\n/** What moved on a recurring item, newest first. */\nexport async function recurringCashItemChanges(tx: EntityManager, domain: { id: string }, id: string) {\n const item = await tx.getRepository(RecurringCashItem).findOne({ where: { domain: { id: domain.id }, id } as any, withDeleted: true })\n if (!item) throw new Refusal('recurring-not-found', {}, 'a recurring item was not found - reload and try again')\n return readFactChanges(tx, domain as any, RECURRING_CASH_ITEM_FACT, item.id)\n}\n"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which recurring items fall in a month, and on which day of it.
|
|
3
3
|
*
|
|
4
|
-
* An item falls in a month when it is active
|
|
4
|
+
* An item falls in a month when it is active, the month lies within [startMonth, endMonth] and, for an item paid every
|
|
5
|
+
* few months (`everyMonths` - 12 for a yearly one), the month is a whole number of those after startMonth (ADR-0177
|
|
6
|
+
* decision 4: a subscription paid yearly shares this rule). Its day is the day of
|
|
5
7
|
* the month it names - one past the month's end is the last day, as a ledger means it - or the last day for 말일. An
|
|
6
8
|
* item with neither has no day: the calendar shows its month undated, as it does any money nobody gave a day.
|
|
7
9
|
*
|
|
@@ -18,6 +20,8 @@ export interface RecurringItem {
|
|
|
18
20
|
startMonth: string;
|
|
19
21
|
endMonth?: string | null;
|
|
20
22
|
active?: boolean | null;
|
|
23
|
+
/** Paid every so many months from startMonth; every month when empty */
|
|
24
|
+
everyMonths?: number | null;
|
|
21
25
|
}
|
|
22
26
|
export declare function fallsIn(item: RecurringItem, year: number, month: number): boolean;
|
|
23
27
|
/** The `YYYY-MM-DD` the item falls on in a month, or null when it names no day. */
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Which recurring items fall in a month, and on which day of it.
|
|
4
4
|
*
|
|
5
|
-
* An item falls in a month when it is active
|
|
5
|
+
* An item falls in a month when it is active, the month lies within [startMonth, endMonth] and, for an item paid every
|
|
6
|
+
* few months (`everyMonths` - 12 for a yearly one), the month is a whole number of those after startMonth (ADR-0177
|
|
7
|
+
* decision 4: a subscription paid yearly shares this rule). Its day is the day of
|
|
6
8
|
* the month it names - one past the month's end is the last day, as a ledger means it - or the last day for 말일. An
|
|
7
9
|
* item with neither has no day: the calendar shows its month undated, as it does any money nobody gave a day.
|
|
8
10
|
*
|
|
@@ -16,7 +18,13 @@ const monthKey = (year, month) => `${year}-${pad(month)}`;
|
|
|
16
18
|
const lastDay = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
17
19
|
function fallsIn(item, year, month) {
|
|
18
20
|
const key = monthKey(year, month);
|
|
19
|
-
|
|
21
|
+
if (item.active === false || key < item.startMonth || (item.endMonth && key > item.endMonth))
|
|
22
|
+
return false;
|
|
23
|
+
const every = Math.floor(Number(item.everyMonths) || 1);
|
|
24
|
+
if (every <= 1)
|
|
25
|
+
return true;
|
|
26
|
+
const [startYear, startMonth] = item.startMonth.split('-').map(Number);
|
|
27
|
+
return ((year - startYear) * 12 + (month - startMonth)) % every === 0;
|
|
20
28
|
}
|
|
21
29
|
/** The `YYYY-MM-DD` the item falls on in a month, or null when it names no day. */
|
|
22
30
|
function recurringDay(item, year, month) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recurring-schedule.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-schedule.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"recurring-schedule.js","sourceRoot":"","sources":["../../../server/service/recurring-cash-item/recurring-schedule.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAqBH,0BAOC;AAGD,oCAIC;AAlBD,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACrD,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAA;AACzE,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;AAEhG,SAAgB,OAAO,CAAC,IAAmB,EAAE,IAAY,EAAE,KAAa;IACtE,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACjC,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1G,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAA;IACvD,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3B,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACtE,OAAO,CAAC,CAAC,IAAI,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAA;AACvE,CAAC;AAED,mFAAmF;AACnF,SAAgB,YAAY,CAAC,IAA0D,EAAE,IAAY,EAAE,KAAa;IAClH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACjC,IAAI,IAAI,CAAC,cAAc;QAAE,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;IACvE,OAAO,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAC5H,CAAC","sourcesContent":["/**\n * Which recurring items fall in a month, and on which day of it.\n *\n * An item falls in a month when it is active, the month lies within [startMonth, endMonth] and, for an item paid every\n * few months (`everyMonths` - 12 for a yearly one), the month is a whole number of those after startMonth (ADR-0177\n * decision 4: a subscription paid yearly shares this rule). Its day is the day of\n * the month it names - one past the month's end is the last day, as a ledger means it - or the last day for 말일. An\n * item with neither has no day: the calendar shows its month undated, as it does any money nobody gave a day.\n *\n * Pure.\n */\n\nexport interface RecurringItem {\n id: string\n name: string\n type: string\n amount: number\n currency?: string | null\n dayOfMonth?: number | null\n lastDayOfMonth?: boolean | null\n startMonth: string\n endMonth?: string | null\n active?: boolean | null\n /** Paid every so many months from startMonth; every month when empty */\n everyMonths?: number | null\n}\n\nconst pad = (n: number) => String(n).padStart(2, '0')\nconst monthKey = (year: number, month: number) => `${year}-${pad(month)}`\nconst lastDay = (year: number, month: number) => new Date(Date.UTC(year, month, 0)).getUTCDate()\n\nexport function fallsIn(item: RecurringItem, year: number, month: number): boolean {\n const key = monthKey(year, month)\n if (item.active === false || key < item.startMonth || (item.endMonth && key > item.endMonth)) return false\n const every = Math.floor(Number(item.everyMonths) || 1)\n if (every <= 1) return true\n const [startYear, startMonth] = item.startMonth.split('-').map(Number)\n return ((year - startYear) * 12 + (month - startMonth)) % every === 0\n}\n\n/** The `YYYY-MM-DD` the item falls on in a month, or null when it names no day. */\nexport function recurringDay(item: Pick<RecurringItem, 'dayOfMonth' | 'lastDayOfMonth'>, year: number, month: number): string | null {\n const last = lastDay(year, month)\n if (item.lastDayOfMonth) return `${monthKey(year, month)}-${pad(last)}`\n return item.dayOfMonth && item.dayOfMonth >= 1 ? `${monthKey(year, month)}-${pad(Math.min(item.dayOfMonth, last))}` : null\n}\n"]}
|
|
@@ -7,6 +7,11 @@ import { BankStatement } from '../bank-statement/bank-statement';
|
|
|
7
7
|
import { Payment } from '../payment/payment';
|
|
8
8
|
import { Transaction } from './transaction';
|
|
9
9
|
import { TransactionList } from './transaction-type';
|
|
10
|
+
/**
|
|
11
|
+
* Newest first: the period, the day, the time the bank printed, the bank's own order within one second, then when the
|
|
12
|
+
* row was entered.
|
|
13
|
+
*/
|
|
14
|
+
export declare const BOOK_ORDER: readonly ["year", "month", "transactionDate", "transactionAt", "bankSequence", "createdAt"];
|
|
10
15
|
export declare class TransactionQuery {
|
|
11
16
|
transaction(id: string, context: ResolverContext): Promise<Transaction>;
|
|
12
17
|
transactions(params: ListParam, context: ResolverContext): Promise<TransactionList>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TransactionQuery = void 0;
|
|
3
|
+
exports.TransactionQuery = exports.BOOK_ORDER = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const type_graphql_1 = require("type-graphql");
|
|
6
6
|
const shell_1 = require("@things-factory/shell");
|
|
@@ -16,7 +16,7 @@ const transaction_type_1 = require("./transaction-type");
|
|
|
16
16
|
* Newest first: the period, the day, the time the bank printed, the bank's own order within one second, then when the
|
|
17
17
|
* row was entered.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
exports.BOOK_ORDER = ['year', 'month', 'transactionDate', 'transactionAt', 'bankSequence', 'createdAt'];
|
|
20
20
|
let TransactionQuery = class TransactionQuery {
|
|
21
21
|
async transaction(id, context) {
|
|
22
22
|
const { domain } = context.state;
|
|
@@ -43,7 +43,7 @@ let TransactionQuery = class TransactionQuery {
|
|
|
43
43
|
alias: 'transaction'
|
|
44
44
|
});
|
|
45
45
|
/* A book reads newest first - after any order the caller asks (§`BOOK_ORDER`). */
|
|
46
|
-
for (const field of BOOK_ORDER)
|
|
46
|
+
for (const field of exports.BOOK_ORDER)
|
|
47
47
|
if (!params.sortings?.some(sorting => sorting.name === field))
|
|
48
48
|
queryBuilder.addOrderBy(`transaction.${field}`, 'DESC');
|
|
49
49
|
const [items, total] = await queryBuilder.getManyAndCount();
|
|
@@ -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,+CAA2C;AAC3C,yDAAoD;AAEpD;;;GAGG;
|
|
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 { Transaction } from './transaction'\nimport { TransactionList } from './transaction-type'\n\n/**\n * Newest first: the period, the day, the time the bank printed, the bank's own order within one second, then when the\n * row was entered.\n */\nexport const BOOK_ORDER = ['year', 'month', 'transactionDate', 'transactionAt', 'bankSequence', 'createdAt'] as const\n\n@Resolver(Transaction)\nexport class TransactionQuery {\n @Directive('@privilege(category: \"bank\", privilege: \"query\", domainOwnerGranted: false)')\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: \"bank\", privilege: \"query\", domainOwnerGranted: false)')\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 '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 => [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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EntityManager } from 'typeorm';
|
|
2
|
-
import { Transaction } from './transaction';
|
|
2
|
+
import { Transaction, TransactionCategory } from './transaction';
|
|
3
3
|
/** A transaction as a module writes it: what a person may write, and the facts a bank statement gives. */
|
|
4
4
|
export type TransactionInput = Partial<Omit<Transaction, 'bankAccount' | 'account' | 'counterparty' | 'bankStatement'>> & {
|
|
5
5
|
id?: string;
|
|
@@ -21,6 +21,10 @@ export type TransactionInput = Partial<Omit<Transaction, 'bankAccount' | 'accoun
|
|
|
21
21
|
export declare function protectBankFacts(current: Partial<Transaction> & {
|
|
22
22
|
bankAccountId?: string;
|
|
23
23
|
}, patch: Record<string, any>): void;
|
|
24
|
+
/** A line on a card is a charge on it; every other line - one with no account too - is a bank line. */
|
|
25
|
+
export declare function categoryOf(account: {
|
|
26
|
+
accountType?: string;
|
|
27
|
+
} | null | undefined): TransactionCategory;
|
|
24
28
|
/**
|
|
25
29
|
* Apply rows to a domain's transactions: `+` (or no id) adds, `M` changes, `-` deletes (softly - a financial row
|
|
26
30
|
* that was deleted stays readable). A transaction that pays something, is paid, or is named by another row is unlinked
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.protectBankFacts = protectBankFacts;
|
|
4
|
+
exports.categoryOf = categoryOf;
|
|
4
5
|
exports.saveTransactions = saveTransactions;
|
|
5
6
|
const typeorm_1 = require("typeorm");
|
|
6
7
|
const shell_1 = require("@things-factory/shell");
|
|
@@ -41,12 +42,15 @@ const REFERENCES = [
|
|
|
41
42
|
];
|
|
42
43
|
/** What its payments depend on - they are checked again, and their owners follow, when one of these changes. */
|
|
43
44
|
const FACTS = ['amount', 'type', 'currency', 'transactionDate'];
|
|
45
|
+
/** A line on a card is a charge on it; every other line - one with no account too - is a bank line. */
|
|
46
|
+
function categoryOf(account) {
|
|
47
|
+
return account?.accountType === bank_account_1.AccountType.CARD ? transaction_1.TransactionCategory.CARD : transaction_1.TransactionCategory.CONFIRMED;
|
|
48
|
+
}
|
|
44
49
|
/** The row a patch writes over the current one: a transaction has a day and takes its year and month from it. */
|
|
45
50
|
function rowOf(input, current) {
|
|
46
51
|
const { id, cuFlag, ...fields } = input;
|
|
47
52
|
const row = { ...fields };
|
|
48
53
|
const merged = { ...current, ...row };
|
|
49
|
-
row.category = transaction_1.TransactionCategory.CONFIRMED;
|
|
50
54
|
if (!current && !row.type)
|
|
51
55
|
throw new shell_1.Refusal('transaction-type-required', {}, 'type: money in or money out');
|
|
52
56
|
if (row.type !== undefined && !Object.values(cash_flow_type_1.CashFlowType).includes(row.type))
|
|
@@ -105,15 +109,16 @@ async function saveTransactions(tx, domain, user, inputs) {
|
|
|
105
109
|
if (current)
|
|
106
110
|
protectBankFacts(current, input);
|
|
107
111
|
const row = rowOf(input, current);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
const accountId = 'bankAccount' in row ? row.bankAccount?.id : current?.bankAccountId;
|
|
113
|
+
const account = accountId
|
|
114
|
+
? await tx.getRepository(bank_account_1.BankAccount).findOne({ where: { domain: { id: domain.id }, id: accountId } })
|
|
115
|
+
: undefined;
|
|
116
|
+
if (accountId && !account)
|
|
117
|
+
throw new shell_1.Refusal('bank-account-not-found', {}, 'the bank account was not found - reload and try again');
|
|
118
|
+
if (row.bankAccount?.id && !('currency' in input) && account?.currency)
|
|
119
|
+
row.currency = account.currency;
|
|
120
|
+
/* A line is its account's: a charge on a card, or a bank line (ADR-0178 결정 1). */
|
|
121
|
+
row.category = categoryOf(account);
|
|
117
122
|
/* What a row names is the caller's own - never another domain's account, counterparty, item or statement. */
|
|
118
123
|
for (const [field, entity] of REFERENCES)
|
|
119
124
|
if (row[field]?.id && !(await tx.getRepository(entity()).findOne({ where: { domain: { id: domain.id }, id: row[field].id }, withDeleted: true })))
|
|
@@ -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, refuseTiedRows } 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 { Transaction, TransactionCategory, TransactionSource } from './transaction'\nimport { 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' | 'bankStatement'>\n> & {\n id?: string\n cuFlag?: string\n bankAccount?: { id: string } | null\n account?: { id: string } | null\n counterparty?: { 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 ['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/** The row a patch writes over the current one: a transaction has a day and takes its year and month from it. */\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 row.category = TransactionCategory.CONFIRMED\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 const period = yearMonthOf(merged.transactionDate)\n if (!period)\n throw new Refusal('transaction-date-required', { value: merged.transactionDate ?? '' }, 'transactionDate: YYYY-MM-DD')\n Object.assign(row, period)\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, is paid, or is named by another row is unlinked\n * before it is deleted (§`refuseTiedRows`); when its money changes, its payments are checked again - the general rule\n * 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 await refuseTiedRows(tx, Transaction, deletes)\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"]}
|
|
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, refuseTiedRows } from '@things-factory/shell'\n\nimport { Counterparty } from '@things-factory/biz-base'\n\nimport { Account } from '../account/account'\nimport { AccountType, 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 { Transaction, TransactionCategory, TransactionSource } from './transaction'\nimport { 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' | 'bankStatement'>\n> & {\n id?: string\n cuFlag?: string\n bankAccount?: { id: string } | null\n account?: { id: string } | null\n counterparty?: { 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 ['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/** A line on a card is a charge on it; every other line - one with no account too - is a bank line. */\nexport function categoryOf(account: { accountType?: string } | null | undefined): TransactionCategory {\n return account?.accountType === AccountType.CARD ? TransactionCategory.CARD : TransactionCategory.CONFIRMED\n}\n\n/** The row a patch writes over the current one: a transaction has a day and takes its year and month from it. */\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\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 const period = yearMonthOf(merged.transactionDate)\n if (!period)\n throw new Refusal('transaction-date-required', { value: merged.transactionDate ?? '' }, 'transactionDate: YYYY-MM-DD')\n Object.assign(row, period)\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, is paid, or is named by another row is unlinked\n * before it is deleted (§`refuseTiedRows`); when its money changes, its payments are checked again - the general rule\n * 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 await refuseTiedRows(tx, Transaction, deletes)\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 const accountId = 'bankAccount' in row ? row.bankAccount?.id : current?.bankAccountId\n const account = accountId\n ? await tx.getRepository(BankAccount).findOne({ where: { domain: { id: domain.id }, id: accountId } })\n : undefined\n if (accountId && !account)\n throw new Refusal('bank-account-not-found', {}, 'the bank account was not found - reload and try again')\n if (row.bankAccount?.id && !('currency' in input) && account?.currency) row.currency = account.currency\n /* A line is its account's: a charge on a card, or a bank line (ADR-0178 결정 1). */\n row.category = categoryOf(account)\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"]}
|