WIP: Implemented tests & processed tracking information
This commit is contained in:
parent
ad84d1f837
commit
af9ab1a483
|
|
@ -15,7 +15,7 @@ module.exports = {
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['.eslintrc.js'],
|
ignorePatterns: ['.eslintrc.js', 'database/'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"jest-mock": "^29.7.0",
|
||||||
"mysql2": "^3.9.2",
|
"mysql2": "^3.9.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|
@ -1431,7 +1432,6 @@
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||||
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
|
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "^0.27.8"
|
"@sinclair/typebox": "^0.27.8"
|
||||||
},
|
},
|
||||||
|
|
@ -1513,7 +1513,6 @@
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
|
||||||
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
|
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/schemas": "^29.6.3",
|
"@jest/schemas": "^29.6.3",
|
||||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||||
|
|
@ -2015,8 +2014,7 @@
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@sinonjs/commons": {
|
"node_modules/@sinonjs/commons": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
|
|
@ -2199,14 +2197,12 @@
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/istanbul-lib-report": {
|
"node_modules/@types/istanbul-lib-report": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
|
||||||
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
|
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/istanbul-lib-coverage": "*"
|
"@types/istanbul-lib-coverage": "*"
|
||||||
}
|
}
|
||||||
|
|
@ -2215,7 +2211,6 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
|
||||||
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
|
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
|
|
@ -2252,7 +2247,6 @@
|
||||||
"version": "20.11.30",
|
"version": "20.11.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
|
|
@ -2338,7 +2332,6 @@
|
||||||
"version": "17.0.32",
|
"version": "17.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||||
"integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
|
"integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/yargs-parser": "*"
|
"@types/yargs-parser": "*"
|
||||||
}
|
}
|
||||||
|
|
@ -2346,8 +2339,7 @@
|
||||||
"node_modules/@types/yargs-parser": {
|
"node_modules/@types/yargs-parser": {
|
||||||
"version": "21.0.3",
|
"version": "21.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
|
||||||
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
|
|
@ -3388,7 +3380,6 @@
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -5135,8 +5126,7 @@
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
|
@ -5983,7 +5973,6 @@
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
|
||||||
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
|
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
|
|
@ -6213,7 +6202,6 @@
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
|
||||||
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
|
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
|
|
@ -6230,7 +6218,6 @@
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
|
|
@ -9495,8 +9482,7 @@
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"jest-mock": "^29.7.0",
|
||||||
"mysql2": "^3.9.2",
|
"mysql2": "^3.9.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import {
|
import {
|
||||||
Body,
|
|
||||||
ClassSerializerInterceptor,
|
ClassSerializerInterceptor,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { ApiBody, ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
|
import { ApiExtraModels } from '@nestjs/swagger';
|
||||||
import { TrackingDto } from '@/dtos/tracking.dto';
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
|
|
@ -20,15 +18,4 @@ export class AppController {
|
||||||
debug(): unknown {
|
debug(): unknown {
|
||||||
return this.appService.debug();
|
return this.appService.debug();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
|
||||||
@ApiBody({
|
|
||||||
schema: {
|
|
||||||
$ref: getSchemaPath(TrackingDto),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
test(@Body() body: TrackingDto): string {
|
|
||||||
console.log(body);
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { ContainerEntity } from '@/entities/container.entity';
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
import { instanceToPlain, plainToClass } from 'class-transformer';
|
|
||||||
|
|
||||||
export class ContainerDto {
|
export class ContainerDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|
@ -17,14 +16,4 @@ export class ContainerDto {
|
||||||
this.containerNumber = entity.containerNumber;
|
this.containerNumber = entity.containerNumber;
|
||||||
this.shipmentId = entity.shipmentId;
|
this.shipmentId = entity.shipmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromEntity(entity: ContainerEntity): ContainerDto {
|
|
||||||
const data = instanceToPlain(entity);
|
|
||||||
return plainToClass(ContainerDto, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
toEntity(): ContainerEntity {
|
|
||||||
const data = instanceToPlain(this);
|
|
||||||
return plainToClass(ContainerEntity, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
import { uuidTransformer } from '@/utils/uuidTransformer';
|
import { uuidTransformer } from '@/utils/uuidTransformer';
|
||||||
import { ShipmentEntity } from '@/entities/shipment.entity';
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
name: 'containers',
|
name: 'containers',
|
||||||
})
|
})
|
||||||
export class ContainerEntity extends BaseEntity {
|
export class ContainerEntity {
|
||||||
@PrimaryColumn({
|
@PrimaryColumn({
|
||||||
type: 'binary',
|
type: 'binary',
|
||||||
length: 16,
|
length: 16,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
import {
|
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||||
BaseEntity,
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
OneToOne,
|
|
||||||
PrimaryColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { uuidTransformer } from '@/utils/uuidTransformer';
|
import { uuidTransformer } from '@/utils/uuidTransformer';
|
||||||
import { ShipmentEntity } from '@/entities/shipment.entity';
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
name: 'live_tracking',
|
name: 'live_tracking',
|
||||||
})
|
})
|
||||||
export class LiveTrackingEntity extends BaseEntity {
|
export class LiveTrackingEntity {
|
||||||
@PrimaryColumn({
|
@PrimaryColumn({
|
||||||
type: 'binary',
|
type: 'binary',
|
||||||
length: 16,
|
length: 16,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
import {
|
import { Column, Entity, OneToMany, OneToOne, PrimaryColumn } from 'typeorm';
|
||||||
BaseEntity,
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
OneToMany,
|
|
||||||
OneToOne,
|
|
||||||
PrimaryColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { uuidTransformer } from '@/utils/uuidTransformer';
|
import { uuidTransformer } from '@/utils/uuidTransformer';
|
||||||
import { ContainerEntity } from '@/entities/container.entity';
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
|
import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
|
||||||
|
|
@ -13,7 +6,7 @@ import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
|
||||||
@Entity({
|
@Entity({
|
||||||
name: 'shipments',
|
name: 'shipments',
|
||||||
})
|
})
|
||||||
export class ShipmentEntity extends BaseEntity {
|
export class ShipmentEntity {
|
||||||
@PrimaryColumn({
|
@PrimaryColumn({
|
||||||
type: 'binary',
|
type: 'binary',
|
||||||
length: 16,
|
length: 16,
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,8 @@ describe('ShipmentsController', () => {
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,6 @@ export class ShipmentsController {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
getShipments() {
|
getShipments() {
|
||||||
return this.shipmentsService.getShipments();
|
return this.shipmentsService.getAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,6 @@ import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
],
|
],
|
||||||
controllers: [ShipmentsController],
|
controllers: [ShipmentsController],
|
||||||
providers: [ShipmentsService],
|
providers: [ShipmentsService],
|
||||||
|
exports: [ShipmentsService],
|
||||||
})
|
})
|
||||||
export class ShipmentsModule {}
|
export class ShipmentsModule {}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,13 @@ export class ShipmentsService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// @todo add pagination
|
// @todo add pagination
|
||||||
getShipments() {
|
getAll() {
|
||||||
return this.shipmentRepository.find({
|
return this.shipmentRepository.find({
|
||||||
relations: ['liveTracking', 'containers'],
|
relations: ['liveTracking', 'containers'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(shipment: ShipmentEntity) {
|
||||||
|
return this.shipmentRepository.save(shipment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { NotificationsService } from './notifications.service';
|
||||||
|
|
||||||
|
describe('NotificationsService', () => {
|
||||||
|
let service: NotificationsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [NotificationsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<NotificationsService>(NotificationsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import {
|
||||||
|
INotifier,
|
||||||
|
SupportedNotifiersNames,
|
||||||
|
} from '@/tracking/notifications/notifier.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationsService {
|
||||||
|
constructor(
|
||||||
|
@Inject('NOTIFIER_FACTORY')
|
||||||
|
private readonly notifierFactory: (
|
||||||
|
shipment: ShipmentEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
type?: SupportedNotifiersNames,
|
||||||
|
) => INotifier,
|
||||||
|
) {}
|
||||||
|
public async send(
|
||||||
|
shipment: ShipmentEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
): Promise<void> {
|
||||||
|
const notifier = this.notifierFactory(shipment, trackingInfo);
|
||||||
|
await notifier.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { NotifierAbstract } from '@/tracking/notifications/notifier.abstract';
|
||||||
|
import { INotifier } from '@/tracking/notifications/notifier.types';
|
||||||
|
|
||||||
|
export class ConsoleNotifier extends NotifierAbstract implements INotifier {
|
||||||
|
async send(): Promise<boolean> {
|
||||||
|
const messages = this.generateMessages();
|
||||||
|
|
||||||
|
if (!messages.length) {
|
||||||
|
console.log('Dont send email');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Send email with the following messages: ${messages.join('\n')}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMessages(): string[] {
|
||||||
|
const messages: string[] = [];
|
||||||
|
if (this.isDelayed) {
|
||||||
|
messages.push('Your shipment has been delayed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.daysToArrival <= 0) {
|
||||||
|
messages.push('Your shipment has arrived');
|
||||||
|
}
|
||||||
|
if (this.daysToArrival > 0 && this.daysToArrival <= 1) {
|
||||||
|
messages.push('Your shipment will arrive soon');
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import { INotifier } from '@/tracking/notifications/notifier.types';
|
||||||
|
|
||||||
|
export abstract class NotifierAbstract implements INotifier {
|
||||||
|
readonly isDelayed: boolean;
|
||||||
|
readonly daysToArrival: number;
|
||||||
|
constructor(shipment: ShipmentEntity, trackingInfo: TrackingDto) {
|
||||||
|
const delayedCutoffDate = new Date(shipment.arrivalDateOriginal);
|
||||||
|
delayedCutoffDate.setDate(delayedCutoffDate.getDate() + 1);
|
||||||
|
|
||||||
|
this.isDelayed = trackingInfo.arrivalDateEstimate > delayedCutoffDate;
|
||||||
|
this.daysToArrival =
|
||||||
|
(trackingInfo.arrivalDateEstimate.getTime() - Date.now()) / 86_400_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract send(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import {
|
||||||
|
SupportedNotifiers,
|
||||||
|
SupportedNotifiersNames,
|
||||||
|
} from '@/tracking/notifications/notifier.types';
|
||||||
|
|
||||||
|
export class NotifierFactory {
|
||||||
|
constructor(
|
||||||
|
private readonly shipment: ShipmentEntity,
|
||||||
|
private readonly trackingInfo: TrackingDto,
|
||||||
|
private readonly type: SupportedNotifiersNames = 'debug',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createNotifier(type?: SupportedNotifiersNames) {
|
||||||
|
type = type ?? this.type;
|
||||||
|
const notifier = SupportedNotifiers[type];
|
||||||
|
|
||||||
|
if (!notifier) {
|
||||||
|
throw new Error(`Notifier ${type} not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new notifier(this.shipment, this.trackingInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ConsoleNotifier } from '@/tracking/notifications/console.notifier';
|
||||||
|
|
||||||
|
export type ShipmentStatus =
|
||||||
|
| 'now'
|
||||||
|
| 'soon'
|
||||||
|
| 'delayed'
|
||||||
|
| 'delayed+now'
|
||||||
|
| 'delayed+soon';
|
||||||
|
|
||||||
|
export interface INotifier {
|
||||||
|
isDelayed: boolean;
|
||||||
|
daysToArrival: number;
|
||||||
|
send(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SupportedNotifiers = {
|
||||||
|
debug: ConsoleNotifier,
|
||||||
|
email: ConsoleNotifier,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SupportedNotifiersNames = keyof typeof SupportedNotifiers;
|
||||||
|
|
@ -1,18 +1,56 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { TrackingController } from './tracking.controller';
|
import { TrackingController } from './tracking.controller';
|
||||||
|
import { TrackingService } from '@/tracking/tracking.service';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
|
||||||
describe('TrackingController', () => {
|
describe('TrackingController', () => {
|
||||||
|
let trackingInfo: TrackingDto;
|
||||||
let controller: TrackingController;
|
let controller: TrackingController;
|
||||||
|
let trackingService: TrackingService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [TrackingController],
|
controllers: [TrackingController],
|
||||||
}).compile();
|
})
|
||||||
|
.useMocker((token) => {
|
||||||
|
if (token === TrackingService) {
|
||||||
|
return { track: jest.fn().mockResolvedValue(true) };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.compile();
|
||||||
|
|
||||||
controller = module.get<TrackingController>(TrackingController);
|
controller = module.get<TrackingController>(TrackingController);
|
||||||
|
trackingService = module.get<TrackingService>(TrackingService);
|
||||||
|
|
||||||
|
trackingInfo = {
|
||||||
|
carrierName: 'Evergreen',
|
||||||
|
destinationUnLoCode: 'CNNBO',
|
||||||
|
departureDate: new Date('2023-05-14T11:00:00.000Z'),
|
||||||
|
arrivalDateEstimate: new Date('2023-02-14T15:00:00.000Z'),
|
||||||
|
containerNumber: 'EGLU866139144',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have a method track', () => {
|
||||||
|
expect(controller.track).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an object with message: ok', async () => {
|
||||||
|
await expect(controller.track(trackingInfo)).resolves.toEqual({
|
||||||
|
message: 'Ok',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass on any exception thrown from tracking service', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(trackingService, 'track')
|
||||||
|
.mockRejectedValue(new Error('Test Error'));
|
||||||
|
|
||||||
|
await expect(controller.track(trackingInfo)).rejects.toThrow('Test Error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,42 @@
|
||||||
import { Controller } from '@nestjs/common';
|
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBody,
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiUnprocessableEntityResponse,
|
||||||
|
getSchemaPath,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import { TrackingService } from '@/tracking/tracking.service';
|
||||||
|
|
||||||
@Controller('tracking')
|
@Controller('tracking')
|
||||||
export class TrackingController {}
|
@ApiExtraModels(TrackingDto)
|
||||||
|
export class TrackingController {
|
||||||
|
constructor(private readonly trackingService: TrackingService) {}
|
||||||
|
@Post()
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(TrackingDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Processed',
|
||||||
|
})
|
||||||
|
@ApiUnprocessableEntityResponse({
|
||||||
|
description:
|
||||||
|
'The shipment has been found, but the shipment data does not match.',
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({
|
||||||
|
description: 'We could not find any shipment matching the request.',
|
||||||
|
})
|
||||||
|
@HttpCode(200)
|
||||||
|
track(@Body() body: TrackingDto): Promise<unknown> {
|
||||||
|
return this.trackingService
|
||||||
|
.track(body)
|
||||||
|
|
||||||
|
.then(() => ({
|
||||||
|
message: 'Ok',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,41 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TrackingController } from './tracking.controller';
|
import { TrackingController } from './tracking.controller';
|
||||||
import { TrackingService } from './tracking.service';
|
import { TrackingService } from './tracking.service';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
import { ShipmentsModule } from '@/shipments/shipments.module';
|
||||||
|
import { NotificationsService } from '@/tracking/notifications.service';
|
||||||
|
import { NotifierFactory } from '@/tracking/notifications/notifier.factory';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import {
|
||||||
|
INotifier,
|
||||||
|
SupportedNotifiersNames,
|
||||||
|
} from '@/tracking/notifications/notifier.types';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([ContainerEntity, ShipmentEntity]),
|
||||||
|
ShipmentsModule,
|
||||||
|
],
|
||||||
controllers: [TrackingController],
|
controllers: [TrackingController],
|
||||||
providers: [TrackingService],
|
providers: [
|
||||||
|
TrackingService,
|
||||||
|
NotificationsService,
|
||||||
|
{
|
||||||
|
provide: 'NOTIFIER_FACTORY',
|
||||||
|
useFactory(): (
|
||||||
|
shipment: ShipmentEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
type: SupportedNotifiersNames,
|
||||||
|
) => INotifier {
|
||||||
|
return (
|
||||||
|
shipment: ShipmentEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
type?: SupportedNotifiersNames,
|
||||||
|
) => new NotifierFactory(shipment, trackingInfo, type).createNotifier();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TrackingModule {}
|
export class TrackingModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,126 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { TrackingService } from './tracking.service';
|
import { TrackingService } from './tracking.service';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
|
import { ShipmentsService } from '@/shipments/shipments.service';
|
||||||
|
import { NotificationsService } from '@/tracking/notifications.service';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
describe('TrackingService', () => {
|
describe('TrackingService', () => {
|
||||||
let service: TrackingService;
|
const mockTrackingInfo: TrackingDto = {
|
||||||
|
carrierName: 'Evergreen',
|
||||||
|
destinationUnLoCode: 'NLAMS',
|
||||||
|
departureDate: new Date('2023-05-14T11:00:00.000Z'),
|
||||||
|
arrivalDateEstimate: new Date('2023-02-14T15:00:00.000Z'),
|
||||||
|
containerNumber: 'AAAA000000000',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockShipment: ShipmentEntity = {
|
||||||
|
id: uuid.NIL,
|
||||||
|
createdAt: new Date('2023-05-01T11:00:00.000Z'),
|
||||||
|
carrierName: 'Evergreen',
|
||||||
|
originPort: 'NLAMS',
|
||||||
|
destinationPort: 'NLAMS',
|
||||||
|
departureDate: new Date('2023-05-14T11:00:00.000Z'),
|
||||||
|
arrivalDateOriginal: new Date('2023-02-14T15:00:00.000Z'),
|
||||||
|
arrivalDateEstimate: new Date('2023-02-14T15:00:00.000Z'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockContainerEntity: ContainerEntity = {
|
||||||
|
id: uuid.NIL,
|
||||||
|
shipmentId: uuid.NIL,
|
||||||
|
containerNumber: 'AAAA000000000',
|
||||||
|
shipment: mockShipment,
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerEntityToken = getRepositoryToken(ContainerEntity);
|
||||||
|
let trackingService: TrackingService;
|
||||||
|
let shipmentsService: ShipmentsService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let containerEntityRepository: Repository<ContainerEntity>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [TrackingService],
|
providers: [TrackingService],
|
||||||
}).compile();
|
})
|
||||||
|
.useMocker((token) => {
|
||||||
|
switch (token) {
|
||||||
|
case containerEntityToken:
|
||||||
|
return {
|
||||||
|
findOneOrFail: jest.fn().mockResolvedValue(mockContainerEntity),
|
||||||
|
};
|
||||||
|
case ShipmentsService:
|
||||||
|
return {
|
||||||
|
update: jest.fn().mockResolvedValue(mockShipment),
|
||||||
|
};
|
||||||
|
case NotificationsService:
|
||||||
|
return {
|
||||||
|
send: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
service = module.get<TrackingService>(TrackingService);
|
trackingService = module.get<TrackingService>(TrackingService);
|
||||||
|
shipmentsService = module.get<ShipmentsService>(ShipmentsService);
|
||||||
|
notificationsService =
|
||||||
|
module.get<NotificationsService>(NotificationsService);
|
||||||
|
containerEntityRepository =
|
||||||
|
module.get<Repository<ContainerEntity>>(containerEntityToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(trackingService).toBeDefined();
|
||||||
|
expect(trackingService.track).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a track method that returns true', async () => {
|
||||||
|
await expect(trackingService.track(mockTrackingInfo)).resolves.toBe(true);
|
||||||
|
expect(containerEntityRepository.findOneOrFail).toHaveBeenCalledWith({
|
||||||
|
relations: ['shipment'],
|
||||||
|
where: {
|
||||||
|
containerNumber: mockTrackingInfo.containerNumber,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(shipmentsService.update).toHaveBeenCalledWith({
|
||||||
|
...mockShipment,
|
||||||
|
arrivalDateEstimate: mockTrackingInfo.arrivalDateEstimate,
|
||||||
|
departureDate: mockTrackingInfo.departureDate,
|
||||||
|
});
|
||||||
|
expect(notificationsService.send).toHaveBeenCalledWith(
|
||||||
|
mockShipment,
|
||||||
|
mockTrackingInfo,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a track method that validates the tracking information with the returned container information', async () => {
|
||||||
|
const mockShipmentNotFound = {
|
||||||
|
...mockContainerEntity,
|
||||||
|
shipment: undefined,
|
||||||
|
};
|
||||||
|
const mockShipmentCarrierNameMismatch = {
|
||||||
|
...mockContainerEntity,
|
||||||
|
shipment: { ...mockShipment, carrierName: 'incorrect' },
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(containerEntityRepository, 'findOneOrFail')
|
||||||
|
.mockResolvedValueOnce(mockShipmentNotFound);
|
||||||
|
await expect(trackingService.track(mockTrackingInfo)).rejects.toThrow(
|
||||||
|
'Shipment not found',
|
||||||
|
);
|
||||||
|
expect(notificationsService.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(shipmentsService.update).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(containerEntityRepository, 'findOneOrFail')
|
||||||
|
.mockResolvedValueOnce(mockShipmentCarrierNameMismatch);
|
||||||
|
await expect(trackingService.track(mockTrackingInfo)).rejects.toThrow(
|
||||||
|
'CarrierName does not match',
|
||||||
|
);
|
||||||
|
expect(notificationsService.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(shipmentsService.update).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,68 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
UnprocessableEntityException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
|
import { TrackingDto } from '@/dtos/tracking.dto';
|
||||||
|
import { ShipmentsService } from '@/shipments/shipments.service';
|
||||||
|
import { NotificationsService } from '@/tracking/notifications.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackingService {}
|
export class TrackingService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ContainerEntity)
|
||||||
|
private readonly containerRepository: Repository<ContainerEntity>,
|
||||||
|
private readonly shipmentsService: ShipmentsService,
|
||||||
|
private readonly notificationsService: NotificationsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async track(trackingInfo: TrackingDto) {
|
||||||
|
const container = await this.containerRepository.findOneOrFail({
|
||||||
|
relations: ['shipment'],
|
||||||
|
where: {
|
||||||
|
containerNumber: trackingInfo.containerNumber,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipment = this.validate(container, trackingInfo);
|
||||||
|
|
||||||
|
await this.updateShipment(shipment, trackingInfo);
|
||||||
|
|
||||||
|
await this.notificationsService.send(shipment, trackingInfo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validate(
|
||||||
|
{ shipment }: ContainerEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
): ShipmentEntity {
|
||||||
|
if (!shipment) {
|
||||||
|
throw new NotFoundException('Shipment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shipment.carrierName !== trackingInfo.carrierName) {
|
||||||
|
throw new UnprocessableEntityException('CarrierName does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
return shipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateShipment(
|
||||||
|
shipment: ShipmentEntity,
|
||||||
|
trackingInfo: TrackingDto,
|
||||||
|
) {
|
||||||
|
const updatedShipment: ShipmentEntity = {
|
||||||
|
...shipment,
|
||||||
|
arrivalDateEstimate: trackingInfo.arrivalDateEstimate,
|
||||||
|
departureDate: trackingInfo.departureDate,
|
||||||
|
};
|
||||||
|
await this.shipmentsService.update(updatedShipment);
|
||||||
|
|
||||||
|
return updatedShipment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { NIL, parse } from 'uuid';
|
import { NIL } from 'uuid';
|
||||||
|
|
||||||
// custom implementation of stringify, as provided uuid's are not valid according to RFC4122
|
// custom implementation of stringify, as provided uuid's are not valid according to RFC4122
|
||||||
const stringify = (buffer: Buffer | null): string => {
|
const stringify = (buffer: Buffer | null): string => {
|
||||||
|
|
@ -16,9 +16,20 @@ const stringify = (buffer: Buffer | null): string => {
|
||||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseUuid = (uuid: string): Buffer => {
|
||||||
|
const uuidTester =
|
||||||
|
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
||||||
|
|
||||||
|
if (!uuidTester.test(uuid)) {
|
||||||
|
throw new Error('Invalid UUID');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(uuid.replaceAll('-', ''), 'hex');
|
||||||
|
};
|
||||||
|
|
||||||
export const uuidTransformer = {
|
export const uuidTransformer = {
|
||||||
to(uuid: string): Buffer {
|
to(uuid: string): Buffer {
|
||||||
return Buffer.from(parse(uuid));
|
return parseUuid(uuid);
|
||||||
},
|
},
|
||||||
from(bin: Buffer | null): string {
|
from(bin: Buffer | null): string {
|
||||||
return stringify(bin);
|
return stringify(bin);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
POST http://localhost:3000/tracking
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"carrierName": "Evergreen",
|
||||||
|
"destinationUnLoCode": "CNNBO",
|
||||||
|
"departureDate": "2023-05-14T11:00:00.000Z",
|
||||||
|
"arrivalDateEstimate": "2023-02-14T15:00:00.000Z",
|
||||||
|
"containerNumber": "EGLU866139144"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue