Skip to content

Commit

Permalink
feat: add Address Doctor integration (#1337)
Browse files Browse the repository at this point in the history
* introduce notifier service to notify address doctor for new address check
* use feature event service to notify extensions for further actions from the outside
* use formly for address doctor form
* mapper to map address doctor data to Intershop compatible Address interface
* implement caching functionality to avoid unnecessary REST requests
* add additional documentation for address doctor
* introduce helper to check addresses for equality
* don't open modal when no suggestions are available
* update existing address form with selected address before submitting

Co-authored-by: Marcel Eisentraut <meisentraut@intershop.de>
Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
  • Loading branch information
3 people committed Jun 21, 2023
1 parent c5973ac commit 28eb2ce
Show file tree
Hide file tree
Showing 40 changed files with 1,614 additions and 223 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
# from projects/requisition-management/src/app/exports/.gitignore
/projects/requisition-management/src/app/exports/**/lazy*

# from src/app/extensions/address-doctor/exports/.gitignore
/src/app/extensions/address-doctor/exports/**/lazy*

# from src/app/extensions/compare/exports/.gitignore
/src/app/extensions/compare/exports/**/lazy*

Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ kb_sync_latest_only

### Third-party Integrations

- [Guide - Google Tag Manager](./guides/google-tag-manager.md)
- [Guide - Tracking with Google Tag Manager](./guides/google-tag-manager.md)
- [Guide - Client-Side Error Monitoring with Sentry](./guides/sentry-error-monitoring.md)
- [Guide - Extended Product Configurations with Tacton](./guides/tacton-product-configuration.md)
- [Guide - Monitoring with Prometheus](./guides/prometheus-monitoring.md)
- [Guide - Store Locator with Google Maps](./guides/store-locator.md)
- [Guide - Address Check with Address Doctor](./guides/address-doctor.md)
64 changes: 64 additions & 0 deletions docs/guides/address-doctor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!--
kb_guide
kb_pwa
kb_everyone
kb_sync_latest_only
-->

# Address Check with Address Doctor

We integrated [Address Doctor](https://www.informatica.com/de/products/data-quality/data-as-a-service/address-verification.html) to verify address data for correctness.

## Setup

First, activate the feature toggle `addressDoctor`.
You will also have to provide the endpoint and additional verification data.
This can be done by defining it in [Angular CLI environment](../concepts/configuration.md#angular-cli-environments) files:

```typescript
export const environment: Environment = {
...ENVIRONMENT_DEFAULTS,

addressDoctor: {
url: '<addressDoctor-url>',
login: '<addressDoctor-login>',
password: '<addressDoctor-password>',
maxResultCount: 5,
},
```
This configuration can also be supplied via environment variable `ADDRESS_DOCTOR` as stringified JSON:
```text
ADDRESS_DOCTOR='{ "addressDoctor": { "url": "<addressDoctor-url>", "login": "<addressDoctor-login>", "password": "<addressDoctor-password>", "maxResultCount": "5" } }';
```
## Workflow
To check an address with the address doctor the PWA needs to render the `<ish-lazy-address-doctor>` component.
When the user submits the address data, the PWA needs to send a [feature notification event](../../src/app/core/utils/feature-event/feature-event.service.ts) with the request to check the data.

```typescript
const id = this.featureEventService.sendNotification('addressDoctor', 'check-address', {
address,
});
```
This method will submit the address data to the Address Doctor REST API and open a modal with all suggestions.
The user needs to decide and confirm the address, which is the correct one.
With the subscribe on the [eventResultListener$](../../src/app/core/utils/feature-event/feature-event.service.ts) observable, the initial component can react on the confirmation data.
```typescript
this.featureEventService
.eventResultListener$('addressDoctor', 'check-address', id)
.pipe(whenTruthy(), take(1), takeUntil(this.destroy$))
.subscribe(({ address }) => {
if (address) {
this.accountFacade.updateCustomerAddress(address);
}
});
```
## Further References
- [Concept - Configuration](../concepts/configuration.md)
2 changes: 2 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ The `getOAuthServiceInstance()` static method from the `InstanceCreators` class
Furthermore the handling of the anonymous user token has been changed.
It will only be fetched when an anonymous user intends to create a basket.

We added an Address Doctor integration as a new extension which can be enabled with the feature toggle `addressDoctor` and [additional configuration](./address-doctor.md).

## 3.3 to 4.0

The Intershop PWA now uses Node.js 18.15.0 LTS with the corresponding npm version 9.5.0 and the `"lockfileVersion": 3,`.
Expand Down
1 change: 1 addition & 0 deletions docs/guides/ssr-startup.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Make sure to use them as written in the table below.
| | PROMETHEUS | switch | Exposes Prometheus metrics |
| | IDENTITY_PROVIDER | string | ID of the default identity provider if other than `ICM` |
| | IDENTITY_PROVIDERS | JSON | Configuration of additional identity providers besides the default `ICM` |
| | ADDRESS_DOCTOR | JSON | Configuration of address doctor with login, password, maxResultCount and url |

## Development

Expand Down
22 changes: 22 additions & 0 deletions src/app/extensions/address-doctor/address-doctor.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';

import { FEATURE_EVENT_RESULT_LISTENER } from 'ish-core/utils/feature-event/feature-event.service';
import { SharedModule } from 'ish-shared/shared.module';

import { AddressDoctorEventsService } from './services/address-doctor-events/address-doctor-events.service';
import { AddressDoctorModalComponent } from './shared/address-doctor-modal/address-doctor-modal.component';
import { AddressDoctorComponent } from './shared/address-doctor/address-doctor.component';

@NgModule({
imports: [SharedModule],
declarations: [AddressDoctorComponent, AddressDoctorModalComponent],
exports: [SharedModule],
providers: [
{
provide: FEATURE_EVENT_RESULT_LISTENER,
useFactory: AddressDoctorEventsService.checkAddressResultListenerFactory,
multi: true,
},
],
})
export class AddressDoctorModule {}
1 change: 1 addition & 0 deletions src/app/extensions/address-doctor/exports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/lazy*
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';

import { LazyAddressDoctorComponent } from './lazy-address-doctor/lazy-address-doctor.component';

@NgModule({
imports: [CommonModule, TranslateModule],
declarations: [LazyAddressDoctorComponent],
exports: [LazyAddressDoctorComponent],
})
export class AddressDoctorExportsModule {}
32 changes: 32 additions & 0 deletions src/app/extensions/address-doctor/facades/address-doctor.facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable, inject } from '@angular/core';
import { isEqual } from 'lodash-es';
import { Observable, map, of, race, tap, timer } from 'rxjs';

import { Address } from 'ish-core/models/address/address.model';

import { AddressDoctorService } from '../services/address-doctor/address-doctor.service';

@Injectable({ providedIn: 'root' })
export class AddressDoctorFacade {
private addressDoctorService = inject(AddressDoctorService);

private lastAddressCheck: Address;
private lastAddressCheckResult: Address[] = [];

checkAddress(address: Address): Observable<Address[]> {
if (isEqual(address, this.lastAddressCheck)) {
return of(this.lastAddressCheckResult);
}

this.lastAddressCheck = address;
return race(
this.addressDoctorService.postAddress(address).pipe(
tap(result => {
this.lastAddressCheckResult = result;
})
),
// if the address check takes longer than 5 seconds return with no suggestions
timer(5000).pipe(map(() => []))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface AddressDoctorConfig {
url: string;
login: string;
password: string;
maxResultCount: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum AddressDoctorEvents {
CheckAddress = 'check-address',
CheckAddressSuccess = 'check-address-successful',
CheckAddressCancelled = 'check-address-cancellation',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isEqual, pick } from 'lodash-es';

import { Address } from 'ish-core/models/address/address.model';

import { AddressDoctorMapper } from './address-doctor.mapper';

export class AddressDoctorHelper {
static equalityCheck(address1: Address, address2: Address): boolean {
if (!address1 || !address2) {
return false;
}

const attributes = AddressDoctorMapper.attributes;
return isEqual(pick(address1, ...attributes), pick(address2, ...attributes));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export interface AddressDoctorVariants {
Variants: AddressDoctorVariant[];
}

export interface AddressDoctorVariant {
StatusValues: StatusValues;
AddressElements: AddressElements;
PreformattedData: PreformattedData;
}

interface StatusValues {
AddressType: string;
ResultGroup: string;
LanguageISO3: string;
UsedVerificationLevel: string;
MatchPercentage: string;
Script: string;
AddressCount: string;
ResultQuality: number;
}

interface AddressElements {
Street: AddressElement[];
HouseNumber: AddressElement[];
Locality: AddressElement[];
PostalCode: AddressElement[];
AdministrativeDivision: AdministrativeDivision[];
Country: Country[];
}

interface AddressElement {
Value: string;
SubItems: SubItems;
}

interface SubItems {
Name?: string;
// eslint-disable-next-line id-blacklist
Number?: string;
Base?: string;
}

interface AdministrativeDivision {
Value: string;
Variants: Variants;
}

interface Variants {
Extended: string;
ISO: string;
Abbreviation: string;
}

interface Country {
Code: string;
Name: string;
}

interface PreformattedData {
SingleAddressLine: PreformattedDataValue;
PostalDeliveryAddressLines: PreformattedDataValue[];
PostalFormattedAddressLines: PreformattedDataValue[];
PostalLocalityLine: PreformattedDataValue;
}

interface PreformattedDataValue {
Value: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Address } from 'ish-core/models/address/address.model';

import { AddressDoctorVariant } from './address-doctor.interface';

/**
* Map incoming data from Address Doctor REST API to ICM compliant addresses.
* The mapper is implemented and tested against German and British addresses.
* The implementation with attributes needs to be adapted for other foreign addresses, when the overwrite does not match.
*
*/
export class AddressDoctorMapper {
static attributes = ['addressLine1', 'postalCode', 'city'];

static fromData(variant: AddressDoctorVariant): Partial<Address> {
return {
addressLine1: `${variant.AddressElements.Street ? variant.AddressElements.Street[0].Value : ''} ${
variant.AddressElements.HouseNumber ? variant.AddressElements.HouseNumber[0].Value : ''
}`.trim(),
postalCode: (variant.AddressElements.PostalCode ? variant.AddressElements.PostalCode[0].Value : '').trim(),
city: variant.AddressElements.Locality.map(loc => loc.Value).join(' '),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { inject } from '@angular/core';
import { filter, take, takeUntil } from 'rxjs/operators';

import { FeatureEventResultListener, FeatureEventService } from 'ish-core/utils/feature-event/feature-event.service';
import { whenTruthy } from 'ish-core/utils/operators';

import { AddressDoctorEvents } from '../../models/address-doctor/address-doctor-event.model';

export class AddressDoctorEventsService {
static checkAddressResultListenerFactory(): FeatureEventResultListener {
const featureEventService = inject(FeatureEventService);
return {
feature: 'addressDoctor',
event: AddressDoctorEvents.CheckAddress,
resultListener$: (id: string) => {
if (!id) {
return;
}

return featureEventService.eventResults$.pipe(
whenTruthy(),
// respond only when CheckAddressSuccess event is emitted for specific notification id
filter(
result => result.id === id && result.event === AddressDoctorEvents.CheckAddressSuccess && result.successful
),
take(1),
takeUntil(
featureEventService.eventResults$.pipe(
whenTruthy(),
// close event stream when CheckAddressCancelled event is emitted for specific notification id
filter(result => result.id === id && result.event === AddressDoctorEvents.CheckAddressCancelled)
)
)
);
},
};
}
}
Loading

0 comments on commit 28eb2ce

Please sign in to comment.