parent
2a531a19fe
commit
e163be6cf0
@ -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…
Reference in new issue