Skip to content

Commit

Permalink
feat(condo): DOMA-6331 push notification about the need to send meter…
Browse files Browse the repository at this point in the history
… readings (#3612)

* feat(condo): DOMA-6331 push notification about the need to send meter readings

* feat(condo): DOMA-6331 make push uniq by pattern userId, start or end period and current month

* feat(condo): DOMA-6331 added push notification translates

* feat(condo): DOMA-6331 change pattern of uniqKey to userId_startPeriodDay-endPeriodDay_(start/end). fix tests

* feat(condo): DOMA-6330 added routing on index meter page

* fix(condo): DOMA-6331 fix cron task config

* fix(condo): DOMA-6633 fix text of btn on create period page

* fix(condo): DOMA-6631 fix rerender of selects start and end of period

* fix(condo): DOMA-6630 add placeholder if chosen period for all organization

* fix(condo): DOMA-6331 disable isOrganization checkbox when org period has already been created

* fix(condo): DOMA-6331 makemigrations and types

* fix(condo): DOMA-6330 fix formValuesToMutationDataPreprocessor on update action

* fix(condo): DOMA-6331 try to fix error

* fix(condo): DOMA-6331 fix checkIsDateInPeriod util

* fix(condo): DOMA-6331 fix dayjs locale

* fix(condo): DOMA-6816 fix METER_SUBMIT_READINGS_REMINDER_END_PERIOD and push by deleted meters and periods

* fix(condo): DOMA-6815 fix connect deleted meter from deleted resident to not deleted resident with same accountNumber

* fix(condo): DOMA-6331 some code improvements

* chore(condo): DOMA-6331 rebase & regenerate migration
  • Loading branch information
VKislov authored Aug 11, 2023
1 parent 8008ac4 commit d9013c5
Show file tree
Hide file tree
Showing 28 changed files with 417 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ export const CreatePropertyMeterReadingsForm = ({ organization, role }) => {
const createMeterReadingAction = PropertyMeterReading.useCreate({
source: { connect: { id: CRM_METER_READING_SOURCE_ID } },
}, async () => {
await router.push(`/meter?meterType=${METER_PAGE_TYPES.propertyMeter}`)
await router.push(`/meter?tab=${METER_PAGE_TYPES.propertyMeter}`)
})

const handleSubmit = useCallback(async (values) => {
Expand Down
25 changes: 18 additions & 7 deletions apps/condo/domains/meter/components/MeterReportingPeriodForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { jsx } from '@emotion/react'
import { Col, Form, Row, Typography } from 'antd'
import get from 'lodash/get'
import isEmpty from 'lodash/isEmpty'
import isNil from 'lodash/isNil'
import { useRouter } from 'next/router'
import { Rule } from 'rc-field-form/lib/interface'
import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'
Expand Down Expand Up @@ -60,11 +61,14 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
const OrMessage = intl.formatMessage({ id: 'Or' })
const DeleteButtonLabel = intl.formatMessage({ id: 'Delete' })
const AddressLabel = intl.formatMessage({ id: 'field.Address' })
const SubmitButtonLabel = intl.formatMessage({ id: 'ApplyChanges' })
const SubmitButtonApplyLabel = intl.formatMessage({ id: 'ApplyChanges' })
const SubmitButtonCreateLabel = intl.formatMessage({ id: 'Create' })
const AddressPlaceholderMessage = intl.formatMessage({ id: 'placeholder.Address' })
const ErrorsContainerTitle = intl.formatMessage({ id: 'errorsContainer.requiredErrors' })
const StartLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.start' })
const FinishLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.finish' })
const AddressPlaceholderLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.addressPlaceholder' })
const AddressPlaceholderDefaultPeriodLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.addressPlaceholderIfDefaultPeriod' })
const IncorrectPeriodLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.incorrectPeriod' })
const OrganizationLabel = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.organizationPeriod' })
const OrganizationTooltipMessage = intl.formatMessage({ id: 'pages.condo.meter.reportingPeriod.create.organizationTooltip' })
Expand All @@ -91,6 +95,7 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({

const startNumberRef = useRef<number>(formInitialValues.notifyStartDay)
const finishNumberRef = useRef<number>(formInitialValues.notifyEndDay)
const [selectRerender, execSelectRerender] = useState()
const selectedPropertyIdRef = useRef(selectedPropertyId)

useEffect(() => {
Expand Down Expand Up @@ -135,11 +140,13 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({

const handleStartChange = useCallback((value) => {
startNumberRef.current = value
execSelectRerender(value)
handleDayChange()
}, [])

const handleFinishChange = useCallback((value) => {
finishNumberRef.current = value
execSelectRerender(value)
handleDayChange()
}, [])

Expand All @@ -153,15 +160,18 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
error: periodsLoadingError,
} = MeterReportingPeriod.useObjects({
where: {
property_is_null: false,
organization: { id: organizationId },
},
},
{
fetchPolicy: 'network-only',
})

const search = useMemo(() => searchOrganizationPropertyWithoutPropertyHint(organizationId, reportingPeriods.map(period => period.property.id)),
const hasOrganizationPeriod = Boolean(reportingPeriods.find(period => isNil(period.property) && !isNil(period.organization)))

const periodsWithProperty = reportingPeriods.filter(period => !isNil(period.property))

const search = useMemo(() => searchOrganizationPropertyWithoutPropertyHint(organizationId, periodsWithProperty.map(period => period.property.id)),
[organization, isPeriodsLoading])

const handelGQLInputChange = () => {
Expand Down Expand Up @@ -189,8 +199,8 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
values.property = { connect: { id: selectedPropertyIdRef.current } }
}
values.isOrganizationPeriod = undefined
values.notifyStartDay = parseInt(values.notifyStartDay)
values.notifyEndDay = parseInt(values.notifyEndDay)
values.notifyStartDay = startNumberRef.current
values.notifyEndDay = finishNumberRef.current

if (isCreateMode) {
values.organization = { connect: { id: organizationId } }
Expand Down Expand Up @@ -224,6 +234,7 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
<GraphQlSearchInput
label={AddressPlaceholderMessage}
showArrow={false}
placeholder={isOrganizationPeriod ? AddressPlaceholderDefaultPeriodLabel : AddressPlaceholderLabel}
disabled={isOrganizationPeriod}
onChange={handelGQLInputChange}
initialValue={isCreateMode ? undefined : selectedPropertyId}
Expand All @@ -243,6 +254,7 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
>
<Checkbox
checked={isOrganizationPeriod}
disabled={hasOrganizationPeriod}
eventName='OrganizationReportingPeriodCheckbox'
style={CHECKBOX_STYLE}
onChange={handleCheckboxChange}
Expand All @@ -257,7 +269,6 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
{...INPUT_LAYOUT_PROPS}
labelAlign='left'
required
shouldUpdate
validateFirst
>
<Select
Expand Down Expand Up @@ -326,7 +337,7 @@ export const MeterReportingPeriodForm: React.FC<IMeterReportingPeriodForm> = ({
onClick={handleSave}
loading={isLoading}
>
{SubmitButtonLabel}
{isCreateMode ? SubmitButtonCreateLabel : SubmitButtonApplyLabel}
</ButtonWithDisabledTooltip>,
isCreateMode ? <></> : <DeleteButtonWithConfirmModal
key='delete'
Expand Down
4 changes: 2 additions & 2 deletions apps/condo/domains/meter/hooks/useFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function useFilters (meterPageType: MeterPageTypes): Array<FiltersMeta<Me
},
]
}
case METER_PAGE_TYPES.meter || METER_PAGE_TYPES.propertyMeter: {
default: {
return compact([
{
keyword: 'address',
Expand Down Expand Up @@ -341,5 +341,5 @@ export function useFilters (meterPageType: MeterPageTypes): Array<FiltersMeta<Me
}
}

}, [sources, resources])
}, [sources, resources, isPropertyMeter, meterPageType])
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
const dayjs = require('dayjs')
const { get, uniq, isNull } = require('lodash')
const locale_ru = require('dayjs/locale/ru')
const isBetween = require('dayjs/plugin/isBetween')
const { get, uniq, isNull, isEmpty, isNil } = require('lodash')

const conf = require('@open-condo/config')
const { getLogger } = require('@open-condo/keystone/logging')
const { getSchemaCtx } = require('@open-condo/keystone/schema')
const { getLocalized } = require('@open-condo/locales/loader')

const { COUNTRIES } = require('@condo/domains/common/constants/countries')
const { RU_LOCALE } = require('@condo/domains/common/constants/locale')
const { loadListByChunks } = require('@condo/domains/common/utils/serverSchema')
const { rightJoin, joinResidentsToMeters } = require('@condo/domains/meter/tasks/sendVerificationDateReminder')
const { Meter, MeterReading } = require('@condo/domains/meter/utils/serverSchema')
const { Meter, MeterReading, MeterReportingPeriod } = require('@condo/domains/meter/utils/serverSchema')
const {
METER_SUBMIT_READINGS_REMINDER_TYPE,
METER_VERIFICATION_DATE_EXPIRED_TYPE,
METER_VERIFICATION_DATE_EXPIRED_TYPE, METER_SUBMIT_READINGS_REMINDER_END_PERIOD_TYPE, METER_SUBMIT_READINGS_REMINDER_START_PERIOD_TYPE,
} = require('@condo/domains/notification/constants/constants')
const { sendMessage } = require('@condo/domains/notification/utils/serverSchema')
const { Organization } = require('@condo/domains/organization/utils/serverSchema')


dayjs.extend(isBetween)
const logger = getLogger('meter/sendSubmitMeterReadingsPushNotifications')

const readMetersPage = async ({ context, offset, pageSize }) => {
return await Meter.getAll(
context, { isAutomatic: false }, {
context, {
isAutomatic: false,
deletedAt: null,
organization: {
deletedAt: null,
},
property: {
deletedAt: null,
},
}, {
sortBy: 'id_ASC',
first: pageSize,
skip: offset,
Expand All @@ -48,7 +60,9 @@ const readMeterReadings = async ({ context, meters }) => {
date_lte: endWindowDate,
meter: {
id_in: meterIds,
deletedAt: null,
},
deletedAt: null,
},
})
}
Expand All @@ -64,9 +78,29 @@ const readOrganizations = async ({ context, meters }) => {
where: {
id_in: organizationIds,
},
deletedAt: null,
})
}

const readMeterReportingPeriods = async ({ context, organizations }) => {
const organizationIds = organizations.map(org => org.id)
return await loadListByChunks({
context,
list: MeterReportingPeriod,
where: {
organization: {
id_in: organizationIds,
},
deletedAt: null,
},
})
}

const checkIsDateStartOrEndOfPeriod = (date, today, start, end) => {
return dayjs(date).format('YYYY-MM-DD') === dayjs(today).set('date', start).format('YYYY-MM-DD') ||
dayjs(date).format('YYYY-MM-DD') === dayjs(today).set('date', end).format('YYYY-MM-DD')
}

const sendSubmitMeterReadingsPushNotifications = async () => {
// task implementation algorithm
// * Read all meter resources for all supported languages
Expand Down Expand Up @@ -112,40 +146,62 @@ const sendSubmitMeterReadingsPushNotifications = async () => {
offset: state.offset,
pageSize: state.pageSize,
})
const meterReadings = await readMeterReadings({ context, meters })
const organizations = await readOrganizations({ context, meters })
const reportingPeriods = await readMeterReportingPeriods({ context, organizations })
const meterReadings = await readMeterReadings({ context, meters, reportingPeriods })

const metersWithoutReadings = []
const periodsByProperty = []
const periodsByOrganization = []
let defaultPeriod = null

for (let period of reportingPeriods) {
if (isNil(period.organization) && isNil(period.property)) defaultPeriod = period
else if (!isNil(period.organization) && isNil(period.property)) periodsByOrganization.push(period)
else periodsByProperty.push(period)
}

// right join data to metersPage
const metersWithoutReadings = rightJoin(
meters,
meterReadings,
(meter, item) => meter.id === item.meter.id,
(meter, readings) => ({ meter, readings })
)
.filter(({ readings }) => readings.length === 0)
.map(({ meter }) => meter)
for (const meter of meters) {
const period = periodsByProperty.find(({ property }) => property.id === meter.property.id) ??
periodsByOrganization.find(({ organization }) => organization.id === meter.organization.id) ??
defaultPeriod

if (isNil(period)) continue

const notifyStartDay = get(period, 'notifyStartDay')
const notifyEndDay = get(period, 'notifyEndDay')

const readingsOfCurrentMeter = meterReadings.filter(reading => (
reading.meter.id === meter.id &&
checkIsDateStartOrEndOfPeriod(reading.date, state.startTime, notifyStartDay, notifyEndDay)
))

const isTodayStartOrEndOfPeriod = checkIsDateStartOrEndOfPeriod(state.startTime, state.startTime, notifyStartDay, notifyEndDay)
const isEndPeriodNotification = dayjs(state.startTime).format('YYYY-MM-DD') === dayjs(state.startTime).set('date', notifyEndDay).format('YYYY-MM-DD')
const periodKey = `${dayjs(state.startTime).set('date', notifyStartDay).format('YYYY-MM-DD')}-${dayjs(state.startTime).set('date', notifyEndDay).format('YYYY-MM-DD')}`

if (isTodayStartOrEndOfPeriod) metersWithoutReadings.push({ meter, periodKey, isEndPeriodNotification, isEmptyReadings: isEmpty(readingsOfCurrentMeter) })
}

// right join organizations
const metersWithoutPeriod = rightJoin(
const metersToSendNotification = rightJoin(
metersWithoutReadings,
organizations,
(meter, organization) => meter.organization.id === organization.id,
(meter, organization) => {
({ meter }, organization) => meter.organization.id === organization.id,
({ meter, isEndPeriodNotification, isEmptyReadings, periodKey }, organization) => {
/**
* Detect message language
* Use DEFAULT_LOCALE if organization.country is unknown
* (not defined within @condo/domains/common/constants/countries)
*/
const lang = get(COUNTRIES, [get(organization, 'country', conf.DEFAULT_LOCALE), 'locale'], conf.DEFAULT_LOCALE)
const period = null // TODO calculate this prop after implementation submit period in organisation
return { ...meter, period, lang }
return { ...meter, isEndPeriodNotification, isEmptyReadings, periodKey, lang }
}
)
.filter(({ period }) => period == null) // pick meters without organisation default period
state.metersWithoutReadings += metersWithoutPeriod.length
state.metersWithoutReadings += metersToSendNotification.length

// Join residents
const metersWithResident = await joinResidentsToMeters({ context, meters: metersWithoutPeriod })
const metersWithResident = await joinResidentsToMeters({ context, meters: metersToSendNotification })
state.metersWithoutReadingsAndWithResidents += metersWithResident.length

// Send message with specific unique key for set up readings
Expand Down Expand Up @@ -195,18 +251,20 @@ const sendMessageSafely = async ({ context, message }) => {
const sendMessagesForSetUpReadings = async ({ context, metersWithResident }) => {
await Promise.all(metersWithResident.map(async ({ meter, residents }) => {
await Promise.all(residents.map(async (resident) => {
const { lang } = meter
const uniqKey = `${meter.id}_${resident.id}_${dayjs().format('YYYY-MM')}`
const { lang, periodKey } = meter
const now = dayjs()
const uniqKey = `${resident.user.id}_${periodKey}_${meter.isEndPeriodNotification ? 'end' : 'start'}`

const message = {
sender: { dv: 1, fingerprint: 'meters-readings-submit-reminder-cron-push' },
to: { user: { id: resident.user.id } },
type: METER_SUBMIT_READINGS_REMINDER_TYPE,
type: meter.isEndPeriodNotification ? METER_SUBMIT_READINGS_REMINDER_END_PERIOD_TYPE : METER_SUBMIT_READINGS_REMINDER_START_PERIOD_TYPE,
lang,
uniqKey,
meta: {
dv: 1,
data: {
monthName: lang === RU_LOCALE ? now.locale('ru').format('MMMM') : now.format('MMMM'),
meterId: meter.id,
userId: resident.user.id,
residentId: resident.id,
Expand All @@ -216,7 +274,7 @@ const sendMessagesForSetUpReadings = async ({ context, metersWithResident }) =>
organization: { id: meter.organization.id },
}

await sendMessageSafely({ context, message })
if (meter.isEmptyReadings) await sendMessageSafely({ context, message })
}))
}))
}
Expand Down Expand Up @@ -248,7 +306,7 @@ const sendMessagesForExpiredMeterVerificationDate = async ({ context, metersWith
organization: meter.organization && { id: meter.organization.id },
}

await sendMessageSafely({ context, message })
if (meter.isEmptyReadings) await sendMessageSafely({ context, message })
}))
}))
}
Expand Down
Loading

0 comments on commit d9013c5

Please sign in to comment.