feat(migrate): quality alert migrated

This commit is contained in:
2026-04-10 13:57:15 -05:00
parent 07ebf88806
commit b0e5fd7999
7 changed files with 371 additions and 1 deletions

View File

@@ -0,0 +1,96 @@
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.post("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
if (!hasPermissions) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `You do not have permissions to be here`,
data: [],
status: 400,
});
}
const { data: nName, error: nError } = await tryCatch(
db
.select()
.from(notifications)
.where(eq(notifications.name, req.body.name)),
);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
const { data: sub, error: sError } = await tryCatch(
db
.select()
.from(notifications)
.where(eq(notifications.name, req.body.name)),
);
if (sError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the subs `,
data: [sError],
status: 400,
});
}
const emailString = [
...new Set(
sub.flatMap((e: any) =>
e.emails?.map((email: any) => email.trim().toLowerCase()),
),
),
].join(";");
console.log(emailString);
const { default: runFun } = await import(
`./notification.${req.body.name.trim()}.js`
);
const manual = await runFun(nName[0], "blake.matthes@alpla.com");
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Manual Trigger ran`,
data: manual ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,109 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { v1QueryRun } from "../utils/pgConnectToLst.utils.js";
const func = async (data: any, emails: string) => {
// TODO: remove this disable once all 17 plants are on this new lst
v1QueryRun(`update public.notifications set active = false where name = '${data.name}'`)
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: le as any,
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
// get get the latest blocking order id that was sent
const blockingOrderId = l[0].options[0].lastBlockingOrderIdSent ?? 69;
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[lastBlocking]", blockingOrderId),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: error as any,
notify: true,
});
}
if (queryRun.data.length > 0) {
for (const bo of queryRun.data) {
const sentEmail = await sendEmail({
email: emails,
subject: bo.subject,
template: "qualityBlocking",
context: {
items: bo,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "email",
message: `${l[0].name} failed to send the email`,
data: sentEmail?.data as any,
notify: true,
});
}
await delay(1500);
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ lastBlockingOrderIdSent: bo.blockingNumber }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: dbe as any,
notify: true,
});
}
}
}
};
export default func;

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import manual from "./notification.manualTrigger.js";
import getNotifications from "./notification.route.js"; import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js"; import updateNote from "./notification.update.route.js";
import deleteSub from "./notificationSub.delete.route.js"; import deleteSub from "./notificationSub.delete.route.js";
@@ -11,6 +12,7 @@ export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this //stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications); app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications);
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote); app.use(`${baseUrl}/api/notification`, requireAuth, updateNote);
app.use(`${baseUrl}/api/notification/manual`, requireAuth, manual);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs); app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub); app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub); app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub);

View File

