@things-factory/biz-base 10.1.183 → 10.1.192

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/biz-base",
3
- "version": "10.1.183",
3
+ "version": "10.1.192",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "exports": {
@@ -41,8 +41,8 @@
41
41
  "test": "npm run build && node --test tests/*.test.mjs tests/*.test.cjs"
42
42
  },
43
43
  "dependencies": {
44
- "@things-factory/attachment-base": "^10.1.183",
45
- "@things-factory/auth-base": "^10.1.183"
44
+ "@things-factory/attachment-base": "^10.1.192",
45
+ "@things-factory/auth-base": "^10.1.192"
46
46
  },
47
- "gitHead": "c36684e0db8353f5f4523ed55111df3154bae19e"
47
+ "gitHead": "c44fe3ae4646dc4ffad7947785322acdb84c7374"
48
48
  }
@@ -0,0 +1,105 @@
1
+ import { test, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { createRequire } from 'node:module'
4
+ import { readFileSync } from 'node:fs'
5
+
6
+ /*
7
+ * Choosing a counterparty without reading it (ADR-0139): `counterparty-published` opens what points at a counterparty -
8
+ * its name, kind and business number, an account's bank and last four digits - and nothing else, however it is asked.
9
+ * An in-memory SQLite source; no application database is opened.
10
+ */
11
+ const require = createRequire(import.meta.url)
12
+ const { DataSource } = require('typeorm')
13
+ const shell = require('@things-factory/shell')
14
+ const auth = require('@things-factory/auth-base')
15
+ const biz = require('../dist-server/index.js')
16
+
17
+ let db, domain, other, user, hanbit
18
+ before(async () => {
19
+ db = await new DataSource({
20
+ type: 'sqlite',
21
+ database: ':memory:',
22
+ synchronize: true,
23
+ entities: [...new Set([...shell.entities, ...auth.entities, ...biz.entities])],
24
+ namingStrategy: new shell.NamingStrategy()
25
+ }).initialize()
26
+ domain = await db.getRepository(shell.Domain).save({ name: 'one', subdomain: 'published-one' })
27
+ other = await db.getRepository(shell.Domain).save({ name: 'two', subdomain: 'published-two' })
28
+ user = await db.getRepository(auth.User).save({ name: 'clerk', email: 'clerk@example.test', status: 'activated', domains: [domain] })
29
+ ;[hanbit] = await db.transaction(tx =>
30
+ biz.saveCounterparties(tx, domain, user, [
31
+ { name: '(주)한빛', businessNumber: '123-45-67890', representative: '김비밀', address: '서울 어딘가', notes: '연체주의 메모' },
32
+ { name: '쉬는 곳', active: false },
33
+ ...Array.from({ length: 60 }, (_, i) => ({ name: `거래처 ${String(i).padStart(2, '0')}` }))
34
+ ])
35
+ )
36
+ await db.transaction(tx => biz.saveCounterparties(tx, other, user, [{ name: '(주)한빛 남의 도메인' }]))
37
+ await db.transaction(tx =>
38
+ biz.saveCounterpartyBankAccounts(tx, domain, user, [{ counterpartyId: hanbit.id, bankName: '기업은행', accountNumber: '026-123456-01-017', holderName: '(주)한빛' }])
39
+ )
40
+ })
41
+
42
+ after(async () => {
43
+ await db?.destroy()
44
+ })
45
+
46
+ const choices = (search, pagination, into = domain) => biz.counterpartyChoicesOf(db.getRepository(biz.Counterparty), into, { search, pagination })
47
+
48
+ test('a choice is which counterparty, nothing about it', async () => {
49
+ const { items } = await choices('한빛')
50
+ assert.equal(items.length, 1, 'the domain\'s own, not another domain\'s')
51
+ assert.deepEqual(Object.keys(items[0]).sort(), ['businessNumber', 'id', 'kind', 'name'])
52
+ assert.equal(items[0].businessNumber, '123-45-67890')
53
+ })
54
+
55
+ test('★ a closed field is not searched - a match would tell what it says', async () => {
56
+ for (const word of ['김비밀', '연체주의', '어딘가']) assert.equal((await choices(word)).total, 0, word)
57
+ assert.equal((await choices('123-45-67890')).total, 1, 'the business number is searched - it is shown')
58
+ })
59
+
60
+ test('★ a filter on a closed field is refused - a picker sends filters, and only the shown fields take one', async () => {
61
+ const filtered = filters => biz.counterpartyChoicesOf(db.getRepository(biz.Counterparty), domain, { filters })
62
+ for (const name of ['notes', 'representative', 'address'])
63
+ await assert.rejects(filtered([{ name, operator: 'eq', value: 'x' }]), new RegExp(`filtering by "${name}" is not allowed`), name)
64
+ assert.equal((await filtered([{ name: 'name', operator: 'i_like', value: '한빛' }])).total, 1)
65
+ })
66
+
67
+ test('an inactive counterparty is not offered, and a page is capped', async () => {
68
+ assert.equal((await choices('쉬는 곳')).total, 0)
69
+ const page = await choices(undefined, { page: 1, limit: 500 })
70
+ assert.equal(page.items.length, biz.CHOICE_LIMIT)
71
+ assert.equal(page.total, 61)
72
+ })
73
+
74
+ test("★ an account to pick is its bank and the number's last four digits", async () => {
75
+ const accounts = await biz.counterpartyAccountChoicesOf(db.getRepository(biz.CounterpartyBankAccount), domain, hanbit.id)
76
+ assert.deepEqual(accounts.map(one => Object.keys(one).sort()), [['accountTail', 'bankName', 'id']])
77
+ assert.deepEqual([accounts[0].bankName, accounts[0].accountTail], ['기업은행', '1017'])
78
+ assert.deepEqual(await biz.counterpartyAccountChoicesOf(db.getRepository(biz.CounterpartyBankAccount), other, hanbit.id), [], 'not from another domain')
79
+ })
80
+
81
+ const source = file => readFileSync(new URL(`../server/service/counterparty/${file}`, import.meta.url), 'utf8')
82
+ const guarded = (text, field) => new RegExp(`@privilege\\(category: "counterparty", privilege: "query"[^\\n]*\\n(?:(?! \\w+[?!]?:) [^\\n]*\\n)*? ${field}[?!]?:`).test(text)
83
+
84
+ test("★ the fields past what points at a counterparty are its keeper's, on the declaration", () => {
85
+ const counterparty = source('counterparty.ts')
86
+ for (const field of ['aliases', 'branchNumber', 'representative', 'address', 'billingAddress', 'businessType', 'businessItem', 'zeroRated', 'notes']) assert.ok(guarded(counterparty, field), field)
87
+ for (const field of ['name', 'kind', 'businessNumber', 'active']) assert.ok(!guarded(counterparty, field), `${field} stays open`)
88
+ const account = source('counterparty-bank-account.ts')
89
+ for (const field of ['accountNumber', 'holderName', 'notes']) assert.ok(guarded(account, field), `account ${field}`)
90
+ assert.ok(!guarded(account, 'bankName'), 'the bank stays open')
91
+ const files = source('counterparty-bank-account-query.ts')
92
+ for (const field of ['passbookCopy', 'businessRegistration', 'bankAccountCount'])
93
+ assert.match(files, new RegExp(`@privilege\\(category: "counterparty", privilege: "query"[^\\n]*\\n @FieldResolver\\([^\\n]*\\n async ${field}\\(`), field)
94
+ })
95
+
96
+ test('a choice type has no relation to follow, and both doors read through one page function', () => {
97
+ const types = source('counterparty-choice.ts')
98
+ const fieldTypes = [...types.matchAll(/@Field\(type => \[?(\w+)\]?/g)].map(([, type]) => type)
99
+ assert.deepEqual([...new Set(fieldTypes)].sort(), ['CounterpartyChoice', 'CounterpartyKind', 'ID', 'Int'])
100
+ const query = source('counterparty-query.ts')
101
+ assert.match(query, /counterparties\([\s\S]*?counterpartyPage\(getRepository\(Counterparty\), context\.state\.domain, params, \{ searchables: COUNTERPARTY_SEARCHABLES \}\)/)
102
+ assert.match(types, /counterpartyChoicesOf[\s\S]*?counterpartyPage\(repository, domain, [\s\S]*?filterables: CHOICE_FILTERABLES[\s\S]*?maxLimit: CHOICE_LIMIT/)
103
+ assert.match(query, /@privilege\(category: "counterparty-published", privilege: "query"[^\n]*\n @Query\([^\n]*\n async counterpartyChoices\(/)
104
+ assert.match(query, /@privilege\(category: "counterparty-published", privilege: "query"[^\n]*\n @Query\([^\n]*\n async counterpartyAccountChoices\(/)
105
+ })