Skip to content

Commit 4f4da39

Browse files
committed
feat: integrate APIs for format preferences (#4018)
* integrate APIs for format preferences * failing tests * unit tests * tests * minor * minor * minor * minor * fix tests
1 parent 2f53d75 commit 4f4da39

File tree

8 files changed

+158
-2
lines changed

8 files changed

+158
-2
lines changed

src/app/auth/switch-org/switch-org.page.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { PlatformOrgSettingsService } from 'src/app/core/services/platform/v1/sp
4141
import { orgSettingsCardsDisabled, orgSettingsData } from 'src/app/core/test-data/org-settings.service.spec.data';
4242
import { SpenderOnboardingService } from 'src/app/core/services/spender-onboarding.service';
4343
import { getTranslocoTestingModule } from 'src/app/core/testing/transloco-testing.utils';
44+
import { ConfigService } from 'src/app/core/services/config.service';
4445

4546
const roles = ['OWNER', 'USER', 'FYLER'];
4647
const email = '[email protected]';
@@ -123,6 +124,7 @@ describe('SwitchOrgPage', () => {
123124
const deepLinkServiceSpy = jasmine.createSpyObj('DeepLinkService', ['getExpenseRoute']);
124125
const ldSpy = jasmine.createSpyObj('LaunchDarklyService', ['initializeUser']);
125126
const orgSettingsServiceSpy = jasmine.createSpyObj('PlatformOrgSettingsService', ['get']);
127+
const configServiceSpy = jasmine.createSpyObj('ConfigService', ['loadConfigurationData', 'loadFormatPreferences']);
126128
const spenderOnboardingServiceSpy = jasmine.createSpyObj('SpenderOnboardingService', [
127129
'checkForRedirectionToOnboarding',
128130
]);
@@ -159,6 +161,10 @@ describe('SwitchOrgPage', () => {
159161
provide: PlatformOrgSettingsService,
160162
useValue: orgSettingsServiceSpy,
161163
},
164+
{
165+
provide: ConfigService,
166+
useValue: configServiceSpy,
167+
},
162168
{
163169
provide: SpenderOnboardingService,
164170
useValue: spenderOnboardingServiceSpy,

src/app/auth/switch-org/switch-org.page.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ import { AfterViewChecked, ChangeDetectorRef, Component, ElementRef, OnInit, Vie
22
import { ActivatedRoute, Router } from '@angular/router';
33
import { forkJoin, from, fromEvent, noop, Observable, of, catchError, throwError } from 'rxjs';
44
import { distinctUntilChanged, filter, finalize, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
5-
import { IonBackButton, IonButtons, IonContent, IonHeader, IonIcon, IonToolbar, Platform, PopoverController } from '@ionic/angular/standalone';
5+
import {
6+
IonBackButton,
7+
IonButtons,
8+
IonContent,
9+
IonHeader,
10+
IonIcon,
11+
IonToolbar,
12+
Platform,
13+
PopoverController,
14+
} from '@ionic/angular/standalone';
615
import { Org } from 'src/app/core/models/org.model';
716
import { LoaderService } from 'src/app/core/services/loader.service';
817
import { UserService } from 'src/app/core/services/user.service';
@@ -32,6 +41,7 @@ import { DeepLinkService } from 'src/app/core/services/deep-link.service';
3241
import { ExpensesService } from 'src/app/core/services/platform/v1/spender/expenses.service';
3342
import { LaunchDarklyService } from 'src/app/core/services/launch-darkly.service';
3443
import { SpenderOnboardingService } from 'src/app/core/services/spender-onboarding.service';
44+
import { ConfigService } from 'src/app/core/services/config.service';
3545
import { ActiveOrgCardComponent } from './active-org-card/active-org-card.component';
3646
import { NgClass, AsyncPipe } from '@angular/common';
3747
import { MatFormField, MatPrefix, MatInput, MatSuffix } from '@angular/material/input';
@@ -61,7 +71,7 @@ import { FyZeroStateComponent } from '../../shared/components/fy-zero-state/fy-z
6171
MatPrefix,
6272
MatSuffix,
6373
NgClass,
64-
OrgCardComponent
74+
OrgCardComponent,
6575
],
6676
})
6777
export class SwitchOrgPage implements OnInit, AfterViewChecked {
@@ -115,6 +125,8 @@ export class SwitchOrgPage implements OnInit, AfterViewChecked {
115125

116126
private spenderOnboardingService = inject(SpenderOnboardingService);
117127

128+
private configService = inject(ConfigService);
129+
118130
// TODO: Skipped for migration because:
119131
// Your application code writes to the query. This prevents migration.
120132
@ViewChild('search') searchRef: ElementRef<HTMLElement>;
@@ -458,6 +470,8 @@ export class SwitchOrgPage implements OnInit, AfterViewChecked {
458470
}
459471

460472
async proceed(isFromInviteLink?: boolean): Promise<void> {
473+
await this.configService.loadFormatPreferences();
474+
461475
const pendingDetails$ = this.userService.isPendingDetails().pipe(shareReplay(1));
462476
const eou$ = from(this.authService.getEou());
463477
const roles$ = from(this.authService.getRoles().pipe(shareReplay(1)));

src/app/core/mock-data/org-settings.data.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,17 @@ export const orgSettingsRes: OrgSettings = deepFreeze({
415415
enabled: true,
416416
virtual_card_settings_enabled: true,
417417
},
418+
regional_settings: {
419+
allowed: true,
420+
enabled: true,
421+
time_format: 'h:mm a',
422+
date_format: 'MMM dd, yyyy',
423+
currency_format: {
424+
decimal_separator: '.',
425+
thousand_separator: ',',
426+
symbol_position: 'before',
427+
},
428+
},
418429
});
419430

420431
export const orgSettingsParams2: OrgSettings = deepFreeze({

src/app/core/models/org-settings.model.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,18 @@ export interface CurrencylayerProviderSettings extends CommonOrgSettings {
356356
name?: string;
357357
}
358358

359+
export interface RegionalCurrencyFormat {
360+
decimal_separator?: string;
361+
symbol_position?: 'before' | 'after' | string;
362+
thousand_separator?: string;
363+
}
364+
365+
export interface RegionalSettings extends CommonOrgSettings {
366+
currency_format?: RegionalCurrencyFormat;
367+
date_format?: string;
368+
time_format?: string;
369+
}
370+
359371
export interface OrgSettingsResponse {
360372
id?: string;
361373
created_at?: string;
@@ -464,6 +476,7 @@ export interface OrgSettingsResponse {
464476
pending_cct_expense_restriction?: CommonOrgSettings;
465477
simplified_multi_stage_approvals?: CommonOrgSettings;
466478
is_new_critical_policy_violation_flow_enabled?: boolean;
479+
regional_settings?: RegionalSettings;
467480
}
468481

469482
export interface UiPolicySettings {
@@ -580,4 +593,5 @@ export interface OrgSettings {
580593
pending_cct_expense_restriction?: CommonOrgSettings;
581594
simplified_multi_stage_approvals?: CommonOrgSettings;
582595
is_new_critical_policy_violation_flow_enabled?: boolean;
596+
regional_settings?: RegionalSettings;
583597
}

src/app/core/services/config.service.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,49 @@ import { TokenService } from './token.service';
44
import { StorageService } from './storage.service';
55
import { SecureStorageService } from './secure-storage.service';
66
import { RouterAuthService } from './router-auth.service';
7+
import { PlatformOrgSettingsService } from './platform/v1/spender/org-settings.service';
8+
import { of } from 'rxjs';
9+
import { getFormatPreferenceProviders } from '../testing/format-preference-providers.utils';
10+
import { FORMAT_PREFERENCES } from 'src/app/constants';
11+
import { FormatPreferences } from 'src/app/core/models/format-preferences.model';
12+
import { DATE_PIPE_DEFAULT_OPTIONS } from '@angular/common';
13+
import { orgSettingsRes } from '../mock-data/org-settings.data';
714

815
describe('ConfigService', () => {
916
let configService: ConfigService;
1017
let tokenService: jasmine.SpyObj<TokenService>;
1118
let storageService: jasmine.SpyObj<StorageService>;
1219
let secureStorageService: jasmine.SpyObj<SecureStorageService>;
1320
let routerAuthService: jasmine.SpyObj<RouterAuthService>;
21+
let orgSettingsService: jasmine.SpyObj<PlatformOrgSettingsService>;
22+
let formatPreferences: FormatPreferences;
23+
let datePipeOptions: { dateFormat: string };
1424

1525
beforeEach(() => {
1626
const tokenServiceSpy = jasmine.createSpyObj('TokenService', ['getClusterDomain']);
1727
const storageServiceSpy = jasmine.createSpyObj('StorageService', ['clearAll']);
1828
const secureStorageServiceSpy = jasmine.createSpyObj('SecureStorageService', ['clearAll']);
1929
const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['setClusterDomain']);
30+
const orgSettingsServiceSpy = jasmine.createSpyObj('PlatformOrgSettingsService', ['get']);
2031
TestBed.configureTestingModule({
2132
providers: [
2233
ConfigService,
2334
{ provide: TokenService, useValue: tokenServiceSpy },
2435
{ provide: StorageService, useValue: storageServiceSpy },
2536
{ provide: SecureStorageService, useValue: secureStorageServiceSpy },
2637
{ provide: RouterAuthService, useValue: routerAuthServiceSpy },
38+
{ provide: PlatformOrgSettingsService, useValue: orgSettingsServiceSpy },
39+
...getFormatPreferenceProviders(),
2740
],
2841
});
2942
configService = TestBed.inject(ConfigService);
3043
tokenService = TestBed.inject(TokenService) as jasmine.SpyObj<TokenService>;
3144
storageService = TestBed.inject(StorageService) as jasmine.SpyObj<StorageService>;
3245
secureStorageService = TestBed.inject(SecureStorageService) as jasmine.SpyObj<SecureStorageService>;
3346
routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj<RouterAuthService>;
47+
orgSettingsService = TestBed.inject(PlatformOrgSettingsService) as jasmine.SpyObj<PlatformOrgSettingsService>;
48+
formatPreferences = TestBed.inject(FORMAT_PREFERENCES) as FormatPreferences;
49+
datePipeOptions = TestBed.inject(DATE_PIPE_DEFAULT_OPTIONS) as { dateFormat: string };
3450
});
3551

3652
it('should be created', () => {
@@ -41,16 +57,48 @@ describe('ConfigService', () => {
4157
it('should call setClusterDomain if clusterDomain is present', async () => {
4258
const clusterDomain = 'https://staging.fyle.tech';
4359
tokenService.getClusterDomain.and.resolveTo(clusterDomain);
60+
61+
orgSettingsService.get.and.returnValue(of(orgSettingsRes));
4462
await configService.loadConfigurationData();
4563
expect(routerAuthService.setClusterDomain).toHaveBeenCalledOnceWith(clusterDomain);
4664
expect(tokenService.getClusterDomain).toHaveBeenCalledTimes(1);
4765
});
4866

4967
it('should clear all stored data if clusterDomain is not present', async () => {
5068
tokenService.getClusterDomain.and.resolveTo(null);
69+
orgSettingsService.get.and.returnValue(of(orgSettingsRes));
70+
5171
await configService.loadConfigurationData();
5272
expect(storageService.clearAll).toHaveBeenCalledTimes(1);
5373
expect(secureStorageService.clearAll).toHaveBeenCalledTimes(1);
5474
});
75+
76+
it('should update format preferences and date options from regional settings', async () => {
77+
const clusterDomain = 'https://staging.fyle.tech';
78+
tokenService.getClusterDomain.and.resolveTo(clusterDomain);
79+
const orgSettings = {
80+
...orgSettingsRes,
81+
regional_settings: {
82+
allowed: true,
83+
enabled: true,
84+
time_format: 'H:mm',
85+
date_format: 'dd/MM/yyyy',
86+
currency_format: {
87+
decimal_separator: ',',
88+
thousand_separator: '.',
89+
symbol_position: 'after',
90+
},
91+
},
92+
};
93+
orgSettingsService.get.and.returnValue(of(orgSettings));
94+
95+
await configService.loadConfigurationData();
96+
97+
expect(formatPreferences.timeFormat).toBe('H:mm');
98+
expect(formatPreferences.currencyFormat.placement).toBe('after');
99+
expect(formatPreferences.currencyFormat.decimalSeparator).toBe(',');
100+
expect(formatPreferences.currencyFormat.thousandSeparator).toBe('.');
101+
expect(datePipeOptions.dateFormat).toBe('dd/MM/yyyy');
102+
});
55103
});
56104
});

src/app/core/services/config.service.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { Injectable, inject } from '@angular/core';
2+
import { DATE_PIPE_DEFAULT_OPTIONS } from '@angular/common';
3+
import { firstValueFrom, of } from 'rxjs';
4+
import { catchError, defaultIfEmpty } from 'rxjs/operators';
25
import { RouterAuthService } from './router-auth.service';
36
import { SecureStorageService } from './secure-storage.service';
47
import { StorageService } from './storage.service';
58
import { TokenService } from './token.service';
9+
import { PlatformOrgSettingsService } from './platform/v1/spender/org-settings.service';
10+
import { FORMAT_PREFERENCES } from 'src/app/constants';
11+
import { FormatPreferences } from 'src/app/core/models/format-preferences.model';
12+
import { OrgSettings, RegionalSettings } from 'src/app/core/models/org-settings.model';
613

714
@Injectable()
815
export class ConfigService {
@@ -14,6 +21,12 @@ export class ConfigService {
1421

1522
private secureStorageService = inject(SecureStorageService);
1623

24+
private orgSettingsService = inject(PlatformOrgSettingsService);
25+
26+
private formatPreferences = inject<FormatPreferences>(FORMAT_PREFERENCES);
27+
28+
private datePipeOptions = inject(DATE_PIPE_DEFAULT_OPTIONS);
29+
1730
async loadConfigurationData(): Promise<void> {
1831
const clusterDomain: string = await this.tokenService.getClusterDomain();
1932

@@ -24,5 +37,32 @@ export class ConfigService {
2437
await this.secureStorageService.clearAll();
2538
await this.storageService.clearAll();
2639
}
40+
41+
await this.loadFormatPreferences();
42+
}
43+
44+
async loadFormatPreferences(): Promise<void> {
45+
const orgSettings = (await firstValueFrom(
46+
this.orgSettingsService.get().pipe(
47+
defaultIfEmpty(null),
48+
catchError(() => of(null)),
49+
),
50+
)) as OrgSettings | null;
51+
52+
const regional: RegionalSettings | undefined = orgSettings?.regional_settings;
53+
54+
if (regional) {
55+
this.formatPreferences.timeFormat = regional?.time_format;
56+
57+
const currencyFormat = regional.currency_format;
58+
this.formatPreferences.currencyFormat = {
59+
placement: currencyFormat.symbol_position === 'after' ? 'after' : 'before',
60+
thousandSeparator:
61+
currencyFormat.thousand_separator ?? this.formatPreferences.currencyFormat?.thousandSeparator,
62+
decimalSeparator: currencyFormat.decimal_separator ?? this.formatPreferences.currencyFormat?.decimalSeparator,
63+
};
64+
65+
this.datePipeOptions.dateFormat = regional.date_format;
66+
}
2767
}
2868
}

src/app/core/services/platform/v1/spender/org-settings.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export class PlatformOrgSettingsService {
412412
enabled: incoming.simplified_multi_stage_approvals?.enabled,
413413
},
414414
is_new_critical_policy_violation_flow_enabled: incoming?.is_new_critical_policy_violation_flow_enabled,
415+
regional_settings: incoming.regional_settings,
415416
};
416417

417418
Object.keys(orgSettings).forEach((settingsType) => {

src/app/core/test-data/org-settings.service.spec.data.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,17 @@ export const orgSettingsGetData: OrgSettings = deepFreeze({
453453
pending_cct_expense_restriction: { enabled: true, allowed: true },
454454
simplified_multi_stage_approvals: { enabled: true, allowed: true },
455455
is_new_critical_policy_violation_flow_enabled: false,
456+
regional_settings: {
457+
allowed: true,
458+
enabled: true,
459+
time_format: 'h:mm a',
460+
date_format: 'MMM dd, yyyy',
461+
currency_format: {
462+
decimal_separator: '.',
463+
thousand_separator: ',',
464+
symbol_position: 'before',
465+
},
466+
},
456467
});
457468

458469
export const orgSettingsPostData: OrgSettingsResponse = deepFreeze({
@@ -899,6 +910,17 @@ export const orgSettingsPostData: OrgSettingsResponse = deepFreeze({
899910
allowed: true,
900911
},
901912
is_new_critical_policy_violation_flow_enabled: false,
913+
regional_settings: {
914+
allowed: true,
915+
enabled: true,
916+
time_format: 'h:mm a',
917+
date_format: 'MMM dd, yyyy',
918+
currency_format: {
919+
decimal_separator: '.',
920+
thousand_separator: ',',
921+
symbol_position: 'before',
922+
},
923+
},
902924
});
903925

904926
export const orgSettingsAmexFeedDataRequest: OrgSettingsResponse = deepFreeze({

0 commit comments

Comments
 (0)