@@ -22,7 +22,7 @@ const note: NewNotification[] = [
"Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.", "Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.",
active: false, active: false,
interval: "10", interval: "10",
options: [{ sentBlockingOrders: [{ timeStamp: "0", blockingOrder: 1 }] }], options: [{ lastBlockingOrderIdSent: 1 }],
}, },
{ {
name: "alplaPurchaseHistory", name: "alplaPurchaseHistory",

View File

@@ -0,0 +1,44 @@
use [test1_AlplaPROD2.0_Read]
SELECT
'Alert! new blocking order: #' + cast(bo.HumanReadableId as varchar) + ' - ' + bo.ArticleVariantDescription as subject
,cast(bo.[HumanReadableId] as varchar) as blockingNumber
,bo.[ArticleVariantDescription] as article
,cast(bo.[CustomerHumanReadableId] as varchar) + ' - ' + bo.[CustomerDescription] as customer
,convert(varchar(10), bo.[BlockingDate], 101) + ' ' + convert(varchar(5), bo.[BlockingDate], 108) as blockingDate
,cast(ArticleVariantHumanReadableId as varchar) + ' - ' + ArticleVariantDescription as av
,case when bo.Remark = '' or bo.Remark is NULL then 'Please reach out to quality for the reason this was placed on hold as a remark was not entered during the blocking processs' else bo.Remark end as remark
,cast(FORMAT(TotalAmountOfPieces, '###,###') as varchar) + ' / ' + cast(LoadingUnit as varchar) as peicesAndLoadingUnits
,bo.ProductionLotHumanReadableId as lotNumber
,cast(osd.IdBlockingDefectsGroup as varchar) + ' - ' + osd.Description as mainDefectGroup
,cast(df.HumanReadableId as varchar) + ' - ' + os.Description as mainDefect
,lot.MachineLocation as line
--,*
FROM [blocking].[BlockingOrder] (nolock) as bo
/*** get the defect details ***/
join
[blocking].[BlockingDefect] (nolock) AS df
on df.id = bo.MainDefectId
/*** pull description from 1.0 ***/
left join
[AlplaPROD_test1].[dbo].[T_BlockingDefects] (nolock) as os
on os.IdGlobalBlockingDefect = df.HumanReadableId
/*** join in 1.0 defect group ***/
left join
[AlplaPROD_test1].[dbo].[T_BlockingDefectsGroups] (nolock) as osd
on osd.IdBlockingDefectsGroup = os.IdBlockingDefectsGroup
left join
[productionControlling].[ProducedLot] (nolock) as lot
on lot.id = bo.ProductionLotId
where
bo.[BlockingDate] between getdate() - 2 and getdate() + 3 and
bo.BlockingTrigger = 1 -- so we only get the ir blocking and not coa
--and HumanReadableId NOT IN ([sentBlockingOrders])
and bo.HumanReadableId > [lastBlocking]

View File

@@ -0,0 +1,73 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
<style>
.email-wrapper {
max-width: 80%; /* Limit width to 80% of the window */
margin: 0 auto; /* Center the content horizontally */
}
.email-table {
width: 100%;
border-collapse: collapse;
}
.email-table td {
vertical-align: top;
padding: 10px;
border: 1px solid #000;
border-radius: 25px; /* Rounded corners */
background-color: #f0f0f0; /* Optional: Add a background color */
}
.email-table h2 {
margin: 0;
}
.remarks {
border: 1px solid black;
padding: 10px;
background-color: #f0f0f0;
border-radius: 25px;
}
</style>
</head>
<body>
<div class="email-wrapper">
<p>All,</p>
<p>Please see the new blocking order that was created.</p>
<div>
<div class="email-table">
<table>
<tr>
<td>
<p><strong>Blocking number: </strong>{{items.blockingNumber}}</p>
<p><strong>Blocking Date: </strong>{{items.blockingDate}}</p>
<p><strong>Article: </strong>{{items.av}}</p>
<p><strong>Production Lot: </strong>{{items.lotNumber}}</p>
<p><strong>Line: </strong>{{items.line}}</p>
</td>
<td>
<p><strong>Customer: </strong>{{items.customer}}</p>
<p><strong>Blocked pieces /LUs: </strong>{{items.peicesAndLoadingUnits}}</p>
<p><strong>Main defect group: </strong>{{items.mainDefectGroup}}</p>
<p><strong>Main defect: </strong>{{items.mainDefect}}</p>
</td>
</tr>
</table>
</div>
</div>
<div class="remarks">
<h4>Remarks:</h4>
<p>{{items.remark}}</p>
</div>
</div>
<br>
<p>For further questions please reach out to quality.</p>
<p>Thank you,</p>
<p>Quality Department</p>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,46 @@
import pkg from "pg";
const { Client } = pkg;
const v1client = new Client({
host: "localhost",
port: 5432,
user: "postgres",
password: "obelix",
database: "lst",
});
const v2client = new Client({
host: "localhost",
port: 5432,
user: "postgres",
password: "obelix",
database: "lst_db",
});
export const v1QueryRun= async (query:string)=> {
try {
await v1client.connect();
const res = await v1client.query(query);
console.log("Rows affected:", res.rowCount);
} catch (err) {
console.error("Error running query:", err);
} finally {
await v1client.end();
}
}
export const v2QueryRun = async (query:string)=> {
try {
await v2client.connect();
const res = await v2client.query(query);
console.log("Rows affected:", res.rowCount);
} catch (err) {
console.error("Error running query:", err);
} finally {
await v2client.end();
}
}