Skip to content

Commit

Permalink
feat: add personal data request handling - GDPR (#1214)
Browse files Browse the repository at this point in the history
* landing page for GDPR data request confirmation

REQUIRED ICM VERSION: 7.10.38.14-LTS

Co-authored-by: Silke <s.grueber@intershop.de>
Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
  • Loading branch information
3 people committed Oct 17, 2022
1 parent b2104e7 commit b8ba675
Show file tree
Hide file tree
Showing 30 changed files with 669 additions and 0 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/.nb-gradle
/nbproject/
/.nvmrc
/.vim/
/.angular/cache
**/*.log
/src/environments/environment.local.ts
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ node_modules
/.nb-gradle
/nbproject/
/.nvmrc
/.vim/

# misc
/.angular/cache
Expand Down
1 change: 1 addition & 0 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export function app() {
'/login',
'/logout',
'/forgotPassword',
'/gdpr-requests',
'/contact',
],
})
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/configuration.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';

import { SPECIAL_HTTP_ERROR_HANDLER } from './interceptors/icm-error-mapper.interceptor';
import { createPaymentErrorHandler } from './utils/http-error/create-payment.error-handler';
import { dataRequestErrorHandler } from './utils/http-error/data-request.error-handler';
import { editPasswordErrorHandler } from './utils/http-error/edit-password.error-handler';
import { LoginUserErrorHandler } from './utils/http-error/login-user.error-handler';
import { requestReminderErrorHandler } from './utils/http-error/request-reminder.error-handler';
Expand All @@ -12,6 +13,7 @@ import { updatePasswordErrorHandler } from './utils/http-error/update-password.e
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: updatePasswordErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useClass: LoginUserErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: requestReminderErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: dataRequestErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: editPasswordErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: createPaymentErrorHandler, multi: true },
],
Expand Down
12 changes: 12 additions & 0 deletions src/app/core/facades/account.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import {
loadAddresses,
} from 'ish-core/store/customer/addresses';
import { getUserRoles } from 'ish-core/store/customer/authorization';
import {
firstGDPRDataRequest,
getDataRequestError,
getDataRequestLoading,
} from 'ish-core/store/customer/data-requests';
import { getOrders, getOrdersLoading, getSelectedOrder, loadOrders } from 'ish-core/store/customer/orders';
import {
cancelRegistration,
Expand Down Expand Up @@ -233,6 +238,13 @@ export class AccountFacade {
this.store.dispatch(deleteCustomerAddress({ addressId }));
}

// DATA REQUESTS

dataRequestLoading$ = this.store.pipe(select(getDataRequestLoading));
dataRequestError$ = this.store.pipe(select(getDataRequestError));
// boolean to check wether the GDPR data request is dispatched for the first time
isFirstGDPRDataRequest$ = this.store.pipe(select(firstGDPRDataRequest));

// SSO

ssoRegistrationError$ = this.store.pipe(select(getSsoRegistrationError));
Expand Down
19 changes: 19 additions & 0 deletions src/app/core/models/data-request/data-request.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* response data type for confirm data request
*/
export interface DataRequestData {
data: [
{
hash: string;
}
];
infos?: DataRequestInfo[];
}

export interface DataRequestInfo {
causes?: [
{
code: string;
}
];
}
20 changes: 20 additions & 0 deletions src/app/core/models/data-request/data-request.mapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DataRequestData, DataRequestInfo } from './data-request.interface';
import { DataRequestMapper } from './data-request.mapper';

describe('Data Request Mapper', () => {
describe('fromData', () => {
it(`should return DataRequestConfirmation information when getting DataRequestData`, () => {
const payloadData = {
infos: [{ causes: [{ code: 'already confirmed' }] } as DataRequestInfo],
} as DataRequestData;

const dataRequest = DataRequestMapper.fromData(payloadData);

expect(dataRequest).toMatchInlineSnapshot(`
Object {
"infoCode": "already confirmed",
}
`);
});
});
});
13 changes: 13 additions & 0 deletions src/app/core/models/data-request/data-request.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DataRequestData } from './data-request.interface';
import { DataRequestConfirmation } from './data-request.model';

export class DataRequestMapper {
/**
* Map data request data to data request confirmation information
*/
static fromData(data: DataRequestData): DataRequestConfirmation {
return {
infoCode: data?.infos[0]?.causes[0]?.code,
};
}
}
8 changes: 8 additions & 0 deletions src/app/core/models/data-request/data-request.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface DataRequest {
requestID: string;
hash: string;
}

export interface DataRequestConfirmation {
infoCode: string;
}
61 changes: 61 additions & 0 deletions src/app/core/services/data-requests/data-requests.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';

import { DataRequestData, DataRequestInfo } from 'ish-core/models/data-request/data-request.interface';
import { DataRequest } from 'ish-core/models/data-request/data-request.model';
import { ApiService } from 'ish-core/services/api/api.service';

import { DataRequestsService } from './data-requests.service';

describe('Data Requests Service', () => {
let apiServiceMock: ApiService;
let dataRequestsService: DataRequestsService;

beforeEach(() => {
apiServiceMock = mock(ApiService);
TestBed.configureTestingModule({
providers: [{ provide: ApiService, useFactory: () => instance(apiServiceMock) }],
});
dataRequestsService = TestBed.inject(DataRequestsService);
});

it('should be created', () => {
expect(dataRequestsService).toBeTruthy();
});

describe('Confirm a data request', () => {
it('should return an error when called with undefined', done => {
when(apiServiceMock.put(anything(), anything())).thenReturn(of({}));

dataRequestsService.confirmGDPRDataRequest(undefined).subscribe({
next: fail,
error: err => {
expect(err).toMatchInlineSnapshot(`[Error: confirmGDPRDataRequest() called without data body]`);
done();
},
});

verify(apiServiceMock.put(anything(), anything())).never();
});

it("should confirm data request when 'confirmDataRequest' is called", done => {
const requestData = {
requestID: 'test_ID',
hash: 'test_hash',
} as DataRequest;
const payloadData = {
infos: [{ causes: [{ code: 'already confirmed' }] } as DataRequestInfo],
} as DataRequestData;

when(apiServiceMock.put(anything(), anything(), anything())).thenReturn(of(payloadData));

dataRequestsService.confirmGDPRDataRequest(requestData).subscribe(payload => {
verify(apiServiceMock.put('gdpr-requests/test_ID/confirmations', anything(), anything())).once();
expect(capture(apiServiceMock.put).last()[0]).toMatchInlineSnapshot(`"gdpr-requests/test_ID/confirmations"`);
expect(payload).toHaveProperty('infoCode', 'already confirmed');
done();
});
});
});
});
37 changes: 37 additions & 0 deletions src/app/core/services/data-requests/data-requests.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, map, throwError } from 'rxjs';

import { DataRequestData } from 'ish-core/models/data-request/data-request.interface';
import { DataRequestMapper } from 'ish-core/models/data-request/data-request.mapper';
import { DataRequest, DataRequestConfirmation } from 'ish-core/models/data-request/data-request.model';
import { ApiService } from 'ish-core/services/api/api.service';

@Injectable({ providedIn: 'root' })
export class DataRequestsService {
constructor(private apiService: ApiService) {}

/**
* Confirmation of a GDPR data request with corresponding request id and hash.
*
* @param data The DataRequest model includes request id and hash.
* @returns The enriched DataRequest model includes additional response status and code of the request.
*/
confirmGDPRDataRequest(data: DataRequest): Observable<DataRequestConfirmation> {
if (!data) {
return throwError(() => new Error('confirmGDPRDataRequest() called without data body'));
}

const dataRequestHeaderV1 = new HttpHeaders()
.set('content-type', 'application/json')
.set('Accept', 'application/vnd.intershop.gdpr.v1+json');

return this.apiService
.put<DataRequestData>(
`gdpr-requests/${data.requestID}/confirmations`,
{ hash: data.hash },
{ headers: dataRequestHeaderV1 }
)
.pipe(map(payload => DataRequestMapper.fromData(payload)));
}
}
4 changes: 4 additions & 0 deletions src/app/core/store/customer/customer-store.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { BasketValidationEffects } from './basket/basket-validation.effects';
import { BasketEffects } from './basket/basket.effects';
import { basketReducer } from './basket/basket.reducer';
import { CustomerState } from './customer-store';
import { DataRequestsEffects } from './data-requests/data-requests.effects';
import { dataRequestsReducer } from './data-requests/data-requests.reducer';
import { OrdersEffects } from './orders/orders.effects';
import { ordersReducer } from './orders/orders.reducer';
import { OrganizationManagementEffects } from './organization-management/organization-management.effects';
Expand All @@ -33,6 +35,7 @@ const customerReducers: ActionReducerMap<CustomerState> = {
basket: basketReducer,
authorization: authorizationReducer,
ssoRegistration: ssoRegistrationReducer,
dataRequests: dataRequestsReducer,
};

const customerEffects = [
Expand All @@ -49,6 +52,7 @@ const customerEffects = [
OrganizationManagementEffects,
RequisitionManagementEffects,
SsoRegistrationEffects,
DataRequestsEffects,
];

@Injectable()
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/store/customer/customer-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BasketService } from 'ish-core/services/basket/basket.service';
import { CategoriesService } from 'ish-core/services/categories/categories.service';
import { ConfigurationService } from 'ish-core/services/configuration/configuration.service';
import { CountryService } from 'ish-core/services/country/country.service';
import { DataRequestsService } from 'ish-core/services/data-requests/data-requests.service';
import { FilterService } from 'ish-core/services/filter/filter.service';
import { OrderService } from 'ish-core/services/order/order.service';
import { PaymentService } from 'ish-core/services/payment/payment.service';
Expand Down Expand Up @@ -140,6 +141,7 @@ describe('Customer Store', () => {
const userServiceMock = mock(UserService);
when(userServiceMock.signInUser(anything())).thenReturn(of({ customer, user, pgid }));

const dataRequestsServiceMock = mock(DataRequestsService);
const filterServiceMock = mock(FilterService);
const orderServiceMock = mock(OrderService);
const authorizationServiceMock = mock(AuthorizationService);
Expand Down Expand Up @@ -170,6 +172,7 @@ describe('Customer Store', () => {
{ provide: BasketService, useFactory: () => instance(basketServiceMock) },
{ provide: CategoriesService, useFactory: () => instance(categoriesServiceMock) },
{ provide: CookiesService, useFactory: () => instance(mock(CookiesService)) },
{ provide: DataRequestsService, useFactory: () => instance(dataRequestsServiceMock) },
{ provide: FilterService, useFactory: () => instance(filterServiceMock) },
{ provide: OrderService, useFactory: () => instance(orderServiceMock) },
{ provide: PaymentService, useFactory: () => instance(mock(PaymentService)) },
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/store/customer/customer-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Authorization } from 'ish-core/models/authorization/authorization.model

import { AddressesState } from './addresses/addresses.reducer';
import { BasketState } from './basket/basket.reducer';
import { DataRequestsState } from './data-requests/data-requests.reducer';
import { OrdersState } from './orders/orders.reducer';
import { SsoRegistrationState } from './sso-registration/sso-registration.reducer';
import { UserState } from './user/user.reducer';
Expand All @@ -15,6 +16,7 @@ export interface CustomerState {
basket: BasketState;
authorization: Authorization;
ssoRegistration: SsoRegistrationState;
dataRequests: DataRequestsState;
}

export const getCustomerState = createFeatureSelector<CustomerState>('_customer');
19 changes: 19 additions & 0 deletions src/app/core/store/customer/data-requests/data-requests.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createAction } from '@ngrx/store';

import { DataRequest, DataRequestConfirmation } from 'ish-core/models/data-request/data-request.model';
import { httpError, payload } from 'ish-core/utils/ngrx-creators';

export const confirmGDPRDataRequest = createAction(
'[DataRequest API] Confirm GDPR Data Request',
payload<{ data: DataRequest }>()
);

export const confirmGDPRDataRequestSuccess = createAction(
'[DataRequest API] Confirm GDPR Data Request Success',
payload<DataRequestConfirmation>()
);

export const confirmGDPRDataRequestFail = createAction(
'[DataRequest API] Confirm GDPR Data Request Failed',
httpError()
);
Loading

0 comments on commit b8ba675

Please sign in to comment.