@things-factory/accounting 10.1.208 → 10.1.210

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.
Files changed (53) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/service/account/account-match.d.ts +4 -5
  3. package/dist-server/service/account/account-match.js +7 -8
  4. package/dist-server/service/account/account-match.js.map +1 -1
  5. package/dist-server/service/expenditure-request/expenditure-request-mutation.d.ts +1 -1
  6. package/dist-server/service/expenditure-request/expenditure-request-mutation.js +5 -4
  7. package/dist-server/service/expenditure-request/expenditure-request-mutation.js.map +1 -1
  8. package/dist-server/service/expenditure-request/expenditure-request-service.d.ts +2 -0
  9. package/dist-server/service/expenditure-request/expenditure-request-service.js +17 -0
  10. package/dist-server/service/expenditure-request/expenditure-request-service.js.map +1 -1
  11. package/dist-server/service/expenditure-request/index.d.ts +2 -2
  12. package/dist-server/service/financial-statement/index.d.ts +1 -1
  13. package/dist-server/service/index.d.ts +1 -1
  14. package/dist-server/service/recurring-cash-item/recurring-cash-item-mutation.d.ts +0 -1
  15. package/dist-server/service/recurring-cash-item/recurring-cash-item-mutation.js +0 -17
  16. package/dist-server/service/recurring-cash-item/recurring-cash-item-mutation.js.map +1 -1
  17. package/dist-server/service/recurring-cash-item/recurring-cash-item-service.d.ts +2 -9
  18. package/dist-server/service/recurring-cash-item/recurring-cash-item-service.js +2 -39
  19. package/dist-server/service/recurring-cash-item/recurring-cash-item-service.js.map +1 -1
  20. package/dist-server/service/recurring-cash-item/recurring-cash-item-type.js +1 -1
  21. package/dist-server/service/recurring-cash-item/recurring-cash-item-type.js.map +1 -1
  22. package/dist-server/service/recurring-cash-item/recurring-cash-item.d.ts +2 -2
  23. package/dist-server/service/recurring-cash-item/recurring-cash-item.js +5 -5
  24. package/dist-server/service/recurring-cash-item/recurring-cash-item.js.map +1 -1
  25. package/dist-server/service/recurring-cash-item/recurring-schedule.d.ts +6 -18
  26. package/dist-server/service/recurring-cash-item/recurring-schedule.js +13 -26
  27. package/dist-server/service/recurring-cash-item/recurring-schedule.js.map +1 -1
  28. package/dist-server/service/transaction/transaction-history.d.ts +1 -7
  29. package/dist-server/service/transaction/transaction-history.js +0 -25
  30. package/dist-server/service/transaction/transaction-history.js.map +1 -1
  31. package/dist-server/service/transaction/transaction-period.d.ts +0 -8
  32. package/dist-server/service/transaction/transaction-period.js +0 -30
  33. package/dist-server/service/transaction/transaction-period.js.map +1 -1
  34. package/dist-server/service/transaction/transaction-query.d.ts +0 -2
  35. package/dist-server/service/transaction/transaction-query.js +2 -16
  36. package/dist-server/service/transaction/transaction-query.js.map +1 -1
  37. package/dist-server/service/transaction/transaction-service.d.ts +1 -4
  38. package/dist-server/service/transaction/transaction-service.js +6 -31
  39. package/dist-server/service/transaction/transaction-service.js.map +1 -1
  40. package/dist-server/service/transaction/transaction-type.d.ts +1 -15
  41. package/dist-server/service/transaction/transaction-type.js +2 -58
  42. package/dist-server/service/transaction/transaction-type.js.map +1 -1
  43. package/dist-server/service/transaction/transaction.d.ts +6 -15
  44. package/dist-server/service/transaction/transaction.js +13 -52
  45. package/dist-server/service/transaction/transaction.js.map +1 -1
  46. package/dist-server/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +4 -4
  48. package/translations/en.json +4 -5
  49. package/translations/es.json +4 -5
  50. package/translations/ja.json +4 -1
  51. package/translations/ko.json +4 -5
  52. package/translations/ms.json +4 -1
  53. package/translations/zh.json +4 -1
@@ -13,8 +13,8 @@ export declare function aliasesOf(account: Pick<AccountForMatch, 'name' | 'alias
13
13
  */
14
14
  export declare function accountMatcher<T extends AccountForMatch>(accounts: readonly T[]): (texts: readonly (string | null | undefined)[], type?: string | null) => T | undefined;
15
15
  /**
16
- * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a confirmed line by
17
- * its description then its ledger category, a forecast by its description, among the active accounts of its direction
16
+ * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a bank line by
17
+ * its description then its ledger category, among the active accounts of its direction
18
18
  * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)
19
19
  * keeps what it says; a row whose words mean nothing is saved without one.
20
20
  */
@@ -22,10 +22,9 @@ export declare function assignAccounts<T extends Record<string, any>>(tx: Entity
22
22
  id: string;
23
23
  }, inputs: T[]): Promise<T[]>;
24
24
  export interface AccountLinking {
25
- /** Rows given an account, per kind: confirmed transactions, forecasts, recurring items, expenditure request lines. */
25
+ /** Rows given an account, per kind: bank lines, recurring items, expenditure request lines. */
26
26
  linked: {
27
27
  transactions: number;
28
- forecasts: number;
29
28
  recurring: number;
30
29
  expenditureLines: number;
31
30
  };
@@ -34,7 +33,7 @@ export interface AccountLinking {
34
33
  }
35
34
  /**
36
35
  * Give every row without an account the one its words mean: a confirmed transaction by its description then its
37
- * ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its
36
+ * ledger category, a recurring item by its name, an expenditure request line by its
38
37
  * category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed
39
38
  * are left alone.
40
39
  */
@@ -45,36 +45,36 @@ function accountMatcher(accounts) {
45
45
  };
46
46
  }
47
47
  /**
48
- * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a confirmed line by
49
- * its description then its ledger category, a forecast by its description, among the active accounts of its direction
48
+ * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a bank line by
49
+ * its description then its ledger category, among the active accounts of its direction
50
50
  * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)
51
51
  * keeps what it says; a row whose words mean nothing is saved without one.
52
52
  */
53
53
  async function assignAccounts(tx, domain, inputs) {
54
- const textsOf = (row, category) => category === transaction_1.TransactionCategory.FORECAST ? [row.description] : [row.description, row.ledgerCategory];
54
+ const textsOf = (row) => [row.description, row.ledgerCategory];
55
55
  const wants = (row) => row.cuFlag !== '-' && !('account' in row) && (row.description || row.ledgerCategory);
56
56
  if (!inputs.some(wants))
57
57
  return inputs;
58
58
  const accounts = await tx.getRepository(account_1.Account).find({ where: { domain: { id: domain.id }, active: true } });
59
59
  const match = accountMatcher(accounts);
60
60
  const edited = inputs.filter(row => wants(row) && row.id && row.cuFlag !== '+').map(row => row.id);
61
- const stored = new Map((edited.length ? await tx.getRepository(transaction_1.Transaction).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(edited) }, select: ['id', 'type', 'category'] }) : []).map(row => [row.id, row]));
61
+ const stored = new Map((edited.length ? await tx.getRepository(transaction_1.Transaction).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(edited) }, select: ['id', 'type'] }) : []).map(row => [row.id, row]));
62
62
  return inputs.map(row => {
63
63
  if (!wants(row))
64
64
  return row;
65
65
  const before = stored.get(row.id);
66
- const account = match(textsOf(row, row.category ?? before?.category), row.type ?? before?.type);
66
+ const account = match(textsOf(row), row.type ?? before?.type);
67
67
  return account ? { ...row, account: { id: account.id } } : row;
68
68
  });
69
69
  }
70
70
  /**
71
71
  * Give every row without an account the one its words mean: a confirmed transaction by its description then its
72
- * ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its
72
+ * ledger category, a recurring item by its name, an expenditure request line by its
73
73
  * category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed
74
74
  * are left alone.
75
75
  */
76
76
  async function linkAccounts(tx, domain, match, apply) {
77
- const report = { linked: { transactions: 0, forecasts: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] };
77
+ const report = { linked: { transactions: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] };
78
78
  const run = async (label, entity, texts, typeOf, scope = {}) => {
79
79
  const rows = (await tx.getRepository(entity).find({ where: { domain: { id: domain.id }, ...scope, account: (0, typeorm_1.IsNull)() } }));
80
80
  const groups = new Map();
@@ -95,7 +95,6 @@ async function linkAccounts(tx, domain, match, apply) {
95
95
  await tx.getRepository(entity).update(ids.slice(i, i + 500), { account: { id } });
96
96
  };
97
97
  await run('transactions', transaction_1.Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: transaction_1.TransactionCategory.CONFIRMED });
98
- await run('forecasts', transaction_1.Transaction, row => [row.description], row => row.type, { category: transaction_1.TransactionCategory.FORECAST });
99
98
  await run('recurring', recurring_cash_item_1.RecurringCashItem, row => [row.name], row => row.type);
100
99
  await run('expenditureLines', expenditure_request_item_1.ExpenditureRequestItem, row => [row.category, row.description], () => cash_flow_type_1.CashFlowType.EXPENSE);
101
100
  return report;
@@ -1 +1 @@
1
- {"version":3,"file":"account-match.js","sourceRoot":"","sources":["../../../server/service/account/account-match.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In, IsNull } from 'typeorm'\n\nimport { Account } from './account'\n\nimport { CashFlowType } from '../cash-flow-type'\nimport { ExpenditureRequestItem } from '../expenditure-request/expenditure-request-item'\nimport { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\n\n/*\n * Which account some words mean. An account is known by its name and its aliases; the longest one contained in the\n * text wins, among accounts of the row's direction. Two different accounts matching equally long is no answer, and\n * nothing matching is no answer - a row nobody can place stays unplaced, to be listed, not filed under a catch-all.\n */\n\nexport interface AccountForMatch {\n id: string\n name: string\n type: string\n aliases?: string | null\n}\n\nconst key = (text: string | null | undefined) =>\n String(text ?? '')\n .normalize('NFC')\n .replace(/[\\s\\-_.·,()]+/g, '')\n .toLowerCase()\n\n/** An account's name and its aliases (one per line, or comma separated). */\nexport function aliasesOf(account: Pick<AccountForMatch, 'name' | 'aliases'>): string[] {\n return [account.name, ...String(account.aliases ?? '').split(/[\\n,]/)].map(one => one.trim()).filter(Boolean)\n}\n\n/**\n * A function telling which account some words mean. `type` limits it to that direction - an expense is never filed\n * under an income account because a word happens to match.\n */\nexport function accountMatcher<T extends AccountForMatch>(accounts: readonly T[]) {\n const words: { word: string; account: T }[] = []\n for (const account of accounts) for (const alias of aliasesOf(account)) if (key(alias).length >= 2) words.push({ word: key(alias), account })\n words.sort((a, b) => b.word.length - a.word.length)\n\n return (texts: readonly (string | null | undefined)[], type?: string | null): T | undefined => {\n for (const text of texts) {\n const hay = key(text)\n if (!hay) continue\n const hits = words.filter(one => (!type || one.account.type === type) && hay.includes(one.word))\n if (!hits.length) continue\n const longest = hits[0].word.length\n const top = new Set(hits.filter(one => one.word.length === longest).map(one => one.account.id))\n return top.size === 1 ? hits[0].account : undefined\n }\n return undefined\n }\n}\n\n/**\n * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a confirmed line by\n * its description then its ledger category, a forecast by its description, among the active accounts of its direction\n * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)\n * keeps what it says; a row whose words mean nothing is saved without one.\n */\nexport async function assignAccounts<T extends Record<string, any>>(tx: EntityManager, domain: { id: string }, inputs: T[]): Promise<T[]> {\n const textsOf = (row: Record<string, any>, category?: string) =>\n category === TransactionCategory.FORECAST ? [row.description] : [row.description, row.ledgerCategory]\n const wants = (row: Record<string, any>) => row.cuFlag !== '-' && !('account' in row) && (row.description || row.ledgerCategory)\n if (!inputs.some(wants)) return inputs\n\n const accounts = await tx.getRepository(Account).find({ where: { domain: { id: domain.id }, active: true } as any })\n const match = accountMatcher(accounts)\n const edited = inputs.filter(row => wants(row) && row.id && row.cuFlag !== '+').map(row => row.id)\n const stored = new Map(\n (edited.length ? await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(edited) } as any, select: ['id', 'type', 'category'] as any }) : []).map(row => [row.id, row])\n )\n return inputs.map(row => {\n if (!wants(row)) return row\n const before = stored.get(row.id)\n const account = match(textsOf(row, row.category ?? before?.category), row.type ?? before?.type)\n return account ? { ...row, account: { id: account.id } } : row\n })\n}\n\nexport interface AccountLinking {\n /** Rows given an account, per kind: confirmed transactions, forecasts, recurring items, expenditure request lines. */\n linked: { transactions: number; forecasts: number; recurring: number; expenditureLines: number }\n /** The words of confirmed transactions no account matched. */\n unmatched: string[]\n}\n\n/**\n * Give every row without an account the one its words mean: a confirmed transaction by its description then its\n * ledger category, a forecast by its description, a recurring item by its name, an expenditure request line by its\n * category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed\n * are left alone.\n */\nexport async function linkAccounts(\n tx: EntityManager,\n domain: { id: string },\n match: ReturnType<typeof accountMatcher>,\n apply: boolean\n): Promise<AccountLinking> {\n const report: AccountLinking = { linked: { transactions: 0, forecasts: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] }\n\n const run = async <T extends { id: string }>(\n label: keyof AccountLinking['linked'],\n entity: new () => T,\n texts: (row: T) => (string | null | undefined)[],\n typeOf: (row: T) => string | null,\n scope: Record<string, any> = {}\n ) => {\n const rows = (await tx.getRepository(entity as any).find({ where: { domain: { id: domain.id }, ...scope, account: IsNull() } as any })) as T[]\n const groups = new Map<string, string[]>()\n for (const row of rows) {\n const account = match(texts(row), typeOf(row))\n if (!account) {\n if (label === 'transactions') report.unmatched.push(String(texts(row)[0] ?? ''))\n continue\n }\n groups.set(account.id, [...(groups.get(account.id) ?? []), row.id])\n }\n report.linked[label] = [...groups.values()].reduce((sum, ids) => sum + ids.length, 0)\n if (!apply) return\n for (const [id, ids] of groups)\n for (let i = 0; i < ids.length; i += 500) await tx.getRepository(entity as any).update(ids.slice(i, i + 500), { account: { id } } as any)\n }\n\n await run('transactions', Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: TransactionCategory.CONFIRMED })\n await run('forecasts', Transaction, row => [row.description], row => row.type, { category: TransactionCategory.FORECAST })\n await run('recurring', RecurringCashItem, row => [row.name], row => row.type)\n await run('expenditureLines', ExpenditureRequestItem, row => [row.category, row.description], () => CashFlowType.EXPENSE)\n return report\n}\n"]}
1
+ {"version":3,"file":"account-match.js","sourceRoot":"","sources":["../../../server/service/account/account-match.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In, IsNull } from 'typeorm'\n\nimport { Account } from './account'\n\nimport { CashFlowType } from '../cash-flow-type'\nimport { ExpenditureRequestItem } from '../expenditure-request/expenditure-request-item'\nimport { RecurringCashItem } from '../recurring-cash-item/recurring-cash-item'\nimport { Transaction, TransactionCategory } from '../transaction/transaction'\n\n/*\n * Which account some words mean. An account is known by its name and its aliases; the longest one contained in the\n * text wins, among accounts of the row's direction. Two different accounts matching equally long is no answer, and\n * nothing matching is no answer - a row nobody can place stays unplaced, to be listed, not filed under a catch-all.\n */\n\nexport interface AccountForMatch {\n id: string\n name: string\n type: string\n aliases?: string | null\n}\n\nconst key = (text: string | null | undefined) =>\n String(text ?? '')\n .normalize('NFC')\n .replace(/[\\s\\-_.·,()]+/g, '')\n .toLowerCase()\n\n/** An account's name and its aliases (one per line, or comma separated). */\nexport function aliasesOf(account: Pick<AccountForMatch, 'name' | 'aliases'>): string[] {\n return [account.name, ...String(account.aliases ?? '').split(/[\\n,]/)].map(one => one.trim()).filter(Boolean)\n}\n\n/**\n * A function telling which account some words mean. `type` limits it to that direction - an expense is never filed\n * under an income account because a word happens to match.\n */\nexport function accountMatcher<T extends AccountForMatch>(accounts: readonly T[]) {\n const words: { word: string; account: T }[] = []\n for (const account of accounts) for (const alias of aliasesOf(account)) if (key(alias).length >= 2) words.push({ word: key(alias), account })\n words.sort((a, b) => b.word.length - a.word.length)\n\n return (texts: readonly (string | null | undefined)[], type?: string | null): T | undefined => {\n for (const text of texts) {\n const hay = key(text)\n if (!hay) continue\n const hits = words.filter(one => (!type || one.account.type === type) && hay.includes(one.word))\n if (!hits.length) continue\n const longest = hits[0].word.length\n const top = new Set(hits.filter(one => one.word.length === longest).map(one => one.account.id))\n return top.size === 1 ? hits[0].account : undefined\n }\n return undefined\n }\n}\n\n/**\n * Give each row a person writes that names no account the one its words mean (§`accountMatcher`): a bank line by\n * its description then its ledger category, among the active accounts of its direction\n * - the row's own, or the stored one for a change that does not say. A row that says `account` (an account or none)\n * keeps what it says; a row whose words mean nothing is saved without one.\n */\nexport async function assignAccounts<T extends Record<string, any>>(tx: EntityManager, domain: { id: string }, inputs: T[]): Promise<T[]> {\n const textsOf = (row: Record<string, any>) => [row.description, row.ledgerCategory]\n const wants = (row: Record<string, any>) => row.cuFlag !== '-' && !('account' in row) && (row.description || row.ledgerCategory)\n if (!inputs.some(wants)) return inputs\n\n const accounts = await tx.getRepository(Account).find({ where: { domain: { id: domain.id }, active: true } as any })\n const match = accountMatcher(accounts)\n const edited = inputs.filter(row => wants(row) && row.id && row.cuFlag !== '+').map(row => row.id)\n const stored = new Map(\n (edited.length ? await tx.getRepository(Transaction).find({ where: { domain: { id: domain.id }, id: In(edited) } as any, select: ['id', 'type'] as any }) : []).map(row => [row.id, row])\n )\n return inputs.map(row => {\n if (!wants(row)) return row\n const before = stored.get(row.id)\n const account = match(textsOf(row), row.type ?? before?.type)\n return account ? { ...row, account: { id: account.id } } : row\n })\n}\n\nexport interface AccountLinking {\n /** Rows given an account, per kind: bank lines, recurring items, expenditure request lines. */\n linked: { transactions: number; recurring: number; expenditureLines: number }\n /** The words of confirmed transactions no account matched. */\n unmatched: string[]\n}\n\n/**\n * Give every row without an account the one its words mean: a confirmed transaction by its description then its\n * ledger category, a recurring item by its name, an expenditure request line by its\n * category then its description, among the expense accounts. `apply` false writes nothing. Rows a person already filed\n * are left alone.\n */\nexport async function linkAccounts(\n tx: EntityManager,\n domain: { id: string },\n match: ReturnType<typeof accountMatcher>,\n apply: boolean\n): Promise<AccountLinking> {\n const report: AccountLinking = { linked: { transactions: 0, recurring: 0, expenditureLines: 0 }, unmatched: [] }\n\n const run = async <T extends { id: string }>(\n label: keyof AccountLinking['linked'],\n entity: new () => T,\n texts: (row: T) => (string | null | undefined)[],\n typeOf: (row: T) => string | null,\n scope: Record<string, any> = {}\n ) => {\n const rows = (await tx.getRepository(entity as any).find({ where: { domain: { id: domain.id }, ...scope, account: IsNull() } as any })) as T[]\n const groups = new Map<string, string[]>()\n for (const row of rows) {\n const account = match(texts(row), typeOf(row))\n if (!account) {\n if (label === 'transactions') report.unmatched.push(String(texts(row)[0] ?? ''))\n continue\n }\n groups.set(account.id, [...(groups.get(account.id) ?? []), row.id])\n }\n report.linked[label] = [...groups.values()].reduce((sum, ids) => sum + ids.length, 0)\n if (!apply) return\n for (const [id, ids] of groups)\n for (let i = 0; i < ids.length; i += 500) await tx.getRepository(entity as any).update(ids.slice(i, i + 500), { account: { id } } as any)\n }\n\n await run('transactions', Transaction, row => [row.description, row.ledgerCategory], row => row.type, { category: TransactionCategory.CONFIRMED })\n await run('recurring', RecurringCashItem, row => [row.name], row => row.type)\n await run('expenditureLines', ExpenditureRequestItem, row => [row.category, row.description], () => CashFlowType.EXPENSE)\n return report\n}\n"]}
@@ -4,7 +4,7 @@ import { ExpenditureRequestItem } from './expenditure-request-item';
4
4
  import { ExpenditurePaymentInput, ExpenditureRequestInput, ExpenditureRequestItemPatch, NewExpenditureLine } from './expenditure-request-type';
5
5
  export declare class ExpenditureRequestMutation {
6
6
  /** Write a request and its lines. A new request is the caller's; an existing one only its requester's, while editable. */
7
- saveExpenditureRequest(request: ExpenditureRequestInput, items: ExpenditureRequestItemPatch[], context: ResolverContext): Promise<ExpenditureRequest>;
7
+ saveExpenditureRequest(request: ExpenditureRequestInput, items: ExpenditureRequestItemPatch[], unpaidInvoicesOnly: boolean, context: ResolverContext): Promise<ExpenditureRequest>;
8
8
  /** One line from the phone - it joins this month's editable reimbursement, or starts one. */
9
9
  addExpenditureLine(line: NewExpenditureLine, context: ResolverContext): Promise<ExpenditureRequest>;
10
10
  saveExpenditureLineFiles(itemId: string, attach: FileUpload[], remove: string[], context: ResolverContext): Promise<ExpenditureRequestItem>;
@@ -12,10 +12,10 @@ const expenditure_request_service_1 = require("./expenditure-request-service");
12
12
  const expenditure_request_type_1 = require("./expenditure-request-type");
13
13
  let ExpenditureRequestMutation = class ExpenditureRequestMutation {
14
14
  /** Write a request and its lines. A new request is the caller's; an existing one only its requester's, while editable. */
15
- async saveExpenditureRequest(request, items, context) {
15
+ async saveExpenditureRequest(request, items, unpaidInvoicesOnly, context) {
16
16
  const { domain, user, tx, unsafeIP, prohibitedPrivileges } = context.state;
17
17
  const taxInvoices = await (0, auth_base_1.checkPermission)({ category: 'tax-invoice', privilege: 'query', owner: false }, user, domain, unsafeIP, prohibitedPrivileges);
18
- return await (0, expenditure_request_service_1.saveRequest)(tx, domain, user, request, items ?? [], { taxInvoices });
18
+ return await (0, expenditure_request_service_1.saveRequest)(tx, domain, user, request, items ?? [], { taxInvoices, unpaidInvoicesOnly: !!unpaidInvoicesOnly });
19
19
  }
20
20
  /** One line from the phone - it joins this month's editable reimbursement, or starts one. */
21
21
  async addExpenditureLine(line, context) {
@@ -56,9 +56,10 @@ tslib_1.__decorate([
56
56
  (0, type_graphql_1.Mutation)(returns => expenditure_request_1.ExpenditureRequest, { description: 'Save an expenditure request and its lines' }),
57
57
  tslib_1.__param(0, (0, type_graphql_1.Arg)('request', type => expenditure_request_type_1.ExpenditureRequestInput)),
58
58
  tslib_1.__param(1, (0, type_graphql_1.Arg)('items', type => [expenditure_request_type_1.ExpenditureRequestItemPatch], { nullable: true })),
59
- tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
59
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('unpaidInvoicesOnly', type => Boolean, { nullable: true })),
60
+ tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
60
61
  tslib_1.__metadata("design:type", Function),
61
- tslib_1.__metadata("design:paramtypes", [expenditure_request_type_1.ExpenditureRequestInput, Array, Object]),
62
+ tslib_1.__metadata("design:paramtypes", [expenditure_request_type_1.ExpenditureRequestInput, Array, Boolean, Object]),
62
63
  tslib_1.__metadata("design:returntype", Promise)
63
64
  ], ExpenditureRequestMutation.prototype, "saveExpenditureRequest", null);
64
65
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"expenditure-request-mutation.js","sourceRoot":"","sources":["../../../server/service/expenditure-request/expenditure-request-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Float, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport type { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\n\nimport { checkPermission } from '@things-factory/auth-base'\nimport { SpeakRefusals } from '@things-factory/shell'\n\nimport { ExpenditureRequest } from './expenditure-request'\nimport { ExpenditureRequestItem } from './expenditure-request-item'\nimport { addLine, deleteRequest, payRequests, payWithLine, saveLineFiles, saveRequest, submitRequest, withdrawRequest } from './expenditure-request-service'\nimport { ExpenditurePaymentInput, ExpenditureRequestInput, ExpenditureRequestItemPatch, NewExpenditureLine } from './expenditure-request-type'\n\n@UseMiddleware(SpeakRefusals)\n@Resolver(ExpenditureRequest)\nexport class ExpenditureRequestMutation {\n /** Write a request and its lines. A new request is the caller's; an existing one only its requester's, while editable. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Save an expenditure request and its lines' })\n async saveExpenditureRequest(\n @Arg('request', type => ExpenditureRequestInput) request: ExpenditureRequestInput,\n @Arg('items', type => [ExpenditureRequestItemPatch], { nullable: true }) items: ExpenditureRequestItemPatch[],\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest> {\n const { domain, user, tx, unsafeIP, prohibitedPrivileges } = context.state\n const taxInvoices = await checkPermission({ category: 'tax-invoice', privilege: 'query', owner: false }, user, domain, unsafeIP, prohibitedPrivileges)\n return await saveRequest(tx, domain, user, request, items ?? [], { taxInvoices })\n }\n\n /** One line from the phone - it joins this month's editable reimbursement, or starts one. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: \"Add one expense line to the caller's reimbursement of that month\" })\n async addExpenditureLine(@Arg('line', type => NewExpenditureLine) line: NewExpenditureLine, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await addLine(tx, domain, user, line, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequestItem, { description: \"Attach files to a line and take others off it - the line's receipts and papers\" })\n async saveExpenditureLineFiles(\n @Arg('itemId') itemId: string,\n @Arg('attach', type => [GraphQLUpload], { nullable: true }) attach: FileUpload[],\n @Arg('remove', type => [String], { nullable: true }) remove: string[],\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequestItem> {\n const { domain, user, tx } = context.state\n return await saveLineFiles(tx, domain, user, itemId, { attach: attach ?? [], remove: remove ?? [] }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Put an expenditure request up for approval (상신)' })\n async submitExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await submitRequest(tx, domain, user, id, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Take a submitted request back from its approvers (회수)' })\n async withdrawExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await withdrawRequest(tx, domain, user, id, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'Delete an editable expenditure request of the caller' })\n async deleteExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, tx } = context.state\n return await deleteRequest(tx, domain, user, id)\n }\n\n /** Finance paid approved requests at the bank; each becomes a withdrawal that the next statement import matches. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"expenditure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [ExpenditureRequest], { description: 'Record that approved expenditure requests were paid' })\n async payExpenditureRequests(\n @Arg('ids', type => [String]) ids: string[],\n @Arg('payment', type => ExpenditurePaymentInput) payment: ExpenditurePaymentInput,\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest[]> {\n const { domain, user, tx } = context.state\n return await payRequests(tx, domain, user, ids, payment)\n }\n\n /** Pay a request with a withdrawal already in the books - a bank statement brought it in first. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"expenditure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Put a withdrawal already in the books on an approved expenditure request' })\n async payExpenditureRequestWithLine(\n @Arg('id') id: string,\n @Arg('transactionId') transactionId: string,\n @Arg('amount', type => Float, { nullable: true }) amount: number,\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await payWithLine(tx, domain, user, id, transactionId, amount)\n }\n}\n"]}
1
+ {"version":3,"file":"expenditure-request-mutation.js","sourceRoot":"","sources":["../../../server/service/expenditure-request/expenditure-request-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Float, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport type { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\n\nimport { checkPermission } from '@things-factory/auth-base'\nimport { SpeakRefusals } from '@things-factory/shell'\n\nimport { ExpenditureRequest } from './expenditure-request'\nimport { ExpenditureRequestItem } from './expenditure-request-item'\nimport { addLine, deleteRequest, payRequests, payWithLine, saveLineFiles, saveRequest, submitRequest, withdrawRequest } from './expenditure-request-service'\nimport { ExpenditurePaymentInput, ExpenditureRequestInput, ExpenditureRequestItemPatch, NewExpenditureLine } from './expenditure-request-type'\n\n@UseMiddleware(SpeakRefusals)\n@Resolver(ExpenditureRequest)\nexport class ExpenditureRequestMutation {\n /** Write a request and its lines. A new request is the caller's; an existing one only its requester's, while editable. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Save an expenditure request and its lines' })\n async saveExpenditureRequest(\n @Arg('request', type => ExpenditureRequestInput) request: ExpenditureRequestInput,\n @Arg('items', type => [ExpenditureRequestItemPatch], { nullable: true }) items: ExpenditureRequestItemPatch[],\n @Arg('unpaidInvoicesOnly', type => Boolean, { nullable: true }) unpaidInvoicesOnly: boolean,\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest> {\n const { domain, user, tx, unsafeIP, prohibitedPrivileges } = context.state\n const taxInvoices = await checkPermission({ category: 'tax-invoice', privilege: 'query', owner: false }, user, domain, unsafeIP, prohibitedPrivileges)\n return await saveRequest(tx, domain, user, request, items ?? [], { taxInvoices, unpaidInvoicesOnly: !!unpaidInvoicesOnly })\n }\n\n /** One line from the phone - it joins this month's editable reimbursement, or starts one. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: \"Add one expense line to the caller's reimbursement of that month\" })\n async addExpenditureLine(@Arg('line', type => NewExpenditureLine) line: NewExpenditureLine, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await addLine(tx, domain, user, line, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequestItem, { description: \"Attach files to a line and take others off it - the line's receipts and papers\" })\n async saveExpenditureLineFiles(\n @Arg('itemId') itemId: string,\n @Arg('attach', type => [GraphQLUpload], { nullable: true }) attach: FileUpload[],\n @Arg('remove', type => [String], { nullable: true }) remove: string[],\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequestItem> {\n const { domain, user, tx } = context.state\n return await saveLineFiles(tx, domain, user, itemId, { attach: attach ?? [], remove: remove ?? [] }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Put an expenditure request up for approval (상신)' })\n async submitExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await submitRequest(tx, domain, user, id, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Take a submitted request back from its approvers (회수)' })\n async withdrawExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await withdrawRequest(tx, domain, user, id, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"accounting-self\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'Delete an editable expenditure request of the caller' })\n async deleteExpenditureRequest(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, tx } = context.state\n return await deleteRequest(tx, domain, user, id)\n }\n\n /** Finance paid approved requests at the bank; each becomes a withdrawal that the next statement import matches. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"expenditure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [ExpenditureRequest], { description: 'Record that approved expenditure requests were paid' })\n async payExpenditureRequests(\n @Arg('ids', type => [String]) ids: string[],\n @Arg('payment', type => ExpenditurePaymentInput) payment: ExpenditurePaymentInput,\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest[]> {\n const { domain, user, tx } = context.state\n return await payRequests(tx, domain, user, ids, payment)\n }\n\n /** Pay a request with a withdrawal already in the books - a bank statement brought it in first. */\n @Directive('@transaction')\n @Directive('@privilege(category: \"expenditure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ExpenditureRequest, { description: 'Put a withdrawal already in the books on an approved expenditure request' })\n async payExpenditureRequestWithLine(\n @Arg('id') id: string,\n @Arg('transactionId') transactionId: string,\n @Arg('amount', type => Float, { nullable: true }) amount: number,\n @Ctx() context: ResolverContext\n ): Promise<ExpenditureRequest> {\n const { domain, user, tx } = context.state\n return await payWithLine(tx, domain, user, id, transactionId, amount)\n }\n}\n"]}
@@ -50,6 +50,8 @@ export declare function ownRequest(tx: EntityManager, id: string, domain: {
50
50
  */
51
51
  export interface Citing {
52
52
  taxInvoices: boolean;
53
+ /** Bulk preparation refuses partial/complete payments and amended evidence for individual review. */
54
+ unpaidInvoicesOnly?: boolean;
53
55
  }
54
56
  /**
55
57
  * Save a request and its lines in one go - the head, then the lines. A new request belongs to the caller; an existing
@@ -131,12 +131,29 @@ async function saveItems(tx, domain, user, request, patches, citing) {
131
131
  if ('taxInvoice' in patch) {
132
132
  if (patch.taxInvoice?.id && !citing.taxInvoices)
133
133
  throw new shell_1.Refusal('expenditure-invoice-not-readable', {}, 'citing a tax invoice needs the tax-invoice ledger');
134
+ // Serialize citation checks on PostgreSQL; SQLite already has the request write in this transaction.
135
+ if (patch.taxInvoice?.id && tx.connection.options.type === 'postgres') {
136
+ await tx.getRepository(tax_invoice_1.TaxInvoice).findOne({ where: { id: patch.taxInvoice.id, domain: { id: domain.id } }, lock: { mode: 'pessimistic_write' } });
137
+ }
134
138
  const invoice = await inDomain(tx, tax_invoice_1.TaxInvoice, domain, patch.taxInvoice, 'taxInvoice');
135
139
  if (invoice) {
136
140
  if (request.kind !== expenditure_request_1.ExpenditureKind.VENDOR)
137
141
  throw new shell_1.Refusal('expenditure-invoice-on-reimbursement', {}, 'a reimbursement cites receipts, not tax invoices');
138
142
  if (invoice.category !== tax_invoice_1.Category.IMPORT)
139
143
  throw new shell_1.Refusal('expenditure-invoice-not-purchase', {}, 'cite a purchase tax invoice');
144
+ if (citing.unpaidInvoicesOnly) {
145
+ const party = await tx.getRepository(biz_base_1.Counterparty).findOne({ where: { id: request.counterpartyId, domain: { id: domain.id } } });
146
+ const digits = (value) => String(value ?? '').replace(/\D/g, '');
147
+ if (!party || !digits(party.businessNumber) || digits(party.businessNumber) !== digits(invoice.supplierBusinessRegistrationNumber))
148
+ throw new shell_1.Refusal('expenditure-bulk-party', {}, 'The supplier must match the selected counterparty; review this invoice individually');
149
+ const payments = await tx.getRepository(payment_1.Payment).find({ where: { domain: { id: domain.id }, targetType: document_targets_1.TAX_INVOICE_TARGET, targetId: invoice.id } });
150
+ if (payments.length)
151
+ throw new shell_1.Refusal('expenditure-bulk-paid', {}, 'This invoice already has payment links; review partial or completed payments individually');
152
+ const negatives = await tx.getRepository(tax_invoice_1.TaxInvoice).find({ where: { domain: { id: domain.id }, totalAmount: (0, typeorm_1.LessThan)(0) } });
153
+ const amended = negatives.some(one => one.originalApprovalNo === invoice.approvalNo || (invoice.approvalNo && [one.remark0, one.remark1, one.remark2].some(text => text?.includes(invoice.approvalNo))));
154
+ if (!(Number(invoice.totalAmount) > 0) || invoice.originalApprovalNo || invoice.invoiceType?.startsWith('02') || amended)
155
+ throw new shell_1.Refusal('expenditure-bulk-amended', {}, 'Review amended or non-positive invoices individually before requesting payment');
156
+ }
140
157
  const elsewhere = await repo.find({ where: { domain: { id: domain.id }, taxInvoice: { id: invoice.id } }, relations: ['request'] });
141
158
  if (elsewhere.some(one => one.id !== current?.id && one.request && !one.request.deletedAt))
142
159
  throw new shell_1.Refusal('expenditure-invoice-cited-twice', { issueDate: invoice.issueDate }, 'another request already pays this tax invoice');