implemented packages service

main
Inga 🏳‍🌈 1 year ago
parent 2a531a19fe
commit e163be6cf0
  1. 4
      src/clients/geocoding/osm.ts
  2. 2
      src/clients/geocoding/types.ts
  3. 15
      src/dependencies.ts
  4. 176
      src/packages.service.spec.ts
  5. 50
      src/packages.service.ts

@ -3,9 +3,9 @@ import NodeGeocoder from 'node-geocoder';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { mean } from '../../utils/math'; import { mean } from '../../utils/math';
import type { Geocoder } from './types'; import type { GeocodingProvider } from './types';
export const createOsmClient = (): Geocoder => { export const createOsmClient = (): GeocodingProvider => {
const geocoder = NodeGeocoder({ const geocoder = NodeGeocoder({
provider: 'openstreetmap', provider: 'openstreetmap',
fetch: (url, options) => { fetch: (url, options) => {

@ -1,4 +1,4 @@
export type Geocoder = { export type GeocodingProvider = {
geocode(query: string): Promise<{ geocode(query: string): Promise<{
longitude: number; longitude: number;
latitude: number; latitude: number;

@ -0,0 +1,15 @@
import { GeocodingProvider } from './clients/geocoding/types';
import { WeatherProvider } from './clients/weather/types';
import { PackagesRepository } from './storage/types';
export const packagesRepositoryDependency = Symbol('packages repository');
export const geocodingProviderDependency = Symbol('geocoding provider');
export const weatherProviderDependency = Symbol('weather provider');
export type Dependencies = {
[packagesRepositoryDependency]: PackagesRepository;
[geocodingProviderDependency]: GeocodingProvider;
[weatherProviderDependency]: WeatherProvider;
};
export type Dependency<T extends keyof Dependencies> = Dependencies[T];

@ -0,0 +1,176 @@
import { GeocodingProvider } from './clients/geocoding/types';
import { WeatherProvider } from './clients/weather/types';
import { PackagesService } from './packages.service';
import { PackageInfo, PackagesRepository } from './storage/types';
describe('PackagesService', () => {
describe('with mock providers', () => {
const defaultPackage = {
articleName: 'fake name',
articlePrice: 123,
articleQuantity: 456,
senderAddress: 'fake address',
SKU: 'fake sku',
status: 'delivery',
} satisfies Partial<PackageInfo>;
const packageCold: PackageInfo = {
...defaultPackage,
carrier: 'DHL',
trackingNumber: 'package-cold',
receiverAddress: 'Olympic stadium, Esperanza City',
};
const packageWarm: PackageInfo = {
...defaultPackage,
carrier: 'DHL',
trackingNumber: 'package-warm',
receiverAddress: 'Mitsubishi headquarters, Tōgenkyō',
};
const packageNoGeolocation: PackageInfo = {
...defaultPackage,
carrier: 'GLS',
trackingNumber: 'package-no-geolocation',
receiverAddress: 'Perhaps the stars',
};
const packageNoWeather: PackageInfo = {
...defaultPackage,
carrier: 'GLS',
trackingNumber: 'package-no-weather',
receiverAddress:
'Headquarters of the United Nations, Lagos, Nigeria',
};
const packages = [
packageCold,
packageWarm,
packageNoGeolocation,
packageNoWeather,
];
const packagesRepository: PackagesRepository = {
getPackage: (requestedCarrier, requestedTrackingNumber) => {
const foundPackage = packages.find(
({ carrier, trackingNumber }) =>
carrier === requestedCarrier &&
trackingNumber === requestedTrackingNumber,
);
if (!foundPackage) {
return Promise.resolve({ found: false });
}
return Promise.resolve({
found: true,
value: foundPackage,
});
},
};
const geocodingProvider: GeocodingProvider = {
geocode: (query) => {
switch (query) {
case packageCold.receiverAddress:
return Promise.resolve({
latitude: -63.4,
longitude: -57,
});
case packageWarm.receiverAddress:
return Promise.resolve({ latitude: 0, longitude: 107 });
case packageNoWeather.receiverAddress:
return Promise.resolve({
latitude: 6.5,
longitude: 3.4,
});
default:
throw new Error('unknown address');
}
},
};
const weatherCold = {
apparentTemperature: '0°C',
temperature: '-52°C',
relativeHumidity: '30%',
};
const weatherWarm = {
apparentTemperature: '35°C',
temperature: '30°C',
relativeHumidity: '80%',
};
const weatherProvider: WeatherProvider = {
getCurrentWeather: ({ longitude, latitude }) => {
if (latitude < -60) {
return Promise.resolve(weatherCold);
}
if (latitude === 0 && longitude > 100) {
return Promise.resolve(weatherWarm);
}
throw new Error('No weather data');
},
};
const packagesService = new PackagesService(
packagesRepository,
geocodingProvider,
weatherProvider,
);
it('returns weather for packages with available weather', async () => {
expect(
await packagesService.getPackageInfoWithWeather(
packageCold.carrier,
packageCold.trackingNumber,
),
).toEqual({
packageData: packageCold,
receiverWeather: weatherCold,
});
expect(
await packagesService.getPackageInfoWithWeather(
packageWarm.carrier,
packageWarm.trackingNumber,
),
).toEqual({
packageData: packageWarm,
receiverWeather: weatherWarm,
});
});
it('does not return weather for packages with unavailable weather', async () => {
expect(
await packagesService.getPackageInfoWithWeather(
packageNoWeather.carrier,
packageNoWeather.trackingNumber,
),
).toEqual({
packageData: packageNoWeather,
});
});
it('does not return weather for packages with unresolvable address', async () => {
expect(
await packagesService.getPackageInfoWithWeather(
packageNoGeolocation.carrier,
packageNoGeolocation.trackingNumber,
),
).toEqual({
packageData: packageNoGeolocation,
});
});
it('returns null for non-existent packages', async () => {
expect(
await packagesService.getPackageInfoWithWeather(
'DHL',
'non-existent',
),
).toBe(null);
});
it('returns null for packages with wrong carrier name', async () => {
expect(
await packagesService.getPackageInfoWithWeather(
'UPS',
packageCold.trackingNumber,
),
).toBe(null);
});
});
});

@ -0,0 +1,50 @@
import { Inject, Injectable } from '@nestjs/common';
import {
Dependencies,
geocodingProviderDependency,
packagesRepositoryDependency,
weatherProviderDependency,
} from './dependencies';
import { CarrierType } from './types';
@Injectable()
export class PackagesService {
constructor(
@Inject(packagesRepositoryDependency)
private readonly packagesRepository: Dependencies[typeof packagesRepositoryDependency],
@Inject(geocodingProviderDependency)
private readonly geocodingProvider: Dependencies[typeof geocodingProviderDependency],
@Inject(weatherProviderDependency)
private readonly weatherProvider: Dependencies[typeof weatherProviderDependency],
) {}
async getPackageInfoWithWeather(
carrier: CarrierType,
trackingNumber: string,
) {
const packageDataResponse = await this.packagesRepository.getPackage(
carrier,
trackingNumber,
);
if (!packageDataResponse.found) {
return null;
}
const packageData = packageDataResponse.value;
try {
const receiverLocation = await this.geocodingProvider.geocode(
packageData.receiverAddress,
);
const receiverWeather =
await this.weatherProvider.getCurrentWeather(receiverLocation);
return {
packageData,
receiverWeather,
};
} catch {
return { packageData };
}
}
}
Loading…
Cancel
Save