Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SAH-37]: Matching Algorithm #150

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
Draft
9 changes: 7 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "tsc --project ./tsconfig.json",
"build": "tsc --project tsconfig.json",
"clean": "rm -r dist",
"dev": "nodemon src/server.ts --experimental-global-webcrypto",
"dev": "nodemon src/server.ts",
"format": "",
"work": "ts-node ./src/workers.ts",
"work:order": "ts-node ./src/orders/worker.ts"
Expand All @@ -24,8 +24,10 @@
"graphql-request": "^6.1.0",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"js-combinatorics": "^2.1.2",
"jsonwebtoken": "^9.0.2",
"kysely": "^0.27.3",
"libphonenumber-js": "^1.10.60",
"morgan": "^1.10.0",
"nodemon": "^3.0.2",
"oslo": "^1.1.1",
Expand All @@ -40,10 +42,13 @@
"@types/concurrently": "^7.0.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/js-combinatorics": "^1.2.0",
"@types/node": "^20.10.5",
"@types/pg": "^8.11.5",
"chalk": "^5.3.0",
"concurrently": "^8.2.2",
"otplib": "^12.0.1",
"qrcode": "^1.5.3",
"typescript": "^5.3.3"
}
}
2 changes: 2 additions & 0 deletions apps/api/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cors from "cors";
// import compression from "compression";
import * as helmet from "helmet";
import { catchErrors } from "./middleware/errors";
import { logRequest } from "./middleware/requestLogger";

export function init(app: any) {
app.use(cors());
Expand All @@ -16,5 +17,6 @@ export function init(app: any) {
// app.use(helmet());
// app.use(morganLogger("dev"));
app.use(routers);
app.use(logRequest);
app.use(catchErrors);
}
9 changes: 9 additions & 0 deletions apps/api/src/matcher/sahilScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const calculateSahilScore = (pair: any) => {
const totalQuantity = pair[0].quantity + pair[1].quantity;
const totalDistance = pair[0].distance + pair[1].distance;
const totalPricePerUnit = pair[0].pricePerUnit + pair[1].pricePerUnit;
const distanceCost = totalDistance * 0.5;
const unitCost = totalQuantity * totalPricePerUnit;
return distanceCost + unitCost;
};

6 changes: 6 additions & 0 deletions apps/api/src/matcher/setGeofence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { client } from "../lib/graphql-request";


export const setGeofence = async (lat: number, lng: number) => {
// const coords = client.request();
}
42 changes: 42 additions & 0 deletions apps/api/src/matcher/sumOrderItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const sumOrderItems = (suppliersArray: any, target: number) => {
const map = new Map();
const pairs = [];

for (let i = 0; i < suppliersArray.length; i++) {
const complement = target - suppliersArray[i].quantity;

if (map.has(complement)) {
// If the complement is found, add all pairs that include the current supplier
const indices = map.get(complement);
for (const element of indices) {
pairs.push([suppliersArray[element], suppliersArray[i]]);
}
}

// Update the map with the current supplier's quantity and its index
if (map.has(suppliersArray[i].quantity)) {
map.get(suppliersArray[i].quantity).push(i);
} else {
map.set(suppliersArray[i].quantity, [i]);
}
}
return pairs;
};

export const calculateSahilScore = (pair: any) => {
const totalQuantity = pair[0].quantity + pair[1].quantity;
const totalDistance = pair[0].distance + pair[1].distance;
const totalPricePerUnit = pair[0].pricePerUnit + pair[1].pricePerUnit;
const distanceCost = totalDistance * 0.5;
const unitCost = totalQuantity * totalPricePerUnit;
return distanceCost + unitCost;
};


// Calculate total cost for each pair and rank them
export const rankedSuppliersBasedOnScore = (suppliers: any) => {
return suppliers.map((supplier: any) => ({
supplier,
totalCost: calculateSahilScore(supplier)
})).sort((a: any, b: any) => a.totalCost - b.totalCost);
}
34 changes: 34 additions & 0 deletions apps/api/src/matcher/supplierCombinations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Combination } from 'js-combinatorics';

// we could rank the combinations based on the sum of their quantities
// modulus the number of qunatity to get the combination size
export const calculateCombinationSize = (candidates: { name: string, quantity: number }[], targetQuantity: number) : number => {
let remainingQuantity = targetQuantity;
let combinationSize = 0;
for (const candidate of candidates) {
if (remainingQuantity <= 0) {
break;
}
if (candidate.quantity >= remainingQuantity) {
combinationSize++;
break;
} else {
combinationSize++;
remainingQuantity -= candidate.quantity;
}
}
return combinationSize;
}

export const generateCombinations = async (suppliers: { name: string; id: string; quantity: number; }[], combinationSize: number = 2) => {
const combinations = new Combination(suppliers, combinationSize);
return [...combinations];
}

export const findBestCombination = (suppliers: any, target: number) => {
const combinations = generateCombinations(suppliers, target);
let bestCombination: never[] = [];
let bestCombinationSum = 0;

return bestCombination;
}
23 changes: 9 additions & 14 deletions apps/api/src/orders/operations/initOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,19 @@ export const orderSchema = z
.refine((value) => value > new Date())
.optional(),
customerId: z.any(),
destination: z.string(),
origin: z.string(),
processedBy: z.string().optional(),
})
.refine(
({ destination, origin }) => {
return origin !== destination;
},
{ message: "Destination is the same as origin" }
);
orderItems: z.any()
});

export type OrderAttributes = z.infer<typeof orderSchema>;

export const initOrder = async (order: OrderAttributes): Promise<unknown> => {
const data = await client.request(INSERT_NEW_ORDER, {
object: {
...order,
},
});
return Promise.resolve(data);
// const data = await client.request(INSERT_NEW_ORDER, {
// object: {
// ...order,
// },
// });
// return Promise.resolve(data);
return Promise.resolve(order);
};
25 changes: 25 additions & 0 deletions apps/api/src/orders/operations/updateOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { UPDATE_ORDER_STATUS, FETCH_ORDER_BY_PK } from "@sahil/lib/graphql";
import { z } from "zod";
import { client } from "../../lib/graphql-request";

export const updateOrder = async (orderId: string, data = {
status: "IN-PROGRESS"
}) => {
// check if the order exists
const order = await client.request(FETCH_ORDER_BY_PK, {
id: orderId
});

if(!order) {
throw new Error("Order Not Found");
}

const updatedOrder = await client.request(UPDATE_ORDER_STATUS, {
object: {
...order,
status: "IN_PROGRESS"
}
});

return updateOrder;
}
26 changes: 24 additions & 2 deletions apps/api/src/orders/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextFunction, Response, Router, Request } from "express";
import { pushIntoOrders } from "../enqueue";
import { logRequest } from "../middleware/requestLogger";
import { validate } from "../middleware/zodValidate";
import { updateOrder } from "./operations/updateOrder";

import {
initOrder,
Expand All @@ -16,14 +17,35 @@ ordersRouter.use(logRequest);

ordersRouter.post(
"/",
validate(orderSchema),

async (req: Request, res: Response, next: NextFunction) => {
try {
const order = await initOrder(req.body);
// push into Queue
await pushIntoOrders(req.body);
res.status(201).send({
order: {
name: "hello"
}
});
} catch (error) {
next(error);
}
}
);

ordersRouter.put(
"/:orderId",

async (req: Request, res: Response, next: NextFunction) => {
try {
const order = await initOrder(req.body);
// push into Queue
await pushIntoOrders(req.body);
res.status(201).send({
order,
order: {
name: "hello"
}
});
} catch (error) {
next(error);
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/orders/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { Worker } from "bullmq";
import { logger } from "../lib/winston";
import { connection } from "../lib/ioredis";
import { Queues } from "../queue";
import { updateOrder } from "./operations/updateOrder";

const worker = new Worker(
Queues.Order,
async (job) => {
console.log("yerrrrr", job.data);
const updatedOrder = await updateOrder(job.data.id);
logger.info("Processing Job", {
world: "hello 0",
});
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connection } from "./lib/ioredis";
import { redisHost, redisPort } from "./config";

export enum Queues {
Auth = "Auth",
Auth = "Auth",
Client = "Client",
Event = "Event",
Mail = "Mail",
Expand Down
77 changes: 77 additions & 0 deletions apps/api/src/suppliers/operations/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,83 @@
import { db } from "../../lib/kysley/databse";
import {
findBestCombination,
generateCombinations,
} from "../../matcher/supplierCombinations";

import { rankedSuppliersBasedOnScore } from "../../matcher/sumOrderItems";

import { sumOrderItems } from "../../matcher/sumOrderItems";

const candidates = [
{
name: "Emmanuel Office Solutions",
id: "514f5a78-9f4c-4cde-b6c0-5f7649a28d60",
quantity: 9,
pricePerUnit: 0.95,
distance: 144
},
{
name: "Twins Construction",
id: "627460d3-7b29-4bc6-b9ff-3958d6d02fe4",
quantity: 14,
pricePerUnit: 0.9,
distance: 120
},
{
name: "Khan Agricultural",
id: "403140dc-1d5d-435f-ba3d-ccc65abb1e5f",
quantity: 7,
pricePerUnit: 0.8,
distance: 111
},
{
name: "Energi Dealers",
id: "6f911d9b-580b-4283-bc8b-fbd381d298be",
quantity: 6,
pricePerUnit: 1,
distance: 97
},
{
name: "Brown Safety Solutions",
id: "0c8c5d87-b0a3-4409-95ea-d1134e1d72fd",
quantity: 12,
pricePerUnit: 0.85,
distance: 123
},
{
name: "Ozone Supermarket",
id: "b5f93ced-ba1f-42cb-bb96-e0d388bb111d",
quantity: 8,
pricePerUnit: 0.95,
distance: 109
},
{
name: "Mtn Momo",
id: "64f3bac9-12f7-4821-bd5c-4c48e5afe4fb",
quantity: 10,
pricePerUnit: 0.9,
distance: 119
},
];


export const listRecommendedSuppliers = async () => {
const combinations = await generateCombinations(candidates, 20);
// Print the combinations
console.log("Combinations of suppliers that add up to 20kgs:", combinations);


const pairs = sumOrderItems(candidates, 20);

// console.log("pairs", pairs);

const rankedSuppliers = rankedSuppliersBasedOnScore(pairs);

console.log("rankedSuppliers", rankedSuppliers[0]);
console.log("*****************");
console.log("rankedSuppliers", rankedSuppliers[1]);


let query = db.selectFrom("suppliers");
const result = await query.selectAll().execute();
return result;
Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/suppliers/operations/supplierProductInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { db } from "../../lib/kysley/databse";
// given a product label and category, this function returns the supplier as well as the product information
// for instance, suppliers could have milk as a label and diary as a category but a unique brand name for the product which
// we need to get the exact product from each supplier for comparison
export const supplierProductInfo = async (supplierId: string, product: any) => {
// Construct the query
const query = db
.selectFrom("products");
// .where("products.supplier_id", "=", supplierId)
// .andWhere("products.category", "=", product.category)
// .andWhere("products.label", "=", product.label)

// Execute the query
const result = await query.execute();


return result;
};
Loading
Loading