Skip to content

Commit

Permalink
feat(shipping): add calculate service and controller
Browse files Browse the repository at this point in the history
feat(shipping): add create controller
feat(shipping): remove get controller
feat(shipping): add rates controller
feat(shipping): add calculate-cost service
feat(shipping): add calculate-cost tests
feat(shipping): add calculate service
feat(shipping): add calculate tests

The changes in this commit include:
- Added a new calculate service and controller for shipping calculations.
- Added a new create controller for shipping.
- Removed the get controller for shipping.
- Added a new rates controller for shipping.
- Added a new calculate-cost service for shipping calculations.
- Added tests for the calculate-cost service.
- Added a new calculate service for shipping calculations.
- Added tests for the calculate service.

feat(shipping): add create service and tests
feat(shipping): add get service and tests
feat(shipping): add rates service and tests
feat(shipping): add shipping model

feat(shipping): add shipping schema and types

- Add shipping.schema.ts file to define the mongoose schema for the shipping model.
- Define the IShipping and IShippingDocument interfaces to represent the shape of the shipping document in the database.
- Create the ShippingMongooseSchema using mongoose.Schema to define the fields and their types for the shipping model.
- Add the necessary fields and their types to the ShippingMongooseSchema.
- Add timestamps to the ShippingMongooseSchema to automatically track the creation and update timestamps.

refactor(shipping): remove shipping service and types

- Remove the shipping.service.ts file as it is no longer needed.
- Remove the shipping.types.ts file as it is no longer needed.
  • Loading branch information
jamalsoueidan committed Nov 2, 2023
1 parent 1667b9c commit de590ce
Show file tree
Hide file tree
Showing 18 changed files with 679 additions and 275 deletions.
14 changes: 7 additions & 7 deletions src/functions/shipping/controllers/calculate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { _ } from "~/library/handler";

import { z } from "zod";
import { StringOrObjectIdType } from "~/library/zod";
import { ShippingServiceCalculate } from "../shipping.service";
import { ShippingServiceCalculate } from "../services/calculate";
import { ShippingZodSchema } from "../shipping.types";

export type ShippingControllerCalculateRequest = {
body: z.infer<typeof ShippingControllerCalculateSchema>;
};

export const ShippingControllerCalculateSchema = z.object({
locationId: StringOrObjectIdType,
destination: z.object({
fullAddress: z.string(),
}),
});
export const ShippingControllerCalculateSchema = z
.object({
locationId: StringOrObjectIdType,
})
.merge(ShippingZodSchema.pick({ destination: true }));

export type ShippingControllerCalculateResponse = Awaited<
ReturnType<typeof ShippingServiceCalculate>
Expand Down
28 changes: 28 additions & 0 deletions src/functions/shipping/controllers/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { _ } from "~/library/handler";

import { z } from "zod";
import { NumberOrStringType, StringOrObjectIdType } from "~/library/zod";
import { ShippingServiceCreate } from "../services/create";
import { ShippingZodSchema } from "../shipping.types";

export type ShippingControllerCreateRequest = {
body: z.infer<typeof ShippingControllerCreateSchema>;
};

export const ShippingControllerCreateSchema = z
.object({
locationId: StringOrObjectIdType,
customerId: NumberOrStringType,
})
.merge(ShippingZodSchema.pick({ destination: true }));

export type ShippingControllerCreateResponse = Awaited<
ReturnType<typeof ShippingServiceCreate>
>;

export const ShippingControllerCreate = _(
async ({ body }: ShippingControllerCreateRequest) => {
const validateData = ShippingControllerCreateSchema.parse(body);
return ShippingServiceCreate(validateData);
}
);
18 changes: 0 additions & 18 deletions src/functions/shipping/controllers/get.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/functions/shipping/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./calculate";
export * from "./get";
export * from "./create";
export * from "./rates";
17 changes: 17 additions & 0 deletions src/functions/shipping/controllers/rates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { _ } from "~/library/handler";

