diff --git a/FRONTEND_PEDIDO_SEVEN_MIGRATION.md b/FRONTEND_PEDIDO_SEVEN_MIGRATION.md new file mode 100644 index 0000000..31043bd --- /dev/null +++ b/FRONTEND_PEDIDO_SEVEN_MIGRATION.md @@ -0,0 +1,250 @@ +# Migracao do Frontend: `/pedidos/condvenda-7/:numped7` + +## O que mudou + +Antes, a API retornava uma lista achatada de itens: + +```ts +PedidoCondVenda7RowDto[] +``` + +Agora, a API retorna uma lista de grupos logisticos: + +```ts +PedidoCondVenda7GroupDto[] +``` + +Cada grupo ja vem separado no backend pela chave: + +```txt +numped|tipoentrega|dtentrega|codfilialretira +``` + +## Regra para o frontend + +- Nao agrupar por conta propria. +- Nao usar `hash` como chave de separacao. +- Nao inferir `tipoentrega` pelo primeiro item de uma lista mista. +- Gerar 1 etiqueta por grupo retornado pela API. + +## Novo formato de resposta + +```ts +type PedidoCondVenda7RowDto = { + numped: number + cgc: string + razaosocial: string + dtemissao: string + codfilial: number + filialvenda: string + codfilialretira: number + filialretira: string + data: string + numcar: number + codcli: number + cliente: string + codusur: number + vendedor: string + vlfrete: number + valor: number + tipoentrega: string + dtentrega: string | null + codprod: number + descricao: string + qt: number + hash: string +} + +type PedidoCondVenda7GroupDto = { + groupKey: string + numped: number + tipoentrega: string + dtentrega: string | null + codfilialretira: number + filialretira: string + cgc: string + razaosocial: string + dtemissao: string + codfilial: number + filialvenda: string + data: string + numcar: number + codcli: number + cliente: string + codusur: number + vendedor: string + vlfrete: number + valor: number + hash: string + items: PedidoCondVenda7RowDto[] +} +``` + +## Exemplo de resposta + +```json +[ + { + "groupKey": "157073710|RETIRA POSTERIOR|2026-03-11T00:00:00.000Z|4", + "numped": 157073710, + "tipoentrega": "RETIRA POSTERIOR", + "dtentrega": "2026-03-11T00:00:00.000Z", + "codfilialretira": 4, + "filialretira": "JURUNENSE BR", + "cgc": "13772792000407", + "razaosocial": "JURUNENSE HOME CENTER LTDA", + "dtemissao": "2026-03-17T18:58:51.000Z", + "codfilial": 4, + "filialvenda": "JURUNENSE BR", + "data": "2026-03-04T00:00:00.000Z", + "numcar": 0, + "codcli": 247485, + "cliente": "SORAIA CRISTINA SILVA DA COSTA", + "codusur": 157, + "vendedor": "BR - LOURIVAL EDVALDO COSTA FERREIRA", + "vlfrete": 0, + "valor": 827.83, + "hash": "4368D18F69C36357A1FD52271EF58861", + "items": [ + { + "numped": 157073710, + "tipoentrega": "RETIRA POSTERIOR", + "codprod": 12586, + "descricao": "ARG COZ E BANHEIROS 20KG QUARTZ", + "qt": 7, + "hash": "4368D18F69C36357A1FD52271EF58861" + } + ] + }, + { + "groupKey": "157073714|ENTREGA|2026-03-11T00:00:00.000Z|12", + "numped": 157073714, + "tipoentrega": "ENTREGA", + "dtentrega": "2026-03-11T00:00:00.000Z", + "codfilialretira": 12, + "filialretira": "JURUNENSE DISTRITO", + "cgc": "13772792001217", + "razaosocial": "JURUNENSE HOME CENTER LTDA", + "dtemissao": "2026-03-17T18:58:51.000Z", + "codfilial": 12, + "filialvenda": "JURUNENSE DISTRITO", + "data": "2026-03-04T00:00:00.000Z", + "numcar": 8899569, + "codcli": 247485, + "cliente": "SORAIA CRISTINA SILVA DA COSTA", + "codusur": 157, + "vendedor": "BR - LOURIVAL EDVALDO COSTA FERREIRA", + "vlfrete": 0, + "valor": 0, + "hash": "4368D18F69C36357A1FD52271EF58861", + "items": [ + { + "numped": 157073714, + "tipoentrega": "ENTREGA", + "codprod": 54001, + "descricao": "CER 66X66RT MILANO BG GR 2,18M", + "qt": 4, + "hash": "4368D18F69C36357A1FD52271EF58861" + }, + { + "numped": 157073714, + "tipoentrega": "ENTREGA", + "codprod": 54067, + "descricao": "CER 45X45 BEGE 45 2,00M", + "qt": 14, + "hash": "4368D18F69C36357A1FD52271EF58861" + } + ] + } +] +``` + +## O que mudar no frontend + +### 1. Tipagem + +Trocar o tipo esperado da resposta: + +```ts +const data: PedidoCondVenda7RowDto[] = await response.json() +``` + +por: + +```ts +const groups: PedidoCondVenda7GroupDto[] = await response.json() +``` + +### 2. Renderizacao + +Antes: + +```ts +renderLabel(data) +``` + +Agora: + +```ts +groups.forEach((group) => { + renderLabel(group) +}) +``` + +### 3. Cabecalho da etiqueta + +Os dados do cabecalho devem vir do grupo: + +```ts +group.numped +group.tipoentrega +group.dtentrega +group.cliente +group.filialretira +group.vendedor +group.hash +group.valor +``` + +### 4. Itens da etiqueta + +A tabela de produtos deve vir de: + +```ts +group.items +``` + +Exemplo: + +```ts +group.items.map((item) => ({ + codprod: item.codprod, + descricao: item.descricao, + qt: item.qt, +})) +``` + +## Erro antigo que nao pode voltar + +Se existir algo assim: + +```ts +const firstItem = data[0] +const tipoEntrega = firstItem.tipoentrega +``` + +isso precisa sair se `data` representar mais de um pedido misturado. + +Agora o correto e: + +```ts +const tipoEntrega = group.tipoentrega +const items = group.items +``` + +## Resumo operacional + +- 1 grupo retornado = 1 etiqueta +- cabecalho vem do grupo +- produtos vem de `group.items` +- separacao ja vem pronta do backend diff --git a/src/controller/pedido-seven.controller.ts b/src/controller/pedido-seven.controller.ts index 9942a78..08f4fce 100644 --- a/src/controller/pedido-seven.controller.ts +++ b/src/controller/pedido-seven.controller.ts @@ -8,7 +8,8 @@ import { ApiTags, } from '@nestjs/swagger'; import { PedidoSevenService } from '../services/pedido-seven.service'; -import { PedidoCondVenda7RowDto } from '../dto/pedido-condvenda7-row.dto'; +import { PedidoCondVenda7GroupDto } from '../dto/pedido-condvenda7-group.dto'; +import { PedidoSevenParamDto } from '../dto/pedido-seven-param.dto'; @ApiTags('Pedidos') @Controller('pedidos') @@ -21,12 +22,13 @@ export class PedidoSevenController { }) @ApiParam({ name: 'numped7', + type: Number, example: 123456, description: 'Valor para PCPEDC.NUMPEDENTFUT', }) @ApiOkResponse({ - description: 'Linhas retornadas pela consulta (pedido x itens)', - type: [PedidoCondVenda7RowDto], + description: 'Grupos logisticos retornados pela consulta (pedido x itens)', + type: [PedidoCondVenda7GroupDto], }) @ApiNotFoundResponse({ description: 'Pedido nao encontrado para o NUMPEDENTFUT informado', @@ -34,7 +36,9 @@ export class PedidoSevenController { @ApiServiceUnavailableResponse({ description: 'Banco de dados indisponivel (DataSource nao inicializado)', }) - async consultar(@Param('numped7', ParseIntPipe) numped7: number) { + async consultar( + @Param('numped7', ParseIntPipe) numped7: PedidoSevenParamDto['numped7'], + ) { return this.pedidoSevenService.consultarPorNumped7(numped7); } } diff --git a/src/dto/index.ts b/src/dto/index.ts new file mode 100644 index 0000000..ed08e18 --- /dev/null +++ b/src/dto/index.ts @@ -0,0 +1,3 @@ +export * from './pedido-condvenda7-row.dto'; +export * from './pedido-condvenda7-row.mapper'; +export * from './pedido-seven-param.dto'; diff --git a/src/dto/pedido-condvenda7-group.dto.ts b/src/dto/pedido-condvenda7-group.dto.ts new file mode 100644 index 0000000..398b8b7 --- /dev/null +++ b/src/dto/pedido-condvenda7-group.dto.ts @@ -0,0 +1,77 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PedidoCondVenda7RowDto } from './pedido-condvenda7-row.dto'; + +export class PedidoCondVenda7GroupDto { + @ApiProperty({ + example: '157073710|RETIRA POSTERIOR|2026-03-11T00:00:00.000Z|4', + }) + groupKey!: string; + + @ApiProperty({ example: 157073710 }) + numped!: number; + + @ApiProperty({ example: 'ENTREGA' }) + tipoentrega!: string; + + @ApiProperty({ + type: String, + format: 'date-time', + required: false, + nullable: true, + }) + dtentrega!: Date | null; + + @ApiProperty({ example: 4 }) + codfilialretira!: number; + + @ApiProperty({ example: 'JURUNENSE BR' }) + filialretira!: string; + + @ApiProperty({ example: '12345678000199' }) + cgc!: string; + + @ApiProperty({ example: 'MINHA EMPRESA LTDA' }) + razaosocial!: string; + + @ApiProperty({ description: 'SYSDATE', type: String, format: 'date-time' }) + dtemissao!: Date; + + @ApiProperty({ example: 1 }) + codfilial!: number; + + @ApiProperty({ example: 'FILIAL CENTRO' }) + filialvenda!: string; + + @ApiProperty({ type: String, format: 'date-time' }) + data!: Date; + + @ApiProperty({ example: 999 }) + numcar!: number; + + @ApiProperty({ example: 123 }) + codcli!: number; + + @ApiProperty({ example: 'CLIENTE TESTE' }) + cliente!: string; + + @ApiProperty({ example: 10 }) + codusur!: number; + + @ApiProperty({ example: 'VENDEDOR' }) + vendedor!: string; + + @ApiProperty({ example: 25.5 }) + vlfrete!: number; + + @ApiProperty({ example: 199.9 }) + valor!: number; + + @ApiProperty({ example: 'e10adc3949ba59abbe56e057f20f883e' }) + hash!: string; + + @ApiProperty({ + type: [PedidoCondVenda7RowDto], + description: 'Itens pertencentes ao grupo logistico', + }) + items!: PedidoCondVenda7RowDto[]; +} diff --git a/src/dto/pedido-condvenda7-row.mapper.ts b/src/dto/pedido-condvenda7-row.mapper.ts new file mode 100644 index 0000000..ea37e60 --- /dev/null +++ b/src/dto/pedido-condvenda7-row.mapper.ts @@ -0,0 +1,124 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { z, ZodError } from 'zod'; +import { PedidoCondVenda7RowDto } from './pedido-condvenda7-row.dto'; + +function normalizeRowKeys(row: Record): Record { + return Object.entries(row).reduce>((acc, [key, value]) => { + acc[key.replace(/_/g, '').toUpperCase()] = value; + return acc; + }, {}); +} + +function coerceNumber(field: string) { + return z.preprocess((value) => { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string' && value.trim() !== '') { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return value; + }, z.number({ invalid_type_error: `Campo ${field} invalido` })); +} + +function coerceString(field: string) { + return z.preprocess((value) => { + if (typeof value === 'string') return value; + if (value == null) return ''; + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + return value; + }, z.string({ invalid_type_error: `Campo ${field} invalido` })); +} + +function coerceDate(field: string) { + return z.preprocess((value) => { + if (value instanceof Date && !Number.isNaN(value.getTime())) return value; + if (typeof value === 'string' || typeof value === 'number') { + const parsed = new Date(value); + if (!Number.isNaN(parsed.getTime())) return parsed; + } + return value; + }, z.date({ invalid_type_error: `Campo ${field} invalido` })); +} + +const pedidoCondVenda7RowSchema = z.object({ + NUMPED: coerceNumber('NUMPED'), + CGC: coerceString('CGC'), + RAZAOSOCIAL: coerceString('RAZAOSOCIAL'), + DTEMISSAO: coerceDate('DTEMISSAO'), + CODFILIAL: coerceNumber('CODFILIAL'), + FILIALVENDA: coerceString('FILIALVENDA'), + CODFILIALRETIRA: coerceNumber('CODFILIALRETIRA'), + FILIALRETIRA: coerceString('FILIALRETIRA'), + DATA: coerceDate('DATA'), + NUMCAR: coerceNumber('NUMCAR'), + CODCLI: coerceNumber('CODCLI'), + CLIENTE: coerceString('CLIENTE'), + CODUSUR: coerceNumber('CODUSUR'), + VENDEDOR: coerceString('VENDEDOR'), + VLFRETE: coerceNumber('VLFRETE'), + VALOR: z.preprocess((value) => { + if (value == null) return 0; + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string' && value.trim() !== '') { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return 0; + }, z.number()), + TIPOENTREGA: coerceString('TIPOENTREGA'), + DTENTREGA: z.preprocess((value) => { + if (value == null) return null; + if (value instanceof Date && !Number.isNaN(value.getTime())) return value; + if (typeof value === 'string' || typeof value === 'number') { + const parsed = new Date(value); + if (!Number.isNaN(parsed.getTime())) return parsed; + } + return null; + }, z.date().nullable()), + CODPROD: coerceNumber('CODPROD'), + DESCRICAO: coerceString('DESCRICAO'), + QT: coerceNumber('QT'), + HASH: coerceString('HASH'), +}); + +export function mapPedidoCondVenda7Row( + row: Record, +): PedidoCondVenda7RowDto { + try { + const parsed = pedidoCondVenda7RowSchema.parse(normalizeRowKeys(row)); + + return Object.assign(new PedidoCondVenda7RowDto(), { + numped: parsed.NUMPED, + cgc: parsed.CGC, + razaosocial: parsed.RAZAOSOCIAL, + dtemissao: parsed.DTEMISSAO, + codfilial: parsed.CODFILIAL, + filialvenda: parsed.FILIALVENDA, + codfilialretira: parsed.CODFILIALRETIRA, + filialretira: parsed.FILIALRETIRA, + data: parsed.DATA, + numcar: parsed.NUMCAR, + codcli: parsed.CODCLI, + cliente: parsed.CLIENTE, + codusur: parsed.CODUSUR, + vendedor: parsed.VENDEDOR, + vlfrete: parsed.VLFRETE, + valor: parsed.VALOR, + tipoentrega: parsed.TIPOENTREGA, + dtentrega: parsed.DTENTREGA, + codprod: parsed.CODPROD, + descricao: parsed.DESCRICAO, + qt: parsed.QT, + hash: parsed.HASH, + }); + } catch (error) { + if (error instanceof ZodError) { + const message = error.issues[0]?.message ?? 'Linha invalida retornada pelo banco'; + throw new InternalServerErrorException(message); + } + + throw error; + } +} diff --git a/src/dto/pedido-seven-param.dto.ts b/src/dto/pedido-seven-param.dto.ts new file mode 100644 index 0000000..56f8381 --- /dev/null +++ b/src/dto/pedido-seven-param.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PedidoSevenParamDto { + @ApiProperty({ + example: 123456, + description: 'Valor para PCPEDC.NUMPEDENTFUT', + }) + numped7!: number; +} diff --git a/src/services/pedido-seven.service.ts b/src/services/pedido-seven.service.ts index 2d83e54..9dcf07a 100644 --- a/src/services/pedido-seven.service.ts +++ b/src/services/pedido-seven.service.ts @@ -1,4 +1,5 @@ import { + HttpException, Injectable, InternalServerErrorException, NotFoundException, @@ -6,119 +7,17 @@ import { ServiceUnavailableException, } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import { PedidoCondVenda7GroupDto } from '../dto/pedido-condvenda7-group.dto'; import { PedidoCondVenda7RowDto } from '../dto/pedido-condvenda7-row.dto'; +import { mapPedidoCondVenda7Row } from '../dto/pedido-condvenda7-row.mapper'; @Injectable() export class PedidoSevenService { constructor(@Optional() private readonly dataSource?: DataSource) {} - private static toNumber(value: unknown, field: string): number { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim() !== '') { - const n = Number(value); - if (Number.isFinite(n)) return n; - } - throw new InternalServerErrorException(`Campo ${field} invalido`); - } - - private static asString(value: unknown, field: string): string { - if (typeof value === 'string') return value; - if (value == null) return ''; - if (typeof value === 'number' || typeof value === 'boolean') { - return String(value); - } - throw new InternalServerErrorException(`Campo ${field} invalido`); - } - - private static toDate(value: unknown, field: string): Date { - if (value instanceof Date && !Number.isNaN(value.getTime())) return value; - if (typeof value === 'string' || typeof value === 'number') { - const d = new Date(value); - if (!Number.isNaN(d.getTime())) return d; - } - throw new InternalServerErrorException(`Campo ${field} invalido`); - } - - private static toNullableNumber(value: unknown): number { - if (value == null) return 0; - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim() !== '') { - const n = Number(value); - if (Number.isFinite(n)) return n; - } - return 0; - } - - private static toNullableDate(value: unknown): Date | null { - if (value == null) return null; - if (value instanceof Date && !Number.isNaN(value.getTime())) return value; - if (typeof value === 'string' || typeof value === 'number') { - const d = new Date(value); - if (!Number.isNaN(d.getTime())) return d; - } - return null; - } - - private static pick(row: Record, key: string): unknown { - return ( - row[key] ?? - row[key.toUpperCase()] ?? - row[key.toLowerCase()] ?? - row[key.replace(/_/g, '')] ?? - row[key.toUpperCase().replace(/_/g, '')] - ); - } - - private static mapRow(row: Record): PedidoCondVenda7RowDto { - const dto = new PedidoCondVenda7RowDto(); - - dto.numped = this.toNumber(this.pick(row, 'NUMPED'), 'NUMPED'); - dto.cgc = this.asString(this.pick(row, 'CGC'), 'CGC'); - dto.razaosocial = this.asString( - this.pick(row, 'RAZAOSOCIAL'), - 'RAZAOSOCIAL', - ); - dto.dtemissao = this.toDate(this.pick(row, 'DTEMISSAO'), 'DTEMISSAO'); - - dto.codfilial = this.toNumber(this.pick(row, 'CODFILIAL'), 'CODFILIAL'); - dto.filialvenda = this.asString( - this.pick(row, 'FILIALVENDA'), - 'FILIALVENDA', - ); - - dto.codfilialretira = this.toNumber( - this.pick(row, 'CODFILIALRETIRA'), - 'CODFILIALRETIRA', - ); - dto.filialretira = this.asString( - this.pick(row, 'FILIALRETIRA'), - 'FILIALRETIRA', - ); - - dto.data = this.toDate(this.pick(row, 'DATA'), 'DATA'); - dto.numcar = this.toNumber(this.pick(row, 'NUMCAR'), 'NUMCAR'); - dto.codcli = this.toNumber(this.pick(row, 'CODCLI'), 'CODCLI'); - dto.cliente = this.asString(this.pick(row, 'CLIENTE'), 'CLIENTE'); - dto.codusur = this.toNumber(this.pick(row, 'CODUSUR'), 'CODUSUR'); - dto.vendedor = this.asString(this.pick(row, 'VENDEDOR'), 'VENDEDOR'); - dto.vlfrete = this.toNumber(this.pick(row, 'VLFRETE'), 'VLFRETE'); - dto.valor = this.toNullableNumber(this.pick(row, 'VALOR')); - dto.tipoentrega = this.asString( - this.pick(row, 'TIPOENTREGA'), - 'TIPOENTREGA', - ); - dto.dtentrega = this.toNullableDate(this.pick(row, 'DTENTREGA')); - dto.codprod = this.toNumber(this.pick(row, 'CODPROD'), 'CODPROD'); - dto.descricao = this.asString(this.pick(row, 'DESCRICAO'), 'DESCRICAO'); - dto.qt = this.toNumber(this.pick(row, 'QT'), 'QT'); - dto.hash = this.asString(this.pick(row, 'HASH'), 'HASH'); - - return dto; - } - async consultarPorNumped7( numped7: number, - ): Promise { + ): Promise { if (!this.dataSource) { throw new ServiceUnavailableException('DataSource nao disponivel'); } @@ -142,45 +41,39 @@ SELECT PCPEDC.DATA, PCPEDC.NUMCAR, PCPEDC.CODCLI, - PCCLIENT.CLIENTE CLIENTE, + PCCLIENT.CLIENTE, PCNFSAID.CODUSUR, PCUSUARI.NOME VENDEDOR, PCPEDC.VLFRETE, CASE WHEN ROW_NUMBER() OVER ( - PARTITION BY PCPEDC.NUMPEDENTFUT - ORDER BY PCPEDC.NUMPED, PCPEDI.ROWID + PARTITION BY PCPEDC.NUMPED + ORDER BY PCPEDI.ROWID ) = 1 - THEN CASE - WHEN NVL(PCNFSAID.VLTOTGER, 0) = 0 THEN PCNFSAID.VLTOTAL - ELSE PCNFSAID.VLTOTGER - END + THEN SUM(PCPEDI.QT * PCPEDI.PVENDA) OVER (PARTITION BY PCPEDC.NUMPED) ELSE NULL - END VALOR, - DECODE( - PCPEDI.TIPOENTREGA, - 'EN', 'ENTREGA', - 'EF', 'ENCOMENDA', - 'RP', 'RETIRA POSTERIOR', - 'RI', 'RETIRA IMEDIATA', - 'RETIRA IMEDIATA' - ) TIPOENTREGA, + END AS VALOR, + CASE PCPEDI.TIPOENTREGA + WHEN 'EN' THEN 'ENTREGA' + WHEN 'EF' THEN 'ENCOMENDA' + WHEN 'RP' THEN 'RETIRA POSTERIOR' + WHEN 'RI' THEN 'RETIRA IMEDIATA' + ELSE 'RETIRA IMEDIATA' + END AS TIPOENTREGA, PCPEDC.DTENTREGA, PCPEDI.CODPROD, PCPRODUT.DESCRICAO, PCPEDI.QT, MD5(PCNFSAID.NUMPED || '@Juru2025$') HASH -FROM - PCPEDC, - PCPEDI, - PCNFSAID, - PCCLIENT, - PCUSUARI, - PCPRODUT, - PCFILIAL, - PCFILIAL RETIRA -WHERE - PCNFSAID.NUMPED = PCPEDC.NUMPEDENTFUT +FROM PCPEDC, + PCPEDI, + PCNFSAID, + PCCLIENT, + PCUSUARI, + PCPRODUT, + PCFILIAL, + PCFILIAL RETIRA +WHERE PCNFSAID.NUMPED = PCPEDC.NUMPEDENTFUT AND PCPEDC.CODFILIAL = PCFILIAL.CODIGO AND PCPEDI.CODFILIALRETIRA = RETIRA.CODIGO AND PCPEDI.CODPROD = PCPRODUT.CODPROD @@ -190,9 +83,7 @@ WHERE AND PCNFSAID.DTCANCEL IS NULL AND PCNFSAID.CONDVENDA = 7 AND PCPEDC.NUMPEDENTFUT = :1 -ORDER BY - PCPEDC.NUMPED, - PCPEDI.CODPROD +ORDER BY PCPEDC.NUMPED, PCPEDI.CODPROD `; try { @@ -207,18 +98,80 @@ ORDER BY ); } - return rows.map((r) => { + const mappedRows = rows.map((r) => { if (r && typeof r === 'object' && !Array.isArray(r)) { - return PedidoSevenService.mapRow(r as Record); + return mapPedidoCondVenda7Row(r as Record); } throw new InternalServerErrorException( 'Linha invalida retornada pelo banco', ); }); + + return this.groupRows(mappedRows); } catch (err) { + if (err instanceof HttpException) { + throw err; + } + throw new InternalServerErrorException( `Erro ao executar a consulta: ${err instanceof Error ? err.message : String(err)}`, ); } } + + private groupRows(rows: PedidoCondVenda7RowDto[]): PedidoCondVenda7GroupDto[] { + const groups = new Map(); + + for (const row of rows) { + const groupKey = this.buildGroupKey(row); + const existingGroup = groups.get(groupKey); + + if (existingGroup) { + existingGroup.items.push(row); + existingGroup.valor += row.valor ?? 0; + continue; + } + + groups.set( + groupKey, + Object.assign(new PedidoCondVenda7GroupDto(), { + groupKey, + numped: row.numped, + tipoentrega: row.tipoentrega, + dtentrega: row.dtentrega, + codfilialretira: row.codfilialretira, + filialretira: row.filialretira, + cgc: row.cgc, + razaosocial: row.razaosocial, + dtemissao: row.dtemissao, + codfilial: row.codfilial, + filialvenda: row.filialvenda, + data: row.data, + numcar: row.numcar, + codcli: row.codcli, + cliente: row.cliente, + codusur: row.codusur, + vendedor: row.vendedor, + vlfrete: row.vlfrete, + valor: row.valor ?? 0, + hash: row.hash, + items: [row], + }), + ); + } + + return Array.from(groups.values()); + } + + private buildGroupKey(row: PedidoCondVenda7RowDto): string { + const dtentrega = + row.dtentrega instanceof Date ? row.dtentrega.toISOString() : 'null'; + + return [ + row.numped, + row.tipoentrega, + dtentrega, + row.codfilialretira, + ].join('|'); + } }