Compare commits
2 Commits
main
...
chore/orga
| Author | SHA1 | Date |
|---|---|---|
|
|
fc2045da89 | |
|
|
66adb39b9b |
|
|
@ -18,21 +18,20 @@ Retorna a lista de impressoras instaladas no servidor onde a API está rodando.
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "POS-80",
|
"Name": "POS-80",
|
||||||
"portName": "USB001",
|
"DriverName": "Generic / Text Only",
|
||||||
"driverName": "Generic / Text Only",
|
"PrinterStatus": 0
|
||||||
"printerStatus": 0,
|
|
||||||
"deviceId": "POS-80"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Microsoft Print to PDF",
|
"Name": "Microsoft Print to PDF",
|
||||||
"portName": "PORTPROMPT:",
|
"DriverName": "Microsoft Print To PDF",
|
||||||
"driverName": "Microsoft Print To PDF",
|
"PrinterStatus": 0
|
||||||
"printerStatus": 0
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Nota: atualmente esse endpoint repassa o resultado do PowerShell `Get-Printer` (Windows) com chaves em PascalCase.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Imprimir HTML (Etiquetas/Layouts)
|
### 2. Imprimir HTML (Etiquetas/Layouts)
|
||||||
|
|
@ -53,6 +52,16 @@ Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impr
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Exemplo de Resposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Tarefa enviada para a fila de impressão",
|
||||||
|
"jobId": "123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **Detalhes:**
|
- **Detalhes:**
|
||||||
- O backend injeta automaticamente o script do Tailwind CSS.
|
- O backend injeta automaticamente o script do Tailwind CSS.
|
||||||
- O HTML é renderizado via Puppeteer (Chrome headless).
|
- O HTML é renderizado via Puppeteer (Chrome headless).
|
||||||
|
|
@ -82,22 +91,29 @@ Envia comandos de texto diretamente para a impressora. Ideal para cupons simples
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Exemplo de Resposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Print successful!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Tipos (TypeScript Interfaces)
|
## Tipos (TypeScript Interfaces)
|
||||||
|
|
||||||
Se estiver usando TypeScript no Frontend, utilize estas interfaces:
|
Se estiver usando TypeScript no Frontend, utilize estas interfaces:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export interface Printer {
|
export interface Printer {
|
||||||
name: string;
|
Name: string;
|
||||||
portName?: string;
|
DriverName?: string;
|
||||||
driverName?: string;
|
PrinterStatus?: number;
|
||||||
printerStatus?: number;
|
|
||||||
deviceId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrintHtmlPayload {
|
export interface PrintHtmlPayload {
|
||||||
html: string;
|
html: string;
|
||||||
printerName: string;
|
printerName?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
jobId?: string;
|
jobId?: string;
|
||||||
|
|
@ -109,6 +125,17 @@ export interface PrintTextPayload {
|
||||||
upsideDown?: boolean;
|
upsideDown?: boolean;
|
||||||
printerInterface?: string;
|
printerInterface?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PrintHtmlResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
jobId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintTextResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Swagger
|
## Swagger
|
||||||
|
|
|
||||||
26
README.md
26
README.md
|
|
@ -27,14 +27,19 @@ A solução utiliza uma arquitetura baseada em filas para assegurar a resiliênc
|
||||||
|
|
||||||
O serviço utiliza as seguintes definições no arquivo `.env`:
|
O serviço utiliza as seguintes definições no arquivo `.env`:
|
||||||
|
|
||||||
| Variável | Descrição | Exemplo |
|
| Variável | Descrição | Exemplo |
|
||||||
| :------------------- | :------------------------------- | :------------------------ |
|
| :------------------- | :-------------------------------------------- | :------------------------ |
|
||||||
| `REDIS_HOST` | Endereço do servidor Redis | `localhost` |
|
| `BODY_LIMIT` | Limite do body (JSON/urlencoded) | `2mb` |
|
||||||
| `REDIS_PORT` | Porta do servidor Redis | `6379` |
|
| `REDIS_HOST` | Endereço do servidor Redis | `localhost` |
|
||||||
| `DB_USERNAME` | Usuário do Banco de Dados Oracle | `admin` |
|
| `REDIS_PORT` | Porta do servidor Redis | `6379` |
|
||||||
| `DB_PASSWORD` | Senha do Banco de Dados Oracle | `1234` |
|
| `REDIS_PASSWORD` | Senha do Redis (opcional) | `minha-senha` |
|
||||||
| `DB_CONNECT_STRING` | String de conexão Oracle | `(DESCRIPTION=...)` |
|
| `DB_ENABLED` | Habilita inicialização do Oracle/TypeORM | `true` |
|
||||||
| `ORACLE_CLIENT_PATH` | Caminho do Oracle Instant Client | `C:\oracle\instantclient` |
|
| `DB_USERNAME` | Usuário do Banco de Dados Oracle | `admin` |
|
||||||
|
| `DB_PASSWORD` | Senha do Banco de Dados Oracle | `1234` |
|
||||||
|
| `DB_CONNECT_STRING` | String de conexão Oracle | `(DESCRIPTION=...)` |
|
||||||
|
| `DB_RETRY_ATTEMPTS` | Tentativas de conexão ao iniciar | `5` |
|
||||||
|
| `DB_RETRY_DELAY_MS` | Delay base (ms) entre tentativas | `2000` |
|
||||||
|
| `ORACLE_CLIENT_PATH` | Caminho do Oracle Instant Client (thick mode) | `C:\oracle\instantclient` |
|
||||||
|
|
||||||
### Pré-requisitos
|
### Pré-requisitos
|
||||||
|
|
||||||
|
|
@ -102,9 +107,10 @@ A interface de monitoramento em tempo real das filas está disponível no endpoi
|
||||||
|
|
||||||
Permite a visualização de tarefas ativas, concluídas e falhas, além da reinjeção manual de jobs.
|
Permite a visualização de tarefas ativas, concluídas e falhas, além da reinjeção manual de jobs.
|
||||||
|
|
||||||
### Telemetria
|
### Swagger
|
||||||
|
|
||||||
O serviço está instrumentado com **OpenTelemetry**, exportando traces via protocolo OTLP para análise de performance e rastreabilidade distribuída.
|
A documentação interativa (OpenAPI) está disponível em:
|
||||||
|
`http://localhost:3000/api`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { PrinterController } from './controller/printer.controller';
|
import { PrinterController } from './controller/printer.controller';
|
||||||
|
import { PedidoSevenController } from './controller/pedido-seven.controller';
|
||||||
import { PrinterService } from './services/printer.service';
|
import { PrinterService } from './services/printer.service';
|
||||||
import { PrinterProcessor } from './services/printer.processor';
|
import { PrinterProcessor } from './services/printer.processor';
|
||||||
import { InfrastructureModule } from './infrastructure/infrastructure.module';
|
import { InfrastructureModule } from './infrastructure/infrastructure.module';
|
||||||
|
import { PedidoSevenService } from './services/pedido-seven.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [InfrastructureModule],
|
imports: [InfrastructureModule],
|
||||||
controllers: [AppController, PrinterController],
|
controllers: [AppController, PrinterController, PedidoSevenController],
|
||||||
providers: [AppService, PrinterService, PrinterProcessor],
|
providers: [AppService, PrinterService, PrinterProcessor, PedidoSevenService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ export function createTypeOrmOptions(
|
||||||
): TypeOrmModuleOptions {
|
): TypeOrmModuleOptions {
|
||||||
const baseOptions: DataSourceOptions = {
|
const baseOptions: DataSourceOptions = {
|
||||||
type: 'oracle',
|
type: 'oracle',
|
||||||
username: configService.get<string>('DB_USERNAME') || 'teste',
|
username: configService.get<string>('DB_USERNAME') || 'SEVEN',
|
||||||
password: configService.get<string>('DB_PASSWORD') || 'teste',
|
password: configService.get<string>('DB_PASSWORD') || 'USR54SEV',
|
||||||
connectString:
|
connectString:
|
||||||
configService.get<string>('DB_CONNECT_STRING') ||
|
configService.get<string>('DB_CONNECT_STRING') ||
|
||||||
'(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=BDTESTE)))',
|
'(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=WINT)))',
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: true,
|
logging: true,
|
||||||
entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'],
|
entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiServiceUnavailableResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { PedidoSevenService } from '../services/pedido-seven.service';
|
||||||
|
import { PedidoCondVenda7RowDto } from '../dto/pedido-condvenda7-row.dto';
|
||||||
|
|
||||||
|
@ApiTags('Pedidos')
|
||||||
|
@Controller('pedidos')
|
||||||
|
export class PedidoSevenController {
|
||||||
|
constructor(private readonly pedidoSevenService: PedidoSevenService) {}
|
||||||
|
|
||||||
|
@Get('condvenda-7/:numped7')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Consulta pedido (CONDVENDA=7) por NUMPEDENTFUT',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'numped7',
|
||||||
|
example: 123456,
|
||||||
|
description: 'Valor para PCPEDC.NUMPEDENTFUT',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Linhas retornadas pela consulta (pedido x itens)',
|
||||||
|
type: [PedidoCondVenda7RowDto],
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({
|
||||||
|
description: 'Pedido nao encontrado para o NUMPEDENTFUT informado',
|
||||||
|
})
|
||||||
|
@ApiServiceUnavailableResponse({
|
||||||
|
description: 'Banco de dados indisponivel (DataSource nao inicializado)',
|
||||||
|
})
|
||||||
|
async consultar(@Param('numped7', ParseIntPipe) numped7: number) {
|
||||||
|
return this.pedidoSevenService.consultarPorNumped7(numped7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,20 @@ import {
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Param,
|
Param,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
import {
|
||||||
|
ApiBody,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { PrinterService } from '../services/printer.service';
|
import { PrinterService } from '../services/printer.service';
|
||||||
import { PrintDataDto } from '../dto/print-data.dto';
|
import { PrintDataDto } from '../dto/print-data.dto';
|
||||||
import { PrintHtmlDto } from '../dto/print-html.dto';
|
import { PrintHtmlDto } from '../dto/print-html.dto';
|
||||||
import { PrinterDto } from '../dto/printer.dto';
|
import { PrinterDto } from '../dto/printer.dto';
|
||||||
import { getPrinters } from '../services/get-printer';
|
import { getPrinters } from '../services/get-printer';
|
||||||
|
import { PrintResultDto } from '../dto/print-result.dto';
|
||||||
|
import { PrintHtmlResultDto } from '../dto/print-html-result.dto';
|
||||||
|
|
||||||
@ApiTags('Printer')
|
@ApiTags('Printer')
|
||||||
@Controller('printer')
|
@Controller('printer')
|
||||||
|
|
@ -24,7 +32,15 @@ export class PrinterController {
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Envia um comando de impressão de texto simples (ESC/POS)',
|
summary: 'Envia um comando de impressão de texto simples (ESC/POS)',
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Impressão enviada com sucesso' })
|
@ApiParam({
|
||||||
|
name: 'printerName',
|
||||||
|
example: 'POS-80',
|
||||||
|
description: 'Nome da impressora (será usado como printerInterface)',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Impressão enviada com sucesso',
|
||||||
|
type: PrintResultDto,
|
||||||
|
})
|
||||||
async print(
|
async print(
|
||||||
@Param('printerName') printerName: string,
|
@Param('printerName') printerName: string,
|
||||||
@Body() printData: PrintDataDto,
|
@Body() printData: PrintDataDto,
|
||||||
|
|
@ -36,6 +52,15 @@ export class PrinterController {
|
||||||
@Post('print')
|
@Post('print')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Envia um comando de impressão (Compatibilidade)' })
|
@ApiOperation({ summary: 'Envia um comando de impressão (Compatibilidade)' })
|
||||||
|
@ApiBody({
|
||||||
|
type: PrintDataDto,
|
||||||
|
description:
|
||||||
|
'Compatibilidade: informe printerInterface no body ou use /printer/:printerName/print.',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Impressão enviada com sucesso',
|
||||||
|
type: PrintResultDto,
|
||||||
|
})
|
||||||
async printLegacy(@Body() printData: PrintDataDto) {
|
async printLegacy(@Body() printData: PrintDataDto) {
|
||||||
return this.printerService.printReceipt(printData);
|
return this.printerService.printReceipt(printData);
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +71,15 @@ export class PrinterController {
|
||||||
summary:
|
summary:
|
||||||
'Converte HTML para PDF e imprime na impressora especificada na URL',
|
'Converte HTML para PDF e imprime na impressora especificada na URL',
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'HTML enviado para impressão' })
|
@ApiParam({
|
||||||
|
name: 'printerName',
|
||||||
|
example: 'POS-80',
|
||||||
|
description: 'Nome da impressora para o spooler (pdf-to-printer)',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'HTML enviado para impressão (job enfileirado)',
|
||||||
|
type: PrintHtmlResultDto,
|
||||||
|
})
|
||||||
async printHtml(
|
async printHtml(
|
||||||
@Param('printerName') printerName: string,
|
@Param('printerName') printerName: string,
|
||||||
@Body() printHtmlDto: PrintHtmlDto,
|
@Body() printHtmlDto: PrintHtmlDto,
|
||||||
|
|
@ -58,14 +91,22 @@ export class PrinterController {
|
||||||
@Post('print-html')
|
@Post('print-html')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Converte HTML para PDF (Compatibilidade)' })
|
@ApiOperation({ summary: 'Converte HTML para PDF (Compatibilidade)' })
|
||||||
|
@ApiBody({
|
||||||
|
type: PrintHtmlDto,
|
||||||
|
description:
|
||||||
|
'Compatibilidade: informe printerName no body ou use /printer/:printerName/print-html.',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'HTML enviado para impressão (job enfileirado)',
|
||||||
|
type: PrintHtmlResultDto,
|
||||||
|
})
|
||||||
async printHtmlLegacy(@Body() printHtmlDto: PrintHtmlDto) {
|
async printHtmlLegacy(@Body() printHtmlDto: PrintHtmlDto) {
|
||||||
return this.printerService.printHtml(printHtmlDto);
|
return this.printerService.printHtml(printHtmlDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' })
|
@ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' })
|
||||||
@ApiResponse({
|
@ApiOkResponse({
|
||||||
status: 200,
|
|
||||||
description: 'Lista de impressoras encontradas',
|
description: 'Lista de impressoras encontradas',
|
||||||
type: [PrinterDto],
|
type: [PrinterDto],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class PedidoCondVenda7RowDto {
|
||||||
|
@ApiProperty({ example: 123456 })
|
||||||
|
numped!: number;
|
||||||
|
|
||||||
|
@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({ example: 2 })
|
||||||
|
codfilialretira!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'FILIAL RETIRA' })
|
||||||
|
filialretira!: 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: 'ENTREGA' })
|
||||||
|
tipoentrega!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
format: 'date-time',
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
dtentrega!: Date | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 98765 })
|
||||||
|
codprod!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'PRODUTO X' })
|
||||||
|
descricao!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2 })
|
||||||
|
qt!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'e10adc3949ba59abbe56e057f20f883e' })
|
||||||
|
hash!: string;
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,36 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class PrintDataDto {
|
export class PrintDataDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: ['--------------------------------', ' TESTE DE IMPRESSAO ', '--------------------------------'],
|
example: [
|
||||||
description: 'Lista de linhas de texto para imprimir'
|
'--------------------------------',
|
||||||
|
' TESTE DE IMPRESSAO ',
|
||||||
|
'--------------------------------',
|
||||||
|
],
|
||||||
|
description: 'Lista de linhas de texto para imprimir',
|
||||||
})
|
})
|
||||||
lines: string[];
|
lines: string[];
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
required: false,
|
required: false,
|
||||||
enum: ['left', 'center', 'right'],
|
enum: ['left', 'center', 'right'],
|
||||||
example: 'center',
|
example: 'center',
|
||||||
description: 'Alinhamento do texto'
|
description: 'Alinhamento do texto',
|
||||||
})
|
})
|
||||||
alignment?: 'left' | 'center' | 'right';
|
alignment?: 'left' | 'center' | 'right';
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
required: false,
|
required: false,
|
||||||
example: false,
|
example: false,
|
||||||
description: 'Se verdadeiro, imprime de cabeça para baixo (se suportado)'
|
description: 'Se verdadeiro, imprime de cabeça para baixo (se suportado)',
|
||||||
})
|
})
|
||||||
upsideDown?: boolean;
|
upsideDown?: boolean;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
required: false,
|
required: false,
|
||||||
example: 'tcp://10.1.119.13',
|
example: 'tcp://10.1.119.13',
|
||||||
description: 'Interface da impressora. Ex: tcp://ip:porta ou printer:NomeDaImpressora'
|
description:
|
||||||
|
'Interface da impressora. Obrigatorio ao usar /printer/print; ignorado ao usar /printer/:printerName/print. Ex: tcp://ip:porta ou printer:NomeDaImpressora',
|
||||||
})
|
})
|
||||||
printerInterface?: string;
|
printerInterface?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { PrintResultDto } from './print-result.dto';
|
||||||
|
|
||||||
|
export class PrintHtmlResultDto extends PrintResultDto {
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
example: '123',
|
||||||
|
description: 'ID do job enfileirado no BullMQ',
|
||||||
|
})
|
||||||
|
jobId?: string;
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,38 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class PrintHtmlDto {
|
export class PrintHtmlDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '<h1>Titulo</h1><p>Conteudo do pedido...</p>',
|
example: '<h1>Titulo</h1><p>Conteudo do pedido...</p>',
|
||||||
description: 'Conteúdo HTML para impressão'
|
description: 'Conteúdo HTML para impressão',
|
||||||
})
|
})
|
||||||
html: string;
|
html: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'POS-80',
|
example: 'POS-80',
|
||||||
description: 'Nome da impressora onde será impresso o PDF gerado'
|
required: false,
|
||||||
|
description:
|
||||||
|
'Nome da impressora. Obrigatorio ao usar /printer/print-html; ignorado ao usar /printer/:printerName/print-html.',
|
||||||
})
|
})
|
||||||
printerName: string;
|
printerName?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '60mm', required: false, description: 'Largura da etiqueta (default: 80mm)' })
|
@ApiProperty({
|
||||||
|
example: '60mm',
|
||||||
|
required: false,
|
||||||
|
description: 'Largura da etiqueta (default: 80mm)',
|
||||||
|
})
|
||||||
width?: string;
|
width?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' })
|
@ApiProperty({
|
||||||
|
example: '40mm',
|
||||||
|
required: false,
|
||||||
|
description: 'Altura da etiqueta (default: auto)',
|
||||||
|
})
|
||||||
height?: string;
|
height?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'impresso-12345',
|
example: 'impresso-12345',
|
||||||
required: false,
|
required: false,
|
||||||
description: 'ID único do job para idempotência (evita duplicados)'
|
description: 'ID único do job para idempotência (evita duplicados)',
|
||||||
})
|
})
|
||||||
jobId?: string;
|
jobId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class PrintResultDto {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Print successful!' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class PrinterDto {
|
export class PrinterDto {
|
||||||
@ApiProperty({ example: 'POS-80', description: 'Nome da impressora' })
|
@ApiProperty({
|
||||||
name: string;
|
example: 'POS-80',
|
||||||
|
description: 'Nome da impressora (retornado pelo PowerShell: Get-Printer)',
|
||||||
|
})
|
||||||
|
Name: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'USB001', description: 'Porta da impressora (Win32)' })
|
@ApiProperty({
|
||||||
portName?: string;
|
example: 'Generic / Text Only',
|
||||||
|
required: false,
|
||||||
|
description: 'Nome do driver (retornado pelo PowerShell: Get-Printer)',
|
||||||
|
})
|
||||||
|
DriverName?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'Generic / Text Only', description: 'Nome do driver' })
|
@ApiProperty({
|
||||||
driverName?: string;
|
example: 0,
|
||||||
|
required: false,
|
||||||
@ApiProperty({ example: 0, description: 'Status da impressora (0 = Idle)' })
|
description:
|
||||||
printerStatus?: number;
|
'Status da impressora (retornado pelo PowerShell: Get-Printer)',
|
||||||
|
})
|
||||||
@ApiProperty({ example: 'POS-80', description: 'ID do dispositivo (usado para impressão)' })
|
PrinterStatus?: number;
|
||||||
deviceId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ async function bootstrap() {
|
||||||
app.use(urlencoded({ extended: true, limit: bodyLimit }));
|
app.use(urlencoded({ extended: true, limit: bodyLimit }));
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('API Documentation')
|
.setTitle('Servico de Impressao')
|
||||||
.setDescription('The API description')
|
.setDescription(
|
||||||
.setVersion('1.0')
|
'Microservico NestJS para impressao local (ESC/POS e HTML->PDF), com processamento assincrono via BullMQ/Redis e opcional integracao Oracle via TypeORM.',
|
||||||
|
)
|
||||||
|
.setVersion('1.0.0')
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('api', app, document);
|
SwaggerModule.setup('api', app, document);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
NotFoundException,
|
||||||
|
Optional,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { PedidoCondVenda7RowDto } from '../dto/pedido-condvenda7-row.dto';
|
||||||
|
|
||||||
|
@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 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<string, unknown>, 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<string, unknown>): 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.toNumber(this.pick(row, 'VALOR'), '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<PedidoCondVenda7RowDto[]> {
|
||||||
|
if (!this.dataSource) {
|
||||||
|
throw new ServiceUnavailableException('DataSource nao disponivel');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dataSource.isInitialized) {
|
||||||
|
throw new ServiceUnavailableException(
|
||||||
|
'Banco de dados indisponivel (DataSource nao inicializado)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
PCPEDC.NUMPED,
|
||||||
|
PCFILIAL.CGC,
|
||||||
|
PCFILIAL.RAZAOSOCIAL,
|
||||||
|
SYSDATE DTEMISSAO,
|
||||||
|
PCPEDC.CODFILIAL,
|
||||||
|
PCFILIAL.FANTASIA FILIALVENDA,
|
||||||
|
PCPEDI.CODFILIALRETIRA,
|
||||||
|
RETIRA.FANTASIA FILIALRETIRA,
|
||||||
|
PCPEDC.DATA,
|
||||||
|
PCPEDC.NUMCAR,
|
||||||
|
PCPEDC.CODCLI,
|
||||||
|
PCCLIENT.CLIENTE CLIENTE,
|
||||||
|
PCNFSAID.CODUSUR,
|
||||||
|
PCUSUARI.NOME VENDEDOR,
|
||||||
|
PCPEDC.VLFRETE,
|
||||||
|
DECODE(PCNFSAID.VLTOTGER, 0, PCNFSAID.VLTOTAL, PCNFSAID.VLTOTAL) VALOR,
|
||||||
|
DECODE(
|
||||||
|
PCPEDI.TIPOENTREGA,
|
||||||
|
'EN', 'ENTREGA',
|
||||||
|
'EF', 'ENCOMENDA',
|
||||||
|
'RP', 'RETIRA POSTERIOR',
|
||||||
|
'RI', 'RETIRA IMEDIATA',
|
||||||
|
'RETIRA IMEDIATA'
|
||||||
|
) 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
|
||||||
|
AND PCPEDC.CODFILIAL = PCFILIAL.CODIGO
|
||||||
|
AND PCPEDI.CODFILIALRETIRA = RETIRA.CODIGO
|
||||||
|
AND PCPEDI.CODPROD = PCPRODUT.CODPROD
|
||||||
|
AND PCPEDC.CODCLI = PCCLIENT.CODCLI
|
||||||
|
AND PCNFSAID.CODUSUR = PCUSUARI.CODUSUR
|
||||||
|
AND PCPEDC.NUMPED = PCPEDI.NUMPED
|
||||||
|
AND PCNFSAID.DTCANCEL IS NULL
|
||||||
|
AND PCNFSAID.CONDVENDA = 7
|
||||||
|
AND PCPEDC.NUMPEDENTFUT = :1
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rows = (await this.dataSource.query(sql, [numped7])) as unknown;
|
||||||
|
if (!Array.isArray(rows)) {
|
||||||
|
throw new InternalServerErrorException('Resposta invalida do banco');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Pedido nao encontrado para NUMPEDENTFUT=${numped7}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map((r) => {
|
||||||
|
if (r && typeof r === 'object' && !Array.isArray(r)) {
|
||||||
|
return PedidoSevenService.mapRow(r as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Linha invalida retornada pelo banco',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
`Erro ao executar a consulta: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue