Skip to content

Commit

Permalink
feat(condo): DOMA-8295 mutation to delete meter readings (#4335)
Browse files Browse the repository at this point in the history
* feat(condo): DOMA-8295 added mutation for deleting of meter readings

* feat(condo): DOMA-8295 use loadListByChunks for big data

* refactor(condo): DOMA-8295 some refactored

* refactor(condo): DOMA-8295 cleanup

* chore(condo): DOMA-8295 added notes

* refactor(condo): DOMA-8295 some refactoring

* test(condo): DOMA-8295 added test

* feat(condo): DOMA-8295 updated "_internalDeleteMeterAndMeterReadingsService" mutation

test(condo): DOMA-8295 updated tests for "_internalDeleteMeterAndMeterReadingsService"

* chore(condo): DOMA-8295 updated docs

* chore(condo): DOMA-8295 updated schema

* chore(condo): DOMA-8295 updated schema
  • Loading branch information
Alllex202 authored Jan 31, 2024
1 parent cc7ba65 commit 2550f7d
Show file tree
Hide file tree
Showing 12 changed files with 1,090 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Generated by `createservice meter.InternalDeleteMeterReadingsService --type mutations`
*/
const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter')

async function canInternalDeleteMeterReadings ({ authentication: { item: user } }) {
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false
return !!(user.isAdmin || user.isSupport)
}

/*
Rules are logical functions that used for list access, and may return a boolean (meaning
all or no items are available) or a set of filters that limit the available items.
*/
module.exports = {
canInternalDeleteMeterReadings,
}
1 change: 1 addition & 0 deletions apps/condo/domains/meter/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,5 @@ module.exports = {
CRM_METER_READING_SOURCE_ID,
DAY_SELECT_OPTIONS,
METER_REPORTING_PERIOD_FRONTEND_FEATURE_FLAG,
METER_READING_SOURCE_MOBILE_RESIDENT_APP_TYPE,
}
7 changes: 7 additions & 0 deletions apps/condo/domains/meter/constants/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const METER_NUMBER_HAVE_INVALID_VALUE = 'METER_NUMBER_HAVE_INVALID_VALUE'
const METER_ACCOUNT_NUMBER_HAVE_INVALID_VALUE = 'METER_ACCOUNT_NUMBER_HAVE_INVALID_VALUE'
const METER_RESOURCE_OWNED_BY_ANOTHER_ORGANIZATION = 'METER_RESOURCE_OWNED_BY_ANOTHER_ORGANIZATION'

const INVALID_START_DATE_TIME = 'INVALID_START_DATE_TIME'
const INVALID_END_DATE_TIME = 'INVALID_END_DATE_TIME'
const INVALID_PERIOD = 'INVALID_PERIOD'

module.exports = {
VALUE_LESS_THAN_PREVIOUS_ERROR,
EXISTING_METER_NUMBER_IN_SAME_ORGANIZATION,
Expand All @@ -19,4 +23,7 @@ module.exports = {
METER_NUMBER_HAVE_INVALID_VALUE,
METER_ACCOUNT_NUMBER_HAVE_INVALID_VALUE,
METER_RESOURCE_OWNED_BY_ANOTHER_ORGANIZATION,
INVALID_START_DATE_TIME,
INVALID_END_DATE_TIME,
INVALID_PERIOD,
}
9 changes: 8 additions & 1 deletion apps/condo/domains/meter/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ const MeterReportingPeriod = generateGqlQueries('MeterReportingPeriod', METER_RE

const DELETE_METER_AND_METER_READINGS_MUTATION = gql`
mutation _internalDeleteMeterAndMeterReadings ($data: _internalDeleteMeterAndMeterReadingsInput!) {
result: _internalDeleteMeterAndMeterReadings(data: $data) { status }
result: _internalDeleteMeterAndMeterReadings(data: $data) { status metersToDelete deletedMeters }
}
`

const METER_RESOURCE_OWNER_FIELDS = `{ organization { id } resource { id } address addressKey ${COMMON_FIELDS} }`
const MeterResourceOwner = generateGqlQueries('MeterResourceOwner', METER_RESOURCE_OWNER_FIELDS)

const INTERNAL_DELETE_METER_READINGS_MUTATION = gql`
mutation _internalDeleteMeterReadings ($data: _internalDeleteMeterReadingsInput!) {
result: _internalDeleteMeterReadings(data: $data) { status toDelete deleted }
}
`

/* AUTOGENERATE MARKER <CONST> */

module.exports = {
Expand All @@ -66,6 +72,7 @@ module.exports = {
MeterReportingPeriod,
DELETE_METER_AND_METER_READINGS_MUTATION,
MeterResourceOwner,
INTERNAL_DELETE_METER_READINGS_MUTATION,
/* AUTOGENERATE MARKER <EXPORTS> */
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,149 @@
* Generated by `createservice meter._internalDeleteMeterAndMeterReadingsService --type mutations`
*/

const dayjs = require('dayjs')
const chunk = require('lodash/chunk')
const isEmpty = require('lodash/isEmpty')
const { isArray, map } = require('lodash')

const { GQLErrorCode: { BAD_USER_INPUT } } = require('@open-condo/keystone/errors')
const { getLogger } = require('@open-condo/keystone/logging')
const { GQLCustomSchema, find } = require('@open-condo/keystone/schema')
const { checkDvAndSender } = require('@open-condo/keystone/plugins/dvAndSender')
const { GQLCustomSchema, itemsQuery } = require('@open-condo/keystone/schema')

const { DV_VERSION_MISMATCH, WRONG_FORMAT } = require('@condo/domains/common/constants/errors')
const { loadListByChunks } = require('@condo/domains/common/utils/serverSchema')
const access = require('@condo/domains/meter/access/_internalDeleteMeterAndMeterReadingsService')
const { METER_DELETE_STATUS } = require('@condo/domains/meter/constants')
const { Meter } = require('@condo/domains/meter/utils/serverSchema')


const logger = getLogger('_internalDeleteMeterAndMeterReadings')

/**
* List of possible errors, that this custom schema can throw
* They will be rendered in documentation section in GraphiQL for this custom schema
*/
const ERRORS = {
DV_VERSION_MISMATCH: {
query: '_internalDeleteMeterAndMeterReadingsService',
variable: ['data', 'dv'],
code: BAD_USER_INPUT,
type: DV_VERSION_MISMATCH,
message: 'Wrong value for data version number',
},
WRONG_SENDER_FORMAT: {
query: '_internalDeleteMeterAndMeterReadingsService',
variable: ['data', 'sender'],
code: BAD_USER_INPUT,
type: WRONG_FORMAT,
message: 'Invalid format of "sender" field value',
correctExample: '{ dv: 1, fingerprint: \'example-fingerprint-alphanumeric-value\'}',
},
}

const _internalDeleteMeterAndMeterReadingsService = new GQLCustomSchema('_internalDeleteMeterAndMeterReadingsService', {
schemaDoc: 'Mutation to delete meters and meter readings for property',
types: [
{
access: true,
type: 'input _internalDeleteMeterAndMeterReadingsInput { dv: Int!, sender: SenderFieldInput!, propertyIds: [String]! }',
type: 'input _internalDeleteMeterAndMeterReadingsInput { dv: Int!, sender: SenderFieldInput!, propertyIds: [ID], organizationId: ID! }',
},
{
access: true,
type: 'enum Status { success, error }',
},
{
access: true,
type: 'type _internalDeleteMeterAndMeterReadingsOutput { status: Status! }',
type: 'type _internalDeleteMeterAndMeterReadingsOutput { status: Status!, metersToDelete: Int!, deletedMeters: Int! }',
},
],

mutations: [
{
access: access.can_internalDeleteMeterAndMeterReadings,
schema: '_internalDeleteMeterAndMeterReadings(data: _internalDeleteMeterAndMeterReadingsInput!): _internalDeleteMeterAndMeterReadingsOutput',
doc: {
summary: 'This mutation deletes meters and meter readings in specified organization.',
description: 'This mutation deletes meters in specified organization for specified period.' +
'\n Readings are deleted automatically in a deferred task.' +
'\n You can also specify properties in which meters need to be deleted.' +
'\n The response will return the status of the operation: “success” if all meters for the specified filter were deleted, otherwise “error”.',
errors: ERRORS,
},
resolver: async (parent, args, context) => {
const { data } = args
const { dv, sender, propertyIds } = data
const { dv, sender, propertyIds, organizationId } = data

const meters = await find('Meter', {
checkDvAndSender(data, ERRORS.DV_VERSION_MISMATCH, ERRORS.WRONG_SENDER_FORMAT, context)

const metersWhere = {
deletedAt: null,
property: {
id_in: propertyIds,
},
organization: { id: organizationId },
...(isArray(propertyIds) ? { property: { id_in: propertyIds } } : undefined),
}

const { count: metersCount } = await itemsQuery('Meter', {
where: metersWhere,
}, { meta: true })

logger.info({
msg: `${metersCount} meters found to delete`,
meterReadingsWhere: JSON.stringify(metersWhere),
sender: JSON.stringify(sender),
})

if (isEmpty(meters)) {
logger.warn({ msg: 'Could not find meters by specified property ids', data: { propertyIds } })
return
if (!metersCount) {
logger.info({ msg: 'Readings not found', sender: JSON.stringify(sender) })
return { status: METER_DELETE_STATUS.SUCCESS, metersToDelete: 0, deletedMeters: 0 }
}
logger.info({ msg: `Following meters will be deleted: [${meters.map(reading => `'${reading.id}'`).join(', ')}]` })

const deletedAt = dayjs().toISOString()
const payload = meters.map(meter => ({ id: meter.id, data: { dv, sender, deletedAt } }))
const chunks = chunk(payload, 100)
const meterIdsToDeleteByChunk = []

await loadListByChunks({
context,
list: Meter,
where: metersWhere,
sortBy: ['createdAt_ASC'],
chunkSize: 100,
limit: 200_000,
chunkProcessor: async (chunk) => {
const meterIdsToDelete = map(chunk, 'id')
// Why not delete objects immediately in "chunkProcessor"?
// Then at each iteration i > 0, objects that have not yet been deleted will be skipped (i * chunkSize)
meterIdsToDeleteByChunk.push(meterIdsToDelete)
return []
},
})

let deletedMeters = 0
for (const chunkData of chunks) {
const deleted = await Meter.updateMany(context, chunkData)
deletedMeters += deleted.length
let processing = 0
for (const meterIdsToDelete of meterIdsToDeleteByChunk) {
logger.info({
msg: `Process of deleting readings (${processing}-${processing += meterIdsToDelete.length}/${metersCount})`,
meterIdsToDelete,
sender: JSON.stringify(sender),
})

try {
const deleted = await Meter.softDeleteMany(context, meterIdsToDelete, { dv, sender })
deletedMeters += deleted.length
} catch (error) {
logger.error({
msg: 'Failed to delete a meters',
error,
meterIds: meterIdsToDelete,
sender: JSON.stringify(sender),
})
}

}

const deleteStatus = meters.length === deletedMeters
const deleteStatus = metersCount === deletedMeters
? METER_DELETE_STATUS.SUCCESS : METER_DELETE_STATUS.ERROR
logger.info({ msg: `Delete Status: ${deleteStatus}` })

return { status: deleteStatus }
logger.info({
msg: 'Deleting meters completed',
status: deleteStatus,
metersToDelete: metersCount,
deletedMeters,
sender: JSON.stringify(sender),
})

return { status: deleteStatus, metersToDelete: metersCount, deletedMeters }
},
},
],
Expand Down
Loading

0 comments on commit 2550f7d

Please sign in to comment.