From c1b148369d1628a1453c0b00bb1488d942631470 Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Mon, 26 Oct 2026 20:16:27 -0300 Subject: [PATCH 1/4] fix(frenet): Update order delivery status from Frenet tracking again Tracking codes were never read back from Firestore (`data.toDate()` on the whole document threw), so the hourly cron hung until timeout and orders were never updated by Frenet tracking. Delivered and old codes were never cleaned. - Read `createdAt` from each tracking doc - Only track codes from the last 60 days, so customers of old orders are not notified; older codes are removed by the daily cleanup, which now awaits - Store tracking status as string so delivered codes match the queries - Fail Frenet API calls after 10s instead of hanging on HTTP errors - Await removal of delivered codes and stop logging an error when none exist Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011YfvCxVikTh42WGmNu6chf --- .../apps/frenet/src/functions-lib/database.ts | 40 ++++++++++--------- .../src/functions-lib/fetch-tracking-code.ts | 15 +++---- .../src/functions-lib/remove-delivered.ts | 12 ++++-- .../src/functions-lib/tracking-codes.ts | 13 +++++- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/apps/frenet/src/functions-lib/database.ts b/packages/apps/frenet/src/functions-lib/database.ts index 54cf46ae3..aff6c5b6d 100644 --- a/packages/apps/frenet/src/functions-lib/database.ts +++ b/packages/apps/frenet/src/functions-lib/database.ts @@ -47,20 +47,21 @@ const get = async ( }); }); -const clear = async () => new Promise((resolve) => { +// Codes older than this are no longer tracked and get removed +const maxAgeDays = 60; + +const getDeadline = () => { const deadline = new Date(); - deadline.setMonth(deadline.getMonth() - 2); + deadline.setDate(deadline.getDate() - maxAgeDays); + return deadline; +}; - getFirestore().collection(firestoreColl) - .where('createdAt', '<=', deadline) - .get() - .then((docsRef) => { - docsRef.docs.forEach((doc) => { - doc.ref.delete(); - }); - resolve(true); - }); -}); +const clear = async () => { + const docsRef = await getFirestore().collection(firestoreColl) + .where('createdAt', '<=', getDeadline()) + .get(); + await Promise.all(docsRef.docs.map((doc) => doc.ref.delete())); +}; const remove = async ( trackingCode: string, @@ -81,13 +82,13 @@ const remove = async ( }); const update = async ( - trackingStatus: string, + trackingStatus: string | number, trackingCode: string, ) => new Promise((resolve) => { const documentRef = getFirestore().doc(`${firestoreColl}/${trackingCode}`); if (documentRef) { documentRef.set({ - trackingStatus, + trackingStatus: String(trackingStatus), updateAt: new Date().toISOString(), }, { merge: true }) .then(() => resolve(true)); @@ -96,7 +97,7 @@ const update = async ( const getAll = () => new Promise((resolve, reject) => { getFirestore().collection(firestoreColl) - .where('trackingStatus', '!=', '9') + .where('trackingStatus', 'not-in', ['9', 9]) .get() .then((docsRef) => { if (docsRef.size > 0) { @@ -104,7 +105,7 @@ const getAll = () => new Promise((resolve, reject) => { docsRef.forEach((documentSnapshot) => { if (documentSnapshot && documentSnapshot.exists) { const data = documentSnapshot.data(); - list.push({ ...data, createAt: data.toDate() }); + list.push({ ...data, createdAt: data.createdAt?.toDate() }); } }); resolve(list); @@ -118,7 +119,7 @@ const getAll = () => new Promise((resolve, reject) => { const getAllDelivered = () => new Promise((resolve, reject) => { getFirestore().collection(firestoreColl) - .where('trackingStatus', '==', '9') + .where('trackingStatus', 'in', ['9', 9]) .get() .then((docsRef) => { if (docsRef.size > 0) { @@ -126,7 +127,7 @@ const getAllDelivered = () => new Promise((resolve, reject) => { docsRef.forEach((documentSnapshot) => { if (documentSnapshot && documentSnapshot.exists) { const data = documentSnapshot.data(); - list.push({ ...data, createAt: data.toDate() }); + list.push({ ...data, createdAt: data.createdAt?.toDate() }); } }); resolve(list); @@ -139,6 +140,7 @@ const getAllDelivered = () => new Promise((resolve, reject) => { }); const db = { + getDeadline, save, get, clear, @@ -155,6 +157,6 @@ export type TrackingDoc = { trackingStatus: string, trackingCode: string, serviceCode: string, - createdAt: string, + createdAt: Date, updateAt?: string } diff --git a/packages/apps/frenet/src/functions-lib/fetch-tracking-code.ts b/packages/apps/frenet/src/functions-lib/fetch-tracking-code.ts index ed1655327..389474eab 100644 --- a/packages/apps/frenet/src/functions-lib/fetch-tracking-code.ts +++ b/packages/apps/frenet/src/functions-lib/fetch-tracking-code.ts @@ -1,22 +1,23 @@ import axios from 'axios'; -export default ( +export default async ( trackingCode: string, serviceCode: string, token: string, -) => new Promise((resolve) => { - axios({ +) => { + const { data } = await axios({ method: 'post', url: 'http://api.frenet.com.br/tracking/trackinginfo', headers: { 'Content-Type': 'application/json', - Accept: 'application/json', + 'Accept': 'application/json', token, }, data: { ShippingServiceCode: serviceCode, TrackingNumber: trackingCode, }, - }) - .then(({ data }) => resolve(data)); -}); + timeout: 10000, + }); + return data; +}; diff --git a/packages/apps/frenet/src/functions-lib/remove-delivered.ts b/packages/apps/frenet/src/functions-lib/remove-delivered.ts index bb8cb28b8..8765f7299 100644 --- a/packages/apps/frenet/src/functions-lib/remove-delivered.ts +++ b/packages/apps/frenet/src/functions-lib/remove-delivered.ts @@ -8,18 +8,22 @@ const removeDeliveredToFirestore = async () => { try { const codes: TrackingDoc[] = await db.getAllDelivered() as TrackingDoc[]; if (codes.length > 0) { - codes.forEach(async (code) => { + await Promise.all(codes.map(async (code) => { try { await db.remove(code.trackingCode, code.serviceCode); logger.info('> (App Frenet) Code removed, delivered status', code); } catch (err) { logger.error(err); } - }); + })); + } + resolve(null); + } catch (err: any) { + if (err.name === 'NoTrackingCodesForUpdate') { resolve(null); + } else { + reject(err); } - } catch (err) { - reject(err); } }); diff --git a/packages/apps/frenet/src/functions-lib/tracking-codes.ts b/packages/apps/frenet/src/functions-lib/tracking-codes.ts index b50d34037..bfca25d60 100644 --- a/packages/apps/frenet/src/functions-lib/tracking-codes.ts +++ b/packages/apps/frenet/src/functions-lib/tracking-codes.ts @@ -21,7 +21,7 @@ const getConfig = async () => { }; const diffInDays = ( - createdAt: string, + createdAt: Date, ) => { const today = new Date(); const created = new Date(createdAt); @@ -47,6 +47,17 @@ const handleTrackingCodes = async () => { resolve(null); } + if (codes) { + // Skip old codes instead of notifying customers about outdated orders, + // they are removed by the daily cleanup + const deadline = db.getDeadline().getTime(); + const total = codes.length; + codes = codes.filter(({ createdAt }) => createdAt && createdAt.getTime() > deadline); + if (codes.length < total) { + logger.info(`> (App Frenet) Skipping ${total - codes.length} tracking codes older than deadline`); + } + } + if (!appConfig || !appConfig.frenet_access_token || !codes) { // no config for store bye resolve(null); From 56a6620bf7ab784360fb531377d25bd726118f0e Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Mon, 26 Oct 2026 22:35:07 -0300 Subject: [PATCH 2/4] chore(frenet): Trim tracking code comments Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011YfvCxVikTh42WGmNu6chf --- packages/apps/frenet/src/functions-lib/database.ts | 1 - packages/apps/frenet/src/functions-lib/tracking-codes.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/apps/frenet/src/functions-lib/database.ts b/packages/apps/frenet/src/functions-lib/database.ts index aff6c5b6d..0dd425cf3 100644 --- a/packages/apps/frenet/src/functions-lib/database.ts +++ b/packages/apps/frenet/src/functions-lib/database.ts @@ -47,7 +47,6 @@ const get = async ( }); }); -// Codes older than this are no longer tracked and get removed const maxAgeDays = 60; const getDeadline = () => { diff --git a/packages/apps/frenet/src/functions-lib/tracking-codes.ts b/packages/apps/frenet/src/functions-lib/tracking-codes.ts index bfca25d60..eaed3decb 100644 --- a/packages/apps/frenet/src/functions-lib/tracking-codes.ts +++ b/packages/apps/frenet/src/functions-lib/tracking-codes.ts @@ -48,8 +48,7 @@ const handleTrackingCodes = async () => { } if (codes) { - // Skip old codes instead of notifying customers about outdated orders, - // they are removed by the daily cleanup + // Don't notify customers of old orders, old codes are removed by `db.clear` const deadline = db.getDeadline().getTime(); const total = codes.length; codes = codes.filter(({ createdAt }) => createdAt && createdAt.getTime() > deadline); From d7cf1fe9393662b14fc8022d5fc8c3802a48c1f4 Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Mon, 26 Oct 2026 22:39:12 -0300 Subject: [PATCH 3/4] fix(frenet): Keep Frenet tracking cron from hanging on large or failing runs - Propagate Firestore errors in remove, update and code listing instead of leaving promises pending forever - Process tracking codes 10 at a time and raise cron timeout to 540s - Remove old codes on every run with BulkWriter, not only at 23h Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011YfvCxVikTh42WGmNu6chf --- packages/apps/frenet/src/frenet-events.ts | 4 +- .../apps/frenet/src/functions-lib/database.ts | 111 +++++++----------- .../src/functions-lib/tracking-codes.ts | 23 ++-- 3 files changed, 59 insertions(+), 79 deletions(-) diff --git a/packages/apps/frenet/src/frenet-events.ts b/packages/apps/frenet/src/frenet-events.ts index ce35238c0..e65323316 100644 --- a/packages/apps/frenet/src/frenet-events.ts +++ b/packages/apps/frenet/src/frenet-events.ts @@ -9,7 +9,9 @@ import handleApiEvent from './functions-lib/events-to-frenet'; const { httpsFunctionOptions: { region } } = config.get(); export const frenet = { - cronTrackingCodes: functions.region(region).pubsub + cronTrackingCodes: functions.region(region) + .runWith({ timeoutSeconds: 540 }) + .pubsub .schedule(process.env.CRONTAB_FRENET_TRACKING_CODES || '19 * * * *') .onRun(() => { return handleTrackingCodes(); diff --git a/packages/apps/frenet/src/functions-lib/database.ts b/packages/apps/frenet/src/functions-lib/database.ts index 0dd425cf3..dc81fb1f8 100644 --- a/packages/apps/frenet/src/functions-lib/database.ts +++ b/packages/apps/frenet/src/functions-lib/database.ts @@ -19,7 +19,10 @@ const save = ( serviceCode, createdAt: new Date(), }).then(() => resolve(true)) - .catch(logger.error); + .catch((err) => { + logger.error(err); + resolve(false); + }); } }); @@ -59,84 +62,58 @@ const clear = async () => { const docsRef = await getFirestore().collection(firestoreColl) .where('createdAt', '<=', getDeadline()) .get(); - await Promise.all(docsRef.docs.map((doc) => doc.ref.delete())); + const bulkWriter = getFirestore().bulkWriter(); + docsRef.docs.forEach((doc) => { + bulkWriter.delete(doc.ref); + }); + await bulkWriter.close(); }; const remove = async ( trackingCode: string, serviceCode: string, -) => new Promise((resolve) => { - getFirestore().doc(`${firestoreColl}/${trackingCode}`) - .get() - .then(async (documentSnapshot) => { - if (documentSnapshot && documentSnapshot.exists) { - const data = documentSnapshot.data(); - if (data && data.serviceCode === serviceCode) { - await documentSnapshot.ref.delete(); - resolve(true); - } - } - resolve(false); - }); -}); +) => { + const documentSnapshot = await getFirestore().doc(`${firestoreColl}/${trackingCode}`).get(); + if (documentSnapshot.exists && documentSnapshot.data()?.serviceCode === serviceCode) { + await documentSnapshot.ref.delete(); + return true; + } + return false; +}; const update = async ( trackingStatus: string | number, trackingCode: string, -) => new Promise((resolve) => { - const documentRef = getFirestore().doc(`${firestoreColl}/${trackingCode}`); - if (documentRef) { - documentRef.set({ - trackingStatus: String(trackingStatus), - updateAt: new Date().toISOString(), - }, { merge: true }) - .then(() => resolve(true)); +) => { + await getFirestore().doc(`${firestoreColl}/${trackingCode}`).set({ + trackingStatus: String(trackingStatus), + updateAt: new Date().toISOString(), + }, { merge: true }); + return true; +}; + +const listByStatus = async ( + operator: 'in' | 'not-in', +) => { + const docsRef = await getFirestore().collection(firestoreColl) + .where('trackingStatus', operator, ['9', 9]) + .get(); + if (!docsRef.size) { + const err = new Error('Tracking code not found for any store :)'); + err.name = 'NoTrackingCodesForUpdate'; + throw err; } -}); + const list: { [x: string]: any }[] = []; + docsRef.forEach((documentSnapshot) => { + const data = documentSnapshot.data(); + list.push({ ...data, createdAt: data.createdAt?.toDate?.() }); + }); + return list; +}; -const getAll = () => new Promise((resolve, reject) => { - getFirestore().collection(firestoreColl) - .where('trackingStatus', 'not-in', ['9', 9]) - .get() - .then((docsRef) => { - if (docsRef.size > 0) { - const list: { [x: string]: any }[] = []; - docsRef.forEach((documentSnapshot) => { - if (documentSnapshot && documentSnapshot.exists) { - const data = documentSnapshot.data(); - list.push({ ...data, createdAt: data.createdAt?.toDate() }); - } - }); - resolve(list); - } else { - const err = new Error('Tracking code not found for any store :)'); - err.name = 'NoTrackingCodesForUpdate'; - reject(err); - } - }); -}); +const getAll = () => listByStatus('not-in'); -const getAllDelivered = () => new Promise((resolve, reject) => { - getFirestore().collection(firestoreColl) - .where('trackingStatus', 'in', ['9', 9]) - .get() - .then((docsRef) => { - if (docsRef.size > 0) { - const list: { [x: string]: any }[] = []; - docsRef.forEach((documentSnapshot) => { - if (documentSnapshot && documentSnapshot.exists) { - const data = documentSnapshot.data(); - list.push({ ...data, createdAt: data.createdAt?.toDate() }); - } - }); - resolve(list); - } else { - const err = new Error('Tracking code not found for any store :)'); - err.name = 'NoTrackingCodesForUpdate'; - reject(err); - } - }); -}); +const getAllDelivered = () => listByStatus('in'); const db = { getDeadline, diff --git a/packages/apps/frenet/src/functions-lib/tracking-codes.ts b/packages/apps/frenet/src/functions-lib/tracking-codes.ts index eaed3decb..ee88efb1b 100644 --- a/packages/apps/frenet/src/functions-lib/tracking-codes.ts +++ b/packages/apps/frenet/src/functions-lib/tracking-codes.ts @@ -48,7 +48,7 @@ const handleTrackingCodes = async () => { } if (codes) { - // Don't notify customers of old orders, old codes are removed by `db.clear` + // Don't notify customers of old orders const deadline = db.getDeadline().getTime(); const total = codes.length; codes = codes.filter(({ createdAt }) => createdAt && createdAt.getTime() > deadline); @@ -61,9 +61,7 @@ const handleTrackingCodes = async () => { // no config for store bye resolve(null); } else { - // Wait for every code before finishing, the function may be stopped - // right after resolving - await Promise.allSettled(codes.map(async (code) => { + const handleCode = async (code: TrackingDoc) => { const serviceCode = String(code.serviceCode).startsWith('FR') ? String(code.serviceCode.replace(/^FR/, '')).trim() : code.serviceCode; @@ -156,12 +154,21 @@ const handleTrackingCodes = async () => { } } } - })); + }; + // Limit concurrent requests to Frenet and Store API + for (let i = 0; i < codes.length; i += 10) { + // eslint-disable-next-line no-await-in-loop + await Promise.allSettled(codes.slice(i, i + 10).map(handleCode)); + } resolve(null); } }); if (appConfig && appConfig.frenet_access_token) { + await db.clear().catch((err) => { + logger.error('> (App Frenet) Error removing old tracking codes => ', err); + }); + await checkTrackingCodes.catch((err) => { logger.error('> (App Frenet) Error => ', err); }); @@ -169,12 +176,6 @@ const handleTrackingCodes = async () => { await removeDeliveredToFirestore().catch((err) => { logger.error('> (App Frenet) Error removing Delivered to Firestore => ', err); }); - - if (new Date().getHours() === 23) { - await db.clear().catch((err) => { - logger.error('> (App Frenet) Error removing old tracking codes => ', err); - }); - } } }; From d2d399954e3ae8d6c9eea5f925418fc1d288dbe4 Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Mon, 26 Oct 2026 23:11:01 -0300 Subject: [PATCH 4/4] fix(frenet): Don't email customers about tracking events older than a week Fulfillments from Frenet events older than 7 days are posted with `customer_notified: true`, so the emails app skips them while the order status and other apps still get the update. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011YfvCxVikTh42WGmNu6chf --- packages/apps/frenet/src/functions-lib/tracking-codes.ts | 3 +++ packages/apps/frenet/src/functions-lib/update-fulfillments.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/apps/frenet/src/functions-lib/tracking-codes.ts b/packages/apps/frenet/src/functions-lib/tracking-codes.ts index ee88efb1b..3ee37b4ca 100644 --- a/packages/apps/frenet/src/functions-lib/tracking-codes.ts +++ b/packages/apps/frenet/src/functions-lib/tracking-codes.ts @@ -127,11 +127,14 @@ const handleTrackingCodes = async () => { if (trackingEvent && parseInt(code.trackingStatus, 10) !== trackingEvent.status) { try { + // Don't email customers about events from long ago + const isOldEvent = diffInDays(trackingEvent.date) > 7; await updateOrderfulfilllment( code.orderId, code.trackingCode, trackingEvent.status, trackingEvent.date, + isOldEvent, ); await db.update(trackingEvent.status, code.trackingCode); diff --git a/packages/apps/frenet/src/functions-lib/update-fulfillments.ts b/packages/apps/frenet/src/functions-lib/update-fulfillments.ts index 11a5316d0..b2974280a 100644 --- a/packages/apps/frenet/src/functions-lib/update-fulfillments.ts +++ b/packages/apps/frenet/src/functions-lib/update-fulfillments.ts @@ -22,6 +22,7 @@ const updateFulfillments = async ( trackingCode: string, eventType: number, eventDate: Date, + skipNotification?: boolean, ) => { let notes; const status = parseStatus(eventType); @@ -33,6 +34,9 @@ const updateFulfillments = async ( 'app-frenet', ], } as any; // TODO: incompatible type=> amount; + if (skipNotification) { + bodyFulfillments.customer_notified = true; + } if (eventType === 2) { notes = 'Entrega está atrasada.';