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
5 changes: 5 additions & 0 deletions src/app/models/contact-us.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const CONTACT_US_SUPPORT_TYPES = ['general', 'technical', 'onboarding', 'legal'] as const;

export type ContactUsSupportType = typeof CONTACT_US_SUPPORT_TYPES[number];

export type ContactUsDestinations = Record<ContactUsSupportType, string>;
31 changes: 28 additions & 3 deletions src/app/pages/admin/email/email.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<svg class="rtl:rotate-180 w-3 h-3 mx-1 text-gray-400 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
</svg>
<span class="ms-1 text-sm font-medium text-gray-500 dark:text-white md:ms-2 ">{{ 'ADMIN._verification' | translate }}</span>
<span class="ms-1 text-sm font-medium text-gray-500 dark:text-white md:ms-2 ">{{ 'ADMIN._email' | translate }}</span>
</div>
</li>
</ol>
Expand All @@ -26,7 +26,7 @@


<div class="bg-secondary-50 dark:bg-secondary-100 border border-secondary-50 dark:border-gray-800 mt-4 p-8 rounded-lg">
<h2 class="text-3xl font-bold text-primary-100 ml-4 dark:text-white">{{ 'ADMIN._verification' | translate }}</h2>
<h2 class="text-3xl font-bold text-primary-100 ml-4 dark:text-white">{{ 'ADMIN._email' | translate }}</h2>
<hr class="h-px m-4 bg-gray-300 dark:bg-gray-200 border-0">

<div class="md:grid">
Expand All @@ -42,7 +42,7 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 dark:text-white">{{ 'ADMIN._
[ngClass]="emailForm.get('smtpPort')?.invalid == true && emailForm.value.smtpPort != '' ? 'border-red-600' : 'border-gray-300'"
class="mb-2 bg-gray-50 dark:bg-secondary-300 border border-gray-300 dark:border-secondary-200 dark:text-white text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" />

<label for="email" class="font-bold text-lg dark:text-white">{{ 'ADMIN._email' | translate }}</label>
<label for="email" class="font-bold text-lg dark:text-white">{{ 'ADMIN._sourceEmail' | translate }}</label>
<input formControlName="email" type="text" id="email"
[ngClass]="emailForm.get('email')?.invalid == true && emailForm.value.email != '' ? 'border-red-600' : 'border-gray-300'"
class="mb-2 bg-gray-50 dark:bg-secondary-300 border border-gray-300 dark:border-secondary-200 dark:text-white text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" />
Expand All @@ -56,6 +56,15 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 dark:text-white">{{ 'ADMIN._
<input formControlName="emailPassword" type="password" id="email-pass"
[ngClass]="emailForm.get('emailPassword')?.invalid == true && emailForm.value.emailPassword != '' ? 'border-red-600' : 'border-gray-300'"
class="mb-2 bg-gray-50 dark:bg-secondary-300 border border-gray-300 dark:border-secondary-200 dark:text-white text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" />

<h3 class="mt-6 mb-2 font-bold text-xl text-primary-100 dark:text-white">{{ 'ADMIN._contactUsDestinations' | translate }}</h3>

@for(destination of contactUsDestinationFields; track destination.supportType) {
<label [for]="'contact-us-' + destination.supportType + '-email'" class="font-bold text-lg dark:text-white">{{ destination.labelKey | translate }}</label>
<input [formControlName]="destination.controlName" type="text" [id]="'contact-us-' + destination.supportType + '-email'"
[ngClass]="emailForm.get(destination.controlName)?.invalid == true && emailForm.get(destination.controlName)?.value != '' ? 'border-red-600' : 'border-gray-300'"
class="mb-2 bg-gray-50 dark:bg-secondary-300 border border-gray-300 dark:border-secondary-200 dark:text-white text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" />
}
</form>

<div class="flex w-full justify-items-end justify-end ml-4">
Expand All @@ -74,3 +83,19 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 dark:text-white">{{ 'ADMIN._
@if(showError){
<error-message class="flex justify-center items-center fixed w-fit top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50" [message]="errorMessage"></error-message>
}

@if(showSuccess){
<div class="flex justify-center items-center fixed w-fit top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50">
<div class="p-4 mb-4 text-white rounded-lg bg-primary-100" role="alert">
<div class="flex items-center">
<svg class="flex-shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<h3 class="text-lg font-medium">{{ 'ADMIN._updateSuccessTitle' | translate }}</h3>
</div>
<div class="mt-2 text-sm pr-8 pl-8">
{{ successMessage | translate }}
</div>
</div>
</div>
}
128 changes: 127 additions & 1 deletion src/app/pages/admin/email/email.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { EmailComponent } from './email.component';
import { environment } from 'src/environments/environment';

describe('EmailComponent', () => {
let component: EmailComponent;
let fixture: ComponentFixture<EmailComponent>;
let httpMock: HttpTestingController;

const configUrl = `${environment.BASE_URL}/charging/api/orderManagement/notify/config`;
const configResponse = {
smtpServer: 'smtp.example.org',
smtpPort: '587',
email: 'source@example.org',
emailUser: 'smtp-user',
contactUsDestinations: {
general: 'general@example.org',
technical: 'technical@example.org',
onboarding: 'onboarding@example.org',
legal: 'legal@example.org'
}
};

beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
Expand All @@ -19,10 +36,119 @@ describe('EmailComponent', () => {

fixture = TestBed.createComponent(EmailComponent);
component = fixture.componentInstance;
httpMock = TestBed.inject(HttpTestingController);
fixture.detectChanges();

const req = httpMock.expectOne(configUrl);
req.flush(configResponse);
});

afterEach(() => {
httpMock.verify();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should load source email and contact-us destinations separately', () => {
expect(component.emailForm.value.email).toBe('source@example.org');
expect(component.emailForm.value.contactUsGeneralEmail).toBe('general@example.org');
expect(component.emailForm.value.contactUsTechnicalEmail).toBe('technical@example.org');
expect(component.emailForm.value.contactUsOnboardingEmail).toBe('onboarding@example.org');
expect(component.emailForm.value.contactUsLegalEmail).toBe('legal@example.org');
});

it('should leave contact-us destinations empty when loading legacy config', () => {
component.fillData({
smtpServer: 'legacy-smtp.example.org',
smtpPort: '25',
email: 'source@example.org',
emailUser: 'legacy-user'
});

expect(component.emailForm.value.email).toBe('source@example.org');
expect(component.emailForm.value.contactUsGeneralEmail).toBe('');
expect(component.emailForm.value.contactUsTechnicalEmail).toBe('');
expect(component.emailForm.value.contactUsOnboardingEmail).toBe('');
expect(component.emailForm.value.contactUsLegalEmail).toBe('');
});

it('should post source email and contact-us destinations as separate config fields', () => {
component.emailForm.setValue({
smtpServer: 'smtp.example.org',
smtpPort: '587',
email: 'source@example.org',
emailUser: 'smtp-user',
emailPassword: 'secret',
contactUsGeneralEmail: 'general@example.org',
contactUsTechnicalEmail: 'technical@example.org',
contactUsOnboardingEmail: 'onboarding@example.org',
contactUsLegalEmail: 'legal@example.org'
});

component.addConfig();

const req = httpMock.expectOne(configUrl);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({
smtpServer: 'smtp.example.org',
smtpPort: '587',
email: 'source@example.org',
emailUser: 'smtp-user',
emailPassword: 'secret',
contactUsDestinations: {
general: 'general@example.org',
technical: 'technical@example.org',
onboarding: 'onboarding@example.org',
legal: 'legal@example.org'
}
});
req.flush({});

expect(component.emailForm.value.smtpServer).toBe('smtp.example.org');
expect(component.emailForm.value.smtpPort).toBe('587');
expect(component.emailForm.value.email).toBe('source@example.org');
expect(component.emailForm.value.emailUser).toBe('smtp-user');
expect(component.emailForm.value.emailPassword).toBe('');
expect(component.emailForm.value.contactUsGeneralEmail).toBe('general@example.org');
expect(component.emailForm.value.contactUsTechnicalEmail).toBe('technical@example.org');
expect(component.emailForm.value.contactUsOnboardingEmail).toBe('onboarding@example.org');
expect(component.emailForm.value.contactUsLegalEmail).toBe('legal@example.org');
expect(component.showSuccess).toBeTrue();
expect(component.successMessage).toBe('ADMIN._emailConfigUpdated');
});

it('should show an error and keep form values when update fails', () => {
component.emailForm.setValue({
smtpServer: 'smtp.example.org',
smtpPort: '587',
email: 'source@example.org',
emailUser: 'smtp-user',
emailPassword: 'secret',
contactUsGeneralEmail: 'general@example.org',
contactUsTechnicalEmail: 'technical@example.org',
contactUsOnboardingEmail: 'onboarding@example.org',
contactUsLegalEmail: 'legal@example.org'
});

component.addConfig();

const req = httpMock.expectOne(configUrl);
req.flush({ error: 'Invalid config' }, { status: 400, statusText: 'Bad Request' });

expect(component.showError).toBeTrue();
expect(component.errorMessage).toBe('Error: Invalid config');
expect(component.showSuccess).toBeFalse();
expect(component.emailForm.value.emailPassword).toBe('secret');
expect(component.emailForm.value.contactUsGeneralEmail).toBe('general@example.org');
});

it('should validate contact-us destination email format', () => {
component.emailForm.controls.contactUsTechnicalEmail.setValue('not-an-email');
component.emailForm.controls.contactUsTechnicalEmail.updateValueAndValidity();

expect(component.emailForm.controls.contactUsTechnicalEmail.invalid).toBeTrue();
expect(component.emailForm.controls.contactUsTechnicalEmail.hasError('email')).toBeTrue();
});
});
Loading
Loading