Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: controller logic and tests for onboarding home page #3387

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
14 changes: 14 additions & 0 deletions src/app/core/mock-data/onboarding-status.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import deepFreeze from 'deep-freeze-strict';
import { OnboardingStatus } from '../models/onboarding-status.model';
import { OnboardingState } from '../models/onboarding-state.enum';

export const onboardingStatusData: OnboardingStatus = deepFreeze({
user_id: 'us1ymEVgUKqb',
org_id: 'orOTDe765hQp',
step_connect_cards_is_configured: false,
step_connect_cards_is_skipped: false,
step_sms_opt_in_is_configured: false,
step_sms_opt_in_is_skipped: false,
step_show_welcome_modal_is_complete: false,
state: OnboardingState.YET_TO_START,
});
144 changes: 144 additions & 0 deletions src/app/core/services/spender-onboarding.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { TestBed } from '@angular/core/testing';
import { SpenderOnboardingService } from './spender-onboarding.service';
import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service';
import { OnboardingStepStatus } from '../models/onboarding-step-status.model';
import { of } from 'rxjs';
import { onboardingStatusData } from '../mock-data/onboarding-status.data';
import { OnboardingWelcomeStepStatus } from '../models/onboarding-welcome-step-status.model';

describe('SpenderOnboardingService', () => {
let spenderOnboardingService: SpenderOnboardingService;
let spenderPlatformV1ApiService: jasmine.SpyObj<SpenderPlatformV1ApiService>;

beforeEach(() => {
const spenderPlatformV1ApiServiceSpy = jasmine.createSpyObj('SpenderPlatformV1ApiService', ['get', 'post']);
TestBed.configureTestingModule({
providers: [
SpenderOnboardingService,
[
{
provide: SpenderPlatformV1ApiService,
useValue: spenderPlatformV1ApiServiceSpy,
},
],
],
});
spenderOnboardingService = TestBed.inject(SpenderOnboardingService);
spenderPlatformV1ApiService = TestBed.inject(
SpenderPlatformV1ApiService
) as jasmine.SpyObj<SpenderPlatformV1ApiService>;
});
Comment on lines +9 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Test suite for SpenderOnboardingService – a mighty presence!

• Perfect mocking of SpenderPlatformV1ApiService.
• No repeated code smells.
• Make sure the service is re-instantiated in each test if needed.


it('getOnboardingStatus(): should get onboarding status', (done) => {
const onboardingResponse = onboardingStatusData;
spenderPlatformV1ApiService.get.and.returnValue(of({ data: onboardingResponse }));

spenderOnboardingService.getOnboardingStatus().subscribe((res) => {
expect(res).toEqual(onboardingResponse);
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/onboarding');
done();
});
});

it('processConnnectCardsStep(): should process connect card step', (done) => {
const onboardingRequestResponse: OnboardingStepStatus = {
is_configured: true,
is_skipped: false,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processConnectCardsStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_connect_cards', {
data: onboardingRequestResponse,
});
done();
});
});

it('processSmsOptInStep(): should process opt in step', (done) => {
const onboardingRequestResponse: OnboardingStepStatus = {
is_configured: true,
is_skipped: false,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processSmsOptInStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_sms_opt_in', {
data: onboardingRequestResponse,
});
done();
});
});

it('processWelcomeModalStep(): should get category count', (done) => {
const onboardingRequestResponse: OnboardingWelcomeStepStatus = {
is_complete: true,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processWelcomeModalStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_show_welcome_modal', {
data: onboardingRequestResponse,
});
done();
});
});

