diff --git a/src/components/periodic-report/dto/periodic-report.dto.ts b/src/components/periodic-report/dto/periodic-report.dto.ts index 28ac751458..a8803b76ec 100644 --- a/src/components/periodic-report/dto/periodic-report.dto.ts +++ b/src/components/periodic-report/dto/periodic-report.dto.ts @@ -1,4 +1,5 @@ import { Field, InterfaceType, ObjectType } from '@nestjs/graphql'; +import { type MergeExclusive } from 'type-fest'; import { Calculated, CalendarDate, @@ -15,8 +16,14 @@ import { e } from '~/core/gel'; import { RegisterResource } from '~/core/resources'; import { type ScopedRole } from '../../authorization/dto'; import { type DefinedFile } from '../../file/dto'; +import { ProgressReport } from '../../progress-report/dto'; import { ReportType } from './report-type.enum'; +export type AnyReport = MergeExclusive< + FinancialReport, + MergeExclusive +>; + @RegisterResource({ db: e.PeriodicReport }) @Calculated() @InterfaceType({ @@ -44,7 +51,7 @@ class PeriodicReport extends Resource { @Field() readonly skippedReason: SecuredStringNullable; - readonly reportFile: DefinedFile; + readonly reportFile: DefinedFile; //TODO? - Secured | null> @SensitivityField({ description: "Based on the project's sensitivity", @@ -83,6 +90,12 @@ export class NarrativeReport extends PeriodicReport { }) export class SecuredPeriodicReport extends SecuredProperty(PeriodicReport) {} +export const ReportConcretes = { + Financial: FinancialReport, + Narrative: NarrativeReport, + Progress: ProgressReport, +}; + declare module '~/core/resources/map' { interface ResourceMap { PeriodicReport: typeof PeriodicReport; diff --git a/src/components/periodic-report/handlers/abstract-periodic-report-sync.ts b/src/components/periodic-report/handlers/abstract-periodic-report-sync.ts index 399db2b2cf..4e4a3e93b3 100644 --- a/src/components/periodic-report/handlers/abstract-periodic-report-sync.ts +++ b/src/components/periodic-report/handlers/abstract-periodic-report-sync.ts @@ -1,7 +1,7 @@ import { type DateTimeUnit } from 'luxon'; import { type CalendarDate, DateInterval, type ID, type Range } from '~/common'; import { type ReportType } from '../dto'; -import { type PeriodicReportService } from '../periodic-report.service'; +import { type PeriodicReportRepository } from '../periodic-report.repository'; export type Intervals = [ updated: DateInterval | null, @@ -9,7 +9,9 @@ export type Intervals = [ ]; export abstract class AbstractPeriodicReportSync { - constructor(protected readonly periodicReports: PeriodicReportService) {} + constructor( + protected readonly periodicReportsRepo: PeriodicReportRepository, + ) {} protected async sync( parent: ID, @@ -20,9 +22,12 @@ export abstract class AbstractPeriodicReportSync { }, finalAt?: CalendarDate, ) { - await this.periodicReports.delete(parent, type, diff.removals); + if (!diff) { + return; + } + await this.periodicReportsRepo.delete(parent, type, diff.removals); - await this.periodicReports.merge({ + await this.periodicReportsRepo.merge({ type, parent, intervals: diff.additions, @@ -31,7 +36,7 @@ export abstract class AbstractPeriodicReportSync { if (!finalAt) { return; } - await this.periodicReports.mergeFinalReport(parent, type, finalAt); + await this.periodicReportsRepo.mergeFinalReport(parent, type, finalAt); } /** diff --git a/src/components/periodic-report/handlers/sync-periodic-report-to-project.handler.ts b/src/components/periodic-report/handlers/sync-periodic-report-to-project.handler.ts index 0915bceb4e..14bc869c3b 100644 --- a/src/components/periodic-report/handlers/sync-periodic-report-to-project.handler.ts +++ b/src/components/periodic-report/handlers/sync-periodic-report-to-project.handler.ts @@ -1,10 +1,16 @@ import { type DateTimeUnit } from 'luxon'; import { type DateInterval } from '~/common'; -import { EventsHandler, type IEventHandler, ILogger, Logger } from '~/core'; +import { + ConfigService, + EventsHandler, + type IEventHandler, + ILogger, + Logger, +} from '~/core'; import { projectRange } from '../../project/dto'; import { ProjectUpdatedEvent } from '../../project/events'; import { ReportPeriod, ReportType } from '../dto'; -import { PeriodicReportService } from '../periodic-report.service'; +import { PeriodicReportRepository } from '../periodic-report.repository'; import { AbstractPeriodicReportSync, type Intervals, @@ -18,13 +24,18 @@ export class SyncPeriodicReportsToProjectDateRange implements IEventHandler { constructor( - periodicReports: PeriodicReportService, + periodicReportsRepo: PeriodicReportRepository, + private readonly config: ConfigService, @Logger('periodic-reports:project-sync') private readonly logger: ILogger, ) { - super(periodicReports); + super(periodicReportsRepo); } async handle(event: SubscribedEvent) { + if (this.config.databaseEngine === 'gel') { + return; + } + this.logger.debug('Project mutation, syncing periodic reports', { ...event, event: event.constructor.name, diff --git a/src/components/periodic-report/handlers/sync-progress-report-to-engagement.handler.ts b/src/components/periodic-report/handlers/sync-progress-report-to-engagement.handler.ts index 7eaf281bdb..a9850f959c 100644 --- a/src/components/periodic-report/handlers/sync-progress-report-to-engagement.handler.ts +++ b/src/components/periodic-report/handlers/sync-progress-report-to-engagement.handler.ts @@ -1,6 +1,12 @@ import { Settings } from 'luxon'; import { DateInterval, type UnsecuredDto } from '~/common'; -import { EventsHandler, type IEventHandler, ILogger, Logger } from '~/core'; +import { + ConfigService, + EventsHandler, + type IEventHandler, + ILogger, + Logger, +} from '~/core'; import { EngagementService } from '../../engagement'; import { type Engagement, engagementRange } from '../../engagement/dto'; import { @@ -9,7 +15,7 @@ import { } from '../../engagement/events'; import { ProjectUpdatedEvent } from '../../project/events'; import { ReportType } from '../dto'; -import { PeriodicReportService } from '../periodic-report.service'; +import { PeriodicReportRepository } from '../periodic-report.repository'; import { AbstractPeriodicReportSync, type Intervals, @@ -30,14 +36,19 @@ export class SyncProgressReportToEngagementDateRange implements IEventHandler { constructor( - periodicReports: PeriodicReportService, + periodicReportsRepo: PeriodicReportRepository, + private readonly config: ConfigService, private readonly engagements: EngagementService, @Logger('progress-report:engagement-sync') private readonly logger: ILogger, ) { - super(periodicReports); + super(periodicReportsRepo); } async handle(event: SubscribedEvent) { + if (this.config.databaseEngine === 'gel') { + return; + } + // Only LanguageEngagements if ( !( diff --git a/src/components/periodic-report/periodic-report.gel.repository.ts b/src/components/periodic-report/periodic-report.gel.repository.ts new file mode 100644 index 0000000000..c78d61ae4d --- /dev/null +++ b/src/components/periodic-report/periodic-report.gel.repository.ts @@ -0,0 +1,171 @@ +import { Injectable } from '@nestjs/common'; +import { type Without } from 'type-fest/source/merge-exclusive'; +import { + CalendarDate, + EnhancedResource, + type ID, + type PublicOf, + type UnsecuredDto, +} from '~/common'; +import { castToEnum, e, RepoFor } from '~/core/gel'; +import { type ProgressReport } from '../progress-report/dto'; +import { + type FinancialReport, + IPeriodicReport, + type NarrativeReport, + ReportType, + resolveReportType, +} from './dto'; +import { type PeriodicReportRepository } from './periodic-report.repository'; + +@Injectable() +export class PeriodicReportGelRepository + extends RepoFor(IPeriodicReport, { + hydrate: (periodicReport) => ({ + ...periodicReport['*'], + type: castToEnum(periodicReport.__type__.name.slice(9, -7), ReportType), + reportFile: true, + sensitivity: periodicReport.container.is(e.Project.ContextAware) + .sensitivity, + scope: false, + parent: e.select({ + identity: periodicReport.id, + labels: e.array_agg(e.set(periodicReport.__type__.name.slice(9, null))), + properties: e.select({ + id: periodicReport.id, + createdAt: periodicReport.createdAt, + }), + }), + }), + omit: ['create'], + }) + implements PublicOf +{ + //TODO - this is close but the hydrate for ProgressReport needs worked on + async matchCurrentDue(parentId: ID, reportType: ReportType) { + const enhancedResource = EnhancedResource.of( + resolveReportType({ type: reportType }), + ); + const resource = e.cast(enhancedResource.db, e.uuid(parentId)); + const report = e.select(resource, (report) => ({ + ...this.hydrate(report), + filter: e.all( + e.set( + e.op(resource.id, '=', report.container.id), + e.op(report.end, '<', CalendarDate.now()), + ), + ), + order_by: [ + { expression: report.end, direction: e.DESC }, + { expression: report.start, direction: e.ASC }, + ], + })); + return await this.db.run(report); + } + + getCurrentDue( + _parentId: ID, + _reportType: ReportType, + ): Promise< + | UnsecuredDto< + | (Without< + | (Without & NarrativeReport) + | (Without & FinancialReport), + ProgressReport + > & + ProgressReport) + | (Without< + ProgressReport, + | (Without & NarrativeReport) + | (Without & FinancialReport) + > & + ( + | (Without & NarrativeReport) + | (Without & FinancialReport) + )) + > + | undefined + > { + //TODO - is this needed? How does this differ from matchCurrentDue? + throw new Error('Method not implemented.'); + } + + //TODO - this is close but the hydrate for ProgressReport needs worked on + async getByDate(parentId: ID, date: CalendarDate, reportType: ReportType) { + const enhancedResource = EnhancedResource.of( + resolveReportType({ type: reportType }), + ); + const resource = e.cast(enhancedResource.db, e.uuid(parentId)); + const report = e.select(resource, (report) => ({ + ...this.hydrate(report), + filter: e.all( + e.set( + e.op(resource.id, '=', report.container.id), + e.op(report.start, '<=', date), + e.op(report.end, '>=', date), + ), + ), + })); + return await this.db.run(report); + } + + //TODO - this is close but the hydrate for ProgressReport needs worked on + async getNextDue(parentId: ID, reportType: ReportType) { + const enhancedResource = EnhancedResource.of( + resolveReportType({ type: reportType }), + ); + const resource = e.cast(enhancedResource.db, e.uuid(parentId)); + const report = e.select(resource, (report) => ({ + ...this.hydrate(report), + filter: e.all( + e.set( + e.op(resource.id, '=', report.container.id), + e.op(report.end, '>', CalendarDate.now()), + ), + ), + order_by: { expression: report.end, direction: e.ASC }, + })); + + return await this.db.run(report); + } + + //TODO - this is close but the hydrate for ProgressReport needs worked on + async getLatestReportSubmitted(parentId: ID, reportType: ReportType) { + const enhancedResource = EnhancedResource.of( + resolveReportType({ type: reportType }), + ); + const resource = e.cast(enhancedResource.db, e.uuid(parentId)); + const report = e.select(resource, (report) => ({ + ...this.hydrate(report), + filter: e.all( + e.set( + e.op(resource.id, '=', report.container.id), + e.op('exists', report.reportFile), + ), + ), + order_by: { expression: report.start, direction: e.DESC }, + })); + + return await this.db.run(report); + } + + //TODO - this is close but the hydrate for ProgressReport needs worked on + async getFinalReport(parentId: ID, reportType: ReportType) { + const enhancedResource = EnhancedResource.of( + resolveReportType({ type: reportType }), + ); + const resource = e.cast(enhancedResource.db, e.uuid(parentId)); + const report = e.select(resource, (report) => ({ + ...this.hydrate(report), + filter: e.all( + e.set( + e.op(resource.id, '=', report.container.id), + e.op(report.start, '=', report.end), + ), + ), + order_by: { expression: report.start, direction: e.DESC }, + })); + + return await this.db.run(report); + } +} diff --git a/src/components/periodic-report/periodic-report.module.ts b/src/components/periodic-report/periodic-report.module.ts index 8badb371b4..475ef1da61 100644 --- a/src/components/periodic-report/periodic-report.module.ts +++ b/src/components/periodic-report/periodic-report.module.ts @@ -1,4 +1,5 @@ import { forwardRef, Module } from '@nestjs/common'; +import { splitDb } from '~/core'; import { AuthorizationModule } from '../authorization/authorization.module'; import { EngagementModule } from '../engagement/engagement.module'; import { FileModule } from '../file/file.module'; @@ -7,6 +8,7 @@ import { ProjectModule } from '../project/project.module'; import * as handlers from './handlers'; import { PeriodicReportParentResolver } from './periodic-report-parent.resolver'; import { PeriodicReportProjectConnectionResolver } from './periodic-report-project-connection.resolver'; +import { PeriodicReportGelRepository } from './periodic-report.gel.repository'; import { PeriodicReportLoader } from './periodic-report.loader'; import { PeriodicReportRepository } from './periodic-report.repository'; import { PeriodicReportResolver } from './periodic-report.resolver'; @@ -25,7 +27,7 @@ import { PeriodicReportService } from './periodic-report.service'; PeriodicReportResolver, PeriodicReportProjectConnectionResolver, PeriodicReportParentResolver, - PeriodicReportRepository, + splitDb(PeriodicReportRepository, PeriodicReportGelRepository), PeriodicReportLoader, ...Object.values(handlers), ], diff --git a/src/components/periodic-report/periodic-report.repository.ts b/src/components/periodic-report/periodic-report.repository.ts index b390bc779f..2718350811 100644 --- a/src/components/periodic-report/periodic-report.repository.ts +++ b/src/components/periodic-report/periodic-report.repository.ts @@ -11,13 +11,15 @@ import { } from 'cypher-query-builder'; import { type CalendarDate, + CreationFailed, + DateInterval, generateId, type ID, + NotFoundException, type Range, type UnsecuredDto, } from '~/common'; import { DtoRepository } from '~/core/database'; -import { type ChangesOf } from '~/core/database/changes'; import { ACTIVE, createNode, @@ -33,6 +35,7 @@ import { variable, type Variable, } from '~/core/database/query'; +import { ILogger, Logger } from '../../core'; import { File } from '../file/dto'; import { ProgressReport, @@ -60,125 +63,180 @@ export class PeriodicReportRepository extends DtoRepository< >(IPeriodicReport) { constructor( private readonly progressRepo: ProgressReportExtraForPeriodicInterfaceRepository, + @Logger('periodic:report:service') private readonly logger: ILogger, ) { super(); } async merge(input: MergePeriodicReports) { - const Report = resolveReportType(input); + if (input.intervals.length === 0) { + return; + } - // Create IDs here that will feed into the reports that are new. - // If only neo4j had a nanoid generator natively. - const intervals = await Promise.all( - input.intervals.map(async (interval) => ({ - tempId: await generateId(), - start: interval.start, - end: interval.end, - tempFileId: await generateId(), - })), - ); + try { + const Report = resolveReportType(input); - const isProgress = input.type === ReportType.Progress; - const extraCreateOptions = isProgress - ? this.progressRepo.getCreateOptions(input) - : {}; - - const query = this.db - .query() - // before interval list, so it's the same time across reports - .with('datetime() as now') - .matchNode('parent', 'BaseNode', { id: input.parent }) - .unwind(intervals, 'interval') - .comment('Stop processing this row if the report already exists') - .subQuery('parent, interval', (sub) => - sub - .match([ - [ - node('parent'), - relation('out', '', 'report', ACTIVE), - node('node', `${input.type}Report`), - ], - [ - node('node'), - relation('out', '', 'start', ACTIVE), - node('', 'Property', { value: variable('interval.start') }), - ], - [ - node('node'), - relation('out', '', 'end', ACTIVE), - node('', 'Property', { value: variable('interval.end') }), - ], - ]) - // Invert zero rows into one row - // We want to continue out of this sub-query having 1 row when - // the report doesn't exist. - // However, the match above gives us zero rows in this case. - // Use count() to get us back to 1 row, and to create a temp list - // of how many rows we want (0 if report exists, 1 if it doesn't). - // Then use UNWIND to convert this list into rows. - .with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows') - .raw('UNWIND rows as row') - // nonsense value, the 1 row returned is what is important, not this column - .return('true as itIsNew'), - ) - .apply( - await createNode(Report as typeof IPeriodicReport, { - baseNodeProps: { - id: variable('interval.tempId'), - createdAt: variable('now'), - ...extraCreateOptions.baseNodeProps, - }, - initialProps: { - type: input.type, - start: variable('interval.start'), - end: variable('interval.end'), - skippedReason: null, - receivedDate: null, - reportFile: variable('interval.tempFileId'), - ...extraCreateOptions.initialProps, - }, - }), - ) - .apply( - createRelationships(Report, 'in', { - report: variable('parent'), - }), - ) - .apply(isProgress ? this.progressRepo.amendAfterCreateNode() : undefined) - // rename node to report, so we can call create node again for the file - .with('now, interval, node as report') - .apply( - await createNode(File, { - initialProps: { - name: variable('apoc.temporal.format(interval.end, "date")'), - }, - baseNodeProps: { - id: variable('interval.tempFileId'), - createdAt: variable('now'), - }, - }), - ) - .apply( - createRelationships(File, { - in: { reportFileNode: variable('report') }, - out: { createdBy: currentUser }, - }), - ) - .return<{ id: ID; interval: Range }>( - 'report.id as id, interval', + // Create IDs here that will feed into the reports that are new. + // If only neo4j had a nanoid generator natively. + const intervals = await Promise.all( + input.intervals.map(async (interval) => ({ + tempId: await generateId(), + start: interval.start, + end: interval.end, + tempFileId: await generateId(), + })), ); - return await query.run(); + + const isProgress = input.type === ReportType.Progress; + const extraCreateOptions = isProgress + ? this.progressRepo.getCreateOptions(input) + : {}; + + const query = this.db + .query() + // before interval list, so it's the same time across reports + .with('datetime() as now') + .matchNode('parent', 'BaseNode', { id: input.parent }) + .unwind(intervals, 'interval') + .comment('Stop processing this row if the report already exists') + .subQuery('parent, interval', (sub) => + sub + .match([ + [ + node('parent'), + relation('out', '', 'report', ACTIVE), + node('node', `${input.type}Report`), + ], + [ + node('node'), + relation('out', '', 'start', ACTIVE), + node('', 'Property', { value: variable('interval.start') }), + ], + [ + node('node'), + relation('out', '', 'end', ACTIVE), + node('', 'Property', { value: variable('interval.end') }), + ], + ]) + // Invert zero rows into one row + // We want to continue out of this sub-query having 1 row when + // the report doesn't exist. + // However, the match above gives us zero rows in this case. + // Use count() to get us back to 1 row, and to create a temp list + // of how many rows we want (0 if report exists, 1 if it doesn't). + // Then use UNWIND to convert this list into rows. + .with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows') + .raw('UNWIND rows as row') + // nonsense value, the 1 row returned is what is important, not this column + .return('true as itIsNew'), + ) + .apply( + await createNode(Report as typeof IPeriodicReport, { + baseNodeProps: { + id: variable('interval.tempId'), + createdAt: variable('now'), + ...extraCreateOptions.baseNodeProps, + }, + initialProps: { + type: input.type, + start: variable('interval.start'), + end: variable('interval.end'), + skippedReason: null, + receivedDate: null, + reportFile: variable('interval.tempFileId'), + ...extraCreateOptions.initialProps, + }, + }), + ) + .apply( + createRelationships(Report, 'in', { + report: variable('parent'), + }), + ) + .apply( + isProgress ? this.progressRepo.amendAfterCreateNode() : undefined, + ) + // rename node to report, so we can call create node again for the file + .with('now, interval, node as report') + .apply( + await createNode(File, { + initialProps: { + name: variable('apoc.temporal.format(interval.end, "date")'), + }, + baseNodeProps: { + id: variable('interval.tempFileId'), + createdAt: variable('now'), + }, + }), + ) + .apply( + createRelationships(File, { + in: { reportFileNode: variable('report') }, + out: { createdBy: currentUser }, + }), + ) + .return<{ id: ID; interval: Range }>( + 'report.id as id, interval', + ); + + const result = await query.run(); + + this.logger.info(`Merged ${input.type.toLowerCase()} reports`, { + existing: input.intervals.length - result.length, + new: result.length, + parent: input.parent, + newIntervals: result.map(({ interval }) => + DateInterval.fromObject(interval).toISO(), + ), + }); + } catch (exception) { + const Report = resolveReportType({ type: input.type }); + throw new CreationFailed(Report, exception); + } + } + + async mergeFinalReport( + parentId: ID, + type: ReportType, + at: CalendarDate, + ): Promise { + const report = await this.getFinalReport(parentId, type); + + if (report) { + if (+report.start === +at) { + // no change + return; + } + await this.update({ id: report.id, start: at, end: at }); + } else { + await this.merge({ + intervals: [{ start: at, end: at }], + type, + parent: parentId, + }); + } } - async update>( - existing: T, - simpleChanges: Omit< - ChangesOf, - 'reportFile' - > & - Partial>, + async update( + changes: Omit & + Pick, ) { - return await this.updateProperties(existing, simpleChanges); + const { id, ...simpleChanges } = changes; + + await this.updateProperties({ id }, simpleChanges); + + return await this.readOne(id); + } + + async readOne(id: ID) { + if (!id) { + throw new NotFoundException( + 'No periodic report id to search for', + 'periodicReport.id', + ); + } + + return await super.readOne(id); } async list(input: PeriodicReportListInput) { @@ -314,7 +372,12 @@ export class PeriodicReportRepository extends DtoRepository< type: ReportType, intervals: ReadonlyArray>, ) { - return await this.db + intervals = intervals.filter((i) => i.start || i.end); + if (intervals.length === 0) { + return; + } + + const result = await this.db .query() .unwind( intervals.map((i) => ({ start: i.start, end: i.end })), @@ -366,6 +429,8 @@ export class PeriodicReportRepository extends DtoRepository< ) .return<{ count: number }>('count(report) as count') .first(); + + this.logger.info('Deleted reports', { baseNodeId, type, ...result }); } protected hydrate() { diff --git a/src/components/periodic-report/periodic-report.service.ts b/src/components/periodic-report/periodic-report.service.ts index 174f43d7c9..042df5554c 100644 --- a/src/components/periodic-report/periodic-report.service.ts +++ b/src/components/periodic-report/periodic-report.service.ts @@ -1,11 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CalendarDate, - CreationFailed, - DateInterval, type ID, type ObjectView, - type Range, type UnsecuredDto, } from '~/common'; import { HandleIdLookup, IEventBus, ILogger, Logger } from '~/core'; @@ -16,7 +13,6 @@ import { FileService } from '../file'; import { ProgressReport } from '../progress-report/dto'; import { FinancialReport, - type MergePeriodicReports, NarrativeReport, type PeriodicReport, type PeriodicReportListInput, @@ -40,41 +36,31 @@ export class PeriodicReportService { private readonly repo: PeriodicReportRepository, ) {} - async merge(input: MergePeriodicReports) { - if (input.intervals.length === 0) { - return; - } - try { - const result = await this.repo.merge(input); - this.logger.info(`Merged ${input.type.toLowerCase()} reports`, { - existing: input.intervals.length - result.length, - new: result.length, - parent: input.parent, - newIntervals: result.map(({ interval }) => - DateInterval.fromObject(interval).toISO(), - ), - }); - } catch (exception) { - const Report = resolveReportType({ type: input.type }); - throw new CreationFailed(Report); - } - } - async update(input: UpdatePeriodicReportInput) { - const currentRaw = await this.repo.readOne(input.id); - const current = this.secure(currentRaw); - const changes = this.repo.getActualChanges(current, input); + const current = await this.repo.readOne(input.id); + const changes = this.repo.getActualChanges( + current, + //TODO - elimate this type assertion below somehow + input as UpdatePeriodicReportInput & Record, + ); this.privileges - .for(resolveReportType(current), currentRaw) + .for(resolveReportType(current), current) .verifyChanges(changes); const { reportFile, ...simpleChanges } = changes; - const updated = await this.repo.update(current, simpleChanges); + const updated = this.secure( + await this.repo.update({ + id: current.id, + start: current.start, + end: current.end, + ...simpleChanges, + }), + ); if (reportFile) { const file = await this.files.updateDefinedFile( - current.reportFile, + this.secure(current).reportFile, 'file', reportFile, ); @@ -163,20 +149,6 @@ export class PeriodicReportService { : undefined; } - async delete( - parent: ID, - type: ReportType, - intervals: ReadonlyArray>, - ) { - intervals = intervals.filter((i) => i.start || i.end); - if (intervals.length === 0) { - return; - } - const result = await this.repo.delete(parent, type, intervals); - - this.logger.info('Deleted reports', { parent, type, ...result }); - } - async getFinalReport( parentId: ID, type: ReportType, @@ -184,29 +156,4 @@ export class PeriodicReportService { const report = await this.repo.getFinalReport(parentId, type); return report ? this.secure(report) : undefined; } - - async mergeFinalReport( - parentId: ID, - type: ReportType, - at: CalendarDate, - ): Promise { - const report = await this.repo.getFinalReport(parentId, type); - - if (report) { - if (+report.start === +at) { - // no change - return; - } - await this.repo.update(report, { - start: at, - end: at, - }); - } else { - await this.merge({ - intervals: [{ start: at, end: at }], - type, - parent: parentId, - }); - } - } }