Skip to content

Commit 8818719

Browse files
committed
chore: merge main
2 parents 0ebac91 + 8350898 commit 8818719

File tree

20 files changed

+224
-89
lines changed

20 files changed

+224
-89
lines changed

__test__/auth/otp/delivery.test.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
// @ts-expect-error bun:test é reconhecido apenas pelo runner do Bun
2+
import { describe, it, expect, beforeEach, vi } from 'bun:test';
13
import { MetaObject } from '@imports/model/MetaObject';
24
import queueManager from '@imports/queue/QueueManager';
35

46
// Mock WhatsApp module BEFORE importing delivery
5-
const mockSendOtpViaWhatsApp = jest.fn();
6-
jest.mock('@imports/auth/otp/whatsapp', () => ({
7+
const mockSendOtpViaWhatsApp = vi.fn();
8+
vi.mock('@imports/auth/otp/whatsapp', () => ({
79
sendOtpViaWhatsApp: mockSendOtpViaWhatsApp,
810
}));
911

@@ -23,17 +25,17 @@ describe('OTP Delivery Service', () => {
2325
};
2426

2527
beforeEach(() => {
26-
jest.clearAllMocks();
28+
vi.clearAllMocks();
2729

2830
// Mock MetaObject.Collections
2931
if (MetaObject.Collections == null) {
3032
(MetaObject as any).Collections = {};
3133
}
3234
(MetaObject.Collections.User as any) = {
33-
findOne: jest.fn().mockResolvedValue(mockUser),
35+
findOne: vi.fn().mockResolvedValue(mockUser),
3436
};
3537
(MetaObject.Collections.Message as any) = {
36-
insertOne: jest.fn().mockResolvedValue({}),
38+
insertOne: vi.fn().mockResolvedValue({}),
3739
};
3840

3941
// Mock Namespace with WhatsApp config
@@ -57,7 +59,7 @@ describe('OTP Delivery Service', () => {
5759
mockSendOtpViaWhatsApp.mockResolvedValue({ success: false, error: 'WhatsApp API error' });
5860

5961
// RabbitMQ fails - configure QueueConfig
60-
const mockSendMessage = jest.fn().mockResolvedValue({ success: false });
62+
const mockSendMessage = vi.fn().mockResolvedValue({ success: false });
6163
(queueManager.sendMessage as any) = mockSendMessage;
6264

6365
(MetaObject.Namespace as any).otpConfig = {
@@ -95,7 +97,7 @@ describe('OTP Delivery Service', () => {
9597

9698
it('should fallback to RabbitMQ if WhatsApp fails', async () => {
9799
mockSendOtpViaWhatsApp.mockResolvedValue({ success: false, error: 'WhatsApp unavailable' });
98-
const mockSendMessage = jest.fn().mockResolvedValue({ success: true });
100+
const mockSendMessage = vi.fn().mockResolvedValue({ success: true });
99101
(queueManager.sendMessage as any) = mockSendMessage;
100102

101103
(MetaObject.Namespace as any).otpConfig = {
@@ -129,8 +131,8 @@ describe('OTP Delivery Service', () => {
129131

130132
it('should return error if all delivery methods fail', async () => {
131133
mockSendOtpViaWhatsApp.mockResolvedValue({ success: false, error: 'WhatsApp error' });
132-
(queueManager.sendMessage as any) = jest.fn().mockResolvedValue({ success: false });
133-
(MetaObject.Collections.Message.insertOne as any) = jest.fn().mockRejectedValue(new Error('Email error'));
134+
(queueManager.sendMessage as any) = vi.fn().mockResolvedValue({ success: false });
135+
(MetaObject.Collections.Message.insertOne as any) = vi.fn().mockRejectedValue(new Error('Email error'));
134136

135137
const result = await sendOtp('+5511999999999', undefined, '123456', 'test-user-id', mockOtpRequest.expiresAt);
136138

@@ -140,9 +142,9 @@ describe('OTP Delivery Service', () => {
140142

141143
it('should handle user without email gracefully', async () => {
142144
mockSendOtpViaWhatsApp.mockResolvedValue({ success: false, error: 'WhatsApp error' });
143-
(queueManager.sendMessage as any) = jest.fn().mockResolvedValue({ success: false });
145+
(queueManager.sendMessage as any) = vi.fn().mockResolvedValue({ success: false });
144146

145-
(MetaObject.Collections.User.findOne as any) = jest.fn().mockResolvedValue({
147+
(MetaObject.Collections.User.findOne as any) = vi.fn().mockResolvedValue({
146148
...mockUser,
147149
emails: [],
148150
});
@@ -154,7 +156,7 @@ describe('OTP Delivery Service', () => {
154156
});
155157

156158
it('should send directly via email when requested by email', async () => {
157-
(MetaObject.Collections.Message.insertOne as any) = jest.fn().mockResolvedValue({});
159+
(MetaObject.Collections.Message.insertOne as any) = vi.fn().mockResolvedValue({});
158160

159161
const result = await sendOtp(undefined, 'user@example.com', '123456', 'test-user-id', mockOtpRequest.expiresAt);
160162

docs/postman/Konecty-API.postman_collection.json

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,13 @@
475475
"name": "Find Stream",
476476
"request": {
477477
"method": "GET",
478-
"header": [],
478+
"header": [
479+
{
480+
"key": "Authorization",
481+
"value": "{{authToken}}",
482+
"type": "text"
483+
}
484+
],
479485
"url": {
480486
"raw": "{{baseUrl}}/rest/stream/Opportunity/findStream?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\"]}}&limit=100&sort=[{\"property\":\"_id\",\"direction\":\"ASC\"}]",
481487
"host": ["{{baseUrl}}"],
@@ -614,7 +620,13 @@
614620
"name": "Find Stream - Contact",
615621
"request": {
616622
"method": "GET",
617-
"header": [],
623+
"header": [
624+
{
625+
"key": "Authorization",
626+
"value": "{{authToken}}",
627+
"type": "text"
628+
}
629+
],
618630
"url": {
619631
"raw": "{{baseUrl}}/rest/stream/Contact/findStream?limit=50",
620632
"host": ["{{baseUrl}}"],
@@ -634,7 +646,13 @@
634646
"name": "Find Stream - With Filter",
635647
"request": {
636648
"method": "GET",
637-
"header": [],
649+
"header": [
650+
{
651+
"key": "Authorization",
652+
"value": "{{authToken}}",
653+
"type": "text"
654+
}
655+
],
638656
"url": {
639657
"raw": "{{baseUrl}}/rest/stream/Opportunity/findStream?filter={\"match\":\"and\",\"conditions\":[{\"field\":\"status\",\"operator\":\"in\",\"value\":[\"Nova\",\"Em Visitação\",\"Ofertando Imóveis\",\"Proposta\",\"Contrato\"]}]}&limit=1000",
640658
"host": ["{{baseUrl}}"],
@@ -658,7 +676,13 @@
658676
"name": "Pivot Table",
659677
"request": {
660678
"method": "GET",
661-
"header": [],
679+
"header": [
680+
{
681+
"key": "Authorization",
682+
"value": "{{authToken}}",
683+
"type": "text"
684+
}
685+
],
662686
"url": {
663687
"raw": "{{baseUrl}}/rest/data/Opportunity/pivot?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\",\"Contrato\",\"Ofertando Imóveis\",\"Proposta\"]}}&pivotConfig={\"rows\":[{\"field\":\"_user.director.nickname\",\"sort\":\"ASC\"},{\"field\":\"_user.group.name\",\"sort\":\"ASC\"},{\"field\":\"_user.name\",\"sort\":\"ASC\"}],\"columns\":[{\"field\":\"status\"}],\"values\":[{\"field\":\"code\",\"aggregator\":\"count\"},{\"field\":\"amount.value\",\"aggregator\":\"sum\"}]}",
664688
"host": ["{{baseUrl}}"],
@@ -763,7 +787,13 @@
763787
"name": "Graph - Bar Chart by Status",
764788
"request": {
765789
"method": "GET",
766-
"header": [],
790+
"header": [
791+
{
792+
"key": "Authorization",
793+
"value": "{{authToken}}",
794+
"type": "text"
795+
}
796+
],
767797
"url": {
768798
"raw": "{{baseUrl}}/rest/data/Opportunity/graph?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\",\"Contrato\",\"Ofertando Imóveis\",\"Proposta\"]}}&graphConfig={\"type\":\"bar\",\"categoryField\":\"status\",\"aggregation\":\"count\",\"xAxis\":{\"field\":\"status\",\"label\":\"Status\"},\"yAxis\":{\"field\":\"code\",\"label\":\"Quantidade\"},\"title\":\"Oportunidades por Status\"}",
769799
"host": ["{{baseUrl}}"],
@@ -789,7 +819,13 @@
789819
"name": "Graph - Bar Chart by Status (Sum Values)",
790820
"request": {
791821
"method": "GET",
792-
"header": [],
822+
"header": [
823+
{
824+
"key": "Authorization",
825+
"value": "{{authToken}}",
826+
"type": "text"
827+
}
828+
],
793829
"url": {
794830
"raw": "{{baseUrl}}/rest/data/Opportunity/graph?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\",\"Contrato\",\"Ofertando Imóveis\",\"Proposta\"]}}&graphConfig={\"type\":\"bar\",\"categoryField\":\"status\",\"aggregation\":\"sum\",\"xAxis\":{\"field\":\"status\",\"label\":\"Status\"},\"yAxis\":{\"field\":\"amount.value\",\"label\":\"Valor Total\"},\"title\":\"Valor Total por Status\"}",
795831
"host": ["{{baseUrl}}"],
@@ -815,7 +851,13 @@
815851
"name": "Graph - Bar Chart by Director",
816852
"request": {
817853
"method": "GET",
818-
"header": [],
854+
"header": [
855+
{
856+
"key": "Authorization",
857+
"value": "{{authToken}}",
858+
"type": "text"
859+
}
860+
],
819861
"url": {
820862
"raw": "{{baseUrl}}/rest/data/Opportunity/graph?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\",\"Contrato\",\"Ofertando Imóveis\",\"Proposta\"]}}&graphConfig={\"type\":\"bar\",\"categoryField\":\"_user.director.nickname\",\"aggregation\":\"count\",\"xAxis\":{\"field\":\"_user.director.nickname\",\"label\":\"Diretor\"},\"yAxis\":{\"field\":\"code\",\"label\":\"Quantidade\"},\"title\":\"Oportunidades por Diretor\"}",
821863
"host": ["{{baseUrl}}"],
@@ -841,7 +883,13 @@
841883
"name": "Graph - Pie Chart",
842884
"request": {
843885
"method": "GET",
844-
"header": [],
886+
"header": [
887+
{
888+
"key": "Authorization",
889+
"value": "{{authToken}}",
890+
"type": "text"
891+
}
892+
],
845893
"url": {
846894
"raw": "{{baseUrl}}/rest/data/Opportunity/graph?filter={\"status\":{\"$in\":[\"Nova\",\"Em Visitação\",\"Contrato\",\"Ofertando Imóveis\",\"Proposta\"]}}&graphConfig={\"type\":\"pie\",\"categoryField\":\"status\",\"aggregation\":\"count\",\"yAxis\":{\"field\":\"code\"},\"title\":\"Distribuição por Status\"}",
847895
"host": ["{{baseUrl}}"],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"prebuild": "rimraf dist",
1111
"build:login-css": "tailwindcss -i src/private/templates/login/modern-tailwind-input.css -o src/private/templates/login/modern-tailwind-output.css --minify",
1212
"build": "yarn build:login-css && babel src --out-dir dist --no-copy-ignored --extensions '.ts,.js'",
13-
"lint": "eslint src/**/*.ts src/**/*.js --fix",
13+
"lint": "eslint src/**/*.ts --fix",
1414
"prettier": "prettier --write src",
1515
"test": "jest --detectOpenHandles --runInBand",
1616
"prepare": "husky install"

src/imports/auth/info/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export async function userInfo(authTokenId?: string | null | undefined): Promise
3939
watermark: MetaObject.Namespace.watermark,
4040
addressComplementValidation: MetaObject.Namespace.addressComplementValidation,
4141
addressSource: MetaObject.Namespace.addressSource,
42+
enableCustomThemes: (MetaObject.Namespace as { enableCustomThemes?: boolean }).enableCustomThemes !== false,
4243
};
4344

4445
try {

src/imports/auth/otp/delivery.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ async function sendViaEmail(phoneNumber: string | undefined, otpCode: string, us
243243
/**
244244
* Main delivery function
245245
* If email is provided, sends directly via email only (no WhatsApp)
246-
* If phoneNumber is provided, tries: WhatsApp → RabbitMQ → Email
246+
* If phoneNumber is provided, tries: WhatsApp → RabbitMQ (no email fallback)
247247
*/
248248
export async function sendOtp(phoneNumber: string | undefined, email: string | undefined, otpCode: string, userId: string, expiresAt: Date): Promise<DeliveryResult> {
249249
if (email != null) {
@@ -252,7 +252,7 @@ export async function sendOtp(phoneNumber: string | undefined, email: string | u
252252
}
253253

254254
if (phoneNumber != null) {
255-
// Requested by phone - try WhatsApp → RabbitMQ → Email
255+
// Requested by phone - try WhatsApp → RabbitMQ (no email fallback)
256256
const whatsappResult = await sendViaWhatsApp(phoneNumber, otpCode);
257257
if (whatsappResult.success) {
258258
return whatsappResult;
@@ -265,17 +265,10 @@ export async function sendOtp(phoneNumber: string | undefined, email: string | u
265265
return rabbitmqResult;
266266
}
267267

268-
logger.warn(`RabbitMQ delivery failed: ${rabbitmqResult.error}. Trying email...`);
269-
270-
const emailResult = await sendViaEmail(phoneNumber, otpCode, userId, expiresAt);
271-
if (emailResult.success) {
272-
return emailResult;
273-
}
274-
275-
logger.error(`All delivery methods failed. WhatsApp: ${whatsappResult.error}, RabbitMQ: ${rabbitmqResult.error}, Email: ${emailResult.error}`);
268+
logger.error(`All phone delivery methods failed. WhatsApp: ${whatsappResult.error}, RabbitMQ: ${rabbitmqResult.error}`);
276269
return {
277270
success: false,
278-
error: emailResult.error ? `All delivery methods failed. ${emailResult.error}` : 'All delivery methods failed',
271+
error: `Failed to send OTP via phone: WhatsApp (${whatsappResult.error}), RabbitMQ (${rabbitmqResult.error})`,
279272
};
280273
}
281274

src/imports/auth/otp/whatsapp.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export interface WhatsAppConfig {
2020
export interface WhatsAppResult {
2121
success: boolean;
2222
error?: string;
23-
method?: 'whatsapp';
2423
}
2524

2625
/**

src/imports/auth/password/email.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { setPassword } from '@imports/auth/password';
1414
import { templatePath } from '../../utils/templatesPath';
1515
import { logger } from '../../utils/logger';
1616

17-
export async function setRandomPasswordAndSendByEmail({ authTokenId, userIds, host }) {
17+
export async function setRandomPasswordAndSendByEmail({ authTokenId, userIds, host, isNewUser = false }) {
18+
logger.debug(`[setRandomPasswordAndSendByEmail] isNewUser: ${isNewUser}, host: ${host}`);
1819
if (Array.isArray(userIds) === false) {
1920
return {
2021
success: false,
@@ -87,11 +88,17 @@ export async function setRandomPasswordAndSendByEmail({ authTokenId, userIds, ho
8788
url: loginUrl,
8889
};
8990

90-
const resetPasswordTemplatePath = path.join(templatePath(), 'email/resetPassword.html');
91+
// Seleciona o template apropriado baseado no contexto
92+
const templateName = isNewUser ? 'email/newUserPassword.html' : 'email/resetPassword.html';
93+
const emailSubject = isNewUser ? '[Auxiliadora Predial] Bem-vindo! Sua senha de acesso' : '[Konecty] Sua nova senha';
9194

92-
const resetPasswordTemplate = await fs.readFile(resetPasswordTemplatePath, 'utf8');
95+
logger.debug(`[setRandomPasswordAndSendByEmail] Template: ${templateName}, Subject: ${emailSubject}, isNewUser: ${isNewUser}`);
9396

94-
const template = Handlebars.compile(resetPasswordTemplate);
97+
const passwordTemplatePath = path.join(templatePath(), templateName);
98+
99+
const passwordTemplate = await fs.readFile(passwordTemplatePath, 'utf8');
100+
101+
const template = Handlebars.compile(passwordTemplate);
95102

96103
const html = template({
97104
password,
@@ -102,7 +109,7 @@ export async function setRandomPasswordAndSendByEmail({ authTokenId, userIds, ho
102109
_id: randomId(),
103110
from: 'Konecty <support@konecty.com>',
104111
to: get(userRecord, 'emails.0.address'),
105-
subject: '[Konecty] Sua nova senha',
112+
subject: emailSubject,
106113
body: html,
107114
type: 'Email',
108115
status: 'Send',

src/imports/data/api/findUtils.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function buildSortOptions(
3131
const sortResult = parseSortArray(sortArray);
3232

3333
return Object.keys(sortResult.data).reduce<Record<string, number | { $meta: string }>>((acc, key) => {
34-
const sortValue = typeof sortResult.data[key] === 'number' ? sortResult.data[key] : sortResult.data[key] === 'asc' ? 1 : -1;
34+
const sortValue = typeof sortResult.data[key] === 'number' ? sortResult.data[key] : (sortResult.data[key] === 'asc' ? 1 : -1);
3535

3636
if (get(metaObject, `fields.${key}.type`) === 'money') {
3737
acc[`${key}.value`] = sortValue;
@@ -70,7 +70,7 @@ function buildAccessConditionsForField(
7070
if (accessFieldConditions.READ != null) {
7171
const condition = filterConditionToFn(accessFieldConditions.READ, metaObject, { user });
7272
if (condition.success === false) {
73-
return condition as KonectyResultError;
73+
return condition;
7474
}
7575
if ((emptyFields === true && fieldsObject[fieldName] === 0) || (emptyFields !== true && fieldsObject[fieldName] === 1)) {
7676
Object.keys(condition.data).reduce((acc, conditionField) => {
@@ -127,7 +127,11 @@ function buildAccessConditionsMap(
127127
}, {});
128128
}
129129

130-
function calculateConditionsKeys(accessConditions: Record<string, Function>, projection: Record<string, unknown>, emptyFields: boolean): string[] {
130+
function calculateConditionsKeys(
131+
accessConditions: Record<string, Function>,
132+
projection: Record<string, unknown>,
133+
emptyFields: boolean,
134+
): string[] {
131135
const allKeys = Object.keys(accessConditions);
132136

133137
if (Object.keys(projection).length === 0) {
@@ -279,7 +283,7 @@ export async function buildFindQuery({
279283
// Special case: limit === -1 means "no limit" (used by pivot tables that need all data)
280284
const parsedLimit = parseInt(String(limit), 10);
281285
const noLimit = parsedLimit === -1;
282-
const effectiveLimit = noLimit ? undefined : _isNaN(limit) || limit == null || Number(limit) <= 0 ? DEFAULT_PAGE_SIZE : parsedLimit;
286+
const effectiveLimit = noLimit ? undefined : (_isNaN(limit) || limit == null || Number(limit) <= 0 ? DEFAULT_PAGE_SIZE : parsedLimit);
283287

284288
const queryOptions: FindOptions & { projection: Document } = {
285289
limit: effectiveLimit,
@@ -305,8 +309,8 @@ export async function buildFindQuery({
305309
}
306310

307311
tracingSpan?.addEvent('Calculating field permissions');
308-
const accessConditionsResult = Object.keys(metaObject.fields).map<KonectyResult<{ fieldName: string; condition: Function } | null>>(fieldName =>
309-
buildAccessConditionsForField(fieldName, metaObject, access, fieldsObject as Record<string, number | { $meta: string }>, emptyFields, user),
312+
const accessConditionsResult = Object.keys(metaObject.fields).map<KonectyResult<{ fieldName: string; condition: Function } | null>>(
313+
fieldName => buildAccessConditionsForField(fieldName, metaObject, access, fieldsObject as Record<string, number | { $meta: string }>, emptyFields, user),
310314
);
311315

312316
queryOptions.projection = clearProjectionPathCollision(fieldsObject);

src/imports/data/api/graphStream.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,10 @@ async function collectAndPopulateData(
307307
}
308308

309309
try {
310-
const lookupDocs = await collection.find({ _id: { $in: Array.from(ids) } }, { projection }).toArray();
310+
const lookupDocs = await collection.find(
311+
{ _id: { $in: Array.from(ids) } },
312+
{ projection },
313+
).toArray();
311314

312315
const dataMap = new Map<string, Record<string, unknown>>();
313316
for (const doc of lookupDocs) {
@@ -432,7 +435,6 @@ export default async function graphStream({
432435
const GRAPH_MAX_RECORDS = parseInt(process.env.GRAPH_MAX_RECORDS ?? '100000', 10);
433436
tracingSpan?.addEvent('Calling findStream to get data');
434437
logger.debug({ limit: GRAPH_MAX_RECORDS }, 'Graph query (filter omitted to avoid data leakage)');
435-
436438
const streamResult = await findStream({
437439
...findParams,
438440
fields: allFields.length > 0 ? allFields.join(',') : findParams.fields,
@@ -456,7 +458,6 @@ export default async function graphStream({
456458
const populatedData = await collectAndPopulateData(mongoStream, lookupFields);
457459
const collectTime = Date.now() - startCollect;
458460
logger.debug({ count: populatedData.length, collectTimeMs: collectTime, total: total ?? null }, 'Collected documents with populated lookups');
459-
460461
// Check if limit was reached using total count from find
461462
const totalRecords = total ?? populatedData.length;
462463
const limitReached = totalRecords > GRAPH_MAX_RECORDS;

0 commit comments

Comments
 (0)