Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { PermissionsGuard } from './guards/permissions.guard';
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'default-jwt-secret',
signOptions: { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') as any },
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN ?? '15m', },
}),
TypeOrmModule.forFeature([User]),
],
Expand Down
26 changes: 26 additions & 0 deletions src/common/interceptors/locale.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class LocaleInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();

// Safe fallback
request.locale = 'en-US';
request.timezone = 'UTC';

// If user is authenticated and preferences exist
if (request.user?.preferences) {
request.locale = request.user.preferences.locale || 'en-US';
request.timezone = request.user.preferences.timezone || 'UTC';
}

return next.handle();
}
}
75 changes: 60 additions & 15 deletions src/common/interceptors/response-transform.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,67 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { getCorrelationId } from '../utils/correlation.utils';
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FormattingService } from '../../localization/services/formatting.service';
import { UserPreferenceReaderService } from '../../user-preferences/services/user-preference-reader.service';

@Injectable()
export class ResponseTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
export class ResponseFormatInterceptor implements NestInterceptor {
constructor(
private readonly formattingService: FormattingService,
private readonly preferenceReader: UserPreferenceReaderService,
) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.id;

return next.handle().pipe(
map((data) => {
if (data && typeof data === 'object' && 'success' in data) {
return data;
}
map(async (data) => {
if (!userId) return data;

const prefs = await this.preferenceReader.getByUserId(userId);

return {
success: true,
data,
correlationId: getCorrelationId(),
};
if (!prefs) return data;

return this.formatResponse(data, prefs.locale, prefs.timezone);
}),
);
}
}

private formatResponse(data: any, locale: string, timezone: string): any {
if (!data) return data;

if (Array.isArray(data)) {
return data.map(item => this.formatResponse(item, locale, timezone));
}

if (typeof data === 'object') {
const formatted: any = {};

for (const key in data) {
const value = data[key];

if (value instanceof Date) {
formatted[key] = this.formattingService.formatDate(
value,
locale as any,
timezone,
);
} else if (typeof value === 'object') {
formatted[key] = this.formatResponse(value, locale, timezone);
} else {
formatted[key] = value;
}
}

return formatted;
}

return data;
}
}
22 changes: 22 additions & 0 deletions src/common/services/locale-format.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class LocaleFormatService {
formatDate(date: Date, locale: string, timezone: string): string {
return new Intl.DateTimeFormat(locale, {
timeZone: timezone,
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(date));
}

formatCurrency(amount: number, locale: string, currency = 'USD'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
}
2 changes: 1 addition & 1 deletion src/courses/entities/course-review.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class CourseReview {
feedback?: string;

/** Snapshot of the course status before the decision. */
@Column({ type: 'varchar', length: 50, nullable: true })
@Column({ type: 'varchar', length: '50', nullable: true })
previousStatus?: string;

@CreateDateColumn()
Expand Down
17 changes: 14 additions & 3 deletions src/email-marketing/services/email-tracking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class EmailTrackingService {
...data,
eventType: EmailEventType.SENT,
});

await this.emailEventRepo.save(event);
this.logger.log(`Email sent recorded for ${event.recipientId}`);
return event;
Expand All @@ -29,8 +30,12 @@ export class EmailTrackingService {
eventType: EmailEventType.BOUNCED,
bounceReason,
});

await this.emailEventRepo.save(event);
this.logger.warn(`Email bounce recorded for ${event.recipientId}: ${bounceReason}`);
this.logger.warn(
`Email bounce recorded for ${event.recipientId}: ${bounceReason}`,
);

return event;
}

Expand All @@ -40,13 +45,16 @@ export class EmailTrackingService {
eventType: EmailEventType.COMPLAINED,
complaintType,
});

await this.emailEventRepo.save(event);
this.logger.warn(`Email complaint recorded for ${event.recipientId}: ${complaintType}`);
this.logger.warn(
`Email complaint recorded for ${event.recipientId}: ${complaintType}`,
);

return event;
}

async updateReputation(score: number) {
// Implementation depends on where reputation is stored; placeholder for now
this.logger.log(`Reputation score updated to ${score}`);
}

Expand All @@ -55,6 +63,7 @@ export class EmailTrackingService {
...data,
eventType: EmailEventType.DELIVERED,
});

await this.emailEventRepo.save(event);
this.logger.log(`Email delivery recorded for ${event.recipientId}`);
return event;
Expand All @@ -65,6 +74,7 @@ export class EmailTrackingService {
...data,
eventType: EmailEventType.OPENED,
});

await this.emailEventRepo.save(event);
this.logger.log(`Email open recorded for ${event.recipientId}`);
return event;
Expand All @@ -75,6 +85,7 @@ export class EmailTrackingService {
...data,
eventType: EmailEventType.CLICKED,
});

await this.emailEventRepo.save(event);
this.logger.log(`Email click recorded for ${event.recipientId}`);
return event;
Expand Down
8 changes: 4 additions & 4 deletions src/entities/schema_change.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ export class SchemaChange {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: '255' })
schemaName: string;

@Column({ type: 'varchar', length: 50 })
@Column({ type: 'varchar', length: '50' })
fromVersion: string;

@Column({ type: 'varchar', length: 50 })
@Column({ type: 'varchar', length: '50' })
toVersion: string;

@Column({ type: 'enum', enum: ChangeType })
changeType: ChangeType;

@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: '255' })
fieldPath: string;

@Column({ type: 'jsonb', nullable: true })
Expand Down
4 changes: 2 additions & 2 deletions src/entities/schema_version.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ export class SchemaVersion {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: '255' })
schemaName: string;

@Column({ type: 'jsonb' })
definition: Record<string, any>;

@Column({ type: 'varchar', length: 64 })
@Column({ type: 'varchar', length: '64' })
checksum: string;

@CreateDateColumn({ type: 'timestamp with time zone' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class RunbookExecutionService {
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
(stepExecution as any)['status'] = 'failed';
stepExecution['status'] = 'in_progress' as any;
stepExecution['error'] = errorMsg;
allSuccess = false;
this.logger.error(`Step ${step.stepNumber} failed: ${errorMsg}`);
Expand Down
31 changes: 31 additions & 0 deletions src/localization/services/formatting.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class FormattingService {
formatDate(date: Date, locale: string, timeZone: string): string {
return new Intl.DateTimeFormat(locale, {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}

formatCurrency(amount: number, locale: string, currency: string = 'USD'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}

formatDateOnly(date: Date, locale: string, timeZone: string): string {
return new Intl.DateTimeFormat(locale, {
timeZone,
year: 'numeric',
month: 'long',
day: '2-digit',
}).format(date);
}
}
Loading