Skip to content

Commit

Permalink
fix: Application Table/Export/Edit/View should display time in local …
Browse files Browse the repository at this point in the history
…timezone (#3959)

* fix: pass timezone to application csv export

3919

* fix: adjust paper apps to submit in local timezone

3919

* fix: match electronic on table and export

3919

* fix: seed a paper application

* fix: pacific time migration

3919

* fix: address comments

3919

* fix: cleanup nits

3919
  • Loading branch information
mcgarrye authored Apr 1, 2024
1 parent ab12587 commit a235916
Show file tree
Hide file tree
Showing 16 changed files with 232 additions and 71 deletions.
6 changes: 4 additions & 2 deletions api/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"]
CORS_REGEX=["test1", "test2"]
# controls the repetition of the temp file clearing cron job
TEMP_FILE_CLEAR_CRON_STRING=0 * * *
# default time zone for dates in exports
TIME_ZONE=America/Los_Angeles
# how long we maintain our request time outs (60 * 60 * 1000 ms)
THROTTLE_TTL=3600000
THROTTLE_TTL=3600000
# how many requests before we throttle
THROTTLE_LIMIT=100
THROTTLE_LIMIT=100
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Paper applications were originally submitted as though they were in UTC,
-- when they should have been submitted in Pacific (PST/PDT) and converted to UTC.
-- Here we update the times to be in PST or PDT depending on when they were submitted
-- and the yearly cycle of daylight savings.

UPDATE applications
SET submission_date = submission_date + '7 hours'
WHERE submission_date IS NOT NULL
AND submission_type = 'paper';


UPDATE applications
SET submission_date = submission_date + '1 hours'
WHERE submission_date IS NOT NULL
AND submission_type = 'paper'
AND (submission_date > '2024-03-10 09:00:00.000-00'
OR (submission_date <= '2023-11-05 09:00:00.000-00' AND submission_date > '2023-03-12 09:00:00.000-00')
OR (submission_date <= '2022-11-06 09:00:00.000-00' AND submission_date > '2022-03-13 09:00:00.000-00')
OR (submission_date <= '2021-11-07 09:00:00.000-00' AND submission_date > '2021-03-14 09:00:00.000-00'));
5 changes: 4 additions & 1 deletion api/prisma/seed-helpers/application-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const applicationFactory = async (optionalParams?: {
demographics?: Prisma.DemographicsCreateWithoutApplicationsInput;
multiselectQuestions?: Partial<MultiselectQuestions>[];
userId?: string;
submissionType?: ApplicationSubmissionTypeEnum;
}): Promise<Prisma.ApplicationsCreateInput> => {
let preferredUnitTypes: Prisma.UnitTypesCreateNestedManyWithoutApplicationsInput;
if (optionalParams?.unitTypeId) {
Expand All @@ -45,7 +46,9 @@ export const applicationFactory = async (optionalParams?: {
applicant: { create: applicantFactory(optionalParams?.applicant) },
appUrl: '',
status: ApplicationStatusEnum.submitted,
submissionType: ApplicationSubmissionTypeEnum.electronical,
submissionType:
optionalParams?.submissionType ??
ApplicationSubmissionTypeEnum.electronical,
submissionDate: new Date(),
householdSize: optionalParams?.householdSize ?? 1,
income: '40000',
Expand Down
4 changes: 4 additions & 0 deletions api/prisma/seed-staging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApplicationAddressTypeEnum,
ApplicationMethodsTypeEnum,
ApplicationSubmissionTypeEnum,
LanguagesEnum,
ListingsStatusEnum,
MultiselectQuestions,
Expand Down Expand Up @@ -457,6 +458,9 @@ export const stagingSeed = async (
}),
await applicationFactory(),
await applicationFactory(),
await applicationFactory({
submissionType: ApplicationSubmissionTypeEnum.paper,
}),
],
},
{
Expand Down
21 changes: 13 additions & 8 deletions api/src/dtos/applications/application-csv-query-params.dto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { Expose, Transform } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
import { AbstractDTO } from '../shared/abstract.dto';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsOptional, IsString, IsUUID } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class ApplicationCsvQueryParams extends OmitType(AbstractDTO, [
'id',
'createdAt',
'updatedAt',
]) {
export class ApplicationCsvQueryParams {
@Expose()
@ApiProperty({
type: String,
Expand All @@ -32,4 +27,14 @@ export class ApplicationCsvQueryParams extends OmitType(AbstractDTO, [
{ toClassOnly: true },
)
includeDemographics?: boolean;

@Expose()
@ApiPropertyOptional({
type: String,
example: process.env.TIME_ZONE,
required: false,
})
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsString({ groups: [ValidationsGroupsEnum.default] })
timeZone?: string;
}
2 changes: 1 addition & 1 deletion api/src/dtos/listings/listing-csv-query-params.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ListingCsvQueryParams {
@Expose()
@ApiPropertyOptional({
type: String,
example: 'America/Los_Angeles',
example: process.env.TIME_ZONE,
required: false,
})
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
Expand Down
18 changes: 18 additions & 0 deletions api/src/services/application-csv-export.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Injectable, StreamableFile } from '@nestjs/common';
import { Request as ExpressRequest, Response } from 'express';
import { view } from './application.service';
import { PrismaService } from './prisma.service';
import { ApplicationSubmissionTypeEnum } from '@prisma/client';
import { MultiselectQuestionService } from './multiselect-question.service';
import { ApplicationCsvQueryParams } from '../dtos/applications/application-csv-query-params.dto';
import { UnitType } from '../dtos/unit-types/unit-type.dto';
Expand All @@ -15,6 +16,7 @@ import { User } from '../dtos/users/user.dto';
import { ListingService } from './listing.service';
import { PermissionService } from './permission.service';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import { formatLocalDate } from '../utilities/format-local-date';
import {
CsvExporterServiceInterface,
CsvHeader,
Expand Down Expand Up @@ -45,12 +47,14 @@ export const typeMap = {
export class ApplicationCsvExporterService
implements CsvExporterServiceInterface
{
readonly dateFormat: string = 'MM-DD-YYYY hh:mm:ssA z';
constructor(
private prisma: PrismaService,
private multiselectQuestionService: MultiselectQuestionService,
private listingService: ListingService,
private permissionService: PermissionService,
) {}

/**
*
* @param queryParams
Expand All @@ -64,12 +68,14 @@ export class ApplicationCsvExporterService
): Promise<StreamableFile> {
const user = mapTo(User, req['user']);
await this.authorizeCSVExport(user, queryParams.listingId);

const filename = join(
process.cwd(),
`src/temp/listing-${queryParams.listingId}-applications-${
user.id
}-${new Date().getTime()}.csv`,
);

await this.createCsv(filename, queryParams);
const file = createReadStream(filename);
return new StreamableFile(file);
Expand Down Expand Up @@ -109,6 +115,7 @@ export class ApplicationCsvExporterService
const csvHeaders = await this.getCsvHeaders(
maxHouseholdMembers,
multiSelectQuestions,
queryParams.timeZone,
queryParams.includeDemographics,
);

Expand Down Expand Up @@ -330,6 +337,7 @@ export class ApplicationCsvExporterService
async getCsvHeaders(
maxHouseholdMembers: number,
multiSelectQuestions: MultiselectQuestion[],
timeZone: string,
includeDemographics = false,
): Promise<CsvHeader[]> {
const headers: CsvHeader[] = [
Expand All @@ -344,10 +352,20 @@ export class ApplicationCsvExporterService
{
path: 'submissionType',
label: 'Application Type',
format: (val: string): string =>
val === ApplicationSubmissionTypeEnum.electronical
? 'electronic'
: val,
},
{
path: 'submissionDate',
label: 'Application Submission Date',
format: (val: string): string =>
formatLocalDate(
val,
this.dateFormat,
timeZone ?? process.env.TIME_ZONE,
),
},
{
path: 'applicant.firstName',
Expand Down
4 changes: 2 additions & 2 deletions api/src/services/listing-csv-export.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const formatCommunityType = {
@Injectable()
export class ListingCsvExporterService implements CsvExporterServiceInterface {
readonly dateFormat: string = 'MM-DD-YYYY hh:mm:ssA z';
timeZone = 'America/Los_Angeles';
timeZone = process.env.TIME_ZONE;
constructor(
private prisma: PrismaService,
@Inject(Logger)
Expand Down Expand Up @@ -319,7 +319,7 @@ export class ListingCsvExporterService implements CsvExporterServiceInterface {
},
{
path: 'createdAt',
label: 'Crated At Date',
label: 'Created At Date',
format: (val: string): string =>
formatLocalDate(val, this.dateFormat, this.timeZone),
},
Expand Down
129 changes: 129 additions & 0 deletions api/test/unit/services/application-csv-export.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,133 @@ describe('Testing application CSV export service', () => {
expect(readable).toContain(headerRow);
expect(readable).toContain(firstApp);
});

it('should build csv with submission date defaulted to PST', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));

const requestingUser = {
firstName: 'requesting fName',
lastName: 'requesting lName',
email: '[email protected]',
jurisdictions: [{ id: 'juris id' }],
} as unknown as User;

const applications = mockApplicationSet(5, new Date());
prisma.applications.findMany = jest.fn().mockReturnValue(applications);
prisma.listings.findUnique = jest.fn().mockResolvedValue({});
permissionService.canOrThrow = jest.fn().mockResolvedValue(true);

service.maxHouseholdMembers = jest.fn().mockReturnValue(1);

prisma.multiselectQuestions.findMany = jest.fn().mockReturnValue([
{
...mockMultiselectQuestion(
0,
new Date(),
MultiselectQuestionsApplicationSectionEnum.preferences,
),
options: [
{ id: 1, text: 'text' },
{ id: 2, text: 'text', collectAddress: true },
],
},
{
...mockMultiselectQuestion(
1,
new Date(),
MultiselectQuestionsApplicationSectionEnum.programs,
),
options: [{ id: 1, text: 'text' }],
},
]);

service.unitTypeToReadable = jest.fn().mockReturnValue('Studio');
const exportResponse = await service.exportFile(
{ user: requestingUser } as unknown as ExpressRequest,
{} as unknown as Response,
{ listingId: randomUUID() },
);

const mockedStream = new PassThrough();
exportResponse.getStream().pipe(mockedStream);

// In order to make sure the last expect statements are properly hit we need to wrap in a promise and resolve it
const readable = await new Promise((resolve) => {
mockedStream.on('data', async (d) => {
const value = Buffer.from(d).toString();
mockedStream.end();
mockedStream.destroy();
resolve(value);
});
});

expect(readable).toContain('PST');
});

it('should build csv with submission date in custom timezone', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));

const requestingUser = {
firstName: 'requesting fName',
lastName: 'requesting lName',
email: '[email protected]',
jurisdictions: [{ id: 'juris id' }],
} as unknown as User;

const applications = mockApplicationSet(5, new Date());
prisma.applications.findMany = jest.fn().mockReturnValue(applications);
prisma.listings.findUnique = jest.fn().mockResolvedValue({});
permissionService.canOrThrow = jest.fn().mockResolvedValue(true);

service.maxHouseholdMembers = jest.fn().mockReturnValue(1);

prisma.multiselectQuestions.findMany = jest.fn().mockReturnValue([
{
...mockMultiselectQuestion(
0,
new Date(),
MultiselectQuestionsApplicationSectionEnum.preferences,
),
options: [
{ id: 1, text: 'text' },
{ id: 2, text: 'text', collectAddress: true },
],
},
{
...mockMultiselectQuestion(
1,
new Date(),
MultiselectQuestionsApplicationSectionEnum.programs,
),
options: [{ id: 1, text: 'text' }],
},
]);

service.unitTypeToReadable = jest.fn().mockReturnValue('Studio');
const exportResponse = await service.exportFile(
{ user: requestingUser } as unknown as ExpressRequest,
{} as unknown as Response,
{
listingId: randomUUID(),
timeZone: 'America/New_York',
},
);

const mockedStream = new PassThrough();
exportResponse.getStream().pipe(mockedStream);

// In order to make sure the last expect statements are properly hit we need to wrap in a promise and resolve it
const readable = await new Promise((resolve) => {
mockedStream.on('data', async (d) => {
const value = Buffer.from(d).toString();
mockedStream.end();
mockedStream.destroy();
resolve(value);
});
});

expect(readable).toContain('EST');
});
});
3 changes: 3 additions & 0 deletions shared-helpers/src/types/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,8 @@ export class ApplicationsService {
listingId: string
/** */
includeDemographics?: boolean
/** */
timeZone?: string
} = {} as any,
options: IRequestOptions = {}
): Promise<any> {
Expand All @@ -1404,6 +1406,7 @@ export class ApplicationsService {
configs.params = {
listingId: params["listingId"],
includeDemographics: params["includeDemographics"],
timeZone: params["timeZone"],
}

/** 适配ios13,get请求不允许带body */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ export function getColDefs(maxHouseholdSize: number, countyCode: string) {

const { submissionDate } = data

const dateTime = convertDataToLocal(
submissionDate,
data?.submissionType || ApplicationSubmissionTypeEnum.electronical
)
const dateTime = convertDataToLocal(submissionDate)

return `${dateTime.date} ${t("t.at")} ${dateTime.time}`
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { t } from "@bloom-housing/ui-components"
import { FieldValue, Grid } from "@bloom-housing/ui-seeds"
import { ApplicationContext } from "../../ApplicationContext"
import { convertDataToLocal } from "../../../../lib/helpers"
import { ApplicationSubmissionTypeEnum } from "@bloom-housing/shared-helpers/src/types/backend-swagger"
import SectionWithGrid from "../../../shared/SectionWithGrid"

const DetailsApplicationData = () => {
Expand All @@ -12,10 +11,7 @@ const DetailsApplicationData = () => {
const applicationDate = useMemo(() => {
if (!application) return null

return convertDataToLocal(
application?.submissionDate,
application?.submissionType || ApplicationSubmissionTypeEnum.electronical
)
return convertDataToLocal(application?.submissionDate)
}, [application])

return (
Expand Down
Loading

0 comments on commit a235916

Please sign in to comment.