diff --git a/apps/condo/domains/banking/constants.js b/apps/condo/domains/banking/constants.js index d568106ef23..f752abf88cd 100644 --- a/apps/condo/domains/banking/constants.js +++ b/apps/condo/domains/banking/constants.js @@ -42,6 +42,13 @@ const TRANSACTIONS_NOT_ADDED = { messageForUser: 'api.banking.BankSyncTask.TRANSACTIONS_NOT_ADDED', } +const CLASSIFICATION_CODE_IS_INVALID = 'CLASSIFICATION_CODE_IS_INVALID' +const NUMBER_IS_INVALID = 'NUMBER_IS_INVALID' +const ROUTING_NUMBER_IS_INVALID = 'ROUTING_NUMBER_IS_INVALID' +const TIN_IS_INVALID = 'TIN_IS_INVALID' +const INTEGRATION_REASSIGNMENT_NOT_ALLOWED = 'INTEGRATION_REASSIGNMENT_NOT_ALLOWED' +const BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED = 'BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED' + module.exports = { BANK_INTEGRATION_IDS, _1C_CLIENT_BANK_EXCHANGE, @@ -56,4 +63,10 @@ module.exports = { INVALID_DATE, WRONG_INTEGRATION, TRANSACTIONS_NOT_ADDED, + CLASSIFICATION_CODE_IS_INVALID, + NUMBER_IS_INVALID, + ROUTING_NUMBER_IS_INVALID, + TIN_IS_INVALID, + INTEGRATION_REASSIGNMENT_NOT_ALLOWED, + BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED, } diff --git a/apps/condo/domains/banking/schema/BankAccount.js b/apps/condo/domains/banking/schema/BankAccount.js index f02c296f097..7a5081ce316 100644 --- a/apps/condo/domains/banking/schema/BankAccount.js +++ b/apps/condo/domains/banking/schema/BankAccount.js @@ -2,17 +2,23 @@ * Generated by `createschema banking.BankAccount 'organization:Relationship:Organization:CASCADE; tin:Text; country:Text; routingNumber:Text; number:Text; currency:Text; approvedAt?:DateTimeUtc; approvedBy?:Text; importId?:Text; territoryCode?:Text; bankName?:Text; meta?:Json; tinMeta?:Json; routingNumberMeta?:Json'` */ -const { Text, DateTimeUtc, Select, Relationship, Virtual, Checkbox } = require('@keystonejs/fields') -const dayjs = require('dayjs') +const { Text, DateTimeUtc, Select, Relationship } = require('@keystonejs/fields') const { get } = require('lodash') +const { GQLError, GQLErrorCode: { BAD_USER_INPUT } } = require('@open-condo/keystone/errors') const { Json } = require('@open-condo/keystone/fields') const { historical, versioned, uuided, tracked, softDeleted, dvAndSender } = require('@open-condo/keystone/plugins') const { GQLListSchema } = require('@open-condo/keystone/schema') const { getById, find } = require('@open-condo/keystone/schema') const access = require('@condo/domains/banking/access/BankAccount') -const { BankTransaction, BankContractorAccount } = require('@condo/domains/banking/utils/serverSchema') +const { CLASSIFICATION_CODE_IS_INVALID, + ROUTING_NUMBER_IS_INVALID, + NUMBER_IS_INVALID, + TIN_IS_INVALID, + INTEGRATION_REASSIGNMENT_NOT_ALLOWED, + BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED } = require('@condo/domains/banking/constants') +const { validateClassificationCode } = require('@condo/domains/banking/utils/validate/classificationCode.utils') const { validateNumber } = require('@condo/domains/banking/utils/validate/number.utils') const { validateRoutingNumber } = require('@condo/domains/banking/utils/validate/routingNumber.utils') const { validateTin } = require('@condo/domains/banking/utils/validate/tin.utils') @@ -20,7 +26,12 @@ const { COUNTRIES } = require('@condo/domains/common/constants/countries') const { IMPORT_ID_FIELD, CURRENCY_CODE_FIELD } = require('@condo/domains/common/schema/fields') const { ORGANIZATION_OWNED_FIELD } = require('@condo/domains/organization/schema/fields') -const getTotalAmount = (transactions) => transactions.reduce((amount, transaction) => amount + parseFloat(transaction.amount), 0) +const getError = (type, message, field, code = BAD_USER_INPUT) => ({ + code, + type, + message, + variable: ['data', field], +}) const BankAccount = new GQLListSchema('BankAccount', { schemaDoc: 'Bank account, that will have transactions, pulled from various integrated data sources', @@ -33,15 +44,25 @@ const BankAccount = new GQLListSchema('BankAccount', { ref: 'BankIntegrationAccountContext', kmigratorOptions: { null: true, on_delete: 'models.PROTECT' }, hooks: { - validateInput: async ({ existingItem, resolvedData, addFieldValidationError, operation }) => { + validateInput: async ({ context, existingItem, resolvedData, operation }) => { if (operation === 'update' && existingItem.integrationContext) { - return addFieldValidationError(`Integration reassignment is not allowed for BankAccount with id="${existingItem.id}"`) + throw new GQLError(getError( + INTEGRATION_REASSIGNMENT_NOT_ALLOWED, + `Integration reassignment is not allowed for BankAccount with id="${existingItem.id}"`, + 'integrationContext'), + context, + ) } const resolvedFields = { ...existingItem, ...resolvedData } const bankIntegrationAccountContext = await getById('BankIntegrationAccountContext', get(resolvedFields, 'integrationContext')) const alreadyConnectedBankAccounts = await find('BankAccount', { integrationContext: { id: bankIntegrationAccountContext.id } }) if (alreadyConnectedBankAccounts.length > 0) { - return addFieldValidationError(`Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${alreadyConnectedBankAccounts[0].id}")`) + throw new GQLError(getError( + BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED, + `Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${alreadyConnectedBankAccounts[0].id}")`, + 'integrationContext'), + context, + ) } }, }, @@ -59,7 +80,7 @@ const BankAccount = new GQLListSchema('BankAccount', { type: Text, isRequired: true, hooks: { - validateInput: ({ existingItem, resolvedData, addFieldValidationError }) => { + validateInput: ({ context, existingItem, resolvedData }) => { const newItem = { ...existingItem, ...resolvedData } const country = get(newItem, 'country') @@ -68,7 +89,7 @@ const BankAccount = new GQLListSchema('BankAccount', { const { result, errors } = validateTin(tin, country) if ( !result ) { - addFieldValidationError(errors[0]) + throw new GQLError(getError(TIN_IS_INVALID, errors[0], 'tin'), context) } }, }, @@ -92,7 +113,7 @@ const BankAccount = new GQLListSchema('BankAccount', { type: Text, isRequired: true, hooks: { - validateInput: ({ existingItem, resolvedData, addFieldValidationError }) => { + validateInput: ({ context, existingItem, resolvedData }) => { const newItem = { ...existingItem, ...resolvedData } const country = get(newItem, 'country') @@ -101,7 +122,7 @@ const BankAccount = new GQLListSchema('BankAccount', { const { result, errors } = validateRoutingNumber(routingNumber, country) if ( !result ) { - addFieldValidationError(errors[0]) + throw new GQLError(getError(ROUTING_NUMBER_IS_INVALID, errors[0], 'routingNumber'), context) } }, }, @@ -118,7 +139,7 @@ const BankAccount = new GQLListSchema('BankAccount', { type: Text, isRequired: true, hooks: { - validateInput: ({ existingItem, resolvedData, addFieldValidationError }) => { + validateInput: ({ context, existingItem, resolvedData }) => { const newItem = { ...existingItem, ...resolvedData } const country = get(newItem, 'country') @@ -128,7 +149,7 @@ const BankAccount = new GQLListSchema('BankAccount', { const { result, errors } = validateNumber(number, routingNumber, country) if ( !result ) { - addFieldValidationError(errors[0]) + throw new GQLError(getError(NUMBER_IS_INVALID, errors[0], 'number'), context) } }, }, @@ -179,6 +200,26 @@ const BankAccount = new GQLListSchema('BankAccount', { type: Json, isRequired: false, }, + + classificationCode: { + schemaDoc: 'Budget classification code, used for state-funded organizations', + type: Text, + isRequired: false, + hooks: { + validateInput: ({ context, existingItem, resolvedData }) => { + const newItem = { ...existingItem, ...resolvedData } + + const country = get(newItem, 'country') + const code = get(newItem, 'classificationCode') + + const { result, errors } = validateClassificationCode(code, country) + + if ( !result ) { + throw new GQLError(getError(CLASSIFICATION_CODE_IS_INVALID, errors[0], 'classificationCode'), context) + } + }, + }, + }, }, plugins: [uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), historical()], access: { diff --git a/apps/condo/domains/banking/schema/BankAccount.test.js b/apps/condo/domains/banking/schema/BankAccount.test.js index 87a520540d4..2f4638a7a50 100644 --- a/apps/condo/domains/banking/schema/BankAccount.test.js +++ b/apps/condo/domains/banking/schema/BankAccount.test.js @@ -8,9 +8,8 @@ const { makeClient, makeLoggedInAdminClient, UUID_RE, - expectToThrowValidationFailureError, expectToThrowUniqueConstraintViolationError, - expectValuesOfCommonFields, + expectValuesOfCommonFields, expectToThrowGQLError, } = require('@open-condo/keystone/test.utils') const { expectToThrowAuthenticationErrorToObj, expectToThrowAuthenticationErrorToObjects, @@ -570,11 +569,17 @@ describe('BankAccount', () => { const [anotherBankAccount] = await createTestBankAccount(adminClient, organization, { integrationContext: { connect: { id: bankIntegrationAccountContext.id } }, }) - await expectToThrowValidationFailureError(async () => { + + await expectToThrowGQLError(async () => { await createTestBankAccount(adminClient, organization, { integrationContext: { connect: { id: anotherBankAccount.integrationContext.id } }, }) - }, `Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${anotherBankAccount.id}")`) + }, { + code: 'BAD_USER_INPUT', + type: 'BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED', + message: `Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${anotherBankAccount.id}")`, + variable: ['data', 'integrationContext'], + }) }) test('can\'t connect existing BankAccount without BankIntegrationAccountContext to BankIntegrationAccountContext used by another BankAccount', async () => { @@ -584,14 +589,18 @@ describe('BankAccount', () => { const [anotherBankAccount] = await createTestBankAccount(adminClient, organization, { integrationContext: { connect: { id: anotherBankIntegrationAccountContext.id } }, }) - await expectToThrowValidationFailureError(async () => { + await expectToThrowGQLError(async () => { await updateTestBankAccount(adminClient, bankAccount.id, { integrationContext: { connect: { id: anotherBankIntegrationAccountContext.id }, }, }) - }, `Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${anotherBankAccount.id}")`) - + }, { + code: 'BAD_USER_INPUT', + type: 'BANK_INTEGRATION_ACCOUNT_CONTEXT_ALREADY_USED', + message: `Cannot connect to BankIntegrationAccountContext, used by another BankAccount(id="${anotherBankAccount.id}")`, + variable: ['data', 'integrationContext'], + }) }) test('cannot connect BankIntegrationAccountContext if BankAccount is already connected to some integrationContext', async () => { @@ -604,11 +613,16 @@ describe('BankAccount', () => { integrationContext: { connect: { id: BankIntegrationAccountContext.id } }, }) - await expectToThrowValidationFailureError(async () => { + await expectToThrowGQLError(async () => { await updateTestBankAccount(adminClient, bankAccount.id, { integrationContext: { connect: { id: anotherIntegrationContext.id } }, }) - }, `Integration reassignment is not allowed for BankAccount with id="${bankAccount.id}"`) + }, { + code: 'BAD_USER_INPUT', + type: 'INTEGRATION_REASSIGNMENT_NOT_ALLOWED', + message: `Integration reassignment is not allowed for BankAccount with id="${bankAccount.id}"`, + variable: ['data', 'integrationContext'], + }) }) }) }) diff --git a/apps/condo/domains/banking/utils/testSchema/bankAccount.js b/apps/condo/domains/banking/utils/testSchema/bankAccount.js index 3ec80410d9b..78484fb6535 100644 --- a/apps/condo/domains/banking/utils/testSchema/bankAccount.js +++ b/apps/condo/domains/banking/utils/testSchema/bankAccount.js @@ -11,6 +11,7 @@ function bulidValidRequisitesForRuBankAccount (extra = {}) { const tin = createValidRuTin10() const routingNumber = createValidRuRoutingNumber() const number = createValidRuNumber(routingNumber) + const classificationCode = createValidRuClassificationCode() const validRUBankAccount = { tin, @@ -20,6 +21,7 @@ function bulidValidRequisitesForRuBankAccount (extra = {}) { bankName: faker.company.name(), currencyCode: 'RUB', territoryCode: faker.datatype.number().toString(), + classificationCode, } return { ...validRUBankAccount, @@ -47,6 +49,10 @@ function createValidRuRoutingNumber () { return '04' + faker.datatype.number(getRange(7)).toString() } +function createValidRuClassificationCode () { + return faker.datatype.number(getRange(20)).toString() +} + function createValidRuTin10 () { const tin = faker.datatype.number(getRange(10)).toString() @@ -71,4 +77,5 @@ module.exports = { createValidRuRoutingNumber, createValidRuTin10, createValidRuTin12, + createValidRuClassificationCode, } diff --git a/apps/condo/domains/banking/utils/validate/classificationCode.utils.js b/apps/condo/domains/banking/utils/validate/classificationCode.utils.js new file mode 100644 index 00000000000..901f3e51f87 --- /dev/null +++ b/apps/condo/domains/banking/utils/validate/classificationCode.utils.js @@ -0,0 +1,37 @@ +const { getCountrySpecificValidator } = require('./countrySpecificValidators') + +/* + * Classification code validator + * + * The following check is performed: + * 1) Сhecking for emptiness + * + * Example: + * RU - 90205039900060030131 + */ + +const EMPTY = 'Classification code is empty' +const NOT_NUMERIC = 'Classification code can contain only digits' + +const validateClassificationCode = (routingNumber, country) => { + const errors = [] + + const classificationCodeWithoutSpaces = routingNumber.toString().trim() + if (!classificationCodeWithoutSpaces.length) { + errors.push(EMPTY) + } + if (!/^[0-9]*$/.test(classificationCodeWithoutSpaces)) { + errors.push(NOT_NUMERIC) + } + + getCountrySpecificValidator('classificationCode', country)(classificationCodeWithoutSpaces, errors) + + return { + result: !errors.length, + errors: errors, + } +} + +module.exports = { + validateClassificationCode, +} diff --git a/apps/condo/domains/banking/utils/validate/classificationCode.utils.spec.js b/apps/condo/domains/banking/utils/validate/classificationCode.utils.spec.js new file mode 100644 index 00000000000..6eb3cbe7db8 --- /dev/null +++ b/apps/condo/domains/banking/utils/validate/classificationCode.utils.spec.js @@ -0,0 +1,54 @@ +const { createValidRuClassificationCode } = require('@condo/domains/banking/utils/testSchema/bankAccount') +const { validateClassificationCode } = require('@condo/domains/banking/utils/validate/classificationCode.utils') +const { SPACE_SYMBOLS, SPACE_SYMBOL_LABLES } = require('@condo/domains/common/utils/string.utils') + +const SPACES = SPACE_SYMBOLS.split('') + + +describe('validateClassificationCode', () => { + describe('Ru', () => { + + const COUNTRY_CODE_RU = 'ru' + const VALID_RU_CLASSIFICATION_CODE = ['90205039900060030131', '18205049900160030200', '15305036900250030200'] + const WRONG_LENGTH_RU_CLASSIFICATION_CODE = '902050399000600301311' + const WRONG_FORMAT_RU_CLASSIFICATION_CODE = 'A0205039900060030131' + + VALID_RU_CLASSIFICATION_CODE.forEach(code => { + test(`should pass if valid: (${code})`, () => { + const { result } = validateClassificationCode(code, COUNTRY_CODE_RU) + expect(result).toBe(true) + }) + + SPACES.forEach(spaceSymbol => { + test(`should pass if valid: (${code}) with spaces symbol (${SPACE_SYMBOL_LABLES[spaceSymbol] || spaceSymbol})`, () => { + const classificationCodeValue = `${spaceSymbol}${code}${spaceSymbol}` + + const { result } = validateClassificationCode(classificationCodeValue, COUNTRY_CODE_RU) + expect(result).toBe(true) + }) + }) + }) + + test('classification code from generator should pass', () => { + const code = createValidRuClassificationCode() + const { result } = validateClassificationCode(code, COUNTRY_CODE_RU) + expect(result).toBe(true) + }) + + test('should fail if has wrong length', () => { + const { result, errors } = validateClassificationCode(WRONG_LENGTH_RU_CLASSIFICATION_CODE, COUNTRY_CODE_RU) + expect(result).toBe(false) + expect(errors[0]).toBe('Classification code length was expected to be 20, but received 21') + }) + test('should fail if contain invalid chars', () => { + const { result, errors } = validateClassificationCode(WRONG_FORMAT_RU_CLASSIFICATION_CODE, COUNTRY_CODE_RU) + expect(result).toBe(false) + expect(errors[0]).toBe('Classification code can contain only digits') + }) + test('should fail if empty', () => { + const { result, errors } = validateClassificationCode('', COUNTRY_CODE_RU) + expect(result).toBe(false) + expect(errors[0]).toBe('Classification code is empty') + }) + }) +}) \ No newline at end of file diff --git a/apps/condo/domains/banking/utils/validate/countrySpecificValidators/ru.validator.js b/apps/condo/domains/banking/utils/validate/countrySpecificValidators/ru.validator.js index c3daa6bcdca..951cfa43ccd 100644 --- a/apps/condo/domains/banking/utils/validate/countrySpecificValidators/ru.validator.js +++ b/apps/condo/domains/banking/utils/validate/countrySpecificValidators/ru.validator.js @@ -8,6 +8,9 @@ const RU_TIN_WRONG_LENGTH = 'Ru tin length was expected to be 10 or 12, but rece const RU_TIN_CONTROL_SUM_FAILED = 'Control sum is not valid for tin' const RU_TIN_NOT_NUMERIC = 'Tin can contain only digits' +// Classification Code (КБК): +const WRONG_LENGTH = 'Classification code length was expected to be 20, but received ' + /** * For RU number: * 1) Check checksum verification for number @@ -22,6 +25,8 @@ const RU_TIN_NOT_NUMERIC = 'Tin can contain only digits' */ const validateRuNumber = (number, routingNumber, errors) => { + // no validation for state funded organisations + if (routingNumber.startsWith('01')) return const controlString = routingNumber.substr(-3) + number let controlSum = 0 @@ -37,17 +42,17 @@ const validateRuNumber = (number, routingNumber, errors) => { /** * For RU routing number: * *** In Russia, number routing is equivalent to BIC*** - * 1) For RU organizations country code (first two digits) is 04 + * 1) For RU organizations country code (first two digits) is 04 or 01 (for government organizations) * * @param {string} routingNumber * @param {[]} errors */ const validateRuRoutingNumber = (routingNumber, errors) => { - const WRONG_RU_COUNTRY_CODE = 'For RU organizations country code is 04, but routing number have ' + const WRONG_RU_COUNTRY_CODE = 'For RU organizations country code is 04 or 01, but routing number has ' const countryCode = routingNumber.substr(0, 2) - if (countryCode !== '04') { + if (countryCode !== '04' && countryCode !== '01') { errors.push(WRONG_RU_COUNTRY_CODE + countryCode) } } @@ -91,11 +96,27 @@ const getRuTinControlSum = (num) => { return controlSum % 11 % 10 } +/** + * For RU (budget) classification code : + * *** In Russia, classification code is equivalent to KBK*** + * 2) Check for length and format (Consists of 20 digits) + * * + * Example: 90205039900060030131 + */ +const validateRuClassificationCode = (code, errors) => { + const classificationCodeWithoutSpaces = code.toString().trim() + + if (classificationCodeWithoutSpaces.length !== 20) { + errors.push(WRONG_LENGTH + classificationCodeWithoutSpaces.length) + } +} + module.exports = { validator: { number: validateRuNumber, routingNumber: validateRuRoutingNumber, tin: validateRuTin, + classificationCode: validateRuClassificationCode, }, RU_NUMBER_WEIGHTS, RU_TIN_WEIGHTS, diff --git a/apps/condo/domains/banking/utils/validate/routingNumber.utils.spec.js b/apps/condo/domains/banking/utils/validate/routingNumber.utils.spec.js index b8a8299cf2b..7a4f2311586 100644 --- a/apps/condo/domains/banking/utils/validate/routingNumber.utils.spec.js +++ b/apps/condo/domains/banking/utils/validate/routingNumber.utils.spec.js @@ -9,7 +9,7 @@ describe('validateRoutingNumber', () => { describe('Ru', () => { const COUNTRY_CODE_RU = 'ru' - const VALID_RU_ROUTING_NUMBER = ['045809749', '042612466', '043194972'] + const VALID_RU_ROUTING_NUMBER = ['045809749', '042612466', '043194972', '011806101'] const WRONG_LENGTH_RU_ROUTING_NUMBER = '0484528544' const WRONG_FORMAT_RU_ROUTING_NUMBER = '04845B854' const WRONG_CODE_COUNTRY_RU_ROUTING_NUMBER = '588453854' @@ -54,7 +54,7 @@ describe('validateRoutingNumber', () => { test('for wrong country code as Ru routing number', () => { const { result, errors } = validateRoutingNumber(WRONG_CODE_COUNTRY_RU_ROUTING_NUMBER, COUNTRY_CODE_RU) expect(result).toBe(false) - expect(errors[0]).toBe('For RU organizations country code is 04, but routing number have 58') + expect(errors[0]).toBe('For RU organizations country code is 04 or 01, but routing number has 58') }) }) }) diff --git a/apps/condo/domains/billing/constants/errors.js b/apps/condo/domains/billing/constants/errors.js index 563941416e2..0ba5b77aeae 100644 --- a/apps/condo/domains/billing/constants/errors.js +++ b/apps/condo/domains/billing/constants/errors.js @@ -3,9 +3,11 @@ const { WRONG_FORMAT } = require('@condo/domains/common/constants/errors') const BILLING_INTEGRATION_EXTENDS_NO_APP_URL_ERROR = '[extendsBillingPage:appUrl:empty] Extends billing page is marked to true, but no appUrl is specified' const BILLING_INTEGRATION_SINGLE_CONNECT_WAY_ERROR = '[setupUrl:instruction] Billing integration must have 1 and only 1 of these fields filled: [setupUrl, instruction]' const BILLING_INTEGRATION_WRONG_GROUP_FORMAT_ERROR = `[${WRONG_FORMAT}:BillingIntegration:group] group should be a sequence of lowercase latin characters` +const CLASSIFICATION_CODE_IS_INVALID = 'CLASSIFICATION_CODE_IS_INVALID' module.exports = { BILLING_INTEGRATION_WRONG_GROUP_FORMAT_ERROR, BILLING_INTEGRATION_EXTENDS_NO_APP_URL_ERROR, BILLING_INTEGRATION_SINGLE_CONNECT_WAY_ERROR, + CLASSIFICATION_CODE_IS_INVALID, } \ No newline at end of file diff --git a/apps/condo/domains/billing/gql.js b/apps/condo/domains/billing/gql.js index 1ab7ee665ec..3baa8f2a141 100644 --- a/apps/condo/domains/billing/gql.js +++ b/apps/condo/domains/billing/gql.js @@ -30,7 +30,7 @@ const BillingProperty = generateGqlQueries('BillingProperty', BILLING_PROPERTY_F const BILLING_ACCOUNT_FIELDS = `{ context ${BILLING_INTEGRATION_ORGANIZATION_CONTEXT_FIELDS} importId property { id address addressKey } number unitName unitType raw globalId meta fullName isClosed ownerType ${COMMON_FIELDS} }` const BillingAccount = generateGqlQueries('BillingAccount', BILLING_ACCOUNT_FIELDS) -const BILLING_RECIPIENT_FIELDS = `{ context { id } importId tin iec bic bankAccount purpose isApproved meta name ${COMMON_FIELDS} }` +const BILLING_RECIPIENT_FIELDS = `{ context { id } importId tin iec bic bankAccount purpose isApproved meta name classificationCode ${COMMON_FIELDS} }` const BillingRecipient = generateGqlQueries('BillingRecipient', BILLING_RECIPIENT_FIELDS) const BILLING_CATEGORY_FIELDS = `{ name nameNonLocalized ${COMMON_FIELDS} }` diff --git a/apps/condo/domains/billing/schema/BillingRecipient.js b/apps/condo/domains/billing/schema/BillingRecipient.js index a4ee598ec1b..24f58e9a8aa 100644 --- a/apps/condo/domains/billing/schema/BillingRecipient.js +++ b/apps/condo/domains/billing/schema/BillingRecipient.js @@ -85,6 +85,12 @@ const BillingRecipient = new GQLListSchema('BillingRecipient', { }, }, + classificationCode: { + schemaDoc: 'Budget classification code, used for state-funded organizations', + type: Text, + isRequired: false, + }, + meta: { schemaDoc: 'Structured metadata obtained from the `billing data source`. The structure depends on the integration system.', type: Json, diff --git a/apps/condo/domains/billing/schema/BillingRecipient.test.js b/apps/condo/domains/billing/schema/BillingRecipient.test.js index 84a8d86818a..040ecd9da90 100644 --- a/apps/condo/domains/billing/schema/BillingRecipient.test.js +++ b/apps/condo/domains/billing/schema/BillingRecipient.test.js @@ -45,6 +45,7 @@ describe('BillingRecipient', () => { expect(obj.name).toBeDefined() expect(obj.isApproved).toBeDefined() expect(obj.meta).toBeDefined() + expect(obj.classificationCode).toBeDefined() }) test('support can create BillingRecipient', async () => { diff --git a/apps/condo/migrations/20240208171234-0365_bankaccount_classificationcode_and_more.js b/apps/condo/migrations/20240208171234-0365_bankaccount_classificationcode_and_more.js new file mode 100644 index 00000000000..840c7c51331 --- /dev/null +++ b/apps/condo/migrations/20240208171234-0365_bankaccount_classificationcode_and_more.js @@ -0,0 +1,50 @@ +// auto generated by kmigrator +// KMIGRATOR:0365_bankaccount_classificationcode_and_more:IyBHZW5lcmF0ZWQgYnkgRGphbmdvIDQuMi45IG9uIDIwMjQtMDItMDggMTQ6MTIKCmZyb20gZGphbmdvLmRiIGltcG9ydCBtaWdyYXRpb25zLCBtb2RlbHMKCgpjbGFzcyBNaWdyYXRpb24obWlncmF0aW9ucy5NaWdyYXRpb24pOgoKICAgIGRlcGVuZGVuY2llcyA9IFsKICAgICAgICAoJ19kamFuZ29fc2NoZW1hJywgJzAzNjRfbWFya2V0cHJpY2VzY29wZV90eXBlX2FuZF9tb3JlJyksCiAgICBdCgogICAgb3BlcmF0aW9ucyA9IFsKICAgICAgICBtaWdyYXRpb25zLkFkZEZpZWxkKAogICAgICAgICAgICBtb2RlbF9uYW1lPSdiYW5rYWNjb3VudCcsCiAgICAgICAgICAgIG5hbWU9J2NsYXNzaWZpY2F0aW9uQ29kZScsCiAgICAgICAgICAgIGZpZWxkPW1vZGVscy5UZXh0RmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSwKICAgICAgICApLAogICAgICAgIG1pZ3JhdGlvbnMuQWRkRmllbGQoCiAgICAgICAgICAgIG1vZGVsX25hbWU9J2JhbmthY2NvdW50aGlzdG9yeXJlY29yZCcsCiAgICAgICAgICAgIG5hbWU9J2NsYXNzaWZpY2F0aW9uQ29kZScsCiAgICAgICAgICAgIGZpZWxkPW1vZGVscy5UZXh0RmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSwKICAgICAgICApLAogICAgICAgIG1pZ3JhdGlvbnMuQWRkRmllbGQoCiAgICAgICAgICAgIG1vZGVsX25hbWU9J2JpbGxpbmdyZWNpcGllbnQnLAogICAgICAgICAgICBuYW1lPSdjbGFzc2lmaWNhdGlvbkNvZGUnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuVGV4dEZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSksCiAgICAgICAgKSwKICAgICAgICBtaWdyYXRpb25zLkFkZEZpZWxkKAogICAgICAgICAgICBtb2RlbF9uYW1lPSdiaWxsaW5ncmVjaXBpZW50aGlzdG9yeXJlY29yZCcsCiAgICAgICAgICAgIG5hbWU9J2NsYXNzaWZpY2F0aW9uQ29kZScsCiAgICAgICAgICAgIGZpZWxkPW1vZGVscy5UZXh0RmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSwKICAgICAgICApLAogICAgXQo= + +exports.up = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Add field classificationCode to bankaccount +-- +ALTER TABLE "BankAccount" ADD COLUMN "classificationCode" text NULL; +-- +-- Add field classificationCode to bankaccounthistoryrecord +-- +ALTER TABLE "BankAccountHistoryRecord" ADD COLUMN "classificationCode" text NULL; +-- +-- Add field classificationCode to billingrecipient +-- +ALTER TABLE "BillingRecipient" ADD COLUMN "classificationCode" text NULL; +-- +-- Add field classificationCode to billingrecipienthistoryrecord +-- +ALTER TABLE "BillingRecipientHistoryRecord" ADD COLUMN "classificationCode" text NULL; +COMMIT; + + `) +} + +exports.down = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Add field classificationCode to billingrecipienthistoryrecord +-- +ALTER TABLE "BillingRecipientHistoryRecord" DROP COLUMN "classificationCode" CASCADE; +-- +-- Add field classificationCode to billingrecipient +-- +ALTER TABLE "BillingRecipient" DROP COLUMN "classificationCode" CASCADE; +-- +-- Add field classificationCode to bankaccounthistoryrecord +-- +ALTER TABLE "BankAccountHistoryRecord" DROP COLUMN "classificationCode" CASCADE; +-- +-- Add field classificationCode to bankaccount +-- +ALTER TABLE "BankAccount" DROP COLUMN "classificationCode" CASCADE; +COMMIT; + + `) +} diff --git a/apps/condo/schema.graphql b/apps/condo/schema.graphql index af67028006f..0a1297ae548 100644 --- a/apps/condo/schema.graphql +++ b/apps/condo/schema.graphql @@ -13369,6 +13369,7 @@ type BillingRecipientHistoryRecord { purpose: String name: String isApproved: Boolean + classificationCode: String meta: JSON id: ID! v: Int @@ -13574,6 +13575,24 @@ input BillingRecipientHistoryRecordWhereInput { name_not_in: [String] isApproved: Boolean isApproved_not: Boolean + classificationCode: String + classificationCode_not: String + classificationCode_contains: String + classificationCode_not_contains: String + classificationCode_starts_with: String + classificationCode_not_starts_with: String + classificationCode_ends_with: String + classificationCode_not_ends_with: String + classificationCode_i: String + classificationCode_not_i: String + classificationCode_contains_i: String + classificationCode_not_contains_i: String + classificationCode_starts_with_i: String + classificationCode_not_starts_with_i: String + classificationCode_ends_with_i: String + classificationCode_not_ends_with_i: String + classificationCode_in: [String] + classificationCode_not_in: [String] meta: JSON meta_not: JSON meta_in: [JSON] @@ -13683,6 +13702,8 @@ enum SortBillingRecipientHistoryRecordsBy { name_DESC isApproved_ASC isApproved_DESC + classificationCode_ASC + classificationCode_DESC id_ASC id_DESC v_ASC @@ -13714,6 +13735,7 @@ input BillingRecipientHistoryRecordUpdateInput { purpose: String name: String isApproved: Boolean + classificationCode: String meta: JSON v: Int createdAt: String @@ -13747,6 +13769,7 @@ input BillingRecipientHistoryRecordCreateInput { purpose: String name: String isApproved: Boolean + classificationCode: String meta: JSON v: Int createdAt: String @@ -13816,6 +13839,9 @@ type BillingRecipient { """ isApproved: Boolean + """ Budget classification code, used for state-funded organizations """ + classificationCode: String + """ Structured metadata obtained from the `billing data source`. The structure depends on the integration system. """ meta: JSON @@ -14029,6 +14055,24 @@ input BillingRecipientWhereInput { name_not_in: [String] isApproved: Boolean isApproved_not: Boolean + classificationCode: String + classificationCode_not: String + classificationCode_contains: String + classificationCode_not_contains: String + classificationCode_starts_with: String + classificationCode_not_starts_with: String + classificationCode_ends_with: String + classificationCode_not_ends_with: String + classificationCode_i: String + classificationCode_not_i: String + classificationCode_contains_i: String + classificationCode_not_contains_i: String + classificationCode_starts_with_i: String + classificationCode_not_starts_with_i: String + classificationCode_ends_with_i: String + classificationCode_not_ends_with_i: String + classificationCode_in: [String] + classificationCode_not_in: [String] meta: JSON meta_not: JSON meta_in: [JSON] @@ -14120,6 +14164,8 @@ enum SortBillingRecipientsBy { name_DESC isApproved_ASC isApproved_DESC + classificationCode_ASC + classificationCode_DESC id_ASC id_DESC v_ASC @@ -14151,6 +14197,7 @@ input BillingRecipientUpdateInput { purpose: String name: String isApproved: Boolean + classificationCode: String meta: JSON v: Int createdAt: String @@ -14181,6 +14228,7 @@ input BillingRecipientCreateInput { purpose: String name: String isApproved: Boolean + classificationCode: String meta: JSON v: Int createdAt: String @@ -15121,6 +15169,7 @@ type BankAccountHistoryRecord { territoryCode: String bankName: String meta: JSON + classificationCode: String id: ID! v: Int createdAt: String @@ -15319,6 +15368,24 @@ input BankAccountHistoryRecordWhereInput { meta_not: JSON meta_in: [JSON] meta_not_in: [JSON] + classificationCode: String + classificationCode_not: String + classificationCode_contains: String + classificationCode_not_contains: String + classificationCode_starts_with: String + classificationCode_not_starts_with: String + classificationCode_ends_with: String + classificationCode_not_ends_with: String + classificationCode_i: String + classificationCode_not_i: String + classificationCode_contains_i: String + classificationCode_not_contains_i: String + classificationCode_starts_with_i: String + classificationCode_not_starts_with_i: String + classificationCode_ends_with_i: String + classificationCode_not_ends_with_i: String + classificationCode_in: [String] + classificationCode_not_in: [String] id: ID id_not: ID id_in: [ID] @@ -15420,6 +15487,8 @@ enum SortBankAccountHistoryRecordsBy { territoryCode_DESC bankName_ASC bankName_DESC + classificationCode_ASC + classificationCode_DESC id_ASC id_DESC v_ASC @@ -15455,6 +15524,7 @@ input BankAccountHistoryRecordUpdateInput { territoryCode: String bankName: String meta: JSON + classificationCode: String v: Int createdAt: String updatedAt: String @@ -15491,6 +15561,7 @@ input BankAccountHistoryRecordCreateInput { territoryCode: String bankName: String meta: JSON + classificationCode: String v: Int createdAt: String updatedAt: String @@ -15591,6 +15662,9 @@ type BankAccount { """ Structured non-typed metadata, can be used by mini-apps or external services to store information """ meta: JSON + + """ Budget classification code, used for state-funded organizations """ + classificationCode: String id: ID! v: Int createdAt: String @@ -15761,6 +15835,24 @@ input BankAccountWhereInput { meta_not: JSON meta_in: [JSON] meta_not_in: [JSON] + classificationCode: String + classificationCode_not: String + classificationCode_contains: String + classificationCode_not_contains: String + classificationCode_starts_with: String + classificationCode_not_starts_with: String + classificationCode_ends_with: String + classificationCode_not_ends_with: String + classificationCode_i: String + classificationCode_not_i: String + classificationCode_contains_i: String + classificationCode_not_contains_i: String + classificationCode_starts_with_i: String + classificationCode_not_starts_with_i: String + classificationCode_ends_with_i: String + classificationCode_not_ends_with_i: String + classificationCode_in: [String] + classificationCode_not_in: [String] id: ID id_not: ID id_in: [ID] @@ -15850,6 +15942,8 @@ enum SortBankAccountsBy { territoryCode_DESC bankName_ASC bankName_DESC + classificationCode_ASC + classificationCode_DESC id_ASC id_DESC v_ASC @@ -15885,6 +15979,7 @@ input BankAccountUpdateInput { territoryCode: String bankName: String meta: JSON + classificationCode: String v: Int createdAt: String updatedAt: String @@ -15918,6 +16013,7 @@ input BankAccountCreateInput { territoryCode: String bankName: String meta: JSON + classificationCode: String v: Int createdAt: String updatedAt: String diff --git a/apps/condo/schema.ts b/apps/condo/schema.ts index 55df40ca676..1a1936b3fb4 100644 --- a/apps/condo/schema.ts +++ b/apps/condo/schema.ts @@ -7389,6 +7389,8 @@ export type BankAccount = { bankName?: Maybe; /** Structured non-typed metadata, can be used by mini-apps or external services to store information */ meta?: Maybe; + /** Budget classification code, used for state-funded organizations */ + classificationCode?: Maybe; id: Scalars['ID']; v?: Maybe; createdAt?: Maybe; @@ -7427,6 +7429,7 @@ export type BankAccountCreateInput = { territoryCode?: Maybe; bankName?: Maybe; meta?: Maybe; + classificationCode?: Maybe; v?: Maybe; createdAt?: Maybe; updatedAt?: Maybe; @@ -7465,6 +7468,7 @@ export type BankAccountHistoryRecord = { territoryCode?: Maybe; bankName?: Maybe; meta?: Maybe; + classificationCode?: Maybe; id: Scalars['ID']; v?: Maybe; createdAt?: Maybe; @@ -7497,6 +7501,7 @@ export type BankAccountHistoryRecordCreateInput = { territoryCode?: Maybe; bankName?: Maybe; meta?: Maybe; + classificationCode?: Maybe; v?: Maybe; createdAt?: Maybe; updatedAt?: Maybe; @@ -7534,6 +7539,7 @@ export type BankAccountHistoryRecordUpdateInput = { territoryCode?: Maybe; bankName?: Maybe; meta?: Maybe; + classificationCode?: Maybe; v?: Maybe; createdAt?: Maybe; updatedAt?: Maybe; @@ -7731,6 +7737,24 @@ export type BankAccountHistoryRecordWhereInput = { meta_not?: Maybe; meta_in?: Maybe>>; meta_not_in?: Maybe>>; + classificationCode?: Maybe; + classificationCode_not?: Maybe; + classificationCode_contains?: Maybe; + classificationCode_not_contains?: Maybe; + classificationCode_starts_with?: Maybe; + classificationCode_not_starts_with?: Maybe; + classificationCode_ends_with?: Maybe; + classificationCode_not_ends_with?: Maybe; + classificationCode_i?: Maybe; + classificationCode_not_i?: Maybe; + classificationCode_contains_i?: Maybe; + classificationCode_not_contains_i?: Maybe; + classificationCode_starts_with_i?: Maybe; + classificationCode_not_starts_with_i?: Maybe; + classificationCode_ends_with_i?: Maybe; + classificationCode_not_ends_with_i?: Maybe; + classificationCode_in?: Maybe>>; + classificationCode_not_in?: Maybe>>; id?: Maybe; id_not?: Maybe; id_in?: Maybe>>; @@ -8787,6 +8811,7 @@ export type BankAccountUpdateInput = { territoryCode?: Maybe; bankName?: Maybe; meta?: Maybe; + classificationCode?: Maybe; v?: Maybe; createdAt?: Maybe; updatedAt?: Maybe; @@ -8945,6 +8970,24 @@ export type BankAccountWhereInput = { meta_not?: Maybe; meta_in?: Maybe>>; meta_not_in?: Maybe>>; + classificationCode?: Maybe; + classificationCode_not?: Maybe; + classificationCode_contains?: Maybe; + classificationCode_not_contains?: Maybe; + classificationCode_starts_with?: Maybe; + classificationCode_not_starts_with?: Maybe; + classificationCode_ends_with?: Maybe; + classificationCode_not_ends_with?: Maybe; + classificationCode_i?: Maybe; + classificationCode_not_i?: Maybe; + classificationCode_contains_i?: Maybe; + classificationCode_not_contains_i?: Maybe; + classificationCode_starts_with_i?: Maybe; + classificationCode_not_starts_with_i?: Maybe; + classificationCode_ends_with_i?: Maybe; + classificationCode_not_ends_with_i?: Maybe; + classificationCode_in?: Maybe>>; + classificationCode_not_in?: Maybe>>; id?: Maybe; id_not?: Maybe; id_in?: Maybe>>; @@ -17776,6 +17819,8 @@ export type BillingRecipient = { name?: Maybe; /** If set to True, then this billing recipient info is considered allowed and users are allowed to pay for receipts with this recipient */ isApproved?: Maybe; + /** Budget classification code, used for state-funded organizations */ + classificationCode?: Maybe; /** Structured metadata obtained from the `billing data source`. The structure depends on the integration system. */ meta?: Maybe; id: Scalars['ID']; @@ -17807,6 +17852,7 @@ export type BillingRecipientCreateInput = { purpose?: Maybe; name?: Maybe; isApproved?: Maybe; + classificationCode?: Maybe; meta?: Maybe; v?: Maybe; createdAt?: Maybe; @@ -17842,6 +17888,7 @@ export type BillingRecipientHistoryRecord = { purpose?: Maybe; name?: Maybe; isApproved?: Maybe; + classificationCode?: Maybe; meta?: Maybe; id: Scalars['ID']; v?: Maybe; @@ -17871,6 +17918,7 @@ export type BillingRecipientHistoryRecordCreateInput = { purpose?: Maybe; name?: Maybe; isApproved?: Maybe; + classificationCode?: Maybe; meta?: Maybe; v?: Maybe; createdAt?: Maybe; @@ -17905,6 +17953,7 @@ export type BillingRecipientHistoryRecordUpdateInput = { purpose?: Maybe; name?: Maybe; isApproved?: Maybe; + classificationCode?: Maybe; meta?: Maybe; v?: Maybe; createdAt?: Maybe; @@ -18109,6 +18158,24 @@ export type BillingRecipientHistoryRecordWhereInput = { name_not_in?: Maybe>>; isApproved?: Maybe; isApproved_not?: Maybe; + classificationCode?: Maybe; + classificationCode_not?: Maybe; + classificationCode_contains?: Maybe; + classificationCode_not_contains?: Maybe; + classificationCode_starts_with?: Maybe; + classificationCode_not_starts_with?: Maybe; + classificationCode_ends_with?: Maybe; + classificationCode_not_ends_with?: Maybe; + classificationCode_i?: Maybe; + classificationCode_not_i?: Maybe; + classificationCode_contains_i?: Maybe; + classificationCode_not_contains_i?: Maybe; + classificationCode_starts_with_i?: Maybe; + classificationCode_not_starts_with_i?: Maybe; + classificationCode_ends_with_i?: Maybe; + classificationCode_not_ends_with_i?: Maybe; + classificationCode_in?: Maybe>>; + classificationCode_not_in?: Maybe>>; meta?: Maybe; meta_not?: Maybe; meta_in?: Maybe>>; @@ -18224,6 +18291,7 @@ export type BillingRecipientUpdateInput = { purpose?: Maybe; name?: Maybe; isApproved?: Maybe; + classificationCode?: Maybe; meta?: Maybe; v?: Maybe; createdAt?: Maybe; @@ -18423,6 +18491,24 @@ export type BillingRecipientWhereInput = { name_not_in?: Maybe>>; isApproved?: Maybe; isApproved_not?: Maybe; + classificationCode?: Maybe; + classificationCode_not?: Maybe; + classificationCode_contains?: Maybe; + classificationCode_not_contains?: Maybe; + classificationCode_starts_with?: Maybe; + classificationCode_not_starts_with?: Maybe; + classificationCode_ends_with?: Maybe; + classificationCode_not_ends_with?: Maybe; + classificationCode_i?: Maybe; + classificationCode_not_i?: Maybe; + classificationCode_contains_i?: Maybe; + classificationCode_not_contains_i?: Maybe; + classificationCode_starts_with_i?: Maybe; + classificationCode_not_starts_with_i?: Maybe; + classificationCode_ends_with_i?: Maybe; + classificationCode_not_ends_with_i?: Maybe; + classificationCode_in?: Maybe>>; + classificationCode_not_in?: Maybe>>; meta?: Maybe; meta_not?: Maybe; meta_in?: Maybe>>; @@ -72938,6 +73024,8 @@ export enum SortBankAccountHistoryRecordsBy { TerritoryCodeDesc = 'territoryCode_DESC', BankNameAsc = 'bankName_ASC', BankNameDesc = 'bankName_DESC', + ClassificationCodeAsc = 'classificationCode_ASC', + ClassificationCodeDesc = 'classificationCode_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', VAsc = 'v_ASC', @@ -73113,6 +73201,8 @@ export enum SortBankAccountsBy { TerritoryCodeDesc = 'territoryCode_DESC', BankNameAsc = 'bankName_ASC', BankNameDesc = 'bankName_DESC', + ClassificationCodeAsc = 'classificationCode_ASC', + ClassificationCodeDesc = 'classificationCode_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', VAsc = 'v_ASC', @@ -74195,6 +74285,8 @@ export enum SortBillingRecipientHistoryRecordsBy { NameDesc = 'name_DESC', IsApprovedAsc = 'isApproved_ASC', IsApprovedDesc = 'isApproved_DESC', + ClassificationCodeAsc = 'classificationCode_ASC', + ClassificationCodeDesc = 'classificationCode_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', VAsc = 'v_ASC', @@ -74238,6 +74330,8 @@ export enum SortBillingRecipientsBy { NameDesc = 'name_DESC', IsApprovedAsc = 'isApproved_ASC', IsApprovedDesc = 'isApproved_DESC', + ClassificationCodeAsc = 'classificationCode_ASC', + ClassificationCodeDesc = 'classificationCode_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', VAsc = 'v_ASC', diff --git a/packages/clients/finance-info-client/FinanceInfoClient.js b/packages/clients/finance-info-client/FinanceInfoClient.js index c4cf20bcb9d..a3a1b1bc930 100644 --- a/packages/clients/finance-info-client/FinanceInfoClient.js +++ b/packages/clients/finance-info-client/FinanceInfoClient.js @@ -86,7 +86,7 @@ class FinanceInfoClient { * @returns {BankInfo} */ async getBank (routingNumber) { - if (typeof routingNumber !== 'string' || !routingNumber.startsWith('04') || routingNumber.length !== 9) { + if (typeof routingNumber !== 'string' || !routingNumber.startsWith('04') || !routingNumber.startsWith('01') || routingNumber.length !== 9) { throw new Error(`Invalid routing number: ${routingNumber}`) } const cachedValue = await this.#getFromCache(`BANK_${routingNumber}`)