import { ShippingBody, ShippingServiceRates } from "../services/rates";

export type ShippingControllerRatesRequest = {
body: ShippingBody;
};

export type ShippingControllerRatesResponse = Awaited<
ReturnType<typeof ShippingServiceRates>
>;

export const ShippingControllerRates = _(
async ({ body }: ShippingControllerRatesRequest) => {
return ShippingServiceRates(body);
}
);
60 changes: 60 additions & 0 deletions src/functions/shipping/services/calculate-cost.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { faker } from "@faker-js/faker";
import { getLocationObject } from "~/library/jest/helpers/location";
import { Shipping } from "../shipping.types";
import { ShippingServiceCalculateCost } from "./calculate-cost";

require("~/library/jest/mongoose/mongodb.jest");

jest.mock("~/functions/location", () => ({
LocationServiceLookup: jest.fn(),
}));

describe("ShippingServiceCalculateCost", () => {
it("should correctly calculate the cost", () => {
const shipping: Omit<Shipping, "_id" | "cost" | "location"> = {
origin: getLocationObject({
distanceHourlyRate: 100,
fixedRatePerKm: 20,
distanceForFree: 5,
startFee: 0,
}),
destination: {
name: faker.person.firstName(),
fullAddress: faker.location.streetAddress(),
},
duration: {
text: "1 hour",
value: 60,
},
distance: { text: "5.3 km", value: 5.3 },
};

const actualCost = ShippingServiceCalculateCost(shipping);

expect(actualCost).toEqual(106);
});

it("should correctly calculate the cost with start fee", () => {
const shipping: Omit<Shipping, "_id" | "cost" | "location"> = {
origin: getLocationObject({
distanceHourlyRate: 100,
fixedRatePerKm: 20,
distanceForFree: 5,
startFee: 400,
}),
destination: {
name: faker.person.firstName(),
fullAddress: faker.location.streetAddress(),
},
duration: {
text: "1 hour",
value: 60,
},
distance: { text: "5.3 km", value: 5.3 },
};

const actualCost = ShippingServiceCalculateCost(shipping);

expect(actualCost).toEqual(506);
});
});
25 changes: 25 additions & 0 deletions src/functions/shipping/services/calculate-cost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Shipping } from "../shipping.types";

export const ShippingServiceCalculateCost = ({
duration: { value: duration },
distance: { value: distance },
origin,
}: Omit<Shipping, "_id" | "location" | "destination" | "cost">) => {
const { distanceForFree, fixedRatePerKm, distanceHourlyRate, startFee } =
origin;

// Calculate the chargeable distance.
const chargeableDistance = Math.max(0, distance - distanceForFree);

// Calculate the cost for the distance.
const distanceCost = chargeableDistance * fixedRatePerKm;

// Calculate the cost for the duration.
const durationInHours = duration / 60;
const durationCost = durationInHours * distanceHourlyRate;

// Total cost is the sum of the cost for the distance, the duration, and start fee.
const totalCost = distanceCost + durationCost + startFee;

return Math.round(totalCost);
};
123 changes: 123 additions & 0 deletions src/functions/shipping/services/calculate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { LocationServiceGet } from "~/functions/location/services/get";
import { LocationServiceGetTravelTime } from "~/functions/location/services/get-travel-time";
import { createLocation } from "~/library/jest/helpers/location";
import { ShippingServiceCalculate } from "./calculate";

require("~/library/jest/mongoose/mongodb.jest");

jest.mock("~/functions/location/services/get-travel-time", () => ({
LocationServiceGetTravelTime: jest.fn(),
}));

jest.mock("~/functions/location/services/get", () => ({
LocationServiceGet: jest.fn(),
}));

describe("ShippingServiceCalculate", () => {
it("should calculate destination from locationId", async () => {
const location = await createLocation({
locationType: "destination" as any,
customerId: 1,
distanceHourlyRate: 200,
fixedRatePerKm: 2,
distanceForFree: 0,
});

(LocationServiceGet as jest.Mock).mockResolvedValue(location);

(LocationServiceGetTravelTime as jest.Mock).mockResolvedValue({
duration: {
text: "120 min",
value: 120,
},
distance: {
text: "100 km",
value: 100,
},
} as Awaited<ReturnType<typeof LocationServiceGetTravelTime>>);

const response = await ShippingServiceCalculate({
locationId: location._id,
destination: {
name: "hotel a",
fullAddress: "Dortesvej 17 1 th",
},
});

expect(response).toEqual({
duration: { text: "120 min", value: 120 },
distance: { text: "100 km", value: 100 },
cost: { value: 600, currency: "DKK" },
});
});

it("should throw error since maxDriveDistance 50, and distance is 100", async () => {
const location = await createLocation({
locationType: "destination" as any,
customerId: 1,
distanceHourlyRate: 200,
fixedRatePerKm: 2,
minDriveDistance: 20,
maxDriveDistance: 50,
distanceForFree: 0,
});

(LocationServiceGet as jest.Mock).mockResolvedValue(location);

(LocationServiceGetTravelTime as jest.Mock).mockResolvedValue({
duration: {
text: "120 min",
value: 120,
},
distance: {
text: "100 km",
value: 100,
},
} as Awaited<ReturnType<typeof LocationServiceGetTravelTime>>);

await expect(
ShippingServiceCalculate({
locationId: location._id,
destination: {
name: "hotel c",
fullAddress: "Dortesvej 17 1 th",
},
})
).rejects.toThrowError();
});

it("should throw error since minDriveDistance is 40, and distance is 30", async () => {
const location = await createLocation({
locationType: "destination" as any,
customerId: 1,
distanceHourlyRate: 200,
fixedRatePerKm: 2,
minDriveDistance: 40,
maxDriveDistance: 500,
distanceForFree: 0,
});

(LocationServiceGet as jest.Mock).mockResolvedValue(location);

(LocationServiceGetTravelTime as jest.Mock).mockResolvedValue({
duration: {
text: "120 min",
value: 120,
},
distance: {
text: "100 km",
value: 30,
},
} as Awaited<ReturnType<typeof LocationServiceGetTravelTime>>);

await expect(
ShippingServiceCalculate({
locationId: location._id,
destination: {
name: "hotel c",
fullAddress: "Dortesvej 17 1 th",
},
})
).rejects.toThrowError();
});
});
63 changes: 63 additions & 0 deletions src/functions/shipping/services/calculate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
LocationServiceGet,
LocationServiceGetProps,
} from "~/functions/location/services/get";
import { LocationServiceGetTravelTime } from "~/functions/location/services/get-travel-time";
import { NotFoundError } from "~/library/handler";
import { Shipping } from "../shipping.types";
import { ShippingServiceCalculateCost } from "./calculate-cost";

export const ShippingServiceCalculate = async (
props: Required<LocationServiceGetProps & Pick<Shipping, "destination">>
) => {
const location = await LocationServiceGet(props);
const { destination } = props;

if (!destination) {
throw new NotFoundError([
{
code: "custom",
message: "DESTINATION_MISSING",
path: ["destination"],
},
]);
}

const travelTime = await LocationServiceGetTravelTime({
origin: location.fullAddress,
destination: destination.fullAddress,
});

if (location.minDriveDistance > travelTime.distance.value) {
throw new NotFoundError([
{
code: "custom",
message: "MIN_DRIVE_DISTANCE_EXCEEDED",
path: ["minDriveDistance"],
},
]);
}

if (location.maxDriveDistance < travelTime.distance.value) {
throw new NotFoundError([
{
code: "custom",
message: "MAX_DRIVE_DISTANCE_EXCEEDED",
path: ["maxDriveDistance"],
},
]);
}

const cost = ShippingServiceCalculateCost({
...travelTime,
origin: location,
});

return {
...travelTime,
cost: {
value: cost,
currency: "DKK",
},
};
};
Loading

0 comments on commit de590ce

Please sign in to comment.