import axios from "axios"; import { addHours } from "date-fns"; import { formatInTimeZone } from "date-fns-tz"; import { sql } from "drizzle-orm"; import { db } from "../db/db.controller.js"; import { opendockApt } from "../db/schema/opendock.schema.js"; import { createLogger } from "../logger/logger.controller.js"; import { prodQuery } from "../prodSql/prodSqlQuery.controller.js"; import { type SqlQuery, sqlQuerySelector, } from "../prodSql/prodSqlQuerySelector.utils.js"; import { createCronJob } from "../utils/croner.utils.js"; import { delay } from "../utils/delay.utils.js"; import { returnFunc } from "../utils/returnHelper.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js"; let lastCheck = formatInTimeZone( new Date().toISOString(), "America/New_York", "yyyy-MM-dd HH:mm:ss", ); //const queue: unknown[] = []; //const isProcessing: boolean = false; type Releases = { ReleaseNumber: number; DeliveryState: number; DeliveryDate: Date; LineItemHumanReadableId: number; ArticleAlias: string; LoadingUnits: string; Quantity: number; LineItemArticleWeight: number; CustomerReleaseNumber: string; }; type ODToken = { odToken: string | null; tokenDate: Date | null; }; let odToken: ODToken = { odToken: null, tokenDate: new Date(), }; const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; const log = createLogger({ module: "opendock", subModule: "releaseMonitor" }); const postRelease = async (release: Releases) => { if (!odToken.odToken) { log.info("Getting Auth Token"); await getToken(); } if ( new Date(odToken.tokenDate || Date.now()).getTime() < Date.now() - TWENTY_FOUR_HOURS ) { log.info("Refreshing Auth Token"); await getToken(); } /** * ReleaseState * 0 = open * 1 = planned * 2 = CustomCanceled * 4 = internally canceled */ /** * DeliveryState * 0 = open * 1 = inprogress * 2 = loading * 3 = partly shipped * 4 = delivered */ const newDockApt = { status: release.DeliveryState === 0 || release.DeliveryState === 1 ? "Scheduled" : release.DeliveryState === 2 ? "InProgress" : release.DeliveryState === 3 // this will consider finished and if a correction needs made to the bol we need to cancel and reactivate the order ? "Completed" : release.DeliveryState === 4 && "Completed", userId: "2629b4f6-0003-472d-8b26-66a69ce5ac50", // this should be the carrierid loadTypeId: "0aa7988e-b17b-4f10-acdd-3d029b44a773", // well get this and make it a default one dockId: "00ba4386-ce5a-4dd1-9356-6e6d10a24609", // this the warehouse we want it in to start out refNumbers: [release.ReleaseNumber], refNumber: release.ReleaseNumber, start: release.DeliveryDate, end: addHours(release.DeliveryDate, 1), notes: "", ccEmails: [""], muteNotifications: true, metadata: { externalValidationFailed: false, externalValidationErrorMessage: null, }, units: null, customFields: [ { name: "strArticle", type: "str", label: "Article", value: `${release.LineItemHumanReadableId} - ${release.ArticleAlias}`, description: "What bottle are we sending ", placeholder: "", dropDownValues: [], minLengthOrValue: 1, hiddenFromCarrier: false, requiredForCarrier: false, requiredForWarehouse: false, }, { name: "intPallet Count", type: "int", label: "Pallet Count", value: parseInt(release.LoadingUnits, 10), description: "How many pallets", placeholder: "22", dropDownValues: [], minLengthOrValue: 1, hiddenFromCarrier: false, requiredForCarrier: false, requiredForWarehouse: false, }, { name: "strTotal Weight", type: "str", label: "Total Weight", value: `${(((release.Quantity * release.LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`, description: "What is the total weight of the load", placeholder: "", dropDownValues: [], minLengthOrValue: 1, hiddenFromCarrier: false, requiredForCarrier: false, requiredForWarehouse: false, }, { name: "strCustomer ReleaseNumber", type: "str", label: "Customer Release Number", value: `${release.CustomerReleaseNumber}`, description: "What is the customer release number", placeholder: "", dropDownValues: [], minLengthOrValue: 1, hiddenFromCarrier: false, requiredForCarrier: false, requiredForWarehouse: false, }, ], }; // TODO: pull the current added releases from the db and if one matches then we want to get its id and run the update vs create const { data: apt, error: aptError } = await tryCatch( db.select().from(opendockApt), ); if (aptError) { log.error({ error: aptError }, "Error getting apt data"); // TODO: send an error email on this one as it will cause issues return; } const releaseCheck = apt.filter((r) => r.release === release.ReleaseNumber); //console.log(releaseCheck); if (releaseCheck.length > 0) { const id = releaseCheck[0]?.openDockAptId; try { const response = await axios.patch( `${process.env.OPENDOCK_URL}/appointment/${id}`, newDockApt, { headers: { "content-type": "application/json; charset=utf-8", Authorization: `Bearer ${odToken.odToken}`, }, }, ); if (response.status === 400) { log.error({}, response.data.data.message); return; } // update the release in the db leaving as insert just incase something weird happened try { await db .insert(opendockApt) .values({ release: release.ReleaseNumber, openDockAptId: response.data.data.id, appointment: response.data.data, }) .onConflictDoUpdate({ target: opendockApt.release, set: { appointment: response.data.data, upd_date: sql`NOW()` }, }) .returning(); log.info(`${release.ReleaseNumber} was updated`); } catch (e) { log.error(e); } } catch (e: any) { //console.info(newDockApt); log.error(e.response.data); return; } } else { try { const response = await axios.post( `${process.env.OPENDOCK_URL}/appointment`, newDockApt, { headers: { "content-type": "application/json; charset=utf-8", Authorization: `Bearer ${odToken}`, }, }, ); // we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it. // this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages. if (response.status === 400) { log.error(response.data.data.message); return; } // the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here. // TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things //console.info(response.data.data, "Was Created"); try { await db .insert(opendockApt) .values({ release: release.ReleaseNumber, openDockAptId: response.data.data.id, appointment: response.data.data, }) .onConflictDoUpdate({ target: opendockApt.id, set: { appointment: response.data.data, upd_date: sql`NOW()` }, }) .returning(); log.info(`${release.ReleaseNumber} was created`); } catch (e) { log.error(e); } } catch (e: any) { log.error(e.response.data); return; } } await delay(500); // rate limit protection }; export const monitorReleaseChanges = async () => { // TODO: validate if the setting for opendocks is active and start / stop the system based on this // if it changes we set to false and the next loop will stop. const openDockMonitor = true; // console.info("Starting release monitor", lastCheck); const sqlQuery = sqlQuerySelector(`releaseChecks`) as SqlQuery; if (!sqlQuery.success) { return returnFunc({ success: false, level: "error", module: "datamart", subModule: "query", message: `Error getting releaseChecks info`, data: [sqlQuery.message], notify: false, }); } if (openDockMonitor) { createCronJob("open-dock-monitor", "*/15 * * * * *", async () => { try { const result = await prodQuery( sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`), "Get release info", ); if (result.data.length) { for (const release of result.data) { await postRelease(release); lastCheck = formatInTimeZone( new Date(release.Upd_Date).toISOString(), "UTC", "yyyy-MM-dd HH:mm:ss", ); await delay(500); } } } catch (e) { console.error(e); log.error({ error: e }, "Monitor error"); } }); } // run the main game loop // while (openDockSetting) { // try { // const result = await prodQuery( // sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`), // "Get release info", // ); // if (result.data.length) { // for (const release of result.data) { // // potentially move this to a buffer table to easy up on memory // await postRelease(release); // // Move checkpoint AFTER successful post // lastCheck = formatInTimeZone( // new Date(release.Upd_Date).toISOString(), // "UTC", // "yyyy-MM-dd HH:mm:ss", // ); // await delay(500); // } // } // } catch (e) { // console.error("Monitor error:", e); // } // await delay(15 * 1000); // making this 15 seconds as we would really only see issues if we have a mass burst. // } }; const getToken = async () => { try { const { status, data } = await axios.post( `${process.env.OPENDOCK_URL}/auth/login`, { email: "blake.matthes@alpla.com", password: process.env.OPENDOCK_PASSWORD, }, ); if (status === 400) { log.error(data.message); return; } odToken = { odToken: data.access_token, tokenDate: new Date() }; log.info("Token added"); } catch (e) { log.error(e); } }; // export const monitorReleaseChanges = async () => { // console.log("Starting release monitor", lastCheck); // setInterval(async () => { // try { // const result = await prodQuery( // releaseQuery.replace("[dateCheck]", `'${lastCheck}'`), // "get last release change", // ); // //console.log(releaseQuery.replace("[dateCheck]", `'${lastCheck}'`)); // if (result.data.length > 0) { // console.log( // formatInTimeZone( // result.data[result.data.length - 1].Upd_Date, // "UTC", // "yyyy-MM-dd HH:mm:ss", // ), // lastCheck, // ); // lastCheck = formatInTimeZone( // result.data[result.data.length - 1].Upd_Date, // "UTC", // "yyyy-MM-dd HH:mm:ss", // ); // const releases = result.data; // for (let i = 0; i < releases.length; i++) { // const newDockApt = { // status: "Scheduled", // userId: "ee956455-e193-47fc-b53b-dff30fabdf4b", // this should be the carrierid // loadTypeId: "0aa7988e-b17b-4f10-acdd-3d029b44a773", // well get this and make it a default one // dockId: "00ba4386-ce5a-4dd1-9356-6e6d10a24609", // this the warehouse we want it in to start out // refNumbers: [releases[i].ReleaseNumber], // refNumber: releases[i].ReleaseNumber, // start: releases[i].DeliveryDate, // end: addHours(releases[i].DeliveryDate, 1), // notes: "", // ccEmails: [""], // muteNotifications: true, // metadata: { // externalValidationFailed: false, // externalValidationErrorMessage: null, // }, // units: null, // customFields: [ // { // name: "strArticle", // type: "str", // label: "Article", // value: `${releases[i].LineItemHumanReadableId} - ${releases[i].ArticleAlias}`, // description: "What bottle are we sending ", // placeholder: "", // dropDownValues: [], // minLengthOrValue: 1, // hiddenFromCarrier: false, // requiredForCarrier: false, // requiredForWarehouse: false, // }, // { // name: "intPallet Count", // type: "int", // label: "Pallet Count", // value: parseInt(releases[i].LoadingUnits, 10), // description: "How many pallets", // placeholder: "22", // dropDownValues: [], // minLengthOrValue: 1, // hiddenFromCarrier: false, // requiredForCarrier: false, // requiredForWarehouse: false, // }, // { // name: "strTotal Weight", // type: "str", // label: "Total Weight", // value: `${(((releases[i].Quantity * releases[i].LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`, // description: "What is the total weight of the load", // placeholder: "", // dropDownValues: [], // minLengthOrValue: 1, // hiddenFromCarrier: false, // requiredForCarrier: false, // requiredForWarehouse: false, // }, // { // name: "strCustomer ReleaseNumber", // type: "str", // label: "Customer Release Number", // value: `${releases[i].CustomerReleaseNumber}`, // description: "What is the customer release number", // placeholder: "", // dropDownValues: [], // minLengthOrValue: 1, // hiddenFromCarrier: false, // requiredForCarrier: false, // requiredForWarehouse: false, // }, // ], // }; // //console.log(newDockApt); // const newDockResult = await axios.post( // "https://neutron.staging.opendock.com/appointment", // newDockApt, // { // headers: { // "content-type": "application/json; charset=utf-8", // }, // }, // ); // console.log(newDockResult.statusText); // await delay(500); // } // } // } catch (e) { // console.log(e); // } // }, 5 * 1000); // };