Skip to content

Commit

Permalink
feat(condo): DOMA-6776 add primaryFile field to BillingReceipt (#3684)
Browse files Browse the repository at this point in the history
* feat(condo): DOMA-6776 add primaryFile field to BillingReceipt

* feat(condo): DOMA-6776 add primaryFile field to AllResidentBillingReceiptsService

* feat(condo): DOMA-6776 update schema

* feat(condo): DOMA-6776 rename primaryFile -> file

* feat(condo): DOMA-6776 fix for files privacy

* feat(condo): DOMA-6776 add public/sensitive files tests

* feat(condo): DOMA-6776 update migration

* feat(condo): DOMA-6776 fix lint
  • Loading branch information
ekabardinsky authored Aug 4, 2023
1 parent e80bf08 commit f45efc9
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 23 deletions.
3 changes: 3 additions & 0 deletions apps/condo/domains/billing/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const BILLING_CONTEXT_INTEGRATION_OPTION_INPUT_NAME = 'BillingIntegrationOrganiz
const BILLING_INTEGRATION_DATA_FORMAT_FIELD_NAME = 'BillingIntegrationDataFormatField'
const BILLING_INTEGRATION_DATA_FORMAT_INPUT_NAME = 'BillingIntegrationDataFormatFieldInput'

const BILLING_RECEIPT_FILE_FOLDER_NAME = 'billing-receipt-pdf'

const DEFAULT_BILLING_INTEGRATION_NAME = 'default'
const DEFAULT_BILLING_INTEGRATION_GROUP = 'common'

Expand Down Expand Up @@ -54,6 +56,7 @@ module.exports = {
BILLING_CONTEXT_INTEGRATION_OPTION_INPUT_NAME,
BILLING_INTEGRATION_DATA_FORMAT_FIELD_NAME,
BILLING_INTEGRATION_DATA_FORMAT_INPUT_NAME,
BILLING_RECEIPT_FILE_FOLDER_NAME,
ACCRUALS_TAB_KEY,
PAYMENTS_TAB_KEY,
EXTENSION_TAB_KEY,
Expand Down
4 changes: 2 additions & 2 deletions apps/condo/domains/billing/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ const BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS = 'toPayDetails { charge formula bal
const BILLING_RECEIPT_SERVICE_TO_PAY_DETAILS_FIELDS = BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS.replace('}', 'volume tariff measure }')
const BILLING_RECEIPT_SERVICE_FIELDS = `services { id name toPay ${BILLING_RECEIPT_SERVICE_TO_PAY_DETAILS_FIELDS} }`
const BILLING_RECEIPT_RECIPIENT_FIELDS = 'recipient { tin iec bic bankAccount }'
const BILLING_RECEIPT_FIELDS = `{ context ${BILLING_INTEGRATION_ORGANIZATION_CONTEXT_FIELDS} importId property { id, address } account { id, number, unitType, unitName, fullName } period toPay printableNumber ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} charge formula balance recalculation privilege penalty paid receiver { id tin iec bic bankAccount isApproved } ${BILLING_RECEIPT_RECIPIENT_FIELDS} ${COMMON_FIELDS} category ${BILLING_CATEGORY_FIELDS} invalidServicesError }`
const BILLING_RECEIPT_FIELDS = `{ context ${BILLING_INTEGRATION_ORGANIZATION_CONTEXT_FIELDS} importId property { id, address } account { id, number, unitType, unitName, fullName } period toPay printableNumber ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} charge formula balance recalculation privilege penalty paid receiver { id tin iec bic bankAccount isApproved } ${BILLING_RECEIPT_RECIPIENT_FIELDS} ${COMMON_FIELDS} category ${BILLING_CATEGORY_FIELDS} invalidServicesError file { id sensitiveDataFile { id filename originalFilename publicUrl mimetype } publicDataFile { id filename originalFilename publicUrl mimetype } controlSum } }`
const BillingReceipt = generateGqlQueries('BillingReceipt', BILLING_RECEIPT_FIELDS)

const RESIDENT_BILLING_RECEIPTS_FIELDS = `{ id ${BILLING_RECEIPT_RECIPIENT_FIELDS} period toPay paid ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} printableNumber serviceConsumer { id paymentCategory } currencyCode category { id name } isPayable }`
const RESIDENT_BILLING_RECEIPTS_FIELDS = `{ id ${BILLING_RECEIPT_RECIPIENT_FIELDS} period toPay paid ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} printableNumber serviceConsumer { id paymentCategory } currencyCode category { id name } isPayable file { file { id originalFilename publicUrl mimetype } controlSum } }`
const ResidentBillingReceipt = generateGqlQueries('ResidentBillingReceipt', RESIDENT_BILLING_RECEIPTS_FIELDS)

const BILLING_RECEIPT_FILE_FIELDS = `{ file { id originalFilename publicUrl mimetype } context { id } receipt { id } controlSum ${COMMON_FIELDS} }`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@

const Big = require('big.js')
const dayjs = require('dayjs')
const { pick, get } = require('lodash')
const { pick, get, isNil } = require('lodash')

const { generateQuerySortBy } = require('@open-condo/codegen/generate.gql')
const { generateQueryWhereInput } = require('@open-condo/codegen/generate.gql')
const { GQLCustomSchema, find } = require('@open-condo/keystone/schema')
const { GQLCustomSchema, find, getById } = require('@open-condo/keystone/schema')

const { PAYMENT_DONE_STATUS, PAYMENT_WITHDRAWN_STATUS } = require('@condo/domains/acquiring/constants/payment')
const { getAcquiringIntegrationContextFormula, FeeDistribution } = require('@condo/domains/acquiring/utils/serverSchema/feeDistribution')
const access = require('@condo/domains/billing/access/AllResidentBillingReceipts')
const { BILLING_RECEIPT_FILE_FOLDER_NAME } = require('@condo/domains/billing/constants/constants')
const { BillingReceipt, getPaymentsSum } = require('@condo/domains/billing/utils/serverSchema')
const FileAdapter = require('@condo/domains/common/utils/fileAdapter')
const { Contact } = require('@condo/domains/contact/utils/serverSchema')

const {
BILLING_RECEIPT_RECIPIENT_FIELD_NAME,
BILLING_RECEIPT_TO_PAY_DETAILS_FIELD_NAME,
BILLING_RECEIPT_SERVICES_FIELD,
} = require('../constants/constants')


const Adapter = new FileAdapter(BILLING_RECEIPT_FILE_FOLDER_NAME)

const ALL_RESIDENT_BILLING_RECEIPTS_FIELDS = {
id: 'ID',
Expand All @@ -31,6 +34,29 @@ const ALL_RESIDENT_BILLING_RECEIPTS_FIELDS = {
serviceConsumer: 'ServiceConsumer',
}

const getFile = (receipt, contacts) => {
if (isNil(receipt.file)) {
return receipt.file
}
const accountUnitName = get(receipt, ['account', 'unitName'])
const accountUnitType = get(receipt, ['account', 'unitType'])
const propertyAddress = get(receipt, ['property', 'address'])

// let's search for a contact
// if any exists = user allowed to see sensitive data
const propertyContacts = contacts.filter(contact => contact.unitName === accountUnitName
&& contact.unitType === accountUnitType
&& contact.property.address === propertyAddress)
const file = propertyContacts.length > 0
? receipt.file.sensitiveDataFile
: receipt.file.publicDataFile
const publicUrl = Adapter.publicUrl({ filename: file.filename })

return {
file: { ...file, publicUrl },
controlSum: receipt.file.controlSum,
}
}

const AllResidentBillingReceiptsService = new GQLCustomSchema('AllResidentBillingReceiptsService', {
types: [
Expand All @@ -44,7 +70,11 @@ const AllResidentBillingReceiptsService = new GQLCustomSchema('AllResidentBillin
},
{
access: true,
type: `type ResidentBillingReceiptOutput { dv: String!, recipient: ${BILLING_RECEIPT_RECIPIENT_FIELD_NAME}!, id: ID!, period: String!, toPay: String!, paid: String!, explicitFee: String!, printableNumber: String, toPayDetails: ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELD_NAME}, services: ${BILLING_RECEIPT_SERVICES_FIELD}, serviceConsumer: ServiceConsumer! currencyCode: String! category: BillingCategory! isPayable: Boolean! }`,
type: 'type ResidentBillingReceiptFile { file: File controlSum: String}',
},
{
access: true,
type: `type ResidentBillingReceiptOutput { dv: String!, recipient: ${BILLING_RECEIPT_RECIPIENT_FIELD_NAME}!, id: ID!, period: String!, toPay: String!, paid: String!, explicitFee: String!, printableNumber: String, toPayDetails: ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELD_NAME}, services: ${BILLING_RECEIPT_SERVICES_FIELD}, serviceConsumer: ServiceConsumer! currencyCode: String! category: BillingCategory! isPayable: Boolean! file: ResidentBillingReceiptFile }`,
},
],

Expand Down Expand Up @@ -106,21 +136,33 @@ const AllResidentBillingReceiptsService = new GQLCustomSchema('AllResidentBillin
}
)

receiptsForConsumer.forEach(receipt => processedReceipts.push({
id: receipt.id,
dv: receipt.dv,
category: receipt.category,
recipient: receipt.recipient,
receiver: receipt.receiver,
account: receipt.account,
period: receipt.period,
toPay: receipt.toPay,
toPayDetails: receipt.toPayDetails,
services: receipt.services,
printableNumber: receipt.printableNumber,
serviceConsumer: serviceConsumers.find(x => get(receipt, ['account', 'number']) === x.accountNumber),
currencyCode: get(receipt, ['context', 'integration', 'currencyCode'], null),
}))
// cache verified contacts for authed user
// in order to determinate if user can see
// a sensitive version of primary file
const contacts = await Contact.getAll(context, {
phone: context.authedItem.phone,
isVerified: true,
})

receiptsForConsumer.forEach(receipt => {
const file = getFile(receipt, contacts)
processedReceipts.push({
id: receipt.id,
dv: receipt.dv,
category: receipt.category,
recipient: receipt.recipient,
receiver: receipt.receiver,
account: receipt.account,
period: receipt.period,
toPay: receipt.toPay,
toPayDetails: receipt.toPayDetails,
services: receipt.services,
printableNumber: receipt.printableNumber,
serviceConsumer: serviceConsumers.find(x => get(receipt, ['account', 'number']) === x.accountNumber),
currencyCode: get(receipt, ['context', 'integration', 'currencyCode'], null),
file,
})
})

//
// Set receipt.isPayable field
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Generated by `createservice billing.BillingReceiptsService --type queries`
*/
const path = require('path')

const { faker } = require('@faker-js/faker')

Expand All @@ -14,13 +15,16 @@ const {
} = require('@condo/domains/acquiring/utils/testSchema')
const { makeClientWithPropertyAndBilling, createTestRecipient } = require('@condo/domains/billing/utils/testSchema')
const { createTestBillingAccount, createTestBillingProperty, createTestBillingIntegrationOrganizationContext, createTestBillingIntegrationAccessRight } = require('@condo/domains/billing/utils/testSchema')
const {
createTestContact,
} = require('@condo/domains/contact/utils/testSchema')
const { createTestOrganization } = require('@condo/domains/organization/utils/testSchema')
const { makeClientWithProperty, createTestProperty } = require('@condo/domains/property/utils/testSchema')
const { registerServiceConsumerByTestClient, updateTestServiceConsumer, registerResidentByTestClient, createTestResident, ServiceConsumer } = require('@condo/domains/resident/utils/testSchema')
const { addResidentAccess, makeClientWithResidentUser, makeClientWithSupportUser, makeClientWithServiceUser } = require('@condo/domains/user/utils/testSchema')

const { createTestBillingIntegration, createTestBillingReceipt, updateTestBillingReceipt, ResidentBillingReceipt,
generateServicesData,
generateServicesData, createTestBillingReceiptFile, PUBLIC_FILE, PRIVATE_FILE,
} = require('../utils/testSchema')


Expand Down Expand Up @@ -89,6 +93,177 @@ describe('AllResidentBillingReceiptsService', () => {
expect(receipt.services).not.toBeNull()
})
})

it('returns public file', async () => {
const userClient = await makeClientWithProperty()
const support = await makeClientWithSupportUser()
const adminClient = await makeLoggedInAdminClient()

const [integration] = await createTestBillingIntegration(support)
const [billingContext] = await createTestBillingIntegrationOrganizationContext(userClient, userClient.organization, integration)

const integrationClient = await makeClientWithServiceUser()
await createTestBillingIntegrationAccessRight(support, integration, integrationClient.user)
const [billingProperty] = await createTestBillingProperty(integrationClient, billingContext, {
address: userClient.property.address,
})
const [billingAccount, billingAccountAttrs] = await createTestBillingAccount(integrationClient, billingContext, billingProperty)

const residentUser = await makeClientWithResidentUser()
const [resident] = await registerResidentByTestClient(residentUser, {
address: userClient.property.address,
addressMeta: userClient.property.addressMeta,
unitName: billingAccountAttrs.unitName,
})
await registerServiceConsumerByTestClient(residentUser, {
residentId: resident.id,
accountNumber: billingAccountAttrs.number,
organizationId: userClient.organization.id,
})
const [receipt] = await createTestBillingReceipt(integrationClient, billingContext, billingProperty, billingAccount)

// create BillingReceiptFile
const [receiptFile] = await createTestBillingReceiptFile(adminClient, receipt, billingContext)

// set primary file
await updateTestBillingReceipt(integrationClient, receipt.id, {
file: { connect: { id: receiptFile.id } },
})

// getting the result
const residentBillingReceipts = await ResidentBillingReceipt.getAll(residentUser)

expect(residentBillingReceipts).toBeDefined()
expect(residentBillingReceipts).not.toHaveLength(0)
residentBillingReceipts.forEach(receipt => {
expect(receipt).toHaveProperty('id')
expect(receipt.id).not.toBeNull()

expect(receipt).toHaveProperty('toPay')
expect(receipt.toPay).not.toBeNull()

expect(receipt).toHaveProperty('paid')
expect(receipt.paid).not.toBeNull()

expect(receipt).toHaveProperty('period')
expect(receipt.period).not.toBeNull()

expect(receipt).toHaveProperty('recipient')
expect(receipt.recipient).not.toBeNull()

expect(receipt).toHaveProperty('serviceConsumer')
expect(receipt.serviceConsumer).not.toBeNull()

expect(receipt).toHaveProperty('serviceConsumer.id')
expect(receipt.serviceConsumer.id).not.toBeNull()

expect(receipt).toHaveProperty('currencyCode')
expect(receipt.currencyCode).not.toBeNull()

expect(receipt).toHaveProperty('category')
expect(receipt.category).not.toBeNull()

expect(receipt).toHaveProperty('services')
expect(receipt.services).not.toBeNull()

expect(receipt).toHaveProperty('file')
expect(receipt.file).not.toBeNull()

expect(receipt.file).toHaveProperty('file')
expect(receipt.file.file).not.toBeNull()
expect(receipt.file.file.originalFilename).toEqual(path.basename(PUBLIC_FILE))
})
})

it('returns sensetive file', async () => {
const userClient = await makeClientWithProperty()
const support = await makeClientWithSupportUser()
const adminClient = await makeLoggedInAdminClient()

const [integration] = await createTestBillingIntegration(support)
const [billingContext] = await createTestBillingIntegrationOrganizationContext(userClient, userClient.organization, integration)

const integrationClient = await makeClientWithServiceUser()
await createTestBillingIntegrationAccessRight(support, integration, integrationClient.user)
const [billingProperty] = await createTestBillingProperty(integrationClient, billingContext, {
address: userClient.property.address,
})
const [billingAccount, billingAccountAttrs] = await createTestBillingAccount(integrationClient, billingContext, billingProperty)

const residentUser = await makeClientWithResidentUser()
const [resident] = await registerResidentByTestClient(residentUser, {
address: userClient.property.address,
addressMeta: userClient.property.addressMeta,
unitName: billingAccountAttrs.unitName,
})
await registerServiceConsumerByTestClient(residentUser, {
residentId: resident.id,
accountNumber: billingAccountAttrs.number,
organizationId: userClient.organization.id,
})
const [receipt] = await createTestBillingReceipt(integrationClient, billingContext, billingProperty, billingAccount)

// create BillingReceiptFile
const [receiptFile] = await createTestBillingReceiptFile(adminClient, receipt, billingContext)

// set primary file
await updateTestBillingReceipt(integrationClient, receipt.id, {
file: { connect: { id: receiptFile.id } },
})

// set contact
await createTestContact(adminClient, userClient.organization, userClient.property, {
phone: residentUser.userAttrs.phone,
unitName: billingAccountAttrs.unitName,
unitType: billingAccountAttrs.unitType,
isVerified: true,
})

// getting the result
const residentBillingReceipts = await ResidentBillingReceipt.getAll(residentUser)

expect(residentBillingReceipts).toBeDefined()
expect(residentBillingReceipts).not.toHaveLength(0)
residentBillingReceipts.forEach(receipt => {
expect(receipt).toHaveProperty('id')
expect(receipt.id).not.toBeNull()

expect(receipt).toHaveProperty('toPay')
expect(receipt.toPay).not.toBeNull()

expect(receipt).toHaveProperty('paid')
expect(receipt.paid).not.toBeNull()

expect(receipt).toHaveProperty('period')
expect(receipt.period).not.toBeNull()

expect(receipt).toHaveProperty('recipient')
expect(receipt.recipient).not.toBeNull()

expect(receipt).toHaveProperty('serviceConsumer')
expect(receipt.serviceConsumer).not.toBeNull()

expect(receipt).toHaveProperty('serviceConsumer.id')
expect(receipt.serviceConsumer.id).not.toBeNull()

expect(receipt).toHaveProperty('currencyCode')
expect(receipt.currencyCode).not.toBeNull()

expect(receipt).toHaveProperty('category')
expect(receipt.category).not.toBeNull()

expect(receipt).toHaveProperty('services')
expect(receipt.services).not.toBeNull()

expect(receipt).toHaveProperty('file')
expect(receipt.file).not.toBeNull()

expect(receipt.file).toHaveProperty('file')
expect(receipt.file.file).not.toBeNull()
expect(receipt.file.file.originalFilename).toEqual(path.basename(PRIVATE_FILE))
})
})

//TODO: @abshnko return services check to mobile
it.skip('returns correct services field', async () => {
const userClient = await makeClientWithProperty()
Expand Down
8 changes: 8 additions & 0 deletions apps/condo/domains/billing/schema/BillingReceipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ const BillingReceipt = new GQLListSchema('BillingReceipt', {
},
},

file: {
schemaDoc: 'A BillingReceiptFile that related to this billing receipt (filled up by integration)',
type: Relationship,
ref: 'BillingReceiptFile',
isRequired: false,
knexOptions: { isNotNullable: false },
kmigratorOptions: { null: true, on_delete: 'models.SET_NULL' },
},
},
plugins: [uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), historical()],
access: {
Expand Down
Loading

0 comments on commit f45efc9

Please sign in to comment.