Feat/company custom fields#39
Conversation
…ndar-b2b feat: integrar Google Calendar via n8n para atividades MEETING
- Nº de Funcionários (number) - CNAE (text) - NRs Aplicáveis (text) - Data do Último ASO (date)
|
Someone is attempting to deploy a commit to the Thales Laray Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR introduces company-specific form fields to contact management, implements webhook notifications for meeting activities, optimizes deal item cache updates, improves table overflow handling, adjusts form field validation schema, and modifies the stage-evaluations cron frequency. Changes
Sequence DiagramsequenceDiagram
actor Client
participant Activity Service as Activity Service
participant Cache as Cache Layer
participant External Webhook as External Webhook
Client->>Activity Service: createActivity(data)
Activity Service->>Activity Service: Validate activity type
alt Activity type is MEETING
Activity Service->>External Webhook: POST webhook<br/>(title, description,<br/>start/end times, attendee)
External Webhook-->>Activity Service: Response (success or error)
Activity Service->>Activity Service: Log webhook result
end
Activity Service->>Cache: Invalidate queries
Activity Service->>Cache: Update optimistic state
Activity Service-->>Client: Return created activity
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (4)
lib/query/hooks/useActivitiesQuery.ts (1)
193-197: Add a timeout, idempotency key, and extract to a helper.A few smaller hardening items on the same call:
- No timeout /
AbortSignal: if the n8n endpoint hangs, the request lingers until the browser/tab decides to abort. UseAbortSignal.timeout(...)(or anAbortController).- No idempotency:
onSuccesscan fire again if the mutation is retried or if the component remounts and replays state in some flows; the downstream calendar will create duplicate events. Includingactivity.idin the payload (e.g. asexternal_id) lets n8n dedupe.- Inline fetch in
onSuccessmakes this hook do two unrelated things; extracting to e.g.lib/integrations/meetingWebhook.tskeeps the mutation focused and unit-testable.♻️ Sketch
- if (data.type === "MEETING") { - const startDate = data.date ? new Date(data.date) : null; - if (startDate && !isNaN(startDate.getTime())) { - fetch("https://n8n-production-9012a.up.railway.app/webhook/0ebbdfef-a03e-4109-bdce-7d00e70218f0", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" }) - }).catch((err) => console.error("[Calendar] Webhook error:", err)); - } - } + if (data.type === 'MEETING') { + void notifyMeetingCreated(data, user?.email ?? ''); + }…with the helper owning the URL lookup, timeout, and
external_id:// lib/integrations/meetingWebhook.ts export async function notifyMeetingCreated(activity: Activity, attendeeEmail: string) { const start = new Date(activity.date); if (Number.isNaN(start.getTime())) return; try { await fetch(/* url from org settings */, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ external_id: activity.id, title: activity.title, description: activity.description || activity.dealTitle, start_time: start.toISOString(), end_time: new Date(start.getTime() + 60 * 60_000).toISOString(), attendees: attendeeEmail, }), signal: AbortSignal.timeout(5_000), }); } catch (err) { console.error('[Calendar] Webhook error:', err); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/query/hooks/useActivitiesQuery.ts` around lines 193 - 197, The inline fetch in useActivitiesQuery's onSuccess should be moved to a dedicated helper (e.g., export async function notifyMeetingCreated(activity, attendeeEmail) in lib/integrations/meetingWebhook.ts) that reads the webhook URL from org settings, validates/parses activity.date, includes activity.id as external_id for idempotency, sets a request timeout using AbortSignal.timeout (or an AbortController) and logs errors; replace the inline fetch call inside useActivitiesQuery's onSuccess with a call to notifyMeetingCreated(activity, user?.email || "") so the mutation remains focused and the webhook logic is testable and hardened.lib/validations/schemas.ts (2)
125-125: UseMAX_LENGTHS.MEDIUM_TEXTinstead of magic500.
MAX_LENGTHS.MEDIUM_TEXTalready equals500(line 26). Using the constant keeps the schema consistent with the rest of the file.♻️ Proposed fix
- nrs_aplicaveis: optionalString.pipe(z.string().max(500)), + nrs_aplicaveis: optionalString.pipe(z.string().max(MAX_LENGTHS.MEDIUM_TEXT)),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/validations/schemas.ts` at line 125, Replace the magic number 500 in the schema for nrs_aplicaveis with the existing constant MAX_LENGTHS.MEDIUM_TEXT: locate the nrs_aplicaveis line that uses optionalString.pipe(z.string().max(500)) and change the .max argument to MAX_LENGTHS.MEDIUM_TEXT so it uses the shared constant and stays consistent with other schema definitions.
126-126: Add ISO date format validation todata_ultimo_asofield.The field receives
YYYY-MM-DDstrings from<InputField type="date" />but only validates string length. Add format validation usingz.iso.date()to prevent malformed dates if the input type or population method changes.Suggested fix
- data_ultimo_aso: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)), + data_ultimo_aso: optionalString.pipe( + z.union([z.literal(''), z.iso.date()]) + ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/validations/schemas.ts` at line 126, The data_ultimo_aso schema currently only enforces max length; update the validation for the data_ultimo_aso field (where optionalString.pipe(...) is used) to also enforce an ISO date (YYYY-MM-DD) format — replace the current z.string().max(...) validator with a string-format validator (e.g. a regex that matches /^\d{4}-\d{2}-\d{2}$/ or an equivalent zod ISO-date check) so the field is rejected if it is not a valid YYYY-MM-DD date while keeping the optionalString wrapper.features/contacts/components/CompanyFormModal.tsx (1)
63-68: Redundant re-parse —zodResolveralready validated.
handleSubmit(handleFormSubmit)only fires after the resolver has parsed the input throughcompanyFormSchema, so callingcompanyFormSchema.parse(data)again on Line 64 duplicates the work and will throw on transform-only mismatches if the input/output types ever diverge. Use the resolved data directly:♻️ Proposed fix
- const handleFormSubmit = (data: CompanyFormInput) => { - const parsed = companyFormSchema.parse(data); - onSubmit(parsed); - onClose(); - reset(); - }; + const handleFormSubmit = handleSubmit((data) => { + onSubmit(data as CompanyFormData); + onClose(); + reset(); + });…and pass
handleFormSubmitdirectly to<ModalForm onSubmit={…}>. Or simply type the handler as(data: CompanyFormData) => …since react-hook-form returns the resolver's output type.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/contacts/components/CompanyFormModal.tsx` around lines 63 - 68, The handler handleFormSubmit redundantly re-parses data with companyFormSchema.parse even though react-hook-form's zodResolver already returns the parsed type; remove the parse call and accept the resolved type instead (change the handler signature to (data: CompanyFormData) or the resolver output type), then pass that handler directly to ModalForm's onSubmit (or to handleSubmit(handleFormSubmit) without an extra parse) and keep the existing onSubmit(parsed); onClose(); reset(); sequence unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@features/contacts/components/CompanyFormModal.tsx`:
- Line 26: The code is using an unsafe cast (editingCompany as any) to access
custom_fields; instead extend the Organization interface in your types module
(add optional property custom_fields?: { num_funcionarios?: number; cnae?:
string; nrs_aplicaveis?: string; data_ultimo_aso?: string }) so the
Company/Organization type includes custom_fields, then remove the casts in
CompanyFormModal.tsx and access with optional chaining
(editingCompany?.custom_fields ?? {}); also review any DB schema/queries that
persist crm_companies to ensure custom_fields is persisted or handled
appropriately if it should be stored.
In `@lib/query/hooks/useActivitiesQuery.ts`:
- Line 196: The request body currently hardcodes end_time to start_time plus 1
hour in useActivitiesQuery.ts (where the payload is built for the webhook),
causing incorrect durations; update the payload construction in that function to
compute end_time from a provided Activity field (preferably data.endDate or
data.durationMinutes) and fall back to a per-organization default duration if
those are absent (read the org default from the existing org/config API or pass
it into the hook), and ensure the body uses the computed ISO string for end_time
instead of new Date(startDate.getTime() + 3600000).
- Around line 190-199: The client-side webhook call in useActivitiesQuery.ts
(inside the useActivitiesQuery hook) must be removed and replaced with a
server-side trigger: add per-organization webhook settings to the
organization_settings table and expose them via the existing org-config pattern
(similar to getOrgAIConfig) so each org has an enabled flag and webhook_url;
create a Route Handler/Edge Function endpoint that accepts a minimal meeting
payload (title, description, start_time, end_time, organization_id,
attendee_email) and from server-side reads the organization_settings (filtering
by organization_id) to get and sanitize the webhook_url with sanitizeUrl()
before calling fetch, include any auth header from settings, and log the
organization_id; update useActivitiesQuery to POST to this new internal endpoint
instead of calling the hardcoded external URL and ensure no PII or webhook URLs
remain in the client bundle.
In `@lib/query/hooks/useDealsQuery.ts`:
- Around line 547-554: The current onSuccess handler updates the DEALS_VIEW_KEY
optimistically but uses setQueryData without a generic (leaving old as unknown)
and then onSettled calls invalidateQueries(queryKeys.deals.lists()) which
prefix-matches and immediately refetches, negating the optimization; fix by
changing setQueryData to use the explicit generic setQueryData<DealView[]> (so
old is typed) and update only the specific view cache key pattern
[...queryKeys.deals.lists(), 'view'] (i.e. use
queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, ...) where DEALS_VIEW_KEY
is the view key), and change the onSettled invalidation to invalidate only
[...queryKeys.deals.lists(), 'view'] (or remove the invalidate if you prefer
relying on setQueryData) so there is no cascade invalidation from
queryKeys.deals.lists().
- Around line 520-527: The direct cache update in the onSuccess handler for
adding an item uses queryClient.setQueryData without the <DealView[]> generic
(so old is inferred as unknown) and is immediately undone because the existing
onSettled still calls queryClient.invalidateQueries({ queryKey:
queryKeys.deals.lists() }) which prefix-invalidates DEALS_VIEW_KEY; fix by
adding the explicit generic to setQueryData (setQueryData<DealView[]>) and stop
invalidating the parent lists key in the onSettled for this mutation (either
remove or narrow the invalidateQueries call so it does not target
queryKeys.deals.lists()) so the optimized in-place update to DEALS_VIEW_KEY
persists and remains type-safe.
In `@lib/validations/schemas.ts`:
- Line 123: The num_funcionarios schema silently coerces an empty string to 0
causing cleared numeric inputs to persist as 0; update the num_funcionarios
definition to first preprocess the value (in the schema for num_funcionarios)
converting '' to undefined, then apply z.coerce.number().int().min(0).optional()
so that empty string becomes undefined and respects .optional()—locate the
num_funcionarios symbol in the schema and wrap the existing coercion with a
.preprocess(fn) that returns undefined for '' and the original value otherwise.
- Around line 123-126: The schema is validating fields (num_funcionarios, cnae,
nrs_aplicaveis, data_ultimo_aso) that are never persisted because
handleCompanySubmit only sends { name, industry, website } and
companiesService.create/update and the crm_companies table lack these columns;
either stop validating/displaying unused fields or wire them end-to-end. Fix by
one of two options: (A) Remove these four fields from companyFormSchema and from
CompanyFormModal.tsx so validation and UI match companiesService inputs, or (B)
Add them to the persistence layer: extend the DTO/type used by
handleCompanySubmit to include num_funcionarios, cnae, nrs_aplicaveis,
data_ultimo_aso; update companiesService.create and companiesService.update to
accept and pass these fields through; add corresponding columns to the
crm_companies table (with a DB migration) and update any repository/ORM mappings
so the new fields are saved and returned.
In `@vercel.json`:
- Line 13: The cron entry for the route "/api/cron/stage-evaluations" was
changed to "0 0 * * *", which will run once daily and create a backlog/stale
state; revert or update this schedule to a high-frequency cron (e.g., every
minute or every few minutes like "*/1 * * * *" or "*/5 * * * *") to preserve
current near-real-time processing, or if you must keep a low frequency,
implement a compensating fix in the stage evaluation handler (the code behind
"/api/cron/stage-evaluations") to process much larger batches and add
parallelization and explicit product-level acceptance of delayed updates before
changing the schedule.
---
Nitpick comments:
In `@features/contacts/components/CompanyFormModal.tsx`:
- Around line 63-68: The handler handleFormSubmit redundantly re-parses data
with companyFormSchema.parse even though react-hook-form's zodResolver already
returns the parsed type; remove the parse call and accept the resolved type
instead (change the handler signature to (data: CompanyFormData) or the resolver
output type), then pass that handler directly to ModalForm's onSubmit (or to
handleSubmit(handleFormSubmit) without an extra parse) and keep the existing
onSubmit(parsed); onClose(); reset(); sequence unchanged.
In `@lib/query/hooks/useActivitiesQuery.ts`:
- Around line 193-197: The inline fetch in useActivitiesQuery's onSuccess should
be moved to a dedicated helper (e.g., export async function
notifyMeetingCreated(activity, attendeeEmail) in
lib/integrations/meetingWebhook.ts) that reads the webhook URL from org
settings, validates/parses activity.date, includes activity.id as external_id
for idempotency, sets a request timeout using AbortSignal.timeout (or an
AbortController) and logs errors; replace the inline fetch call inside
useActivitiesQuery's onSuccess with a call to notifyMeetingCreated(activity,
user?.email || "") so the mutation remains focused and the webhook logic is
testable and hardened.
In `@lib/validations/schemas.ts`:
- Line 125: Replace the magic number 500 in the schema for nrs_aplicaveis with
the existing constant MAX_LENGTHS.MEDIUM_TEXT: locate the nrs_aplicaveis line
that uses optionalString.pipe(z.string().max(500)) and change the .max argument
to MAX_LENGTHS.MEDIUM_TEXT so it uses the shared constant and stays consistent
with other schema definitions.
- Line 126: The data_ultimo_aso schema currently only enforces max length;
update the validation for the data_ultimo_aso field (where
optionalString.pipe(...) is used) to also enforce an ISO date (YYYY-MM-DD)
format — replace the current z.string().max(...) validator with a string-format
validator (e.g. a regex that matches /^\d{4}-\d{2}-\d{2}$/ or an equivalent zod
ISO-date check) so the field is rejected if it is not a valid YYYY-MM-DD date
while keeping the optionalString wrapper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: aeffeb9e-12c7-439c-b6df-4470beedee01
📒 Files selected for processing (6)
features/boards/components/Modals/DealDetailModal.tsxfeatures/contacts/components/CompanyFormModal.tsxlib/query/hooks/useActivitiesQuery.tslib/query/hooks/useDealsQuery.tslib/validations/schemas.tsvercel.json
| onSubmit, | ||
| editingCompany, | ||
| }) => { | ||
| const customFields = (editingCompany as any)?.custom_fields ?? {}; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the Company type definition to confirm custom_fields isn't typed.
fd -e ts -e tsx | xargs rg -nP -C3 '\b(interface|type)\s+Company\b'Repository: thaleslaray/nossocrm
Length of output: 455
🏁 Script executed:
#!/bin/bash
# Find the Organization type definition
rg -n 'interface Organization|type Organization' --type ts --type tsx -B2 -A10Repository: thaleslaray/nossocrm
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Read the CompanyFormModal.tsx file to see the actual usage
cat -n features/contacts/components/CompanyFormModal.tsx | head -100Repository: thaleslaray/nossocrm
Length of output: 3716
🏁 Script executed:
#!/bin/bash
# Search for Organization type definition without using --type flag
rg 'interface Organization|type Organization' -B2 -A15 types/Repository: thaleslaray/nossocrm
Length of output: 1568
🏁 Script executed:
#!/bin/bash
# List and explore types directory structure
ls -la types/
cat -n types/types.ts | head -200Repository: thaleslaray/nossocrm
Length of output: 7926
🏁 Script executed:
#!/bin/bash
# Find the companyFormSchema definition
fd -name '*schema*' -type f | xargs rg -l 'companyFormSchema' | head -5Repository: thaleslaray/nossocrm
Length of output: 390
🏁 Script executed:
#!/bin/bash
# Look for any custom_fields references in the codebase
rg 'custom_fields' -n | head -20Repository: thaleslaray/nossocrm
Length of output: 1341
🏁 Script executed:
#!/bin/bash
# Check the database schema for companies table
rg 'CREATE TABLE.*companies' -A 30 supabase/migrations/Repository: thaleslaray/nossocrm
Length of output: 2764
🏁 Script executed:
#!/bin/bash
# Read the companyFormSchema to understand expected structure
cat -n lib/validations/schemas.ts | grep -A 30 'companyFormSchema'Repository: thaleslaray/nossocrm
Length of output: 2165
🏁 Script executed:
#!/bin/bash
# Check where editingCompany comes from and is used in the component
rg -B 5 'editingCompany' features/contacts/components/CompanyFormModal.tsx | head -40Repository: thaleslaray/nossocrm
Length of output: 1140
🏁 Script executed:
#!/bin/bash
# Search for where CompanyFormModal is used to see how editingCompany is passed
rg 'CompanyFormModal' -B 3 -A 3 --type ts --type tsx 2>/dev/null || rg 'CompanyFormModal' -B 3 -A 3Repository: thaleslaray/nossocrm
Length of output: 2938
🏁 Script executed:
#!/bin/bash
# Check ContactsPage to see where editingCompany is set for the modal
rg -n 'editingCompany\|setEditingCompany\|handleCompanySubmit' features/contacts/ContactsPage.tsx | head -20Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Look for the controller and its company-related methods
rg -n 'controller\.\(editingCompany\|setEditingCompany\|handleCompanySubmit\)' features/contacts/ContactsPage.tsxRepository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Read the ContactsPage file to understand the controller structure
wc -l features/contacts/ContactsPage.tsxRepository: thaleslaray/nossocrm
Length of output: 103
🏁 Script executed:
#!/bin/bash
# Search for useSettingsController or similar controller hook
rg 'useSettingsController\|useContactsController\|controller.*=' features/contacts/ContactsPage.tsx | head -10Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Read the ContactsPage file to understand data flow
cat -n features/contacts/ContactsPage.tsx | head -150Repository: thaleslaray/nossocrm
Length of output: 7604
🏁 Script executed:
#!/bin/bash
# Continue reading ContactsPage to find CompanyFormModal usage
cat -n features/contacts/ContactsPage.tsx | sed -n '150,220p'Repository: thaleslaray/nossocrm
Length of output: 3850
🏁 Script executed:
#!/antml/bash
# Search for the useContactsController hook to understand what data it manages
fd -name 'useContactsController*' -type fRepository: thaleslaray/nossocrm
Length of output: 295
🏁 Script executed:
#!/bin/bash
# Find the useContactsController hook
fd 'useContactsController' -type fRepository: thaleslaray/nossocrm
Length of output: 235
🏁 Script executed:
#!/bin/bash
# Read the useContactsController hook
cat -n features/contacts/hooks/useContactsController.ts | head -150Repository: thaleslaray/nossocrm
Length of output: 7169
🏁 Script executed:
#!/bin/bash
# Continue reading useContactsController to find editingCompany state and handleCompanySubmit
cat -n features/contacts/hooks/useContactsController.ts | sed -n '150,300p'Repository: thaleslaray/nossocrm
Length of output: 6336
🏁 Script executed:
#!/bin/bash
# Search for where custom_fields might be populated/set in the companies data
rg 'custom_fields' -B 3 -A 3 lib/query/hooks/useContactsQuery.ts 2>/dev/null || echo "Not found in useContactsQuery"Repository: thaleslaray/nossocrm
Length of output: 94
🏁 Script executed:
#!/antml:bash
# Check if there's any data transformation or query that adds custom_fields
fd 'useContactsQuery' -type fRepository: thaleslaray/nossocrm
Length of output: 235
🏁 Script executed:
#!/bin/bash
# Find the useContactsQuery file
find features/contacts/hooks -name '*useContactsQuery*'Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Search for where companies are fetched and if custom_fields are added
rg 'useCompanies' -B 2 -A 10 | head -50Repository: thaleslaray/nossocrm
Length of output: 3212
🏁 Script executed:
#!/bin/bash
# Find the companiesService to see what getAll() returns
rg 'companiesService' -lRepository: thaleslaray/nossocrm
Length of output: 232
🏁 Script executed:
#!/an/bash
# Search for where companiesService is defined
rg 'export.*companiesService\|const companiesService' -B 2 -A 5Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Look at lib/supabase/contacts.ts for companiesService
cat -n lib/supabase/contacts.ts | head -100Repository: thaleslaray/nossocrm
Length of output: 3771
🏁 Script executed:
#!/bin/bash
# Search for companiesService definition more broadly
rg 'companiesService.*=' lib/ -B 2 -A 5Repository: thaleslaray/nossocrm
Length of output: 476
🏁 Script executed:
#!/bin/bash
# Find the getAll method in companiesService
rg -A 30 'companiesService = \{' lib/supabase/contacts.ts | grep -A 25 'getAll'Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Read more of the companiesService to find getAll
cat -n lib/supabase/contacts.ts | sed -n '150,300p'Repository: thaleslaray/nossocrm
Length of output: 6674
Remove as any casts and extend the Company type to include custom_fields.
The as any casts at lines 26 and 50 bypass TypeScript strict mode, removing compile-time safety for the custom_fields property. Per coding guidelines, TypeScript 5.x must run with strict mode enabled. The Organization interface (which Company aliases) does not currently define custom_fields, and the database schema for crm_companies does not include this column either.
To fix this properly:
- Add
custom_fields?: { num_funcionarios?: number; cnae?: string; nrs_aplicaveis?: string; data_ultimo_aso?: string }to theOrganizationinterface in@/types - Remove the
as anycasts and use optional chaining directly:editingCompany?.custom_fields ?? {} - Ensure the database schema and queries include this field if custom fields are meant to be persisted
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@features/contacts/components/CompanyFormModal.tsx` at line 26, The code is
using an unsafe cast (editingCompany as any) to access custom_fields; instead
extend the Organization interface in your types module (add optional property
custom_fields?: { num_funcionarios?: number; cnae?: string; nrs_aplicaveis?:
string; data_ultimo_aso?: string }) so the Company/Organization type includes
custom_fields, then remove the casts in CompanyFormModal.tsx and access with
optional chaining (editingCompany?.custom_fields ?? {}); also review any DB
schema/queries that persist crm_companies to ensure custom_fields is persisted
or handled appropriately if it should be stored.
| if (data.type === "MEETING") { | ||
| const startDate = data.date ? new Date(data.date) : null; | ||
| if (startDate && !isNaN(startDate.getTime())) { | ||
| fetch("https://n8n-production-9012a.up.railway.app/webhook/0ebbdfef-a03e-4109-bdce-7d00e70218f0", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" }) | ||
| }).catch((err) => console.error("[Calendar] Webhook error:", err)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Move webhook URL out of client bundle and per-organization config.
The webhook URL is hardcoded into a client-side hook, so it ships in the browser bundle, fires for every tenant, cannot be disabled per organization, and routes user PII (email, meeting title/description) to a single external endpoint regardless of tenant. This breaks multi-tenant isolation and makes the integration impossible to audit or rotate without a code deploy.
Recommended approach:
- Store the webhook URL (and an enable flag) in
organization_settings, retrieved via the existing org-config pattern (similar togetOrgAIConfig). - Trigger the webhook from a server-side route (Route Handler) or Edge Function rather than from the browser, so the URL and any auth header stay off the client and an
organization_idcan be enforced/logged. - Sanitize the URL with
sanitizeUrl()from@/lib/utils/sanitize.tsbefore callingfetch.
As per coding guidelines: "Store AI provider API keys in organization_settings database table, not in environment variables; retrieve via getOrgAIConfig(orgId)" — the same principle applies to per-tenant webhook destinations, and "All database queries must filter by organization_id to ensure multi-tenant security" implies external integrations should also be tenant-scoped.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/query/hooks/useActivitiesQuery.ts` around lines 190 - 199, The
client-side webhook call in useActivitiesQuery.ts (inside the useActivitiesQuery
hook) must be removed and replaced with a server-side trigger: add
per-organization webhook settings to the organization_settings table and expose
them via the existing org-config pattern (similar to getOrgAIConfig) so each org
has an enabled flag and webhook_url; create a Route Handler/Edge Function
endpoint that accepts a minimal meeting payload (title, description, start_time,
end_time, organization_id, attendee_email) and from server-side reads the
organization_settings (filtering by organization_id) to get and sanitize the
webhook_url with sanitizeUrl() before calling fetch, include any auth header
from settings, and log the organization_id; update useActivitiesQuery to POST to
this new internal endpoint instead of calling the hardcoded external URL and
ensure no PII or webhook URLs remain in the client bundle.
| fetch("https://n8n-production-9012a.up.railway.app/webhook/0ebbdfef-a03e-4109-bdce-7d00e70218f0", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" }) |
There was a problem hiding this comment.
end_time hardcoded to start + 1 hour.
Every meeting is reported as exactly 60 minutes, regardless of the user’s actual scheduling. Calendar events created from this webhook will have wrong durations for any non‑1h meeting. If the Activity model doesn’t carry an explicit end time, either:
- Add an
endDate/durationMinutesto the create payload and use it here, or - Make the default duration configurable per organization rather than baked into the hook.
🛠️ Suggested adjustment (assuming an optional duration is added to the activity payload)
- body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" })
+ body: JSON.stringify({
+ title: data.title,
+ description: data.description || data.dealTitle,
+ start_time: startDate.toISOString(),
+ end_time: new Date(
+ startDate.getTime() + (data.durationMinutes ?? 60) * 60_000
+ ).toISOString(),
+ attendees: user?.email || '',
+ })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/query/hooks/useActivitiesQuery.ts` at line 196, The request body
currently hardcodes end_time to start_time plus 1 hour in useActivitiesQuery.ts
(where the payload is built for the webhook), causing incorrect durations;
update the payload construction in that function to compute end_time from a
provided Activity field (preferably data.endDate or data.durationMinutes) and
fall back to a per-organization default duration if those are absent (read the
org default from the existing org/config API or pass it into the hook), and
ensure the body uses the computed ISO string for end_time instead of new
Date(startDate.getTime() + 3600000).
| onSuccess: (data, { dealId }) => { | ||
| queryClient.setQueryData(DEALS_VIEW_KEY, (old) => { | ||
| if (!old) return old; | ||
| return old.map((d) => | ||
| d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d | ||
| ); | ||
| }); | ||
| }, |
There was a problem hiding this comment.
onSettled invalidation cascades to DEALS_VIEW_KEY, undoing this optimization; also missing <DealView[]> generic.
Two concerns with the new direct cache update:
-
The unchanged
onSettledat line 530 callsqueryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }). BecauseDEALS_VIEW_KEY = [...queryKeys.deals.lists(), 'view'], TanStack's prefix matching will invalidateDEALS_VIEW_KEYas well, triggering an immediate refetch right after thissetQueryData. That largely defeats the "optimize deal item cache updates" intent, and it diverges from the pattern used everywhere else in this file (e.g.useUpdateDeal,useDeleteDeal) where the explicit comment is "NÃO fazer invalidateQueries para deals - Realtime gerencia a sincronização". -
setQueryDatais called without the<DealView[]>generic, sooldis inferred asunknownandold.map(...)is not type-safe under strict mode. Every other call site in this file passes the generic explicitly (e.g. lines 272, 299, 363, 487).
Based on learnings: "For Deals entity mutations, always use [...queryKeys.deals.lists(), 'view'] cache key pattern" and "Prefer setQueryData over invalidateQueries for instant UI updates".
♻️ Proposed fix
- onSuccess: (data, { dealId }) => {
- queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
- if (!old) return old;
- return old.map((d) =>
- d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
- );
- });
- },
- onSettled: (_data, _error, { dealId }) => {
- queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
- queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
- },
+ onSuccess: (data, { dealId }) => {
+ queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
+ if (!old) return old;
+ return old.map((d) =>
+ d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
+ );
+ });
+ },
+ onSettled: (_data, _error, { dealId }) => {
+ // NÃO invalidar queryKeys.deals.lists() — cascateia para DEALS_VIEW_KEY
+ // e desfaz o setQueryData acima. Realtime mantém a sincronização.
+ queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onSuccess: (data, { dealId }) => { | |
| queryClient.setQueryData(DEALS_VIEW_KEY, (old) => { | |
| if (!old) return old; | |
| return old.map((d) => | |
| d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d | |
| ); | |
| }); | |
| }, | |
| onSuccess: (data, { dealId }) => { | |
| queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => { | |
| if (!old) return old; | |
| return old.map((d) => | |
| d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d | |
| ); | |
| }); | |
| }, | |
| onSettled: (_data, _error, { dealId }) => { | |
| // NÃO invalidar queryKeys.deals.lists() — cascateia para DEALS_VIEW_KEY | |
| // e desfaz o setQueryData acima. Realtime mantém a sincronização. | |
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) }); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/query/hooks/useDealsQuery.ts` around lines 520 - 527, The direct cache
update in the onSuccess handler for adding an item uses queryClient.setQueryData
without the <DealView[]> generic (so old is inferred as unknown) and is
immediately undone because the existing onSettled still calls
queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }) which
prefix-invalidates DEALS_VIEW_KEY; fix by adding the explicit generic to
setQueryData (setQueryData<DealView[]>) and stop invalidating the parent lists
key in the onSettled for this mutation (either remove or narrow the
invalidateQueries call so it does not target queryKeys.deals.lists()) so the
optimized in-place update to DEALS_VIEW_KEY persists and remains type-safe.
| onSuccess: (data) => { | ||
| queryClient.setQueryData(DEALS_VIEW_KEY, (old) => { | ||
| if (!old) return old; | ||
| return old.map((d) => | ||
| d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d | ||
| ); | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Same cascade-invalidation and missing-generic issues as useAddDealItem.
The onSettled at line 557 invalidates queryKeys.deals.lists(), which prefix-matches DEALS_VIEW_KEY and triggers a refetch immediately after the optimization here. Also missing <DealView[]> generic on setQueryData, leaving old as unknown.
Based on learnings: "For Deals entity mutations, always use [...queryKeys.deals.lists(), 'view'] cache key pattern" and "Prefer setQueryData over invalidateQueries for instant UI updates".
♻️ Proposed fix
- onSuccess: (data) => {
- queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
- if (!old) return old;
- return old.map((d) =>
- d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d
- );
- });
- },
- onSettled: (_data, _error, { dealId }) => {
- queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
- queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
- },
+ onSuccess: (data) => {
+ queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
+ if (!old) return old;
+ return old.map((d) =>
+ d.id === data.dealId
+ ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) }
+ : d
+ );
+ });
+ },
+ onSettled: (_data, _error, { dealId }) => {
+ // Mesmo motivo de useAddDealItem: lists() cascateia em DEALS_VIEW_KEY.
+ queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onSuccess: (data) => { | |
| queryClient.setQueryData(DEALS_VIEW_KEY, (old) => { | |
| if (!old) return old; | |
| return old.map((d) => | |
| d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d | |
| ); | |
| }); | |
| }, | |
| onSuccess: (data) => { | |
| queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => { | |
| if (!old) return old; | |
| return old.map((d) => | |
| d.id === data.dealId | |
| ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } | |
| : d | |
| ); | |
| }); | |
| }, | |
| onSettled: (_data, _error, { dealId }) => { | |
| // Mesmo motivo de useAddDealItem: lists() cascateia em DEALS_VIEW_KEY. | |
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) }); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/query/hooks/useDealsQuery.ts` around lines 547 - 554, The current
onSuccess handler updates the DEALS_VIEW_KEY optimistically but uses
setQueryData without a generic (leaving old as unknown) and then onSettled calls
invalidateQueries(queryKeys.deals.lists()) which prefix-matches and immediately
refetches, negating the optimization; fix by changing setQueryData to use the
explicit generic setQueryData<DealView[]> (so old is typed) and update only the
specific view cache key pattern [...queryKeys.deals.lists(), 'view'] (i.e. use
queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, ...) where DEALS_VIEW_KEY
is the view key), and change the onSettled invalidation to invalidate only
[...queryKeys.deals.lists(), 'view'] (or remove the invalidate if you prefer
relying on setQueryData) so there is no cascade invalidation from
queryKeys.deals.lists().
| // ============ COMPANY SCHEMAS ============ | ||
|
|
||
| export const companyFormSchema = z.object({ | ||
| num_funcionarios: z.coerce.number().int().min(0).optional(), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Zod 4.3.6, does z.coerce.number().optional() treat an empty string input as undefined or coerce it to 0?
💡 Result:
In Zod 4.3.6, z.coerce.number.optional coerces an empty string input ("") to 0, not undefined. The coercion step uses JavaScript's Number(""), which returns 0. This 0 is then passed to the inner z.number validation (which accepts 0) and the .optional wrapper. Since 0 is defined and valid, the output is 0 | undefined (from optional), but specifically 0 for empty string input. This behavior is confirmed by: - Official test in Zod 4.3.6 source: expect(schema.parse("")).toEqual(0); for z.coerce.number. - Multiple GitHub issues/discussions reporting z.coerce.number.parse("") returns 0. - Documentation stating z.coerce.number uses Number(input). No changes to this behavior in 4.3.x releases. Users often use preprocess/transform/pipe for custom empty string handling (e.g., to undefined).
Citations:
- 1: z.coerce.number() defaults empty strings to 0 colinhacks/zod#2461
- 2: z.coerce.number() defaults empty strings to 0 colinhacks/zod#2814
- 3: https://www.mintlify.com/colinhacks/zod/api/utilities/coerce
- 4: https://cdn.jsdelivr.net/npm/zod@4.3.6/src/v4/classic/tests/coerce.test.ts
- 5: https://zod.dev/CHANGELOG
- 6: https://github.com/colinhacks/zod/releases/tag/v4.3.0
- 7: https://zod.dev/CHANGELOG?id=_122111x64x40x33x2ex3046x30x2dx61x6c112104x61x2ex35
🏁 Script executed:
git ls-files | grep -E 'schemas\.(ts|js)' | head -20Repository: thaleslaray/nossocrm
Length of output: 171
🏁 Script executed:
wc -l lib/validations/schemas.tsRepository: thaleslaray/nossocrm
Length of output: 95
🏁 Script executed:
sed -n '110,135p' lib/validations/schemas.tsRepository: thaleslaray/nossocrm
Length of output: 1195
Preprocess empty string input to undefined to avoid silent coercion to 0.
<input type="number"> with an empty value yields '' from react-hook-form. The current schema coerces '' to 0 (only undefined bypasses .optional()), so clearing the field persists 0 employees instead of leaving it unspecified. Use .preprocess() to convert empty strings to undefined:
♻️ Proposed fix
- num_funcionarios: z.coerce.number().int().min(0).optional(),
+ num_funcionarios: z
+ .preprocess(
+ (val) => (val === '' || val === null ? undefined : val),
+ z.coerce.number().int().min(0).optional()
+ ),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| num_funcionarios: z.coerce.number().int().min(0).optional(), | |
| num_funcionarios: z | |
| .preprocess( | |
| (val) => (val === '' || val === null ? undefined : val), | |
| z.coerce.number().int().min(0).optional() | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/validations/schemas.ts` at line 123, The num_funcionarios schema silently
coerces an empty string to 0 causing cleared numeric inputs to persist as 0;
update the num_funcionarios definition to first preprocess the value (in the
schema for num_funcionarios) converting '' to undefined, then apply
z.coerce.number().int().min(0).optional() so that empty string becomes undefined
and respects .optional()—locate the num_funcionarios symbol in the schema and
wrap the existing coercion with a .preprocess(fn) that returns undefined for ''
and the original value otherwise.
| num_funcionarios: z.coerce.number().int().min(0).optional(), | ||
| cnae: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)), | ||
| nrs_aplicaveis: optionalString.pipe(z.string().max(500)), | ||
| data_ultimo_aso: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether a migration adds custom_fields to crm_companies and whether
# the create/update paths and read selects include it.
fd -e sql . supabase/migrations | xargs rg -n -C2 'crm_companies'
echo '--- handleCompanySubmit / mutations ---'
rg -nP -C3 'handleCompanySubmit|crm_companies' --type=ts --type=tsx
echo '--- API routes for companies ---'
fd -t f -e ts . app/api | xargs rg -n -C3 'crm_companies|custom_fields'Repository: thaleslaray/nossocrm
Length of output: 14680
🏁 Script executed:
sed -n '116,126p' supabase/migrations/20251201000000_schema_init.sqlRepository: thaleslaray/nossocrm
Length of output: 470
🏁 Script executed:
rg -n 'handleCompanySubmit' --type=ts --type=jsx --type=jsRepository: thaleslaray/nossocrm
Length of output: 92
🏁 Script executed:
rg -n 'custom_fields' --type=ts --type=jsx --type=jsRepository: thaleslaray/nossocrm
Length of output: 92
🏁 Script executed:
rg 'handleCompanySubmit' -nRepository: thaleslaray/nossocrm
Length of output: 389
🏁 Script executed:
rg 'num_funcionarios|cnae|nrs_aplicaveis|data_ultimo_aso' -nRepository: thaleslaray/nossocrm
Length of output: 2080
🏁 Script executed:
rg 'companyFormSchema|CompanyFormSchema' -nRepository: thaleslaray/nossocrm
Length of output: 664
🏁 Script executed:
sed -n '200,230p' features/contacts/hooks/useContactsController.tsRepository: thaleslaray/nossocrm
Length of output: 1265
🏁 Script executed:
sed -n '60,75p' features/contacts/components/CompanyFormModal.tsxRepository: thaleslaray/nossocrm
Length of output: 412
🏁 Script executed:
rg 'createCompanyMutation|updateCompanyMutation' -A5 -B2 features/contacts/hooks/useContactsController.ts | head -50Repository: thaleslaray/nossocrm
Length of output: 1982
🏁 Script executed:
sed -n '64,70p' features/contacts/components/CompanyFormModal.tsxRepository: thaleslaray/nossocrm
Length of output: 181
🏁 Script executed:
rg 'export.*useCreateCompany|export.*useUpdateCompany' -A30 -B2 | head -80Repository: thaleslaray/nossocrm
Length of output: 3308
🏁 Script executed:
rg 'useCreateCompany|useUpdateCompany' --type=ts lib/hooks features/contacts/hooks --files-with-matchesRepository: thaleslaray/nossocrm
Length of output: 113
🏁 Script executed:
rg 'companiesService' -B2 -A2 lib/query/hooks/useContactsQuery.ts | head -20Repository: thaleslaray/nossocrm
Length of output: 981
🏁 Script executed:
fd -type f -name '*companies*service*' -o -name '*service*' lib/ | grep -i companRepository: thaleslaray/nossocrm
Length of output: 235
🏁 Script executed:
rg 'const companiesService|export.*companiesService' -A5Repository: thaleslaray/nossocrm
Length of output: 1211
🏁 Script executed:
sed -n '/^export const companiesService/,/^export const [a-zA-Z]/p' lib/supabase/contacts.ts | head -150Repository: thaleslaray/nossocrm
Length of output: 5117
Form collects fields that are discarded before database persistence.
The form (CompanyFormModal.tsx) renders and validates num_funcionarios, cnae, nrs_aplicaveis, and data_ultimo_aso using companyFormSchema, but handleCompanySubmit accepts only { name, industry, website }. These four fields are parsed by validation but never passed to the mutation, and companiesService.create() and companiesService.update() only handle name, industry, and website. The underlying crm_companies table has no columns to store the new fields. Users will enter data in the form with no indication it won't be saved.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/validations/schemas.ts` around lines 123 - 126, The schema is validating
fields (num_funcionarios, cnae, nrs_aplicaveis, data_ultimo_aso) that are never
persisted because handleCompanySubmit only sends { name, industry, website } and
companiesService.create/update and the crm_companies table lack these columns;
either stop validating/displaying unused fields or wire them end-to-end. Fix by
one of two options: (A) Remove these four fields from companyFormSchema and from
CompanyFormModal.tsx so validation and UI match companiesService inputs, or (B)
Add them to the persistence layer: extend the DTO/type used by
handleCompanySubmit to include num_funcionarios, cnae, nrs_aplicaveis,
data_ultimo_aso; update companiesService.create and companiesService.update to
accept and pass these fields through; add corresponding columns to the
crm_companies table (with a DB migration) and update any repository/ORM mappings
so the new fields are saved and returned.
| { "path": "/api/cron/daily-briefing", "schedule": "0 8 * * 1-5" }, | ||
| { "path": "/api/cron/template-sync", "schedule": "0 6 * * *" }, | ||
| { "path": "/api/cron/stage-evaluations", "schedule": "* * * * *" } | ||
| { "path": "/api/cron/stage-evaluations", "schedule": "0 0 * * *" } |
There was a problem hiding this comment.
Daily schedule likely breaks stage-evaluation SLA and can create queue backlog.
On Line 13, changing /api/cron/stage-evaluations to 0 0 * * * can delay stage advancement/HITL outcomes by up to 24 hours, even though evaluations are enqueued continuously after AI replies and processed in small batches. This materially changes behavior and risks stale pipeline state.
Please keep this cron high-frequency (e.g., every minute or every few minutes), or pair a lower frequency with a larger batch/parallelization strategy and explicit product acceptance of delayed stage updates.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@vercel.json` at line 13, The cron entry for the route
"/api/cron/stage-evaluations" was changed to "0 0 * * *", which will run once
daily and create a backlog/stale state; revert or update this schedule to a
high-frequency cron (e.g., every minute or every few minutes like "*/1 * * * *"
or "*/5 * * * *") to preserve current near-real-time processing, or if you must
keep a low frequency, implement a compensating fix in the stage evaluation
handler (the code behind "/api/cron/stage-evaluations") to process much larger
batches and add parallelization and explicit product-level acceptance of delayed
updates before changing the schedule.
Summary by CodeRabbit
New Features
Improvements