From e163be6cf056be26862032358738eb8f598a2531 Mon Sep 17 00:00:00 2001 From: Inga Date: Thu, 26 Oct 2023 18:45:36 +0000 Subject: [PATCH] implemented packages service --- src/clients/geocoding/osm.ts | 4 +- src/clients/geocoding/types.ts | 2 +- src/dependencies.ts | 15 +++ src/packages.service.spec.ts | 176 +++++++++++++++++++++++++++++++++ src/packages.service.ts | 50 ++++++++++ 5 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 src/dependencies.ts create mode 100644 src/packages.service.spec.ts create mode 100644 src/packages.service.ts diff --git a/src/clients/geocoding/osm.ts b/src/clients/geocoding/osm.ts index 5941880..84e46a9 100644 --- a/src/clients/geocoding/osm.ts +++ b/src/clients/geocoding/osm.ts @@ -3,9 +3,9 @@ import NodeGeocoder from 'node-geocoder'; import fetch from 'node-fetch'; import PQueue from 'p-queue'; 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({ provider: 'openstreetmap', fetch: (url, options) => { diff --git a/src/clients/geocoding/types.ts b/src/clients/geocoding/types.ts index ffc8086..3acad6a 100644 --- a/src/clients/geocoding/types.ts +++ b/src/clients/geocoding/types.ts @@ -1,4 +1,4 @@ -export type Geocoder = { +export type GeocodingProvider = { geocode(query: string): Promise<{ longitude: number; latitude: number; diff --git a/src/dependencies.ts b/src/dependencies.ts new file mode 100644 index 0000000..8d1ea9c --- /dev/null +++ b/src/dependencies.ts @@ -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 = Dependencies[T]; diff --git a/src/packages.service.spec.ts b/src/packages.service.spec.ts new file mode 100644 index 0000000..bfeee6e --- /dev/null +++ b/src/packages.service.spec.ts @@ -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; + 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); + }); + }); +}); diff --git a/src/packages.service.ts b/src/packages.service.ts new file mode 100644 index 0000000..b9cfddd --- /dev/null +++ b/src/packages.service.ts @@ -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 }; + } + } +}