From b304947aab2eb669d4cd69ffc7d056ad1afd90c4 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 23 Jan 2024 15:53:03 +0900 Subject: [PATCH] fix: dashboard data api (#141) * fix: update count timezone bug * fix: update statistics data real time --- .../feedback/feedback.mysql.service.ts | 41 ++++++++++++++--- .../domains/feedback/feedback.service.spec.ts | 44 +++++++++++++++++++ .../src/domains/feedback/feedback.service.ts | 16 ------- .../project/issue/issue.service.spec.ts | 9 ++++ .../domains/project/issue/issue.service.ts | 29 +++++++++--- .../feedback-issue-statistics.service.spec.ts | 18 +++++++- .../feedback-issue-statistics.service.ts | 18 +++++++- .../feedback-statistics.service.spec.ts | 18 +++++++- .../feedback/feedback-statistics.service.ts | 18 +++++++- .../issue/issue-statistics.service.spec.ts | 18 +++++++- .../issue/issue-statistics.service.ts | 18 +++++++- 11 files changed, 209 insertions(+), 38 deletions(-) diff --git a/apps/api/src/domains/feedback/feedback.mysql.service.ts b/apps/api/src/domains/feedback/feedback.mysql.service.ts index 03830aae3..c7fd67b2b 100644 --- a/apps/api/src/domains/feedback/feedback.mysql.service.ts +++ b/apps/api/src/domains/feedback/feedback.mysql.service.ts @@ -28,14 +28,13 @@ import { ChannelEntity } from '../channel/channel/channel.entity'; import type { FieldEntity } from '../channel/field/field.entity'; import { OptionEntity } from '../channel/option/option.entity'; import { IssueEntity } from '../project/issue/issue.entity'; -import type { - CountByProjectIdDto, - DeleteByIdsDto, - FindFeedbacksByChannelIdDto, -} from './dtos'; +import { FeedbackIssueStatisticsService } from '../statistics/feedback-issue/feedback-issue-statistics.service'; +import { FeedbackStatisticsService } from '../statistics/feedback/feedback-statistics.service'; +import type { CountByProjectIdDto, FindFeedbacksByChannelIdDto } from './dtos'; import { AddIssueDto, CreateFeedbackMySQLDto, + DeleteByIdsDto, RemoveIssueDto, UpdateFeedbackMySQLDto, } from './dtos'; @@ -54,6 +53,8 @@ export class FeedbackMySQLService { @InjectRepository(OptionEntity) private readonly optionRepository: Repository, private readonly cls: ClsService, + private readonly feedbackStatisticsService: FeedbackStatisticsService, + private readonly feedbackIssueStatisticsService: FeedbackIssueStatisticsService, ) {} @Transactional() @@ -62,6 +63,13 @@ export class FeedbackMySQLService { feedback.channel = new ChannelEntity(); feedback.channel.id = channelId; feedback.rawData = data; + + await this.feedbackStatisticsService.updateCount({ + channelId, + date: DateTime.utc().toJSDate(), + count: 1, + }); + return await this.feedbackRepository.save(feedback); } @@ -326,6 +334,12 @@ export class FeedbackMySQLService { feedbackCount: () => 'feedback_count + 1', updatedAt: () => `'${DateTime.utc().toFormat('yyyy-MM-dd HH:mm:ss')}'`, }); + + await this.feedbackIssueStatisticsService.updateFeedbackCount({ + issueId: dto.issueId, + date: feedback.createdAt, + feedbackCount: 1, + }); } catch (e) { if (e instanceof QueryFailedError) { if (e.driverError.code === 'ER_NO_REFERENCED_ROW_2') { @@ -363,6 +377,12 @@ export class FeedbackMySQLService { feedbackCount: () => 'feedback_count - 1', updatedAt: () => `'${DateTime.utc().toFormat('yyyy-MM-dd HH:mm:ss')}'`, }); + + await this.feedbackIssueStatisticsService.updateFeedbackCount({ + issueId, + date: feedback.createdAt, + feedbackCount: -1, + }); } catch (e) { if (e instanceof QueryFailedError) { this.logger.error(e); @@ -379,10 +399,11 @@ export class FeedbackMySQLService { }); } - async deleteByIds({ feedbackIds }: DeleteByIdsDto) { + @Transactional() + async deleteByIds({ channelId, feedbackIds }: DeleteByIdsDto) { const feedbacks = await this.feedbackRepository.find({ where: { id: In(feedbackIds) }, - relations: ['issues'], + relations: { issues: true }, }); for (const feedback of feedbacks) { @@ -393,6 +414,12 @@ export class FeedbackMySQLService { `'${DateTime.utc().toFormat('yyyy-MM-dd HH:mm:ss')}'`, }); } + + await this.feedbackStatisticsService.updateCount({ + channelId, + date: feedback.createdAt, + count: -1, + }); } await this.feedbackRepository.remove(feedbacks); diff --git a/apps/api/src/domains/feedback/feedback.service.spec.ts b/apps/api/src/domains/feedback/feedback.service.spec.ts index fb55c775f..684d80bb2 100644 --- a/apps/api/src/domains/feedback/feedback.service.spec.ts +++ b/apps/api/src/domains/feedback/feedback.service.spec.ts @@ -37,6 +37,8 @@ import { ChannelEntity } from '../channel/channel/channel.entity'; import { RESERVED_FIELD_KEYS } from '../channel/field/field.constants'; import { FieldEntity } from '../channel/field/field.entity'; import { IssueEntity } from '../project/issue/issue.entity'; +import { ProjectEntity } from '../project/project/project.entity'; +import { FeedbackIssueStatisticsEntity } from '../statistics/feedback-issue/feedback-issue-statistics.entity'; import { FeedbackStatisticsEntity } from '../statistics/feedback/feedback-statistics.entity'; import { IssueStatisticsEntity } from '../statistics/issue/issue-statistics.entity'; import { CreateFeedbackDto, FindFeedbacksByChannelIdDto } from './dtos'; @@ -72,9 +74,11 @@ describe('FeedbackService Test Suite', () => { let fieldRepo: Repository; let issueRepo: Repository; let channelRepo: Repository; + let projectRepo: Repository; let osRepo: OpensearchRepository; let feedbackStatsRepo: Repository; let issueStatsRepo: Repository; + let feedbackIssueStatsRepo: Repository; beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], @@ -87,11 +91,15 @@ describe('FeedbackService Test Suite', () => { fieldRepo = module.get(getRepositoryToken(FieldEntity)); issueRepo = module.get(getRepositoryToken(IssueEntity)); channelRepo = module.get(getRepositoryToken(ChannelEntity)); + projectRepo = module.get(getRepositoryToken(ProjectEntity)); osRepo = module.get(OpensearchRepository); feedbackStatsRepo = module.get( getRepositoryToken(FeedbackStatisticsEntity), ); issueStatsRepo = module.get(getRepositoryToken(IssueStatisticsEntity)); + feedbackIssueStatsRepo = module.get( + getRepositoryToken(FeedbackIssueStatisticsEntity), + ); }); describe('create', () => { @@ -99,6 +107,12 @@ describe('FeedbackService Test Suite', () => { const dto = new CreateFeedbackDto(); dto.channelId = faker.number.int(); dto.data = JSON.parse(JSON.stringify(feedbackFixture)); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(fieldRepo, 'find').mockResolvedValue(fieldsFixture); jest.spyOn(feedbackRepo, 'save').mockResolvedValue({ id: faker.number.int(), @@ -312,6 +326,12 @@ describe('FeedbackService Test Suite', () => { }).map(() => faker.string.sample()); dto.data.issueNames = [...issueNames, faker.string.sample()]; const feedbackId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(fieldRepo, 'find').mockResolvedValue(fieldsFixture); jest.spyOn(feedbackRepo, 'save').mockResolvedValue({ id: feedbackId, @@ -338,6 +358,7 @@ describe('FeedbackService Test Suite', () => { jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue({ id: feedbackId, issues: [], + createdAt: new Date(), } as FeedbackEntity); jest .spyOn(feedbackStatsRepo, 'findOne') @@ -345,6 +366,9 @@ describe('FeedbackService Test Suite', () => { jest .spyOn(issueStatsRepo, 'createQueryBuilder') .mockImplementation(() => createQueryBuilder); + jest + .spyOn(feedbackIssueStatsRepo, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); clsService.set = jest.fn(); await feedbackService.create(dto); @@ -365,6 +389,12 @@ describe('FeedbackService Test Suite', () => { }).map(() => faker.string.sample()); dto.data.issueNames = [...issueNames]; const feedbackId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(fieldRepo, 'find').mockResolvedValue(fieldsFixture); jest.spyOn(feedbackRepo, 'save').mockResolvedValue({ id: feedbackId, @@ -378,10 +408,14 @@ describe('FeedbackService Test Suite', () => { jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue({ id: feedbackId, issues: [], + createdAt: new Date(), } as FeedbackEntity); jest .spyOn(feedbackStatsRepo, 'findOne') .mockResolvedValue({ count: 1 } as FeedbackStatisticsEntity); + jest + .spyOn(feedbackIssueStatsRepo, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); clsService.set = jest.fn(); await feedbackService.create(dto); @@ -397,6 +431,12 @@ describe('FeedbackService Test Suite', () => { dto.data = JSON.parse(JSON.stringify(feedbackFixture)); dto.data.issueNames = [faker.string.sample()]; const feedbackId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(fieldRepo, 'find').mockResolvedValue(fieldsFixture); jest.spyOn(feedbackRepo, 'save').mockResolvedValue({ id: feedbackId, @@ -420,6 +460,7 @@ describe('FeedbackService Test Suite', () => { jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue({ id: feedbackId, issues: [], + createdAt: new Date(), } as FeedbackEntity); jest .spyOn(feedbackStatsRepo, 'findOne') @@ -427,6 +468,9 @@ describe('FeedbackService Test Suite', () => { jest .spyOn(issueStatsRepo, 'createQueryBuilder') .mockImplementation(() => createQueryBuilder); + jest + .spyOn(feedbackIssueStatsRepo, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); clsService.set = jest.fn(); await feedbackService.create(dto); diff --git a/apps/api/src/domains/feedback/feedback.service.ts b/apps/api/src/domains/feedback/feedback.service.ts index 36e46a6cd..5c4b79c0b 100644 --- a/apps/api/src/domains/feedback/feedback.service.ts +++ b/apps/api/src/domains/feedback/feedback.service.ts @@ -42,8 +42,6 @@ import type { FieldEntity } from '../channel/field/field.entity'; import { FieldService } from '../channel/field/field.service'; import { OptionService } from '../channel/option/option.service'; import { IssueService } from '../project/issue/issue.service'; -import { FeedbackIssueStatisticsService } from '../statistics/feedback-issue/feedback-issue-statistics.service'; -import { FeedbackStatisticsService } from '../statistics/feedback/feedback-statistics.service'; import type { CountByProjectIdDto, CreateImageUploadUrlDto, @@ -72,8 +70,6 @@ export class FeedbackService { private readonly optionService: OptionService, private readonly channelService: ChannelService, private readonly configService: ConfigService, - private readonly feedbackStatisticsService: FeedbackStatisticsService, - private readonly feedbackIssueStatisticsService: FeedbackIssueStatisticsService, ) {} private validateQuery( @@ -369,12 +365,6 @@ export class FeedbackService { data: feedbackData, }); - await this.feedbackStatisticsService.updateCount({ - channelId, - date: DateTime.utc().toJSDate(), - count: 1, - }); - if (issueNames) { for (const issueName of issueNames) { let issue = await this.issueService.findByName({ name: issueName }); @@ -510,12 +500,6 @@ export class FeedbackService { async addIssue(dto: AddIssueDto) { await this.feedbackMySQLService.addIssue(dto); - await this.feedbackIssueStatisticsService.updateFeedbackCount({ - issueId: dto.issueId, - date: DateTime.utc().toJSDate(), - feedbackCount: 1, - }); - if (this.configService.get('opensearch.use')) { await this.feedbackOSService.upsertFeedbackItem({ channelId: dto.channelId, diff --git a/apps/api/src/domains/project/issue/issue.service.spec.ts b/apps/api/src/domains/project/issue/issue.service.spec.ts index 4c9b9afe5..02adcdf71 100644 --- a/apps/api/src/domains/project/issue/issue.service.spec.ts +++ b/apps/api/src/domains/project/issue/issue.service.spec.ts @@ -23,6 +23,7 @@ import type { TimeRange } from '@/common/dtos'; import { IssueStatisticsEntity } from '@/domains/statistics/issue/issue-statistics.entity'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; import { IssueServiceProviders } from '../../../test-utils/providers/issue.service.providers'; +import { ProjectEntity } from '../project/project.entity'; import { CreateIssueDto, FindIssuesByProjectIdDto, @@ -38,6 +39,7 @@ import { IssueService } from './issue.service'; describe('IssueService test suite', () => { let issueService: IssueService; let issueRepo: Repository; + let projectRepo: Repository; let issueStatsRepo: Repository; beforeEach(async () => { @@ -48,6 +50,7 @@ describe('IssueService test suite', () => { issueService = module.get(IssueService); issueRepo = module.get(getRepositoryToken(IssueEntity)); + projectRepo = module.get(getRepositoryToken(ProjectEntity)); issueStatsRepo = module.get(getRepositoryToken(IssueStatisticsEntity)); }); @@ -61,6 +64,12 @@ describe('IssueService test suite', () => { it('creating an issue succeeds with valid inputs', async () => { dto.name = faker.string.sample(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null as IssueEntity); jest.spyOn(issueRepo, 'save').mockResolvedValue({ id: faker.number.int(), diff --git a/apps/api/src/domains/project/issue/issue.service.ts b/apps/api/src/domains/project/issue/issue.service.ts index c2b1e8cb0..7533f4259 100644 --- a/apps/api/src/domains/project/issue/issue.service.ts +++ b/apps/api/src/domains/project/issue/issue.service.ts @@ -197,18 +197,35 @@ export class IssueService { @Transactional() async deleteById(id: number) { - const issue = new IssueEntity(); - issue.id = id; + const issue = await this.repository.findOne({ + where: { id }, + relations: { project: true }, + }); + + await this.issueStatisticsService.updateCount({ + projectId: issue.project.id, + date: issue.createdAt, + count: -1, + }); + await this.repository.remove(issue); } @Transactional() async deleteByIds(ids: number[]) { - const issues = ids.map((id) => { - const issue = new IssueEntity(); - issue.id = id; - return issue; + const issues = await this.repository.find({ + where: { id: In(ids) }, + relations: { project: true }, }); + + for (const issue of issues) { + await this.issueStatisticsService.updateCount({ + projectId: issue.project.id, + date: issue.createdAt, + count: -1, + }); + } + await this.repository.remove(issues); } diff --git a/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts b/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts index 4d811bf5d..333f57e9d 100644 --- a/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts +++ b/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts @@ -17,6 +17,7 @@ import { faker } from '@faker-js/faker'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { DateTime } from 'luxon'; import type { Repository } from 'typeorm'; import { FeedbackEntity } from '@/domains/feedback/feedback.entity'; @@ -289,6 +290,12 @@ describe('FeedbackIssueStatisticsService suite', () => { const issueId = faker.number.int(); const date = faker.date.past(); const feedbackCount = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ feedbackCount: 1, } as FeedbackIssueStatisticsEntity); @@ -309,6 +316,12 @@ describe('FeedbackIssueStatisticsService suite', () => { const issueId = faker.number.int(); const date = faker.date.past(); const feedbackCount = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue(null); jest .spyOn(feedbackIssueStatsRepo, 'createQueryBuilder') @@ -325,7 +338,10 @@ describe('FeedbackIssueStatisticsService suite', () => { expect(feedbackIssueStatsRepo.createQueryBuilder).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledWith({ - date: new Date(date.toISOString().split('T')[0] + 'T00:00:00'), + date: new Date( + DateTime.fromJSDate(date).plus({ hours: 9 }).toISO().split('T')[0] + + 'T00:00:00', + ), feedbackCount, issue: { id: issueId }, }); diff --git a/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.ts b/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.ts index a9038c864..492cb1dee 100644 --- a/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.ts +++ b/apps/api/src/domains/statistics/feedback-issue/feedback-issue-statistics.service.ts @@ -195,9 +195,23 @@ export class FeedbackIssueStatisticsService { if (dto.feedbackCount === 0) return; if (!dto.feedbackCount) dto.feedbackCount = 1; + const { timezone } = await this.projectRepository.findOne({ + where: { issues: { id: dto.issueId } }, + }); + const timezoneOffset = timezone.offset; + const [hours, minutes] = timezoneOffset.split(':'); + const offset = Number(hours) + Number(minutes) / 60; + + const date = new Date( + DateTime.fromJSDate(dto.date) + .plus({ hours: offset }) + .toISO() + .split('T')[0] + 'T00:00:00', + ); + const stats = await this.repository.findOne({ where: { - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, issue: { id: dto.issueId }, }, }); @@ -211,7 +225,7 @@ export class FeedbackIssueStatisticsService { .createQueryBuilder() .insert() .values({ - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, feedbackCount: dto.feedbackCount, issue: { id: dto.issueId }, }) diff --git a/apps/api/src/domains/statistics/feedback/feedback-statistics.service.spec.ts b/apps/api/src/domains/statistics/feedback/feedback-statistics.service.spec.ts index d8c246aca..9c1ad35ee 100644 --- a/apps/api/src/domains/statistics/feedback/feedback-statistics.service.spec.ts +++ b/apps/api/src/domains/statistics/feedback/feedback-statistics.service.spec.ts @@ -17,6 +17,7 @@ import { faker } from '@faker-js/faker'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { DateTime } from 'luxon'; import type { Repository } from 'typeorm'; import { ChannelEntity } from '@/domains/channel/channel/channel.entity'; @@ -345,6 +346,12 @@ describe('FeedbackStatisticsService suite', () => { const channelId = faker.number.int(); const date = faker.date.past(); const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(feedbackStatsRepo, 'findOne').mockResolvedValue({ count: 1, } as FeedbackStatisticsEntity); @@ -365,6 +372,12 @@ describe('FeedbackStatisticsService suite', () => { const channelId = faker.number.int(); const date = faker.date.past(); const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(feedbackStatsRepo, 'findOne').mockResolvedValue(null); jest .spyOn(feedbackStatsRepo, 'createQueryBuilder') @@ -381,7 +394,10 @@ describe('FeedbackStatisticsService suite', () => { expect(feedbackStatsRepo.createQueryBuilder).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledWith({ - date: new Date(date.toISOString().split('T')[0] + 'T00:00:00'), + date: new Date( + DateTime.fromJSDate(date).plus({ hours: 9 }).toISO().split('T')[0] + + 'T00:00:00', + ), count, channel: { id: channelId }, }); diff --git a/apps/api/src/domains/statistics/feedback/feedback-statistics.service.ts b/apps/api/src/domains/statistics/feedback/feedback-statistics.service.ts index d79d0e750..16ae4e139 100644 --- a/apps/api/src/domains/statistics/feedback/feedback-statistics.service.ts +++ b/apps/api/src/domains/statistics/feedback/feedback-statistics.service.ts @@ -234,9 +234,23 @@ export class FeedbackStatisticsService { if (dto.count === 0) return; if (!dto.count) dto.count = 1; + const { timezone } = await this.projectRepository.findOne({ + where: { channels: { id: dto.channelId } }, + }); + const timezoneOffset = timezone.offset; + const [hours, minutes] = timezoneOffset.split(':'); + const offset = Number(hours) + Number(minutes) / 60; + + const date = new Date( + DateTime.fromJSDate(dto.date) + .plus({ hours: offset }) + .toISO() + .split('T')[0] + 'T00:00:00', + ); + const stats = await this.repository.findOne({ where: { - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, channel: { id: dto.channelId }, }, }); @@ -250,7 +264,7 @@ export class FeedbackStatisticsService { .createQueryBuilder() .insert() .values({ - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, count: dto.count, channel: { id: dto.channelId }, }) diff --git a/apps/api/src/domains/statistics/issue/issue-statistics.service.spec.ts b/apps/api/src/domains/statistics/issue/issue-statistics.service.spec.ts index 067963044..0b2fdeb9f 100644 --- a/apps/api/src/domains/statistics/issue/issue-statistics.service.spec.ts +++ b/apps/api/src/domains/statistics/issue/issue-statistics.service.spec.ts @@ -17,6 +17,7 @@ import { faker } from '@faker-js/faker'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { DateTime } from 'luxon'; import type { Repository } from 'typeorm'; import { IssueEntity } from '@/domains/project/issue/issue.entity'; @@ -271,6 +272,12 @@ describe('IssueStatisticsService suite', () => { const projectId = faker.number.int(); const date = faker.date.past(); const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(issueStatsRepo, 'findOne').mockResolvedValue({ count: 1, } as IssueStatisticsEntity); @@ -291,6 +298,12 @@ describe('IssueStatisticsService suite', () => { const projectId = faker.number.int(); const date = faker.date.past(); const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); jest.spyOn(issueStatsRepo, 'findOne').mockResolvedValue(null); jest .spyOn(issueStatsRepo, 'createQueryBuilder') @@ -307,7 +320,10 @@ describe('IssueStatisticsService suite', () => { expect(issueStatsRepo.createQueryBuilder).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledTimes(1); expect(createQueryBuilder.values).toBeCalledWith({ - date: new Date(date.toISOString().split('T')[0] + 'T00:00:00'), + date: new Date( + DateTime.fromJSDate(date).plus({ hours: 9 }).toISO().split('T')[0] + + 'T00:00:00', + ), count, project: { id: projectId }, }); diff --git a/apps/api/src/domains/statistics/issue/issue-statistics.service.ts b/apps/api/src/domains/statistics/issue/issue-statistics.service.ts index 318fb6b44..b21e4d75d 100644 --- a/apps/api/src/domains/statistics/issue/issue-statistics.service.ts +++ b/apps/api/src/domains/statistics/issue/issue-statistics.service.ts @@ -193,9 +193,23 @@ export class IssueStatisticsService { if (dto.count === 0) return; if (!dto.count) dto.count = 1; + const { timezone } = await this.projectRepository.findOne({ + where: { id: dto.projectId }, + }); + const timezoneOffset = timezone.offset; + const [hours, minutes] = timezoneOffset.split(':'); + const offset = Number(hours) + Number(minutes) / 60; + + const date = new Date( + DateTime.fromJSDate(dto.date) + .plus({ hours: offset }) + .toISO() + .split('T')[0] + 'T00:00:00', + ); + const stats = await this.repository.findOne({ where: { - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, project: { id: dto.projectId }, }, }); @@ -209,7 +223,7 @@ export class IssueStatisticsService { .createQueryBuilder() .insert() .values({ - date: new Date(dto.date.toISOString().split('T')[0] + 'T00:00:00'), + date, count: dto.count, project: { id: dto.projectId }, })