it('markWelcomeModalStepAsComplete(): should call processWelcomeModalStep with the correct data', (done) => {
const mockData: OnboardingWelcomeStepStatus = { is_complete: true };
spyOn(spenderOnboardingService, 'processWelcomeModalStep').and.returnValue(of(mockData));

spenderOnboardingService.markWelcomeModalStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processWelcomeModalStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('markConnectCardsStepAsComplete(): should call processConnectCardsStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: true, is_skipped: false };
spyOn(spenderOnboardingService, 'processConnectCardsStep').and.returnValue(of(mockData));

spenderOnboardingService.markConnectCardsStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processConnectCardsStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('skipConnectCardsStep(): should call processConnectCardsStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: false, is_skipped: true };
spyOn(spenderOnboardingService, 'processConnectCardsStep').and.returnValue(of(mockData));

spenderOnboardingService.skipConnectCardsStep().subscribe((result) => {
expect(spenderOnboardingService.processConnectCardsStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('markSmsOptInStepAsComplete(): should call processSmsOptInStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: true, is_skipped: false };
spyOn(spenderOnboardingService, 'processSmsOptInStep').and.returnValue(of(mockData));

spenderOnboardingService.markSmsOptInStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processSmsOptInStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('skipSmsOptInStep(): should call processSmsOptInStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: false, is_skipped: true };
spyOn(spenderOnboardingService, 'processSmsOptInStep').and.returnValue(of(mockData));

spenderOnboardingService.skipSmsOptInStep().subscribe((result) => {
expect(spenderOnboardingService.processSmsOptInStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});
});
13 changes: 7 additions & 6 deletions src/app/core/services/spender-onboarding.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,37 @@ import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service';
import { PlatformApiResponse } from '../models/platform/platform-api-response.model';
import { OnboardingWelcomeStepStatus } from '../models/onboarding-welcome-step-status.model';
import { OnboardingStepStatus } from '../models/onboarding-step-status.model';
import { OnboardingStatus } from '../models/onboarding-status.model';

@Injectable({
providedIn: 'root',
})
export class OnboardingService {
export class SpenderOnboardingService {
constructor(private spenderPlatformV1ApiService: SpenderPlatformV1ApiService) {}

getOnboardingStatus(): Observable<OnboardingStepStatus> {
getOnboardingStatus(): Observable<OnboardingStatus> {
return this.spenderPlatformV1ApiService
.get<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding')
.get<PlatformApiResponse<OnboardingStatus>>('/onboarding')
.pipe(map((res) => res.data));
}

processConnectCardsStep(data: OnboardingStepStatus): Observable<OnboardingStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding/process_step_connect_cards', {
.post<PlatformApiResponse<OnboardingStepStatus>>('/onboarding/process_step_connect_cards', {
data,
})
.pipe(map((res) => res.data));
}

processSmsOptInStep(data: OnboardingStepStatus): Observable<OnboardingStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding/process_step_sms_opt_in', { data })
.post<PlatformApiResponse<OnboardingStepStatus>>('/onboarding/process_step_sms_opt_in', { data })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Using '/onboarding/process_step_sms_opt_in' – bullet-level logic!

The endpoint name is short and sweet; no confusion. Perfectly done, da!

You might consider standardizing naming across steps to maintain uniform route patterns.

.pipe(map((res) => res.data));
}

processWelcomeModalStep(data: OnboardingWelcomeStepStatus): Observable<OnboardingWelcomeStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingWelcomeStepStatus>>('/spender/onboarding/process_step_show_welcome_modal', {
.post<PlatformApiResponse<OnboardingWelcomeStepStatus>>('/onboarding/process_step_show_welcome_modal', {
data,
})
.pipe(map((res) => res.data));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum OnboardingStep {
CONNECT_CARD,
OPT_IN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<div class="connect-card__body">
<div>
<div class="connect-card__heading">Connect corporate card</div>
<div class="connect-card__sub-heading">
This will help you bring your card transactions into Fyle as expenses instantly.
</div>

<div class="connect-card__input-label">
<span>Corporate card</span>
</div>

<div
class="connect-card__input-inner-container"
[ngClass]="{ 'connect-card__input-inner-container--error': cardForm.touched && cardForm.invalid }"
>
<input
class="smartlook-show connect-card__card-number-input pl-0"
inputmode="numeric"
[formControl]="cardForm"
mask="0000 0000 0000 0000"
data-testid="card-number-input"
appAutofocus
[timeout]="500"
required
placeholder="Enter corporate card number"
/>

<ion-icon
*ngIf="!cardType || cardType === cardNetworkTypes.OTHERS"
src="../../../../assets/svg/card.svg"
class="connect-card__input-default-icon"
data-testid="default-icon"
></ion-icon>

<img
*ngIf="cardType === cardNetworkTypes.VISA"
src="../../../../assets/images/visa-logo.png"
class="connect-card__input-visa-icon"
data-testid="visa-icon"
/>

<img
*ngIf="cardType === cardNetworkTypes.MASTERCARD"
src="../../../../assets/images/mastercard-logo.png"
class="connect-card__input-mastercard-icon"
data-testid="mastercard-icon"
/>
</div>

<div class="connect-card__input-error-space"></div>

<div *ngIf="cardForm.touched && cardForm.invalid" class="connect-card__input-errors" data-testid="error-message">
<span *ngIf="cardForm.errors.invalidCardNumber">Please enter a valid card number.</span>

<ng-container *ngIf="cardForm.errors.invalidCardNetwork">
<span *ngIf="isVisaRTFEnabled && isMastercardRTFEnabled; else visaOnlyOrg"
>Enter a valid Visa or Mastercard number. If you have other cards, please contact your admin.</span
>

<ng-template #visaOnlyOrg>
<!-- Check if only visa is enabled -->
<span *ngIf="cardForm.errors.invalidCardNetwork && isVisaRTFEnabled; else mastercardOnlyOrg"
>Enter a valid Visa number. If you have other cards, please contact your admin.</span
>
</ng-template>

<ng-template #mastercardOnlyOrg>
<!-- Check if only mastercard is enabled -->
<span *ngIf="cardForm.errors.invalidCardNetwork && isMastercardRTFEnabled"
>Enter a valid Mastercard number. If you have other cards, please contact your admin.</span
>
</ng-template>
</ng-container>

<span *ngIf="cardForm.errors.enrollmentError">
{{ enrollmentFailureMessage }}
</span>
</div>
</div>
<div class="connect-card__primary-cta-container">
<ion-button
class="btn-primary connect-card__primary-cta"
fill="clear"
aria-label="Navigate back to sign in page"
role="button"
>
Continue
</ion-button>
</div>
</div>
Loading
Loading