Browse Source

Refuse account balance with date in future

pull/8177/head
Thomas Kaul 1 day ago
parent
commit
526b01ea7c
  1. 6
      apps/api/src/app/account-balance/account-balance.service.ts
  2. 4
      apps/api/src/app/account/account.service.ts
  3. 6
      apps/api/src/app/activities/activities.service.ts
  4. 31
      apps/api/src/helper/account.helper.spec.ts
  5. 11
      apps/api/src/helper/account.helper.ts
  6. 5
      libs/common/src/lib/dtos/create-account-balance.dto.ts
  7. 39
      libs/common/src/lib/helper.spec.ts
  8. 10
      libs/common/src/lib/helper.ts
  9. 29
      libs/common/src/lib/validator-constraints/is-not-in-the-future.spec.ts
  10. 21
      libs/common/src/lib/validator-constraints/is-not-in-the-future.ts

6
apps/api/src/app/account-balance/account-balance.service.ts

@ -1,8 +1,5 @@
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -11,6 +8,7 @@ import {
DATE_FORMAT,
getStartOfUtcDateOfTomorrow,
getSum,
isAccountBalanceInFuture,
resetHours
} from '@ghostfolio/common/helper';
import {

4
apps/api/src/app/account/account.service.ts

@ -2,7 +2,6 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
getWhereAccountBalanceNotInFuture,
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -10,7 +9,8 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DATE_FORMAT,
getStartOfUtcDateOfTomorrow
getStartOfUtcDateOfTomorrow,
isAccountBalanceInFuture
} from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { AccountWithBalance } from '@ghostfolio/common/types';

6
apps/api/src/app/activities/activities.service.ts

@ -3,10 +3,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import {
getTagsWithDraftTag,
isActivityInFuture,
@ -38,6 +35,7 @@ import {
getAssetProfileIdentifier,
getStartOfUtcDateOfTomorrow,
hasGhostfolioPrefix,
isAccountBalanceInFuture,
isDraftActivity
} from '@ghostfolio/common/helper';
import {

31
apps/api/src/helper/account.helper.spec.ts

@ -1,7 +1,4 @@
import {
getWhereAccountBalanceNotInFuture,
isAccountBalanceInFuture
} from './account.helper';
import { getWhereAccountBalanceNotInFuture } from './account.helper';
// A user in a time zone ahead of the instance is already on the next day at
// this moment. The expected limit is a moment in UTC, hence this suite must
@ -26,30 +23,4 @@ describe('account.helper', () => {
});
});
});
describe('isAccountBalanceInFuture', () => {
it('should accept the account balance of the current day', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-03T00:00:00.000Z') })
).toBe(false);
});
it('should accept the account balance of a user in a time zone ahead of the instance', () => {
expect(
isAccountBalanceInFuture({ date: START_OF_UTC_DATE_OF_TOMORROW })
).toBe(false);
});
it('should reject an account balance after the start of the UTC date of tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.001Z') })
).toBe(true);
});
it('should reject an account balance of the day after tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-05T00:00:00.000Z') })
).toBe(true);
});
});
});

11
apps/api/src/helper/account.helper.ts

@ -2,7 +2,6 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { getStartOfUtcDateOfTomorrow } from '@ghostfolio/common/helper';
import { Prisma } from '@prisma/client';
import { isAfter } from 'date-fns';
export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = {
tags: {
@ -31,13 +30,3 @@ export function getWhereAccountBalanceNotInFuture(): Prisma.AccountBalanceWhereI
date: { lte: getStartOfUtcDateOfTomorrow() }
};
}
export function isAccountBalanceInFuture({
date,
startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow()
}: {
date: Date;
startOfUtcDateOfTomorrow?: Date;
}) {
return isAfter(date, startOfUtcDateOfTomorrow);
}

5
libs/common/src/lib/dtos/create-account-balance.dto.ts

@ -1,4 +1,6 @@
import { IsISO8601, IsNumber, IsUUID } from 'class-validator';
import { IsNotInTheFutureConstraint } from '@ghostfolio/common/validator-constraints/is-not-in-the-future';
import { IsISO8601, IsNumber, IsUUID, Validate } from 'class-validator';
export class CreateAccountBalanceDto {
@IsUUID()
@ -8,5 +10,6 @@ export class CreateAccountBalanceDto {
balance: number;
@IsISO8601()
@Validate(IsNotInTheFutureConstraint)
date: string;
}

39
libs/common/src/lib/helper.spec.ts

@ -12,6 +12,7 @@ import {
getNumberFormatGroup,
getStringOrNull,
getStringOrUndefined,
isAccountBalanceInFuture,
isAccountExcluded,
isCurrency,
isCurrencySymbol,
@ -362,6 +363,44 @@ describe('Helper', () => {
});
});
// A user in a time zone ahead of the instance is already on the next day at
// this moment. The expected limit is a moment in UTC, hence this suite must
// give the same result with the instance in any time zone. Set TEST_TZ to run
// it with the instance in another time zone.
describe('Is account balance in future', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-03T23:30:00.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('should accept the account balance of the current day', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-03T00:00:00.000Z') })
).toBe(false);
});
it('should accept the account balance of a user in a time zone ahead of the instance', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.000Z') })
).toBe(false);
});
it('should reject an account balance after the start of the UTC date of tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.001Z') })
).toBe(true);
});
it('should reject an account balance of the day after tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-05T00:00:00.000Z') })
).toBe(true);
});
});
describe('Is account excluded', () => {
it('Account with Exclude from Analysis tag', () => {
expect(

10
libs/common/src/lib/helper.ts

@ -610,6 +610,16 @@ export function interpolate(template: string, context: any) {
});
}
export function isAccountBalanceInFuture({
date,
startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow()
}: {
date: Date;
startOfUtcDateOfTomorrow?: Date;
}) {
return isAfter(date, startOfUtcDateOfTomorrow);
}
export function isAccountExcluded(account?: { tags?: { id: string }[] }) {
return (
account?.tags?.some(({ id }) => {

29
libs/common/src/lib/validator-constraints/is-not-in-the-future.spec.ts

@ -0,0 +1,29 @@
import { IsNotInTheFutureConstraint } from './is-not-in-the-future';
describe('IsNotInTheFutureConstraint', () => {
const constraint = new IsNotInTheFutureConstraint();
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-03T23:30:00.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('Accepts a date in the past', () => {
expect(constraint.validate('2026-09-02')).toBe(true);
});
it('Accepts the current date', () => {
expect(constraint.validate('2026-09-03')).toBe(true);
});
it('Accepts the current date of a user in a time zone ahead of the instance', () => {
expect(constraint.validate('2026-09-04')).toBe(true);
});
it('Refuses a date in the future', () => {
expect(constraint.validate('2026-09-05')).toBe(false);
});
});

21
libs/common/src/lib/validator-constraints/is-not-in-the-future.ts

@ -0,0 +1,21 @@
import {
isAccountBalanceInFuture,
resetHours
} from '@ghostfolio/common/helper';
import {
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
import { parseISO } from 'date-fns';
@ValidatorConstraint({ name: 'isNotInTheFuture' })
export class IsNotInTheFutureConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return '$property must not be a date in the future';
}
public validate(aDate: string) {
return !isAccountBalanceInFuture({ date: resetHours(parseISO(aDate)) });
}
}
Loading…
Cancel
Save