diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 317fad98..3c1bde80 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -4,6 +4,9 @@ on: push: branches: - "main" + paths-ignore: + - 'README.md' + - 'docs' env: PROJECT_ID: ${{ secrets.GKE_PROJECT }} diff --git a/.github/workflows/cd_stag.yaml b/.github/workflows/cd_stag.yaml index 028826f9..83b0b2f1 100644 --- a/.github/workflows/cd_stag.yaml +++ b/.github/workflows/cd_stag.yaml @@ -6,6 +6,9 @@ on: - "release/*" - "devops" - "develop" + paths-ignore: + - 'README.md' + - 'docs' env: PROJECT_ID: ${{ secrets.GKE_PROJECT }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1e67d56f..8f183727 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,9 @@ on: branches: [develop] pull_request: branches: [develop] + paths-ignore: + - 'README.md' + - 'docs' jobs: lint: diff --git a/.github/workflows/docker-e2e.yml b/.github/workflows/docker-e2e.yml index 71542ecc..c0194c55 100644 --- a/.github/workflows/docker-e2e.yml +++ b/.github/workflows/docker-e2e.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: branches: [main] + paths-ignore: + - 'README.md' + - 'docs' jobs: build: diff --git a/package.json b/package.json index 04f11d5a..c72b6d12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "api-cct", - "version": "0.12.6", + "version": "0.13.1", "description": "", "author": "", "private": true, diff --git a/src/app.module.ts b/src/app.module.ts index 7924f152..b8600042 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -43,8 +43,8 @@ import { SftpModule } from './sftp/sftp.module'; import { TestModule } from './test/test.module'; import { TicketRevenuesModule } from './ticket-revenues/ticket-revenues.module'; import { UsersModule } from './users/users.module'; -import { TransacaoViewService } from './transacao-bq/transacao-view.service'; -import { TransacaoViewModule } from './transacao-bq/transacao-view.module'; +import { TransacaoViewService } from './transacao-view/transacao-view.service'; +import { TransacaoViewModule } from './transacao-view/transacao-view.module'; import { AppLoggerMiddleware } from './utils/logger-middleware'; import { RelatorioModule } from './relatorio/relatorio.module'; import { AppService } from './app.service'; diff --git a/src/bank-statements/bank-statements.controller.ts b/src/bank-statements/bank-statements.controller.ts index e16609c5..f7bad1e7 100644 --- a/src/bank-statements/bank-statements.controller.ts +++ b/src/bank-statements/bank-statements.controller.ts @@ -10,7 +10,7 @@ import { TimeIntervalEnum } from 'src/utils/enums/time-interval.enum'; import { getPagination } from 'src/utils/get-pagination'; import { IRequest } from 'src/utils/interfaces/request.interface'; import { ParseNumberPipe } from 'src/utils/pipes/parse-number.pipe'; -import { ValidateEnumPipe } from 'src/utils/pipes/validate-enum.pipe'; +import { ParseEnumPipe } from 'src/utils/pipes/parse-enum.pipe'; import { DateQueryParams } from 'src/utils/query-param/date.query-param'; import { PaginationQueryParams } from 'src/utils/query-param/pagination.query-param'; import { getRequestLog } from 'src/utils/request-utils'; @@ -60,12 +60,10 @@ export class BankStatementsController { @ApiQuery(CommonApiParams.userId) @HttpCode(HttpStatus.OK) async getMe( - @Request() request: IRequest, + @Request() request: IRequest, // @Query(...DateQueryParams.yearMonth) yearMonth: string, - @Query('timeInterval', new ValidateEnumPipe(BSMeTimeIntervalEnum, false, BSMeTimeIntervalEnum.LAST_MONTH)) - timeInterval?: BSMeTimeIntervalEnum | undefined, - @Query('userId', new ParseNumberPipe({ min: 1, optional: true })) - userId?: number | null, + @Query('timeInterval', new ParseEnumPipe(BSMeTimeIntervalEnum, { optional: true, defaultValue: BSMeTimeIntervalEnum.LAST_MONTH })) timeInterval: BSMeTimeIntervalEnum | undefined, + @Query('userId', new ParseNumberPipe({ min: 1, optional: true })) userId?: number | null, ): Promise { this.logger.log(getRequestLog(request)); @@ -118,7 +116,7 @@ export class BankStatementsController { @Query(...PaginationQueryParams.page) page: number, @Query(...PaginationQueryParams.limit) limit: number, @Query('endDate', new ParseDatePipe()) endDate: string, - @Query('timeInterval', new ValidateEnumPipe(BSMePrevDaysTimeIntervalEnum, true)) timeInterval: BSMePrevDaysTimeIntervalEnum, + @Query('timeInterval', new ParseEnumPipe(BSMePrevDaysTimeIntervalEnum)) timeInterval: BSMePrevDaysTimeIntervalEnum, @Query('userId', new ParseNumberPipe({ min: 1, optional: true })) userId?: number | null, ): Promise> { const isUserIdParam = userId !== null && !isNaN(Number(userId)); diff --git a/src/bank-statements/bank-statements.module.ts b/src/bank-statements/bank-statements.module.ts index 6de84436..3ea2e8d6 100644 --- a/src/bank-statements/bank-statements.module.ts +++ b/src/bank-statements/bank-statements.module.ts @@ -5,7 +5,7 @@ import { BankStatementsRepositoryService } from './bank-statements.repository'; import { BankStatementsController } from './bank-statements.controller'; import { BankStatementsService } from './bank-statements.service'; import { CnabModule } from 'src/cnab/cnab.module'; -import { TransacaoViewModule } from 'src/transacao-bq/transacao-view.module'; +import { TransacaoViewModule } from 'src/transacao-view/transacao-view.module'; @Module({ providers: [BankStatementsService, BankStatementsRepositoryService], diff --git a/src/bank-statements/bank-statements.repository.ts b/src/bank-statements/bank-statements.repository.ts index f8387d32..86070c47 100644 --- a/src/bank-statements/bank-statements.repository.ts +++ b/src/bank-statements/bank-statements.repository.ts @@ -1,16 +1,13 @@ import { Injectable } from '@nestjs/common'; import { endOfDay, isFriday, nextFriday, nextThursday, startOfDay, subDays } from 'date-fns'; -import { DetalheA } from 'src/cnab/entity/pagamento/detalhe-a.entity'; -import { ArquivoPublicacaoService } from 'src/cnab/service/arquivo-publicacao.service'; -import { DetalheAService } from 'src/cnab/service/pagamento/detalhe-a.service'; -import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; +import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; +import { TicketRevenuesService } from 'src/ticket-revenues/ticket-revenues.service'; import { User } from 'src/users/entities/user.entity'; import { formatDateYMD } from 'src/utils/date-utils'; import { TimeIntervalEnum } from 'src/utils/enums/time-interval.enum'; import { getPagination } from 'src/utils/get-pagination'; import { PaginationOptions } from 'src/utils/types/pagination-options'; import { Pagination } from 'src/utils/types/pagination.type'; -import { In } from 'typeorm'; import { BankStatementPreviousDaysDTO } from './dtos/bank-statement-previous-days.dto'; import { BankStatementDTO } from './dtos/bank-statement.dto'; import { IBSCounts } from './interfaces/bs-counts.interface'; @@ -22,11 +19,7 @@ import { IBSGetMePreviousDaysResponse } from './interfaces/bs-get-me-previous-da */ @Injectable() export class BankStatementsRepositoryService { - constructor( - private readonly transacaoViewService: TransacaoViewService, // - private readonly detalheAService: DetalheAService, - private arquivoPublicacaoService: ArquivoPublicacaoService, - ) {} + constructor(private readonly ticketRevenuesService: TicketRevenuesService) {} /** * Parâmetros validados: @@ -78,7 +71,6 @@ export class BankStatementsRepositoryService { * * Requisitos: * - Mostra sempre as transações individuais - * - */ private async buildPreviousDays(validArgs: { user: User; // @@ -105,37 +97,26 @@ export class BankStatementsRepositoryService { endDate = qua; } - const transacoes = await this.transacaoViewService.findPreviousDays({ - startDate: startDate, - endDate: endDate, - cpfCnpjs: [validArgs.user.getCpfCnpj()], - }); - const publicacoes = await this.arquivoPublicacaoService.findMany({ where: { itemTransacao: { itemTransacaoAgrupado: { id: In(transacoes.map((t) => t.itemTransacaoAgrupadoId)) } } } }); - const revenues = transacoes.map((i) => i.toTicketRevenue(publicacoes)); - const detalhesA = await this.detalheAService.findMany({ - itemTransacaoAgrupado: { - id: In(revenues.map((i) => i.arquivoPublicacao?.itemTransacao.itemTransacaoAgrupado.id)), - }, - }); + const revenues = await this.ticketRevenuesService.findManyIndividual({ startDate, endDate, cpfCnpj: [validArgs.user.getCpfCnpj()], previousDays: true }); // Gerar BankStatements - const statements = revenues.map((item, index) => { - const isPago = item.arquivoPublicacao?.isPago; - const amount = Number((item.transactionValue || 0).toFixed(2)); - const paidAmount = Number(item.paidValue.toFixed(2)); + const statements = revenues.map((revenue, index) => { + const isPago = revenue.isPago; + const amount = Number((revenue.transactionValue || 0).toFixed(2)); + const paidAmount = Number(revenue.paidValue.toFixed(2)); const ticketCount = 1; - const foundDetalhesA = detalhesA.filter((i) => i.itemTransacaoAgrupado.id === item.arquivoPublicacao?.itemTransacao.itemTransacaoAgrupado.id); - const errors = DetalheA.getOcorrenciaErrors(foundDetalhesA); - const orderDate = nextThursday(startOfDay(new Date(item.processingDateTime))); + // const foundDetalhesA = detalhesA.filter((i) => i.itemTransacaoAgrupado.id === item.arquivoPublicacao?.itemTransacao.itemTransacaoAgrupado.id); + const errors = Ocorrencia.getErrors(revenue.ocorrencias); + const orderDate = nextThursday(startOfDay(new Date(revenue.processingDateTime))); const status = !errors.length ? (amount ? (isPago ? 'Pago' : 'A pagar') : null) : 'Pendente'; - const dataEfetivacao = item.arquivoPublicacao?.dataEfetivacao; + const dataEfetivacao = revenue.dataEfetivacao; return new BankStatementPreviousDaysDTO({ id: index + 1, - date: formatDateYMD(new Date(String(item.processingDateTime))), + date: formatDateYMD(new Date(String(revenue.processingDateTime))), effectivePaymentDate: isPago && dataEfetivacao ? formatDateYMD(new Date(dataEfetivacao)) : null, paymentOrderDate: formatDateYMD(orderDate), - transactionDate: formatDateYMD(new Date(item.transactionDateTime)), - processingDate: formatDateYMD(new Date(item.processingDateTime)), + transactionDate: formatDateYMD(new Date(revenue.transactionDateTime)), + processingDate: formatDateYMD(new Date(revenue.processingDateTime)), cpfCnpj: validArgs.user.getCpfCnpj(), permitCode: validArgs.user.getPermitCode(), amount: amount, diff --git a/src/bank-statements/bank-statements.service.spec.ts b/src/bank-statements/bank-statements.service.spec.ts index ced8b0a7..065a4449 100644 --- a/src/bank-statements/bank-statements.service.spec.ts +++ b/src/bank-statements/bank-statements.service.spec.ts @@ -2,7 +2,7 @@ import { Provider } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { BigqueryService } from 'src/bigquery/bigquery.service'; import { TicketRevenuesGroupDto } from 'src/ticket-revenues/dtos/ticket-revenues-group.dto'; -import { TicketRevenuesRepositoryService } from 'src/ticket-revenues/ticket-revenues-repository'; +import { TicketRevenuesRepositoryService } from 'src/ticket-revenues/ticket-revenues.repository'; import { TicketRevenuesService } from 'src/ticket-revenues/ticket-revenues.service'; import { User } from 'src/users/entities/user.entity'; import { UsersService } from 'src/users/users.service'; diff --git a/src/bigquery/bigquery.service.ts b/src/bigquery/bigquery.service.ts index 8a82c1bb..5f30815a 100644 --- a/src/bigquery/bigquery.service.ts +++ b/src/bigquery/bigquery.service.ts @@ -98,14 +98,13 @@ export class BigqueryService { * @throws `HttpException` */ public async query(source: BigquerySource, query: string) { - this.logger.debug('Query fetch started'); const _query = compactQuery(query); console.log(`${formatSqlTitle('bigquery:')} ${formatSqlQuery(query)}`); try { const [rows] = await this.getBqInstance(source).query({ query: _query, }); - this.logger.debug(`Query fetch finished. Count: ${rows.length}`); + this.logger.debug(`Query finished. Count: ${rows.length}`); return rows; } catch (error) { console.log(`${formatSqlTitleFailed('bigquery failed:')} ${_query}`); diff --git a/src/bigquery/dtos/bigquery-ordem-pagamento.dto.ts b/src/bigquery/dtos/bigquery-ordem-pagamento.dto.ts index 9adad5ac..35434693 100644 --- a/src/bigquery/dtos/bigquery-ordem-pagamento.dto.ts +++ b/src/bigquery/dtos/bigquery-ordem-pagamento.dto.ts @@ -1,13 +1,6 @@ -import { - IsDateString, - IsNotEmpty, - IsNumber, - IsNumberString, - IsString, - ValidateIf, -} from 'class-validator'; +import { IsDateString, IsNotEmpty, IsNumber, IsNumberString, IsString, ValidateIf } from 'class-validator'; import { isSameDay, nextFriday } from 'date-fns'; -import { TipoFavorecidoEnum } from 'src/tipo-favorecido/tipo-favorecido.enum'; +import { TipoFavorecidoEnum } from 'src/cnab/enums/tipo-favorecido.enum'; import { DeepPartial } from 'typeorm'; /** @@ -158,16 +151,8 @@ export class BigqueryOrdemPagamentoDTO { tipoFavorecido: TipoFavorecidoEnum | null; - public static findAgrupado( - ordemAgs: BigqueryOrdemPagamentoDTO[], - ordem: BigqueryOrdemPagamentoDTO, - newDataOrdem = nextFriday(new Date()), - ) { - const filtered = ordemAgs.filter( - (i) => - isSameDay(new Date(i.dataOrdem), newDataOrdem) && - i.idConsorcio === ordem.idConsorcio, - )[0]; + public static findAgrupado(ordemAgs: BigqueryOrdemPagamentoDTO[], ordem: BigqueryOrdemPagamentoDTO, newDataOrdem = nextFriday(new Date())) { + const filtered = ordemAgs.filter((i) => isSameDay(new Date(i.dataOrdem), newDataOrdem) && i.idConsorcio === ordem.idConsorcio)[0]; return filtered ? filtered : null; } } diff --git a/src/bigquery/interfaces/bigquery-find-ordem-pagamento.interface.ts b/src/bigquery/interfaces/bigquery-find-ordem-pagamento.interface.ts index 5ca6d1c5..c168c6ce 100644 --- a/src/bigquery/interfaces/bigquery-find-ordem-pagamento.interface.ts +++ b/src/bigquery/interfaces/bigquery-find-ordem-pagamento.interface.ts @@ -1,4 +1,4 @@ -import { TipoFavorecidoEnum } from 'src/tipo-favorecido/tipo-favorecido.enum'; +import { TipoFavorecidoEnum } from 'src/cnab/enums/tipo-favorecido.enum'; export interface IBigqueryFindOrdemPagamento { operadorCpfs?: string[]; diff --git a/src/bigquery/interfaces/bq-find-transacao-by.interface.ts b/src/bigquery/interfaces/bq-find-transacao-by.interface.ts deleted file mode 100644 index ce767486..00000000 --- a/src/bigquery/interfaces/bq-find-transacao-by.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface IBqFindTransacao { - cpfCnpj?: string; - manyCpfCnpj?: string[]; - startDate?: Date; - endDate?: Date; - limit?: number; - offset?: number; - getToday?: boolean; - previousDaysOnly?: boolean; - valor_pagamento?: number[] | null | ['>=' | '<=' | '>' | '<', number] | 'NOT NULL'; - id_transacao?: string[] | null; -} diff --git a/src/bigquery/maps/bq-transacao-tipo-transacao.map.ts b/src/bigquery/maps/bq-transacao-tipo-transacao.map.ts deleted file mode 100644 index 4021abaa..00000000 --- a/src/bigquery/maps/bq-transacao-tipo-transacao.map.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Ticket revenues transaction type map - * - * Business rules: - * - "Integral" = Débito + Botoeria (both are considered "Integral" type). - * See {@link https://github.com/RJ-SMTR/api-cct/issues/177#issuecomment-1934531824 Issue #177, item 1 - GitHub} - * - * Matching id or literal values. - * See {@link https://github.com/RJ-SMTR/api-cct/issues/168#issuecomment-1900546567 Issue #168 - GitHub} - */ -export const BqTransacaoTipoTransacaoMap = { - /** Originally 1 = Débito */ - 1: 'Integral', - 2: 'Recarga', - 98: 'Riocard', - 6: 'Bloqueio', - /** Originally 99 = Botoeria */ - 99: 'Integral', - 21: 'Gratuidade', - 3: 'Cancelamento', - 4: 'Integração', - Débito: 'Integral', - /** Botoeria = payment in cash */ - Botoeria: 'Integral', -}; diff --git a/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts b/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts index 9848aa5c..1f9ad544 100644 --- a/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts +++ b/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts @@ -1,10 +1,9 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { SettingsService } from 'src/settings/settings.service'; +import { Injectable } from '@nestjs/common'; +import { CustomLogger } from 'src/utils/custom-logger'; import { bigToNumber } from 'src/utils/pipe-utils'; -import { BigquerySource, BigqueryService } from '../bigquery.service'; +import { BigqueryService, BigquerySource } from '../bigquery.service'; import { BigqueryOrdemPagamento } from '../entities/ordem-pagamento.bigquery-entity'; import { IBigqueryFindOrdemPagamento } from '../interfaces/bigquery-find-ordem-pagamento.interface'; -import { CustomLogger } from 'src/utils/custom-logger'; @Injectable() export class BigqueryOrdemPagamentoRepository { @@ -12,7 +11,6 @@ export class BigqueryOrdemPagamentoRepository { constructor( private readonly bigqueryService: BigqueryService, - private readonly settingsService: SettingsService, ) {} public async findMany( @@ -87,6 +85,7 @@ export class BigqueryOrdemPagamentoRepository { const select = ` SELECT CAST(t.data_ordem AS STRING) AS dataOrdem, + CAST(t.data_pagamento AS STRING) AS dataPagamento, t.id_consorcio AS idConsorcio, t.consorcio, t.id_operadora AS idOperadora, diff --git a/src/bigquery/repositories/bigquery-transacao.repository.ts b/src/bigquery/repositories/bigquery-transacao.repository.ts index a79e4132..c165f16e 100644 --- a/src/bigquery/repositories/bigquery-transacao.repository.ts +++ b/src/bigquery/repositories/bigquery-transacao.repository.ts @@ -1,19 +1,30 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { appSettings } from 'src/settings/app.settings'; import { BigqueryEnvironment } from 'src/settings/enums/bigquery-env.enum'; import { SettingsService } from 'src/settings/settings.service'; import { TRIntegrationTypeMap } from 'src/ticket-revenues/maps/ticket-revenues.map'; import { isCpfOrCnpj } from 'src/utils/cpf-cnpj'; +import { CustomLogger } from 'src/utils/custom-logger'; +import { logWarn } from 'src/utils/log-utils'; import { QueryBuilder } from 'src/utils/query-builder/query-builder'; -import { BigquerySource, BigqueryService } from '../bigquery.service'; +import { BigqueryService, BigquerySource } from '../bigquery.service'; import { BigqueryTransacao } from '../entities/transacao.bigquery-entity'; -import { IBqFindTransacao } from '../interfaces/bq-find-transacao-by.interface'; import { BqTsansacaoTipoIntegracaoMap } from '../maps/bq-transacao-tipo-integracao.map'; import { BqTransacaoTipoPagamentoMap } from '../maps/bq-transacao-tipo-pagamento.map'; -import { BqTransacaoTipoTransacaoMap } from '../maps/bq-transacao-tipo-transacao.map'; -import { logWarn } from 'src/utils/log-utils'; -import { CustomLogger } from 'src/utils/custom-logger'; -import { isArray } from 'class-validator'; + +export interface IBqFindTransacao { + cpfCnpj?: string; + manyCpfCnpj?: string[]; + startDate?: Date; + endDate?: Date; + limit?: number; + offset?: number; + getToday?: boolean; + previousDaysOnly?: boolean; + valor_pagamento?: number[] | null | ['>=' | '<=' | '>' | '<', number] | 'NOT NULL'; + id_transacao?: string[] | null; + id_operadora?: string[]; +} @Injectable() export class BigqueryTransacaoRepository { @@ -149,6 +160,9 @@ export class BigqueryTransacaoRepository { } queryBuilder.pushAND(`t.id_transacao ${value}`); } + if (args?.id_operadora?.length) { + queryBuilder.pushAND(`t.id_operadora IN('${args.id_operadora.join("','")}')`); + } queryBuilder.pushOR([]); if (args?.getToday) { @@ -219,7 +233,6 @@ export class BigqueryTransacaoRepository { valor_transacao: item.valor_transacao || 0, paymentMediaType: tipo_pagamento !== null ? BqTransacaoTipoPagamentoMap?.[tipo_pagamento] || tipo_pagamento : tipo_pagamento, transportIntegrationType: tipo_integracao !== null ? BqTsansacaoTipoIntegracaoMap?.[tipo_integracao] || tipo_integracao : tipo_integracao, - transactionType: tipo_transacao !== null ? BqTransacaoTipoTransacaoMap?.[tipo_transacao] || tipo_transacao : tipo_transacao, }; }); } diff --git a/src/bigquery/services/bigquery-transacao.service.ts b/src/bigquery/services/bigquery-transacao.service.ts index daed26a2..b4aac9a6 100644 --- a/src/bigquery/services/bigquery-transacao.service.ts +++ b/src/bigquery/services/bigquery-transacao.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CustomLogger } from 'src/utils/custom-logger'; import { BigqueryTransacao } from '../entities/transacao.bigquery-entity'; -import { BigqueryTransacaoRepository } from '../repositories/bigquery-transacao.repository'; -import { IBqFindTransacao } from '../interfaces/bq-find-transacao-by.interface'; +import { BigqueryTransacaoRepository, IBqFindTransacao } from '../repositories/bigquery-transacao.repository'; @Injectable() export class BigqueryTransacaoService { diff --git a/src/cnab/cliente-favorecido.module.ts b/src/cnab/cliente-favorecido.module.ts new file mode 100644 index 00000000..eb4ec7af --- /dev/null +++ b/src/cnab/cliente-favorecido.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClienteFavorecido } from './entity/cliente-favorecido.entity'; +import { ClienteFavorecidoRepository } from './repository/cliente-favorecido.repository'; +import { ClienteFavorecidoService } from './service/cliente-favorecido.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([ClienteFavorecido])], + providers: [ClienteFavorecidoRepository, ClienteFavorecidoService], + exports: [ClienteFavorecidoRepository, ClienteFavorecidoService], + controllers: [], +}) +export class ClienteFavorecidoModule {} diff --git a/src/cnab/cnab-manutencao.controller.ts b/src/cnab/cnab-manutencao.controller.ts index 4ff60e66..32515195 100644 --- a/src/cnab/cnab-manutencao.controller.ts +++ b/src/cnab/cnab-manutencao.controller.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Controller, Get, HttpCode, HttpStatus, ParseArrayPipe, Query, UseGuards } from '@nestjs/common'; +import { BadRequestException, Controller, Get, HttpCode, HttpStatus, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { Roles } from 'src/roles/roles.decorator'; @@ -6,8 +6,8 @@ import { RoleEnum } from 'src/roles/roles.enum'; import { RolesGuard } from 'src/roles/roles.guard'; import { ApiDescription } from 'src/utils/api-param/description-api-param'; import { CustomLogger } from 'src/utils/custom-logger'; +import { ParseArrayPipe } from 'src/utils/pipes/parse-array.pipe'; import { ParseDatePipe } from 'src/utils/pipes/parse-date.pipe'; -import { ParseListPipe } from 'src/utils/pipes/parse-list.pipe'; import { ParseNumberPipe } from 'src/utils/pipes/parse-number.pipe'; import { CnabService } from './cnab.service'; @@ -21,7 +21,51 @@ export class CnabManutencaoController { constructor(private readonly cnabService: CnabService) {} - @Get('generateRemessa') + @Get('generateRemessaLancamento') + @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('jwt'), RolesGuard) + @Roles(RoleEnum.master) + @ApiOperation({ description: 'Feito para manutenção pelos admins.\n\nExecuta a geração e envio de remessa - que normalmente é feita via cronjob.' }) + @ApiQuery({ name: 'dataOrdemInicial', type: String, required: false, description: ApiDescription({ _: 'Data da Ordem de Pagamento Inicial - salvar transações', example: '2024-07-15' }) }) + @ApiQuery({ name: 'dataOrdemFinal', type: String, required: false, description: ApiDescription({ _: 'Data da Ordem de Pagamento Final - salvar transações', example: '2024-07-16' }) }) + @ApiQuery({ name: 'dataPagamento', type: String, required: false, description: ApiDescription({ _: 'Data de pagamento', default: 'O dia de hoje' }) }) + @ApiQuery({ name: 'isConference', type: Boolean, required: true, description: 'Conferencia - Se o remessa será gerado numa tabela de teste.', example: true }) + @ApiQuery({ name: 'isCancelamento', type: Boolean, required: true, description: 'Cancelamento', example: false }) + @ApiQuery({ name: 'nsaInicial', type: Number, required: false, description: ApiDescription({ default: 'O NSA atual' }) }) + @ApiQuery({ name: 'nsaFinal', type: Number, required: false, description: ApiDescription({ default: 'nsaInicial' }) }) + @ApiQuery({ name: 'dataCancelamento', type: String, required: false, description: ApiDescription({ _: 'Data de vencimento da transação a ser cancelada (DetalheA).', 'Required if': 'isCancelamento = true' }), example: '2024-07-16' }) + @ApiBearerAuth() + async getGenerateRemessaLancamento( + @Query('dataOrdemInicial', new ParseDatePipe({ transform: true, optional: true })) _dataOrdemInicial: any, // Date + @Query('dataOrdemFinal', new ParseDatePipe({ transform: true, optional: true })) _dataOrdemFinal: any, // Date + @Query('dataPagamento', new ParseDatePipe({ transform: true, optional: true })) dataPagamento: Date | undefined, // Date | undefined + @Query('isConference') isConference: boolean, + @Query('isCancelamento') isCancelamento: boolean, + @Query('nsaInicial', new ParseNumberPipe({ min: 1, optional: true })) nsaInicial: number | undefined, + @Query('nsaFinal', new ParseNumberPipe({ min: 1, optional: true })) nsaFinal: number | undefined, + @Query('dataCancelamento', new ParseDatePipe({ transform: true, optional: true })) _dataCancelamento: any, // Date | undefined + ) { + const dataOrdemInicial = _dataOrdemInicial as Date | undefined; + const dataOrdemFinal = _dataOrdemFinal as Date | undefined; + const dataCancelamento = _dataCancelamento as Date | undefined; + + if (isCancelamento && !dataCancelamento) { + throw new BadRequestException('dataCancelamento é obrigatório se isCancelamento = true'); + } + + return await this.cnabService.getGenerateRemessaLancamento({ + dataOrdemInicial, + dataOrdemFinal, + dataPgto: dataPagamento, + isConference, + isCancelamento, + nsaInicial, + nsaFinal, + dataCancelamento, + }); + } + + @Get('generateRemessaJae') @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles(RoleEnum.master) @@ -37,7 +81,7 @@ export class CnabManutencaoController { @ApiQuery({ name: 'nsaFinal', description: ApiDescription({ default: 'nsaInicial' }), required: false, type: Number }) @ApiQuery({ name: 'dataCancelamento', description: ApiDescription({ _: 'Data de vencimento da transação a ser cancelada (DetalheA).', 'Required if': 'isCancelamento = true' }), required: false, type: String, example: '2024-07-16' }) @ApiBearerAuth() - async getGenerateRemessa( + async getGenerateRemessaJae( @Query('dataOrdemInicial', new ParseDatePipe({ transform: true })) _dataOrdemInicial: any, // Date @Query('dataOrdemFinal', new ParseDatePipe({ transform: true })) _dataOrdemFinal: any, // Date @Query('diasAnterioresOrdem', new ParseNumberPipe({ min: 0, defaultValue: 0 })) diasAnteriores: number, @@ -58,7 +102,7 @@ export class CnabManutencaoController { throw new BadRequestException('dataCancelamento é obrigatório se isCancelamento = true'); } - return await this.cnabService.getGenerateRemessa({ + return await this.cnabService.getGenerateRemessaJae({ dataOrdemInicial, dataOrdemFinal, diasAnteriores, @@ -99,7 +143,7 @@ export class CnabManutencaoController { async getSyncTransacaoViewOrdemPgto( @Query('dataOrdemInicial', new ParseDatePipe({ transform: true, optional: true })) dataOrdemInicial: Date | undefined, // @Query('dataOrdemFinal', new ParseDatePipe({ transform: true, optional: true })) dataOrdemFinal: Date | undefined, - @Query('nomeFavorecido', new ParseListPipe({ transform: true, optional: true })) nomeFavorecido: string[] | undefined, + @Query('nomeFavorecido', new ParseArrayPipe({ transform: true, optional: true })) nomeFavorecido: string[] | undefined, ) { const dataOrdem_between = dataOrdemInicial && dataOrdemFinal && ([dataOrdemInicial, dataOrdemFinal] as [Date, Date]); return await this.cnabService.syncTransacaoViewOrdemPgto({ dataOrdem_between, nomeFavorecido }); @@ -119,7 +163,7 @@ export class CnabManutencaoController { @Query('dataOrdemInicial', new ParseDatePipe({ transform: true })) dataOrdemInicial: any, // @Query('dataOrdemFinal', new ParseDatePipe({ transform: true })) dataOrdemFinal: any, @Query('consorcio') consorcio: string | undefined, - @Query('idTransacao', new ParseArrayPipe({ items: String, separator: ',', optional: true })) idTransacao: string[], // + @Query('idTransacao', new ParseArrayPipe({ optional: true })) idTransacao: string[], // ) { const _dataOrdemInicial: Date = dataOrdemInicial; const _dataOrdemFinal: Date = dataOrdemFinal; @@ -144,9 +188,11 @@ export class CnabManutencaoController { @ApiOperation({ description: 'Feito para manutenção pelos admins.\n\nAtualiza os valores de TransacaoView existentes a a partir do Bigquery.' }) @ApiBearerAuth() @ApiQuery({ name: 'diasAnteriores', type: Number, required: false, description: 'Atualizar apenas os itens até N dias atrás' }) + @ApiQuery({ name: 'idOperadora', type: String, required: false, description: ApiDescription({ _: 'Pesquisar pelo idConsorcio para atualizar', example: '8000123,8000456' }) }) async getUpdateTransacaoViewBigqueryValues( @Query('diasAnteriores', new ParseNumberPipe({ optional: true })) diasAnteriores: number | undefined, // + @Query('idOperadora', new ParseArrayPipe({ optional: true })) idOperadora: string[] | undefined, // ) { - return await this.cnabService.updateTransacaoViewBigqueryValues(diasAnteriores); + return await this.cnabService.updateTransacaoViewBigqueryValues(diasAnteriores, idOperadora); } } diff --git a/src/cnab/cnab.controller.ts b/src/cnab/cnab.controller.ts index 40f6b787..f1bc3d76 100644 --- a/src/cnab/cnab.controller.ts +++ b/src/cnab/cnab.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, HttpStatus, ParseArrayPipe, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, HttpCode, HttpStatus, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { Roles } from 'src/roles/roles.decorator'; @@ -6,10 +6,13 @@ import { RoleEnum } from 'src/roles/roles.enum'; import { RolesGuard } from 'src/roles/roles.guard'; import { ApiDescription } from 'src/utils/api-param/description-api-param'; import { CustomLogger } from 'src/utils/custom-logger'; +import { ParseArrayPipe } from 'src/utils/pipes/parse-array.pipe'; import { ParseDatePipe } from 'src/utils/pipes/parse-date.pipe'; +import { ParseEnumPipe } from 'src/utils/pipes/parse-enum.pipe'; import { ParseNumberPipe } from 'src/utils/pipes/parse-number.pipe'; -import { CnabService } from './cnab.service'; import { ClienteFavorecido } from './entity/cliente-favorecido.entity'; +import { FavorecidoEmpresaCpfCnpjEnum } from './enums/favorecido-empresa.enum'; +import { GetClienteFavorecidoConsorcioEnum } from './enums/get-cliente-favorecido-consorcio.enum'; import { ArquivoPublicacaoService } from './service/arquivo-publicacao.service'; import { ClienteFavorecidoService } from './service/cliente-favorecido.service'; import { ExtratoDto } from './service/dto/extrato.dto'; @@ -30,26 +33,34 @@ export class CnabController { ) {} @Get('clientes-favorecidos') - @ApiQuery({ name: 'nome', description: 'Pesquisa por parte do nome, sem distinção de acento ou maiúsculas.', required: false, type: String, example: 'joao' }) - @ApiQuery({ name: 'limit', description: 'Itens exibidos por página', required: false, type: Number, example: 1 }) - @ApiQuery({ name: 'page', description: ApiDescription({ description: 'Itens exibidos por página', min: 1 }), required: false, type: Number, example: 1 }) + @UseGuards(AuthGuard('jwt'), RolesGuard) + @Roles(RoleEnum.master, RoleEnum.admin, RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro) + @ApiBearerAuth() + @ApiQuery({ name: 'nome', description: 'Pesquisa por parte do nome, sem distinção de acento ou maiúsculas.', required: false, type: String }) + @ApiQuery({ name: 'nomeNot', description: 'Ignora nomes com parte do nome, sem distinção de acento ou maiúsculas.', required: false, type: String }) + @ApiQuery({ name: 'consorcio', description: 'Nome do consorcio', required: false, enum: GetClienteFavorecidoConsorcioEnum }) + @ApiQuery({ name: 'limit', description: ApiDescription({ _: 'Itens exibidos por página', min: 1 }), required: false, type: Number }) + @ApiQuery({ name: 'page', description: ApiDescription({ _: 'Itens exibidos por página', min: 1 }), required: false, type: Number }) getClienteFavorecido( - @Query('nome', new ParseArrayPipe({ items: String, separator: ',', optional: true })) nome: string[], // - @Query('limit', new ParseNumberPipe({ min: 0, optional: true })) limit: number, - @Query('page', new ParseNumberPipe({ min: 1, optional: true })) page: number, + @Query('nome', new ParseArrayPipe({ optional: true })) nome: string[], // + @Query('nomeNot', new ParseArrayPipe({ optional: true, transformOptional: true })) nomeNot: string[], // + @Query('consorcio', new ParseEnumPipe(GetClienteFavorecidoConsorcioEnum, { optional: true })) consorcio: GetClienteFavorecidoConsorcioEnum | undefined, + @Query('limit', new ParseNumberPipe({ min: 0, optional: true })) limit: number | undefined, + @Query('page', new ParseNumberPipe({ min: 1, optional: true })) page: number | undefined, ): Promise { - return this.clienteFavorecidoService.findBy({ nome, limit, page }); + const cpfCnpjNot: string[] = []; + return this.clienteFavorecidoService.getFindBy({ consorcio, cpfCnpj: { not: cpfCnpjNot }, nome: { in: nome, not: nomeNot }, limit, page }); } + @Get('extratoLancamento') + @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('jwt'), RolesGuard) + @Roles(RoleEnum.master, RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro) + @ApiBearerAuth() @ApiQuery({ name: 'conta', required: true, type: String }) @ApiQuery({ name: 'dt_inicio', required: true, type: String, example: '2024-01-01' }) @ApiQuery({ name: 'dt_fim', required: true, type: String, example: '2024-12-25' }) @ApiQuery({ name: 'tipo', required: false, type: String, example: 'cett' }) - @HttpCode(HttpStatus.OK) - @ApiBearerAuth() - @UseGuards(AuthGuard('jwt'), RolesGuard) - @Roles(RoleEnum.master, RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro) - @Get('extratoLancamento') async getLancamentoExtrato( @Query('conta') conta: string, // @Query('dt_inicio', new ParseDatePipe()) dt_inicio: string, @@ -59,15 +70,15 @@ export class CnabController { return await this.extratoHeaderArquivoService.getExtrato(conta, dt_inicio, dt_fim, tipoLancamento); } + @Get('arquivoPublicacao') + @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('jwt')) @ApiOperation({ description: `Endpoint para a equipe de dados (Bigquery) realizar a leitura das publicacoes de pagamentos realizados.` }) + @ApiBearerAuth() @ApiQuery({ name: 'dt_inicio', description: 'dataOrdem', required: true, type: String, example: '2024-01-01' }) @ApiQuery({ name: 'dt_fim', description: 'dataOrdem', required: true, type: String, example: '2024-12-25' }) @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 }) @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) - @HttpCode(HttpStatus.OK) - @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) - @Get('arquivoPublicacao') async getArquivoPublicacao( @Query('dt_inicio', new ParseDatePipe({ dateOnly: true, transform: true })) _dt_inicio: any, // Date @Query('dt_fim', new ParseDatePipe({ dateOnly: true, transform: true })) _dt_fim: any, // Date diff --git a/src/cnab/cnab.module.ts b/src/cnab/cnab.module.ts index b54bf946..eacd3a31 100644 --- a/src/cnab/cnab.module.ts +++ b/src/cnab/cnab.module.ts @@ -6,7 +6,7 @@ import { PagamentosPendentesRepository } from 'src/cnab/repository/pagamento/pag import { LancamentoModule } from 'src/lancamento/lancamento.module'; import { SettingsModule } from 'src/settings/settings.module'; import { SftpModule } from 'src/sftp/sftp.module'; -import { TransacaoViewModule } from 'src/transacao-bq/transacao-view.module'; +import { TransacaoViewModule } from 'src/transacao-view/transacao-view.module'; import { UsersModule } from 'src/users/users.module'; import { CnabController } from './cnab.controller'; import { CnabService } from './cnab.service'; diff --git a/src/cnab/cnab.service.ts b/src/cnab/cnab.service.ts index 0c25a78b..9f1ed971 100644 --- a/src/cnab/cnab.service.ts +++ b/src/cnab/cnab.service.ts @@ -1,27 +1,31 @@ import { Injectable } from '@nestjs/common'; -import { endOfDay, isFriday, nextFriday, nextThursday, startOfDay, subDays } from 'date-fns'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { endOfDay, startOfDay } from 'date-fns'; import { BigqueryOrdemPagamentoDTO } from 'src/bigquery/dtos/bigquery-ordem-pagamento.dto'; +import { BigqueryTransacao } from 'src/bigquery/entities/transacao.bigquery-entity'; import { BigqueryOrdemPagamentoService } from 'src/bigquery/services/bigquery-ordem-pagamento.service'; import { BigqueryTransacaoService } from 'src/bigquery/services/bigquery-transacao.service'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; import { LancamentoService } from 'src/lancamento/lancamento.service'; +import { cnabSettings } from 'src/settings/cnab.settings'; +import { SettingsService } from 'src/settings/settings.service'; import { SftpBackupFolder } from 'src/sftp/enums/sftp-backup-folder.enum'; import { SftpService } from 'src/sftp/sftp.service'; -import { TransacaoView } from 'src/transacao-bq/transacao-view.entity'; -import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; +import { ISyncOrdemPgto } from 'src/transacao-view/interfaces/sync-form-ordem.interface'; +import { TransacaoView } from 'src/transacao-view/transacao-view.entity'; +import { TransacaoViewService } from 'src/transacao-view/transacao-view.service'; import { UsersService } from 'src/users/users.service'; - -import { InjectDataSource } from '@nestjs/typeorm'; -import { BigqueryTransacao } from 'src/bigquery/entities/transacao.bigquery-entity'; -import { cnabSettings } from 'src/settings/cnab.settings'; -import { SettingsService } from 'src/settings/settings.service'; +import { getChunks } from 'src/utils/array-utils'; import { completeCPFCharacter } from 'src/utils/cpf-cnpj'; import { CustomLogger } from 'src/utils/custom-logger'; -import { formatDateInterval, yearMonthDayToDate } from 'src/utils/date-utils'; +import { formatDateInterval } from 'src/utils/date-utils'; +import { CommonHttpException } from 'src/utils/http-exception/common-http-exception'; +import { formatErrMsg } from 'src/utils/log-utils'; import { asNumber } from 'src/utils/pipe-utils'; -import { Between, DataSource, DeepPartial, In, IsNull, Not, QueryRunner } from 'typeorm'; +import { isContent } from 'src/utils/type-utils'; +import { DataSource, DeepPartial, QueryRunner } from 'typeorm'; import { HeaderArquivoDTO } from './dto/pagamento/header-arquivo.dto'; import { HeaderLoteDTO } from './dto/pagamento/header-lote.dto'; +import { OrdemPagamentoDto } from './dto/pagamento/ordem-pagamento.dto'; import { ClienteFavorecido } from './entity/cliente-favorecido.entity'; import { ItemTransacaoAgrupado } from './entity/pagamento/item-transacao-agrupado.entity'; import { ItemTransacao } from './entity/pagamento/item-transacao.entity'; @@ -35,6 +39,7 @@ import { TransacaoStatusEnum } from './enums/pagamento/transacao-status.enum'; import { CnabHeaderArquivo104 } from './interfaces/cnab-240/104/cnab-header-arquivo-104.interface'; import { CnabFile104Extrato } from './interfaces/cnab-240/104/extrato/cnab-file-104-extrato.interface'; import { CnabRegistros104Pgto } from './interfaces/cnab-240/104/pagamento/cnab-registros-104-pgto.interface'; +import { AllPagadorDict } from './interfaces/pagamento/all-pagador-dict.interface'; import { ArquivoPublicacaoService } from './service/arquivo-publicacao.service'; import { ClienteFavorecidoService } from './service/cliente-favorecido.service'; import { ExtratoDetalheEService } from './service/extrato/extrato-detalhe-e.service'; @@ -50,12 +55,7 @@ import { RemessaRetornoService } from './service/pagamento/remessa-retorno.servi import { TransacaoAgrupadoService } from './service/pagamento/transacao-agrupado.service'; import { TransacaoService } from './service/pagamento/transacao.service'; import { parseCnab240Extrato, parseCnab240Pagamento, stringifyCnab104File } from './utils/cnab/cnab-104-utils'; -import { CommonHttpException } from 'src/utils/http-exception/common-http-exception'; -import { formatErrMsg } from 'src/utils/log-utils'; -import e from 'express'; -import { getChunks } from 'src/utils/array-utils'; -import { isContent, isNotContent } from 'src/utils/type-utils'; -import { ISyncOrdemPgto } from 'src/transacao-bq/interfaces/sync-form-ordem.interface'; +import { LancamentoStatus } from 'src/lancamento/enums/lancamento-status.enum'; /** * User cases for CNAB and Payments @@ -90,7 +90,7 @@ export class CnabService { private settingsService: SettingsService, ) {} - async getGenerateRemessa(args: { + async getGenerateRemessaJae(args: { dataOrdemInicial: Date; // dataOrdemFinal: Date; diasAnteriores: number; @@ -102,7 +102,7 @@ export class CnabService { nsaFinal: number | undefined; dataCancelamento: Date | undefined; }) { - const METHOD = 'getGenerateRemessa'; + const METHOD = 'getGenerateRemessaJae'; const { consorcio, dataCancelamento, dataOrdemFinal, dataOrdemInicial, dataPgto, diasAnteriores, isCancelamento, isConference, nsaFinal, nsaInicial } = args; const duration = { saveTransacoesJae: '', generateRemessa: '', sendRemessa: '', total: '' }; const startDate = new Date(); @@ -142,34 +142,86 @@ export class CnabService { }; } + async getGenerateRemessaLancamento(args: { + dataOrdemInicial?: Date; // + dataOrdemFinal?: Date; + dataPgto: Date | undefined; + isConference: boolean; + isCancelamento: boolean; + nsaInicial: number | undefined; + nsaFinal: number | undefined; + dataCancelamento: Date | undefined; + }) { + const METHOD = 'getGenerateRemessaLancamento'; + const { dataCancelamento, dataOrdemFinal, dataOrdemInicial, dataPgto, isCancelamento, isConference, nsaFinal, nsaInicial } = args; + const duration = { saveTransacoesJae: '', generateRemessa: '', sendRemessa: '', total: '' }; + const startDate = new Date(); + let now = new Date(); + this.logger.log('Tarefa iniciada', METHOD); + + this.logger.log('saveTransacoesJae iniciado'); + await this.saveTransacoesLancamento(dataOrdemInicial, dataOrdemFinal); + duration.saveTransacoesJae = formatDateInterval(new Date(), now); + now = new Date(); + this.logger.log(`saveTransacoesJae finalizado - ${duration.saveTransacoesJae}`); + + this.logger.log('generateRemessa started'); + const listCnab = await this.generateRemessa({ + tipo: PagadorContaEnum.CETT, // + dataPgto, + isConference, + isCancelamento, + nsaInicial, + nsaFinal, + dataCancelamento, + }); + duration.generateRemessa = formatDateInterval(new Date(), now); + now = new Date(); + this.logger.log(`generateRemessa finalizado - ${duration.generateRemessa}`); + + this.logger.log('sendRemessa started'); + await this.sendRemessa(listCnab); + duration.sendRemessa = formatDateInterval(new Date(), now); + this.logger.log(`sendRemessa finalizado - ${duration.sendRemessa}`); + + duration.total = formatDateInterval(new Date(), startDate); + this.logger.log(`Tarefa finalizada - ${duration.total}`); + return { + duration, + cnabs: listCnab, + }; + } + /** * Atualiza valores nulos com o Bigquery */ - async updateTransacaoViewBigqueryValues(diasAnteriores?: number) { + async updateTransacaoViewBigqueryValues(diasAnteriores?: number, idOperadora?: string[]) { const METHOD = 'updateTransacaoViewBigqueryValues'; const startDate = new Date(); const result = { updated: 0, duration: '00:00:00' }; this.logger.log(`Tarefa iniciada`, METHOD); const chunkSize = 2000; - let allTransacoesView: any[] = await this.transacaoViewService.findUpdateValues(diasAnteriores); + let allTransacoesView: any[] = await this.transacaoViewService.findUpdateValues({ diasAnteriores, idOperadora }); const max = allTransacoesView.length; allTransacoesView = getChunks(allTransacoesView, chunkSize); for (const transacoesView of allTransacoesView as TransacaoView[][]) { const tvIds = transacoesView.map((i) => i.idTransacao); - const transacaoBq = await this.bigqueryTransacaoService.findMany({ id_transacao: tvIds, valor_pagamento: 'NOT NULL' }); - const bqIds = transacaoBq.map((i) => i.id_transacao); + const transacaoBq = await this.bigqueryTransacaoService.findMany({ id_transacao: tvIds, valor_pagamento: 'NOT NULL', id_operadora: idOperadora }); + const updateDtos = transacoesView - .filter((tv) => bqIds.includes(tv.idTransacao) && tv.valorPago != transacaoBq.find((bq) => bq.id_transacao == tv.idTransacao)?.valor_pagamento) - .map( - (tv) => + .filter((tv) => ((bq = transacaoBq.find((bq1) => bq1.id_transacao == tv.idTransacao)) => bq && (bq.valor_pagamento != tv.valorPago || bq.tipo_transacao != tv.tipoTransacao))()) + .map((tv) => + ((bq = transacaoBq.filter((bq1) => bq1.id_transacao == tv.idTransacao)[0]) => ({ id: tv.id, idTransacao: tv.idTransacao, - valorPago: transacaoBq.find((bq) => bq.id_transacao == tv.idTransacao)?.valor_pagamento, - } as DeepPartial), + valorPago: bq.valor_pagamento || isContent(tv.valorPago) ? tv.valorPago : null, + tipoTransacao: bq.tipo_transacao, + } as DeepPartial))(), ); - await this.transacaoViewService.updateManyRaw(updateDtos, ['valorPago'], 'idTransacao'); + await this.transacaoViewService.updateManyRaw(updateDtos, ['valorPago', 'tipoTransacao'], 'idTransacao'); + result.updated += transacoesView.length; result.duration = formatDateInterval(new Date(), startDate); const percent = ((result.updated / max) * 100).toFixed(2).padStart(5, '0'); @@ -189,8 +241,6 @@ export class CnabService { return { removed, duration }; } - // #region saveTransacoesJae - /** * Obtém dados de OrdemPagamento e salva em ItemTransacao e afins, para gerar remessa. * @@ -202,7 +252,7 @@ export class CnabService { const dataOrdemInicialDate = startOfDay(new Date(dataOrdemIncial)); const dataOrdemFinalDate = endOfDay(new Date(dataOrdemFinal)); await this.updateAllFavorecidosFromUsers(); - let ordens = await this.bigqueryOrdemPagamentoService.getFromWeek(dataOrdemInicialDate, dataOrdemFinalDate, daysBefore); + const ordens = await this.bigqueryOrdemPagamentoService.getFromWeek(dataOrdemInicialDate, dataOrdemFinalDate, daysBefore); let ordensFilter: BigqueryOrdemPagamentoDTO[]; if (consorcio.trim() === 'Empresa') { ordensFilter = ordens.filter((ordem) => ordem.consorcio.trim() !== 'VLT' && ordem.consorcio.trim() !== 'STPC' && ordem.consorcio.trim() !== 'STPL'); @@ -211,15 +261,21 @@ export class CnabService { } else { ordensFilter = ordens.filter((ordem) => ordem.consorcio === consorcio.trim()); } - await this.saveOrdens(ordensFilter); + const ordemDtos = ordensFilter.map((o) => OrdemPagamentoDto.fromBigqueryOrdem(o)); + await this.saveOrdens(ordemDtos, 'contaBilhetagem'); + } + + private async updateAllFavorecidosFromUsers() { + const allUsers = await this.usersService.findManyRegisteredUsers(); + await this.clienteFavorecidoService.updateAllFromUsers(allUsers); } /** - * Atualiza a tabela TransacaoView + * Atualiza a tabela TransacaoView com dados novos ou atualizados do bigquery */ async updateTransacaoViewBigquery(dataOrdemIncial: Date, dataOrdemFinal: Date, daysBack = 0, consorcio: string = 'Todos', idTransacao: string[] = []) { const METHOD = 'updateTransacaoViewBigquery'; - const trs = await this.getTransacoesBQ(dataOrdemIncial, dataOrdemFinal, daysBack, consorcio); + const trs = await this.findBigqueryTransacao(dataOrdemIncial, dataOrdemFinal, daysBack, consorcio); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); try { @@ -237,7 +293,7 @@ export class CnabService { async updateTransacaoViewBigqueryLimit(trsBq: BigqueryTransacao[], queryRunner: QueryRunner, idTransacao: string[] = []) { const trsFilter = idTransacao.length ? trsBq.filter((i) => idTransacao.includes(i.id_transacao)) : trsBq; - const existings = await this.transacaoViewService.findRaw({ idTransacao: trsFilter.map((tv) => tv.id_transacao) }); + const existings = await this.transacaoViewService.findRaw({ where: { idTransacao: trsFilter.map((tv) => tv.id_transacao) } }); const response = { updated: 0, created: 0, deduplicated: 0 }; for (const trBq of trsFilter) { const transacaoViewBq = TransacaoView.fromBigqueryTransacao(trBq); @@ -267,7 +323,7 @@ export class CnabService { return response; } - async getTransacoesBQ(dataOrdemIncial: Date, dataOrdemFinal: Date, daysBack = 0, consorcio: string) { + async findBigqueryTransacao(dataOrdemIncial: Date, dataOrdemFinal: Date, daysBack = 0, consorcio: string): Promise { const transacoesBq = await this.bigqueryTransacaoService.getFromWeek(startOfDay(dataOrdemIncial), endOfDay(dataOrdemFinal), daysBack); let trs = transacoesBq; if (consorcio === 'Van') { @@ -278,26 +334,13 @@ export class CnabService { return trs; } - public async getTransacoesViewOrdem(dataCaptura: Date, itemAg: ItemTransacaoAgrupado, clienteFavorecido: ClienteFavorecido) { - const dataVencimento = nextFriday(dataCaptura); - - let daysbefore = 9; - if (itemAg.nomeConsorcio === 'VLT') { - daysbefore = 2; - } - const trsDia = await this.getTransacoesViewWeek(subDays(startOfDay(dataVencimento), daysbefore), subDays(endOfDay(dataVencimento), 1)); - const trsOrdem = trsDia.filter((transacaoView) => transacaoView.idOperadora === itemAg.idOperadora && transacaoView.idConsorcio === itemAg.idConsorcio && transacaoView.operadoraCpfCnpj === clienteFavorecido.cpfCnpj); - return trsOrdem; - } + // #region saveOrdens - /** - * Salvar Transacao / ItemTransacao e agrupados - */ - async saveOrdens(ordens: BigqueryOrdemPagamentoDTO[]) { - const pagador = (await this.pagadorService.getAllPagador()).contaBilhetagem; + async saveOrdens(ordens: OrdemPagamentoDto[], pagadorKey: keyof AllPagadorDict) { + const pagador = (await this.pagadorService.getAllPagador())[pagadorKey]; for (const ordem of ordens) { - const cpfCnpj = ordem.consorcioCnpj || ordem.operadoraCpfCnpj; + const cpfCnpj = ordem.favorecidoCpfCnpj; if (!cpfCnpj) { continue; } @@ -311,43 +354,25 @@ export class CnabService { } } - private async saveUpdateItemTransacaoAg(transacaoAg: TransacaoAgrupado, ordem: BigqueryOrdemPagamentoDTO, queryRunner: QueryRunner) { - let itemAg = await this.itemTransacaoAgService.findOne({ - where: { - transacaoAgrupado: { - id: transacaoAg.id, - status: { id: TransacaoStatusEnum.created }, - }, - ...(ordem.consorcio === 'STPC' || ordem.consorcio === 'STPL' ? { idOperadora: ordem.idOperadora } : { idConsorcio: ordem.idConsorcio }), - }, - }); - if (itemAg) { - itemAg.valor += asNumber(ordem.valorTotalTransacaoLiquido); - } else { - itemAg = this.convertItemTransacaoAgrupadoDTO(ordem, transacaoAg); - } - return await this.itemTransacaoAgService.save(itemAg, queryRunner); - } - - async saveAgrupamentos(ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador, favorecido: ClienteFavorecido) { + /** + * A partir da Ordem, salva nas tabelas Transacao, ItemTransacao etc + */ + async saveAgrupamentos(ordem: OrdemPagamentoDto, pagador: Pagador, favorecido: ClienteFavorecido) { + const METHOD = 'saveAgrupamentos'; const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); - let itemAg: ItemTransacaoAgrupado | null = null; try { await queryRunner.startTransaction(); - this.logger.debug('Salva Agrupamento Consorcio: ' + ordem.consorcio); - const dataOrdem = yearMonthDayToDate(ordem.dataOrdem); - const fridayOrdem = nextFriday(startOfDay(dataOrdem)); - this.logger.debug('Inicia Consulta TransacaoAgrupado'); + this.logger.debug(`Salvando Agrupamento - ${JSON.stringify({ consorcio: ordem.consorcio, operadora: ordem.operadora, favorecidoCpfCnpj: ordem.favorecidoCpfCnpj })}`, METHOD); let transacaoAg = await this.transacaoAgService.findOne({ - dataOrdem: fridayOrdem, + dataOrdem: ordem.getTransacaoAgrupadoDataOrdem(), pagador: { id: pagador.id }, status: { id: TransacaoStatusEnum.created }, }); - this.logger.debug(ordem.consorcio); + let itemAg: ItemTransacaoAgrupado | null = null; if (transacaoAg) { - itemAg = await this.saveUpdateItemTransacaoAg(transacaoAg, ordem, queryRunner); + itemAg = await this.saveUpdateItemTransacaoAgrupado(transacaoAg, ordem, queryRunner); } else { transacaoAg = await this.saveTransacaoAgrupado(ordem, pagador); itemAg = await this.saveItemTransacaoAgrupado(ordem, transacaoAg, queryRunner); @@ -364,6 +389,32 @@ export class CnabService { } } + /** + * Rergra de negócio: + * - Usa idConsorcio, idOperadora, consorcio para agrupar um itemAgrupado + */ + private async saveUpdateItemTransacaoAgrupado(transacaoAg: TransacaoAgrupado, ordem: OrdemPagamentoDto, queryRunner: QueryRunner): Promise { + if (!ordem.consorcio.length && !ordem.operadora.length && !ordem.favorecidoCpfCnpj) { + throw new Error(`Não implementado - Criar/atualizar ItemAgrupado necessita de consorcio/operadora ou favorecidoCpfCnpj`); + } + + let itemAg = await this.itemTransacaoAgService.findOne({ + where: { + transacaoAgrupado: { + id: transacaoAg.id, + status: { id: TransacaoStatusEnum.created }, + }, + ...(ordem.consorcio.length || ordem.operadora.length ? (/** Se for Jaé, agrupa por vanzeiro ou empresa */ ordem.consorcio === 'STPC' || ordem.consorcio === 'STPL' ? { idOperadora: ordem.idOperadora } : { idConsorcio: ordem.idConsorcio }) : /** Se for Lançamento, agrupa por favorecido e dataOrdem */ { itemTransacoes: { clienteFavorecido: { cpfCnpj: ordem.favorecidoCpfCnpj as string } }, dataOrdem: startOfDay(new Date(ordem.dataOrdem)) }), + }, + }); + if (itemAg) { + itemAg.valor += asNumber(ordem.valorTotalTransacaoLiquido); + } else { + itemAg = ItemTransacaoAgrupado.fromOrdem(ordem, transacaoAg); + } + return await this.itemTransacaoAgService.save(itemAg, queryRunner); + } + async syncTransacaoViewOrdemPgto(args?: ISyncOrdemPgto) { this.logger.debug('Inicio Sync TransacaoView'); const queryRunner = this.dataSource.createQueryRunner(); @@ -376,118 +427,43 @@ export class CnabService { return { duration, count }; } - private async saveTransacaoAgrupado(ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador) { - const transacaoAg = this.convertTransacaoAgrupadoDTO(ordem, pagador); + private async saveTransacaoAgrupado(ordem: OrdemPagamentoDto, pagador: Pagador) { + const transacaoAg = TransacaoAgrupado.fromOrdem(ordem, pagador); return await this.transacaoAgService.save(transacaoAg); } - private async saveItemTransacaoAgrupado(ordem: BigqueryOrdemPagamentoDTO, transacaoAg: TransacaoAgrupado, queryRunner: QueryRunner) { - const itemAg = this.convertItemTransacaoAgrupadoDTO(ordem, transacaoAg); + private async saveItemTransacaoAgrupado(ordem: OrdemPagamentoDto, transacaoAg: TransacaoAgrupado, queryRunner: QueryRunner) { + const itemAg = ItemTransacaoAgrupado.fromOrdem(ordem, transacaoAg); return await this.itemTransacaoAgService.save(itemAg, queryRunner); } /** - * Save or update Transacao. + * A partir do OrdemBigquery salva Transacao * - * Unique id: `idOrdemPagamento` + * Chave primária: TransacaoAgrupado, idOrdemBq, pagador */ - async saveTransacao(ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador, transacaoAgId: number, queryRunner: QueryRunner): Promise { - const transacao = this.convertTransacao(ordem, pagador, transacaoAgId); + async saveTransacao(ordem: OrdemPagamentoDto, pagador: Pagador, transacaoAgId: number, queryRunner: QueryRunner): Promise { + const transacao = Transacao.fromOrdem(ordem, pagador, transacaoAgId); return await this.transacaoService.save(transacao, queryRunner); } - private convertTransacao(ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador, transacaoAgId: number) { - return new Transacao({ dataOrdem: ordem.dataOrdem, dataPagamento: ordem.dataPagamento, pagador: pagador, idOrdemPagamento: ordem.idOrdemPagamento, transacaoAgrupado: { id: transacaoAgId } }); - } - - convertTransacaoAgrupadoDTO(ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador) { - const dataOrdem = yearMonthDayToDate(ordem.dataOrdem); - /** semana de pagamento: sex-qui */ - const fridayOrdem = nextFriday(startOfDay(dataOrdem)); - const transacao = new TransacaoAgrupado({ - dataOrdem: fridayOrdem, - dataPagamento: ordem.dataPagamento, - pagador: pagador, - idOrdemPagamento: ordem.idOrdemPagamento, - status: TransacaoStatus.fromEnum(TransacaoStatusEnum.created), - }); - return transacao; - } - - convertItemTransacaoAgrupadoDTO(ordem: BigqueryOrdemPagamentoDTO, transacaoAg: TransacaoAgrupado) { - const dataOrdem = yearMonthDayToDate(ordem.dataOrdem); - const fridayOrdem = nextFriday(nextThursday(startOfDay(dataOrdem))); - const item = new ItemTransacaoAgrupado({ - dataCaptura: ordem.dataOrdem, - dataOrdem: fridayOrdem, - idConsorcio: ordem.idConsorcio, - idOperadora: ordem.idOperadora, - idOrdemPagamento: ordem.idOrdemPagamento, - nomeConsorcio: ordem.consorcio, - nomeOperadora: ordem.operadora, - valor: ordem.valorTotalTransacaoLiquido, - transacaoAgrupado: transacaoAg, - }); - return item; - } - - async saveItemTransacaoPublicacao(ordem: BigqueryOrdemPagamentoDTO, favorecido: ClienteFavorecido, transacao: Transacao, itemTransacaoAg: ItemTransacaoAgrupado, queryRunner: QueryRunner) { - const item = this.convertItemTransacao(ordem, favorecido, transacao, itemTransacaoAg); + async saveItemTransacaoPublicacao(ordem: OrdemPagamentoDto, favorecido: ClienteFavorecido, transacao: Transacao, itemTransacaoAg: ItemTransacaoAgrupado, queryRunner: QueryRunner) { + const item = ItemTransacao.fromOrdem(ordem, favorecido, transacao, itemTransacaoAg); await this.itemTransacaoService.save(item, queryRunner); - const publicacao = await this.arquivoPublicacaoService.convertPublicacaoDTO(item); - await this.arquivoPublicacaoService.save(publicacao, queryRunner); - } - - private convertItemTransacao(ordem: BigqueryOrdemPagamentoDTO, favorecido: ClienteFavorecido, transacao: Transacao, itemTransacaoAg: ItemTransacaoAgrupado) { - return new ItemTransacao({ - clienteFavorecido: favorecido, - dataCaptura: ordem.dataOrdem, - dataOrdem: startOfDay(new Date(ordem.dataOrdem)), - idConsorcio: ordem.idConsorcio, - idOperadora: ordem.idOperadora, - idOrdemPagamento: ordem.idOrdemPagamento, - nomeConsorcio: ordem.consorcio, - nomeOperadora: ordem.operadora, - valor: ordem.valorTotalTransacaoLiquido, - transacao: transacao, - itemTransacaoAgrupado: { id: itemTransacaoAg.id }, - }); - } - - async getTransacoesViewWeek(dataInicio: Date, dataFim: Date) { - let friday = new Date(); - let startDate; - let endDate; - - if (!isFriday(friday)) { - friday = nextFriday(friday); - } - - if (dataInicio != undefined && dataFim != undefined) { - startDate = dataInicio; - endDate = dataFim; - } else { - startDate = startOfDay(subDays(friday, 8)); - endDate = endOfDay(subDays(friday, 2)); + if (!ordem.lancamento) { + const publicacao = await this.arquivoPublicacaoService.convertPublicacaoDTO(item); + await this.arquivoPublicacaoService.save(publicacao, queryRunner); } - return await this.transacaoViewService.find({ datetimeProcessamento: Between(startDate, endDate) }, false); } - public async saveTransacoesLancamento() { - await this.updateAllFavorecidosFromUsers(); - const newLancamentos = await this.lancamentoService.findToPayWeek(); - const favorecidos = newLancamentos.map((i) => i.id_cliente_favorecido); - const pagador = (await this.pagadorService.getAllPagador()).contaBilhetagem; - const transacaoDTO = this.transacaoService.generateDTOForLancamento(pagador, newLancamentos); - const savedTransacao = await this.transacaoService.saveForLancamento(transacaoDTO); - const updatedLancamentos = savedTransacao.lancamentos as LancamentoEntity[]; - const itemTransacaoDTOs = this.itemTransacaoService.generateDTOsFromLancamentos(updatedLancamentos, favorecidos); - await this.itemTransacaoService.saveMany(itemTransacaoDTOs); - } + // #endregion - private async updateAllFavorecidosFromUsers() { - const allUsers = await this.usersService.findManyRegisteredUsers(); - await this.clienteFavorecidoService.updateAllFromUsers(allUsers); + public async saveTransacoesLancamento(dataOrdemInicial?: Date, dataOrdemFinal?: Date) { + const dataOrdem: [Date, Date] | undefined = dataOrdemInicial && dataOrdemFinal ? [startOfDay(dataOrdemInicial), endOfDay(dataOrdemFinal)] : undefined; + await this.updateAllFavorecidosFromUsers(); + const newLancamentos = await this.lancamentoService.findToPay(dataOrdem); + const ordens = newLancamentos.cett.map((l) => OrdemPagamentoDto.fromLancamento(l)); + await this.saveOrdens(ordens, 'cett'); } public async generateRemessa(args: { @@ -501,6 +477,7 @@ export class CnabService { nsaFinal?: number; dataCancelamento?: Date; }): Promise { + const METHOD = this.sendRemessa.name; const currentNSA = parseInt((await this.settingsService.getOneBySettingData(cnabSettings.any__cnab_current_nsa)).value); const { tipo, dataPgto, isConference, isCancelamento } = args; @@ -508,7 +485,6 @@ export class CnabService { let nsaFinal = args.nsaFinal || nsaInicial; const dataCancelamento = args?.dataCancelamento || new Date(); - const METHOD = this.sendRemessa.name; const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); try { @@ -585,10 +561,8 @@ export class CnabService { private async updateStatusRemessa(headerArquivoDTO: HeaderArquivoDTO, cnabHeaderArquivo: CnabHeaderArquivo104, transacaoAgId: number) { await this.remessaRetornoService.updateHeaderArquivoDTOFrom104(headerArquivoDTO, cnabHeaderArquivo); - await this.transacaoAgService.save({ - id: transacaoAgId, - status: TransacaoStatus.fromEnum(TransacaoStatusEnum.remessa), - }); + await this.transacaoAgService.save({ id: transacaoAgId, status: TransacaoStatus.fromEnum(TransacaoStatusEnum.remessa) }); + await this.lancamentoService.updateRaw({ status: LancamentoStatus._3_remessa }, { transacaoAgrupadoId: transacaoAgId }); } private validateCancel(nsaInicial: number, nsaFinal: number) { @@ -640,6 +614,7 @@ export class CnabService { const durationItem = formatDateInterval(new Date(), startDateItem); this.logger.log(`CNAB '${cnabName}' lido com sucesso - ${durationItem}`); success.push(cnabName); + break; } catch (error) { const durationItem = formatDateInterval(new Date(), startDateItem); this.logger.error(`Erro ao processar CNAB ${cnabName}. Movendo para backup de erros e finalizando - ${durationItem} - ${formatErrMsg(error)}`, error.stack, METHOD); diff --git a/src/cnab/const/bigquery-consorcio.const.ts b/src/cnab/const/bigquery-consorcio.const.ts new file mode 100644 index 00000000..b56d5914 --- /dev/null +++ b/src/cnab/const/bigquery-consorcio.const.ts @@ -0,0 +1,13 @@ +export const BigqueryConsorcio = [ + { consorcio: 'STPC', idConsorcio: '2' }, + { consorcio: 'Sem consórcio', idConsorcio: '7' }, + { consorcio: 'STPL', idConsorcio: '8' }, + { consorcio: 'VLT', idConsorcio: '2' }, + { consorcio: 'Intersul', idConsorcio: '221000023' }, + { consorcio: 'Transcarioca', idConsorcio: '221000014' }, + { consorcio: 'Belford Roxo', idConsorcio: '11' }, + { consorcio: 'Condomínio', idConsorcio: '10' }, + { consorcio: 'Internorte', idConsorcio: '221000032' }, + { consorcio: 'Santa Cruz', idConsorcio: '221000041' }, + { consorcio: 'MobiRio', idConsorcio: '229000010' }, +]; diff --git a/src/cnab/dto/cliente-favorecido.dto.ts b/src/cnab/dto/cliente-favorecido.dto.ts index a161beda..0d0b9067 100644 --- a/src/cnab/dto/cliente-favorecido.dto.ts +++ b/src/cnab/dto/cliente-favorecido.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty, ValidateIf } from 'class-validator'; -import { TipoFavorecidoEnum } from 'src/tipo-favorecido/tipo-favorecido.enum'; +import { TipoFavorecidoEnum } from 'src/cnab/enums/tipo-favorecido.enum'; function isCreate(object: SaveClienteFavorecidoDTO): boolean { return object.id === undefined; diff --git a/src/cnab/dto/pagamento/ordem-pagamento.dto.ts b/src/cnab/dto/pagamento/ordem-pagamento.dto.ts new file mode 100644 index 00000000..fabe5540 --- /dev/null +++ b/src/cnab/dto/pagamento/ordem-pagamento.dto.ts @@ -0,0 +1,126 @@ +import { nextFriday, startOfDay } from 'date-fns'; +import { BigqueryOrdemPagamentoDTO } from 'src/bigquery/dtos/bigquery-ordem-pagamento.dto'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; +import { formatDateYMD, yearMonthDayToDate } from 'src/utils/date-utils'; + +export interface IOrdemPagamento { + dataOrdem: string; + idOrdemPagamento: string; + idConsorcio: string; + consorcio: string; + idOperadora: string; + operadora: string; + valorTotalTransacaoLiquido: number; + favorecidoCpfCnpj: string | null; + lancamento?: Lancamento; +} + +/** + * Um DTO base para gerar itens para o Transacao, Item, ItemAgrupado, TransacaoAgrupado. + */ +export class OrdemPagamentoDto implements IOrdemPagamento { + constructor(dto?: IOrdemPagamento) { + if (dto) { + Object.assign(this, dto); + } + } + + public static fromBigqueryOrdem(bqOrdem: BigqueryOrdemPagamentoDTO) { + return new OrdemPagamentoDto({ + dataOrdem: bqOrdem.dataOrdem, + idOrdemPagamento: bqOrdem.idOrdemPagamento, + idConsorcio: bqOrdem.idConsorcio, + consorcio: bqOrdem.consorcio, + idOperadora: bqOrdem.idOperadora, + operadora: bqOrdem.operadora, + valorTotalTransacaoLiquido: bqOrdem.valorTotalTransacaoLiquido, + favorecidoCpfCnpj: bqOrdem.consorcioCnpj || bqOrdem.operadoraCpfCnpj, + }); + } + + public static fromLancamento(lancamento: Lancamento, idOrdemPagamento?: string) { + return new OrdemPagamentoDto({ + dataOrdem: formatDateYMD(lancamento.data_ordem), + idOrdemPagamento: idOrdemPagamento || OrdemPagamentoDto.getIdOrdemPagamentoLancamento(), + idConsorcio: '', + consorcio: '', + idOperadora: '', + operadora: '', + valorTotalTransacaoLiquido: lancamento.valor, + favorecidoCpfCnpj: lancamento.clienteFavorecido.cpfCnpj, + lancamento, + }); + } + + /** + * Bigquery + * - dataOrdem: sexta de pagamento + * + * Lançamento + * - dataOrdem: dia de hoje + */ + getTransacaoAgrupadoDataOrdem() { + return this.lancamento ? startOfDay(new Date()) : nextFriday(startOfDay(yearMonthDayToDate(this.dataOrdem))); + } + + /** Regra de negócio: O formato do id se refere ao dia, assim como ocorre no Jaé */ + public static getIdOrdemPagamentoLancamento() { + return `L${startOfDay(new Date()).getTime()}`; + } + /** BigqueryOrdem - Dia único, que representa uma ordem de pagamento */ + dataOrdem: string; + + /** + * BiguqeryOrdem: + * - Identificador da ordem pagamento no banco de dados da Jaé + * - Cada **data_ordem** (dia) possui um id_ordem_pagamento único. + * + * Lançamento + * - Para identificar que este ID é do Lançamento CCT, o id pode ser `L` + * - Exemplo: `L1725458795` + */ + idOrdemPagamento: string; + + /** + * BigqueryOrdem.idConsorcio + * + * Lançando: não utiliza + */ + idConsorcio: string; + + /** + * BigqueryOrdem: Nome do consórcio, para referência + * + * Lançamento: não utiliza + */ + consorcio: string; + + /** + * BigqueryOrdem: Identificador da operadora na tabela cadastro.operadoras + * + * Lançamento: não utiliza + */ + idOperadora: string; + + /** BigqueryOrdem - Nome da operadora */ + operadora: string; + + /** Valor total das transações menos o valor_desconto_taxa (R$) */ + valorTotalTransacaoLiquido: number; + + /** + * BigqueryOrdem: consorcioCnpj ou operadoraCpfCnpj, respectivamente + * + * Lançamento: clienteFavorecido.cpfCnpj + */ + favorecidoCpfCnpj: string | null; + + /** + * BigqueryOrdem: não utiliza + * + * Lançamento: + * - Associa com ItemTransacao. + * - Usado para verificar se esta Ordem é do Lançamento. + */ + lancamento?: Lancamento; +} diff --git a/src/cnab/dto/pagamento/transacao.dto.ts b/src/cnab/dto/pagamento/transacao.dto.ts index 80f72c61..62ce12f7 100644 --- a/src/cnab/dto/pagamento/transacao.dto.ts +++ b/src/cnab/dto/pagamento/transacao.dto.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, ValidateIf } from 'class-validator'; import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { DeepPartial } from 'typeorm'; import { Pagador } from '../../entity/pagamento/pagador.entity'; @@ -31,5 +31,5 @@ export class TransacaoDTO { @ValidateIf((obj) => isCreate(obj) && obj.idOrdemPagamento === undefined) @IsNotEmpty() - lancamentos?: LancamentoEntity[] | null; + lancamentos?: Lancamento[] | null; } diff --git a/src/cnab/entity/arquivo-publicacao.entity.ts b/src/cnab/entity/arquivo-publicacao.entity.ts index bb5796ba..37d87155 100644 --- a/src/cnab/entity/arquivo-publicacao.entity.ts +++ b/src/cnab/entity/arquivo-publicacao.entity.ts @@ -16,6 +16,7 @@ export interface IArquivoPublicacao { updatedAt: Date; } /** + * Representa uma transação de uma ordem de pagamento (idOrdem) a um destinatário específico (idOperadora) * Unique Jaé FK: idOrdemPagamento, idConsorcio, idOperadora */ @Entity() diff --git a/src/cnab/entity/cliente-favorecido.entity.ts b/src/cnab/entity/cliente-favorecido.entity.ts index 2699440b..882a77c1 100644 --- a/src/cnab/entity/cliente-favorecido.entity.ts +++ b/src/cnab/entity/cliente-favorecido.entity.ts @@ -1,6 +1,7 @@ import { EntityHelper } from 'src/utils/entity-helper'; import { BeforeInsert, + BeforeUpdate, Column, DeepPartial, Entity, @@ -71,6 +72,7 @@ export class ClienteFavorecido extends EntityHelper { tipo: string | null; @BeforeInsert() + @BeforeUpdate() setWriteValues() { if (typeof this.codigoBanco === 'string') { this.codigoBanco = this.codigoBanco.trim().padStart(3, '0'); diff --git a/src/cnab/entity/pagamento/detalhe-a.entity.ts b/src/cnab/entity/pagamento/detalhe-a.entity.ts index 2c883ffd..5ba35872 100644 --- a/src/cnab/entity/pagamento/detalhe-a.entity.ts +++ b/src/cnab/entity/pagamento/detalhe-a.entity.ts @@ -148,7 +148,7 @@ export class DetalheA extends EntityHelper { public isPago() { const errors = Ocorrencia.getErrorCodesFromString(this.ocorrenciasCnab || ''); - return errors.length === 0; + return errors.length === 0 && Boolean(this.ocorrenciasCnab?.length); } public static getOcorrenciaErrors(detalhes: DetalheA[]) { diff --git a/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts b/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts index a51bcd7b..9b743cf3 100644 --- a/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts +++ b/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts @@ -1,34 +1,32 @@ import { EntityHelper } from 'src/utils/entity-helper'; import { asStringOrNumber } from 'src/utils/pipe-utils'; -import { - AfterLoad, - Column, - CreateDateColumn, - DeepPartial, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { AfterLoad, Column, CreateDateColumn, DeepPartial, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { nextFriday, nextThursday, startOfDay } from 'date-fns'; +import { OrdemPagamentoDto } from 'src/cnab/dto/pagamento/ordem-pagamento.dto'; +import { yearMonthDayToDate } from 'src/utils/date-utils'; +import { ItemTransacao } from './item-transacao.entity'; import { TransacaoAgrupado } from './transacao-agrupado.entity'; +import { Exclude } from 'class-transformer'; /** * Representa um destinatário, a ser pago pelo remetente (TransacaoAgrupado). - * + * * Esta tabela contém a soma de todas as transações (ItemTransacao) * a serem feitas neste CNAB (TransacaoAgrupado). - * + * * Colunas: * - dataOrdem: sexta de pagamento (baseado no BigqueryOrdemPgto.dataOrdem (dia da ordem D+1)) - * + * * Identificador: * - TransacaoAgrupado (CNAB / remetente) * - ClienteFavorecido (destinatário) */ @Entity() export class ItemTransacaoAgrupado extends EntityHelper { + @Exclude() + private readonly FKs = ['transacaoAgrupado', 'clienteFavorecido']; + constructor(dto?: DeepPartial) { super(); if (dto) { @@ -39,7 +37,20 @@ export class ItemTransacaoAgrupado extends EntityHelper { } } - private readonly FKs = ['transacaoAgrupado', 'clienteFavorecido']; + public static fromOrdem(ordem: OrdemPagamentoDto, transacaoAg: TransacaoAgrupado) { + const fridayOrdem = nextFriday(nextThursday(startOfDay(yearMonthDayToDate(ordem.dataOrdem)))); + const item = new ItemTransacaoAgrupado({ + dataOrdem: ordem.lancamento ? ordem.dataOrdem : fridayOrdem, + idConsorcio: ordem.idConsorcio, + idOperadora: ordem.idOperadora, + idOrdemPagamento: ordem.idOrdemPagamento, + nomeConsorcio: ordem.consorcio, + nomeOperadora: ordem.operadora, + valor: ordem.valorTotalTransacaoLiquido, + transacaoAgrupado: transacaoAg, + }); + return item; + } @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_ItemTransacaoAgrupado_id', @@ -50,15 +61,14 @@ export class ItemTransacaoAgrupado extends EntityHelper { eager: true, }) @JoinColumn({ - foreignKeyConstraintName: - 'FK_ItemTransacaoAgrupado_transacaoAgrupado_ManyToOne', + foreignKeyConstraintName: 'FK_ItemTransacaoAgrupado_transacaoAgrupado_ManyToOne', }) transacaoAgrupado: TransacaoAgrupado; @CreateDateColumn() dataProcessamento: Date; - /** Ao gravar pegamos dataOrdem */ + /** TODO: remover, não usaremos mais pois já tem o createdAt - Ao gravar pegamos dataOrdem */ @CreateDateColumn() dataCaptura: Date; @@ -105,13 +115,15 @@ export class ItemTransacaoAgrupado extends EntityHelper { @UpdateDateColumn() updatedAt: Date; + /** Não é uma coluna, usado apenas para consulta no ORM. */ + @OneToMany(() => ItemTransacao, (it) => it.itemTransacaoAgrupado) + itemTransacoes: ItemTransacao[]; + public getLogInfo(): string { return `#{ idOP: ${this.idOrdemPagamento}, op: ${this.idOperadora}, co: ${this.idConsorcio} }`; } - public static getUniqueIdJae( - entity: DeepPartial, - ): string { + public static getUniqueIdJae(entity: DeepPartial): string { return `${entity.idOrdemPagamento}|${entity.idConsorcio}|${entity.idOperadora}`; } diff --git a/src/cnab/entity/pagamento/item-transacao.entity.ts b/src/cnab/entity/pagamento/item-transacao.entity.ts index 7e98bdff..3c59df0a 100644 --- a/src/cnab/entity/pagamento/item-transacao.entity.ts +++ b/src/cnab/entity/pagamento/item-transacao.entity.ts @@ -1,26 +1,19 @@ +import { startOfDay } from 'date-fns'; +import { OrdemPagamentoDto } from 'src/cnab/dto/pagamento/ordem-pagamento.dto'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { EntityHelper } from 'src/utils/entity-helper'; import { asStringOrNumber } from 'src/utils/pipe-utils'; -import { - AfterLoad, - Column, - CreateDateColumn, - DeepPartial, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { AfterLoad, Column, CreateDateColumn, DeepPartial, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { ClienteFavorecido } from '../cliente-favorecido.entity'; import { ItemTransacaoAgrupado } from './item-transacao-agrupado.entity'; import { Transacao } from './transacao.entity'; /** * Representa uma BqOrdemPgto (ArquivoPublicacao) - * + * * Colunas: * - dataOrdem: BqOrdemPgto.dataOrdem - * + * * Identificador: * - ItemTransacaoAgrupado * - dataTransacao @@ -34,11 +27,28 @@ export class ItemTransacao extends EntityHelper { if (dto) { Object.assign(this, dto); if (dto.itemTransacaoAgrupado) { - this.itemTransacaoAgrupado = new ItemTransacaoAgrupado(dto.itemTransacaoAgrupado) + this.itemTransacaoAgrupado = new ItemTransacaoAgrupado(dto.itemTransacaoAgrupado); } } } + public static fromOrdem(ordem: OrdemPagamentoDto, favorecido: ClienteFavorecido, transacao: Transacao, itemTransacaoAg: ItemTransacaoAgrupado) { + return new ItemTransacao({ + clienteFavorecido: favorecido, + dataCaptura: ordem.dataOrdem, + dataOrdem: startOfDay(new Date(ordem.dataOrdem)), + idConsorcio: ordem.idConsorcio, + idOperadora: ordem.idOperadora, + idOrdemPagamento: ordem.idOrdemPagamento, + nomeConsorcio: ordem.consorcio, + nomeOperadora: ordem.operadora, + valor: ordem.valorTotalTransacaoLiquido, + transacao: transacao, + itemTransacaoAgrupado: { id: itemTransacaoAg.id }, + ...(ordem?.lancamento ? { lancamento: { id: ordem.lancamento.id } } : {}), + }); + } + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_ItemTransacao_id' }) id: number; @@ -54,8 +64,7 @@ export class ItemTransacao extends EntityHelper { eager: true, }) @JoinColumn({ - foreignKeyConstraintName: - 'FK_ItemTransacao_itemTransacaoAgrupado_ManyToOne', + foreignKeyConstraintName: 'FK_ItemTransacao_itemTransacaoAgrupado_ManyToOne', }) itemTransacaoAgrupado: ItemTransacaoAgrupado; @@ -85,7 +94,7 @@ export class ItemTransacao extends EntityHelper { }) clienteFavorecido: ClienteFavorecido; - /** + /** * Valor do lançamento. */ @Column({ @@ -112,9 +121,9 @@ export class ItemTransacao extends EntityHelper { // Unique columns Lancamento - /** + /** * DataOrdem from bigquery - * + * * D+1 (sex-qui) */ @Column({ type: Date, unique: false, nullable: false }) @@ -126,6 +135,10 @@ export class ItemTransacao extends EntityHelper { @UpdateDateColumn() updatedAt: Date; + /** Não é uma coluna, usado apenas para consulta no ORM. */ + @OneToOne(() => Lancamento, (l) => l.itemTransacao) + lancamento: Lancamento | null; + public getLogInfo(): string { return `#{ idOP: ${this.idOrdemPagamento}, op: ${this.idOperadora}, co: ${this.idConsorcio} }`; } diff --git a/src/cnab/entity/pagamento/ocorrencia.entity.ts b/src/cnab/entity/pagamento/ocorrencia.entity.ts index 3431b0fe..5a67642e 100644 --- a/src/cnab/entity/pagamento/ocorrencia.entity.ts +++ b/src/cnab/entity/pagamento/ocorrencia.entity.ts @@ -77,7 +77,7 @@ export class Ocorrencia extends EntityHelper { return ocorrencias; } - public static getErrorCodes(ocorrencias: Ocorrencia[]) { + public static getErrorCodes(ocorrencias: Ocorrencia[]): string[] { const codesList = ocorrencias.map((o) => o.code); const errors = codesList.filter((c) => !['00', 'BD'].includes(c)); return errors; @@ -121,4 +121,8 @@ export class Ocorrencia extends EntityHelper { const code = ocorrencia?.code; return code && !['BD', '00'].includes(code); } + + public static getErrors(ocorrencias: Ocorrencia[]) { + return ocorrencias.filter(o => Ocorrencia.isError(o)); + } } diff --git a/src/cnab/entity/pagamento/transacao-agrupado.entity.ts b/src/cnab/entity/pagamento/transacao-agrupado.entity.ts index 4b4ef738..494fb1ce 100644 --- a/src/cnab/entity/pagamento/transacao-agrupado.entity.ts +++ b/src/cnab/entity/pagamento/transacao-agrupado.entity.ts @@ -1,16 +1,10 @@ -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { nextFriday, startOfDay } from 'date-fns'; +import { OrdemPagamentoDto } from 'src/cnab/dto/pagamento/ordem-pagamento.dto'; +import { TransacaoStatusEnum } from 'src/cnab/enums/pagamento/transacao-status.enum'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; +import { yearMonthDayToDate } from 'src/utils/date-utils'; import { EntityHelper } from 'src/utils/entity-helper'; -import { - Column, - CreateDateColumn, - DeepPartial, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { Column, CreateDateColumn, DeepPartial, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { ItemTransacaoAgrupado } from './item-transacao-agrupado.entity'; import { Pagador } from './pagador.entity'; import { TransacaoStatus } from './transacao-status.entity'; @@ -31,6 +25,18 @@ export class TransacaoAgrupado extends EntityHelper { } } + + public static fromOrdem(ordem: OrdemPagamentoDto, pagador: Pagador) { + const transacao = new TransacaoAgrupado({ + dataOrdem: ordem.getTransacaoAgrupadoDataOrdem(), + dataPagamento: null, + pagador: pagador, + idOrdemPagamento: ordem.idOrdemPagamento, + status: TransacaoStatus.fromEnum(TransacaoStatusEnum.created), + }); + return transacao; + } + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_TransacaoAgrupado_id', }) @@ -62,13 +68,13 @@ export class TransacaoAgrupado extends EntityHelper { status: TransacaoStatus; /** Not a physical column */ - @OneToMany(() => LancamentoEntity, (lancamento) => lancamento.transacao, { + @OneToMany(() => Lancamento, (lancamento) => lancamento.itemTransacao, { nullable: true, }) @JoinColumn({ foreignKeyConstraintName: 'FK_TransacaoAgrupado_lancamentos_OneToMany', }) - lancamentos: LancamentoEntity[] | null; + lancamentos: Lancamento[] | null; /** Not a physical column */ @OneToMany(() => ItemTransacaoAgrupado, (item) => item.transacaoAgrupado, { diff --git a/src/cnab/entity/pagamento/transacao.entity.ts b/src/cnab/entity/pagamento/transacao.entity.ts index d1b55a13..6e4f90a9 100644 --- a/src/cnab/entity/pagamento/transacao.entity.ts +++ b/src/cnab/entity/pagamento/transacao.entity.ts @@ -1,16 +1,7 @@ -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { OrdemPagamentoDto } from 'src/cnab/dto/pagamento/ordem-pagamento.dto'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { EntityHelper } from 'src/utils/entity-helper'; -import { - Column, - CreateDateColumn, - DeepPartial, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { Column, CreateDateColumn, DeepPartial, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { ItemTransacao } from './item-transacao.entity'; import { Pagador } from './pagador.entity'; import { TransacaoAgrupado } from './transacao-agrupado.entity'; @@ -40,6 +31,16 @@ export class Transacao extends EntityHelper { } } + public static fromOrdem(ordem: OrdemPagamentoDto, pagador: Pagador, transacaoAgrupadoId: number) { + return new Transacao({ + dataOrdem: ordem.dataOrdem, // + dataPagamento: null, + pagador: pagador, + idOrdemPagamento: ordem.idOrdemPagamento, + transacaoAgrupado: { id: transacaoAgrupadoId }, + }); + } + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_Transacao_id' }) id: number; @@ -68,13 +69,13 @@ export class Transacao extends EntityHelper { transacaoAgrupado: TransacaoAgrupado; /** Not a physical column */ - @OneToMany(() => LancamentoEntity, (lancamento) => lancamento.transacao, { + @OneToMany(() => Lancamento, (lancamento) => lancamento.itemTransacao, { nullable: true, }) @JoinColumn({ foreignKeyConstraintName: 'FK_Transacao_lancamentos_OneToMany', }) - lancamentos: LancamentoEntity[] | null; + lancamentos: Lancamento[] | null; /** Not a physical column */ @OneToMany(() => ItemTransacao, (item) => item.transacao, { eager: false }) diff --git a/src/cnab/enums/favorecido-empresa.enum.ts b/src/cnab/enums/favorecido-empresa.enum.ts new file mode 100644 index 00000000..c3792b7c --- /dev/null +++ b/src/cnab/enums/favorecido-empresa.enum.ts @@ -0,0 +1,17 @@ +export enum FavorecidoEmpresaNomeEnum { + VLT = 'Concessionária do VLT Carioca S.A.', + Intersul = 'Consórcio Intersul Transportes', + Internorte = 'Consórcio Internorte de Transportes', + Transcarioca = 'Consórcio Transcarioca de Transportes', + SantaCruz = 'Consórcio Santa Cruz Transportes', + CMTC = 'Companhia Municipal de Transportes Coletivos CMTC Rio', +} + +export enum FavorecidoEmpresaCpfCnpjEnum { + VLT = '18201378000119', + Intersul = '12464869000176', + Internorte = '12464539000180', + Transcarioca = '12464553000184', + SantaCruz = '12464577000133', + CMTC = '44520687000161', +} \ No newline at end of file diff --git a/src/cnab/enums/get-cliente-favorecido-consorcio.enum.ts b/src/cnab/enums/get-cliente-favorecido-consorcio.enum.ts new file mode 100644 index 00000000..c1f2eb23 --- /dev/null +++ b/src/cnab/enums/get-cliente-favorecido-consorcio.enum.ts @@ -0,0 +1,4 @@ +export enum GetClienteFavorecidoConsorcioEnum { + Van = 'Van', + Empresa = 'Empresa', +} diff --git a/src/cnab/enums/tipo-favorecido.enum.ts b/src/cnab/enums/tipo-favorecido.enum.ts new file mode 100644 index 00000000..14e77e32 --- /dev/null +++ b/src/cnab/enums/tipo-favorecido.enum.ts @@ -0,0 +1,4 @@ +export enum TipoFavorecidoEnum { + van = 'van', + empresa = 'empresa', +} diff --git a/src/cnab/interfaces/cliente-favorecido-find-by.interface.ts b/src/cnab/interfaces/cliente-favorecido-find-by.interface.ts deleted file mode 100644 index 806c99d8..00000000 --- a/src/cnab/interfaces/cliente-favorecido-find-by.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IClienteFavorecidoFindBy { - /** ILIKE unaccent */ - nome?: string[]; - limit?: number; - page?: number; -} diff --git a/src/cnab/interfaces/pagamento/all-pagador-dict.interface.ts b/src/cnab/interfaces/pagamento/all-pagador-dict.interface.ts index 338decf7..499ed398 100644 --- a/src/cnab/interfaces/pagamento/all-pagador-dict.interface.ts +++ b/src/cnab/interfaces/pagamento/all-pagador-dict.interface.ts @@ -3,6 +3,8 @@ import { Pagador } from "src/cnab/entity/pagamento/pagador.entity"; export interface AllPagadorDict { /** * Only for items from Lancamento + * + * Conta de Estabilização Tarifária dos Transportes */ cett: Pagador, diff --git a/src/cnab/repository/cliente-favorecido.repository.ts b/src/cnab/repository/cliente-favorecido.repository.ts index 3a1b0b3d..6825de8d 100644 --- a/src/cnab/repository/cliente-favorecido.repository.ts +++ b/src/cnab/repository/cliente-favorecido.repository.ts @@ -1,13 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { compactQuery } from 'src/utils/console-utils'; import { CustomLogger } from 'src/utils/custom-logger'; import { CommonHttpException } from 'src/utils/http-exception/common-http-exception'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; import { DeepPartial, FindManyOptions, FindOneOptions, InsertResult, Repository, UpdateResult } from 'typeorm'; import { SaveClienteFavorecidoDTO } from '../dto/cliente-favorecido.dto'; import { ClienteFavorecido } from '../entity/cliente-favorecido.entity'; -import { IClienteFavorecidoFindBy } from '../interfaces/cliente-favorecido-find-by.interface'; -import { compactQuery } from 'src/utils/console-utils'; export interface IClienteFavorecidoRawWhere { id?: number[]; @@ -17,6 +16,15 @@ export interface IClienteFavorecidoRawWhere { detalheANumeroDocumento?: number[]; } +export interface IClienteFavorecidoFindBy { + /** ILIKE unaccent */ + nome?: { in?: string[]; not?: string[] }; + consorcio?: string; + cpfCnpj?: { not: string[] }; + limit?: number; + page?: number; +} + @Injectable() export class ClienteFavorecidoRepository { private logger = new CustomLogger(ClienteFavorecidoRepository.name, { timestamp: true }); @@ -97,6 +105,7 @@ export class ClienteFavorecidoRepository { public async findManyBy(where?: IClienteFavorecidoFindBy): Promise { let isFirstWhere = false; let qb = this.clienteFavorecidoRepository.createQueryBuilder('favorecido'); + function cmd() { if (isFirstWhere) { isFirstWhere = false; @@ -105,19 +114,39 @@ export class ClienteFavorecidoRepository { return 'andWhere'; } } - if (where?.nome?.length) { - for (const nome of where.nome) { + + if (where?.nome?.in?.length) { + for (const nome of where.nome?.in) { qb = qb[cmd()]('favorecido."nome" ILIKE UNACCENT(UPPER(:nome))', { nome: `%${nome}%`, }); } } + if (where?.nome?.not?.length) { + for (const nome of where.nome.not as string[]) { + qb = qb[cmd()]('favorecido."nome" NOT ILIKE UNACCENT(UPPER(:nome))', { + nome: `%${nome}%`, + }); + } + } + + if (where?.consorcio) { + const consorcio = where.consorcio; + if (consorcio === 'Van') { + qb = qb[cmd()]('favorecido.tipo = :tipo', { tipo: 'vanzeiro' }); + } else if (consorcio === 'Empresa') { + qb = qb[cmd()]('favorecido.tipo = :tipo', { tipo: 'consorcio' }); + } + } + if (where?.cpfCnpj?.not?.length) { + qb = qb[cmd()](`favorecido."cpfCnpj" NOT IN (${where.cpfCnpj.not.map((i) => `'${i}'`).join(',')})`); + } if (where?.limit && where?.page) { const skip = where?.limit * (where?.page - 1); qb = qb.take(where?.limit).skip(skip); } - qb = qb.orderBy('"id"', 'ASC'); + qb = qb.orderBy('"nome"', 'ASC'); const result = await qb.getMany(); return result; @@ -148,9 +177,13 @@ export class ClienteFavorecidoRepository { compactQuery(` SELECT cf.* FROM cliente_favorecido cf - ${where?.detalheANumeroDocumento ? `INNER JOIN item_transacao it ON it."clienteFavorecidoId" = cf.id + ${ + where?.detalheANumeroDocumento + ? `INNER JOIN item_transacao it ON it."clienteFavorecidoId" = cf.id INNER JOIN item_transacao_agrupado ita ON ita.id = it."itemTransacaoAgrupadoId" - INNER JOIN detalhe_a da ON da."itemTransacaoAgrupadoId" = ita.id` : ''} + INNER JOIN detalhe_a da ON da."itemTransacaoAgrupadoId" = ita.id` + : '' + } ${qWhere.query} ORDER BY cf.id `), diff --git a/src/cnab/repository/pagamento/detalhe-a.repository.ts b/src/cnab/repository/pagamento/detalhe-a.repository.ts index 8d8e1479..981eb9ce 100644 --- a/src/cnab/repository/pagamento/detalhe-a.repository.ts +++ b/src/cnab/repository/pagamento/detalhe-a.repository.ts @@ -93,9 +93,9 @@ export class DetalheARepository { da.id, da."createdAt", da."dataEfetivacao", da."dataVencimento", da."finalidadeDOC", da."indicadorBloqueio", da."indicadorFormaParcelamento", da."loteServico", da.nsr, da."numeroDocumentoBanco", da."numeroDocumentoEmpresa", da."numeroParcela", - da."ocorrenciasCnab", da."periodoVencimento", da."quantidadeMoeda", - da."quantidadeParcelas", da."tipoMoeda", da."updatedAt", da."valorLancamento", - da."valorRealEfetivado", + da."ocorrenciasCnab", da."periodoVencimento", da."quantidadeMoeda"::FLOAT, + da."quantidadeParcelas", da."tipoMoeda", da."updatedAt", da."valorLancamento"::FLOAT, + da."valorRealEfetivado"::FLOAT, json_build_object( 'id', da."itemTransacaoAgrupadoId", 'transacaoAgrupado', json_build_object( diff --git a/src/cnab/service/arquivo-publicacao.service.ts b/src/cnab/service/arquivo-publicacao.service.ts index 3b5374ad..26b9240b 100644 --- a/src/cnab/service/arquivo-publicacao.service.ts +++ b/src/cnab/service/arquivo-publicacao.service.ts @@ -1,17 +1,15 @@ import { Injectable, Logger } from '@nestjs/common'; import { addDays, isDate, isThursday, nextFriday, startOfDay } from 'date-fns'; -import { asNumber } from 'src/utils/pipe-utils'; +import { compactQuery } from 'src/utils/console-utils'; +import { EntityHelper } from 'src/utils/entity-helper'; import { DeepPartial, FindManyOptions, QueryRunner } from 'typeorm'; +import { ArquivoPublicacaoBigqueryDTO } from '../dto/arquivo-publicacao-bigquery.dto'; import { ArquivoPublicacao } from '../entity/arquivo-publicacao.entity'; import { DetalheA } from '../entity/pagamento/detalhe-a.entity'; import { ItemTransacao } from '../entity/pagamento/item-transacao.entity'; import { ArquivoPublicacaoRepository, IArquivoPublicacaoRawWhere } from '../repository/arquivo-publicacao.repository'; import { OcorrenciaService } from './ocorrencia.service'; import { ItemTransacaoService } from './pagamento/item-transacao.service'; -import { IFindPublicacaoRelatorio } from 'src/relatorio/interfaces/find-publicacao-relatorio.interface'; -import { EntityHelper } from 'src/utils/entity-helper'; -import { ArquivoPublicacaoBigqueryDTO } from '../dto/arquivo-publicacao-bigquery.dto'; -import { compactQuery } from 'src/utils/console-utils'; export type ArquivoPublicacaoFields = 'savePublicacaoRetorno'; @@ -79,11 +77,17 @@ export class ArquivoPublicacaoService { } public async updateManyRaw(dtos: DeepPartial[], fields: ArquivoPublicacaoFields, queryRunner: QueryRunner): Promise { + if (!dtos.length) { + return []; + } let fieldNames: (keyof ArquivoPublicacao)[] = []; if (fields == 'savePublicacaoRetorno') { fieldNames = ['id', 'isPago', 'valorRealEfetivado', 'dataEfetivacao', 'dataGeracaoRetorno']; } const fieldValues = dtos.map((dto) => `(${EntityHelper.getQueryFieldValues(dto, fieldNames, ArquivoPublicacao.sqlFieldTypes)})`).join(', '); + // const reference: keyof IArquivoPublicacao = 'id'; + // const updatedAt: keyof IArquivoPublicacao = 'updatedAt'; + // const query = EntityHelper.getQueryUpdate('arquivo_publicacao', dtos, fieldNames, ArquivoPublicacao.sqlFieldTypes, reference, updatedAt); const query = ` UPDATE arquivo_publicacao SET ${fieldNames.map((f) => `"${f}" = sub.${f == 'id' ? '_id' : `"${f}"`}`).join(', ')}, "updatedAt" = NOW() diff --git a/src/cnab/service/cliente-favorecido.service.ts b/src/cnab/service/cliente-favorecido.service.ts index 38efd62f..93d69efc 100644 --- a/src/cnab/service/cliente-favorecido.service.ts +++ b/src/cnab/service/cliente-favorecido.service.ts @@ -1,28 +1,24 @@ import { HttpStatus, Injectable, Logger } from '@nestjs/common'; import { BigqueryOrdemPagamentoDTO } from 'src/bigquery/dtos/bigquery-ordem-pagamento.dto'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; -import { TipoFavorecidoEnum } from 'src/tipo-favorecido/tipo-favorecido.enum'; +import { TipoFavorecidoEnum } from 'src/cnab/enums/tipo-favorecido.enum'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { User } from 'src/users/entities/user.entity'; +import { CustomLogger } from 'src/utils/custom-logger'; import { CommonHttpException } from 'src/utils/http-exception/common-http-exception'; import { asString } from 'src/utils/pipe-utils'; import { parseStringUpperUnaccent } from 'src/utils/string-utils'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; import { validateDTO } from 'src/utils/validation-utils'; -import { FindManyOptions, FindOneOptions, In } from 'typeorm'; +import { FindOneOptions, In } from 'typeorm'; import { SaveClienteFavorecidoDTO } from '../dto/cliente-favorecido.dto'; import { ClienteFavorecido } from '../entity/cliente-favorecido.entity'; -import { ClienteFavorecidoRepository, IClienteFavorecidoRawWhere } from '../repository/cliente-favorecido.repository'; -import { IClienteFavorecidoFindBy } from '../interfaces/cliente-favorecido-find-by.interface'; +import { ClienteFavorecidoRepository, IClienteFavorecidoFindBy, IClienteFavorecidoRawWhere } from '../repository/cliente-favorecido.repository'; @Injectable() export class ClienteFavorecidoService { - private logger: Logger = new Logger('ClienteFavorecidoService', { - timestamp: true, - }); + private logger: Logger = new CustomLogger('ClienteFavorecidoService', { timestamp: true }); - constructor( - private clienteFavorecidoRepository: ClienteFavorecidoRepository, - ) {} + constructor(private clienteFavorecidoRepository: ClienteFavorecidoRepository) {} /** * All ClienteFavoecidos will be created or updated from users based of cpfCnpj. @@ -43,9 +39,7 @@ export class ClienteFavorecidoService { await this.clienteFavorecidoRepository.upsert(newFavorecidos); } - public async findBy( - where?: IClienteFavorecidoFindBy, - ): Promise { + public async getFindBy(where?: IClienteFavorecidoFindBy): Promise { return await this.clienteFavorecidoRepository.findManyBy(where); } @@ -55,10 +49,8 @@ export class ClienteFavorecidoService { }); } - public async findManyFromLancamentos( - lancamentos: LancamentoEntity[], - ): Promise { - const ids = [...new Set(lancamentos.map((i) => i.id_cliente_favorecido))]; + public async findManyFromLancamentos(lancamentos: Lancamento[]): Promise { + const ids = [...new Set(lancamentos.map((i) => i.clienteFavorecido))]; return await this.clienteFavorecidoRepository.findMany({ where: { id: In(ids), @@ -66,17 +58,8 @@ export class ClienteFavorecidoService { }); } - public async findManyFromOrdens( - ordens: BigqueryOrdemPagamentoDTO[], - ): Promise { - const documentos = ordens.reduce( - (l, i) => [ - ...l, - ...(i.consorcioCnpj ? [i.consorcioCnpj] : []), - ...(i.operadoraCpfCnpj ? [i.operadoraCpfCnpj] : []), - ], - [], - ); + public async findManyFromOrdens(ordens: BigqueryOrdemPagamentoDTO[]): Promise { + const documentos = ordens.reduce((l, i) => [...l, ...(i.consorcioCnpj ? [i.consorcioCnpj] : []), ...(i.operadoraCpfCnpj ? [i.operadoraCpfCnpj] : [])], []); const uniqueDocumentos = [...new Set(documentos)]; return await this.clienteFavorecidoRepository.findMany({ where: { @@ -93,18 +76,12 @@ export class ClienteFavorecidoService { return await this.clienteFavorecidoRepository.findAll(); } - public async getOneByIdClienteFavorecido( - idClienteFavorecido: number, - ): Promise { + public async getOneByIdClienteFavorecido(idClienteFavorecido: number): Promise { const cliente_favorecido = await this.clienteFavorecidoRepository.getOne({ id: idClienteFavorecido, }); if (!cliente_favorecido) { - throw CommonHttpException.errorDetails( - 'cliente_favorecido.conta not found', - { pagadorConta: idClienteFavorecido }, - HttpStatus.NOT_FOUND, - ); + throw CommonHttpException.errorDetails('cliente_favorecido.conta not found', { pagadorConta: idClienteFavorecido }, HttpStatus.NOT_FOUND); } else { return cliente_favorecido; } @@ -113,20 +90,13 @@ export class ClienteFavorecidoService { public async getClienteFavorecido(): Promise { const cliente_favorecido = await this.clienteFavorecidoRepository.findAll(); if (!cliente_favorecido) { - throw CommonHttpException.errorDetails( - 'cliente_favorecido.conta not found', - {}, - HttpStatus.NOT_FOUND, - ); + throw CommonHttpException.errorDetails('cliente_favorecido.conta not found', {}, HttpStatus.NOT_FOUND); } else { return cliente_favorecido; } } - private async getManyFavorecidoDTOsFromUsers( - users: User[], - existingId_facorecido?: number, - ): Promise { + private async getManyFavorecidoDTOsFromUsers(users: User[], existingId_facorecido?: number): Promise { const newItems: SaveClienteFavorecidoDTO[] = []; for (const user of users) { const newItem: SaveClienteFavorecidoDTO = { @@ -146,7 +116,7 @@ export class ClienteFavorecidoService { cep: null, complementoCep: null, uf: null, - tipo: TipoFavorecidoEnum.operadora, + tipo: TipoFavorecidoEnum.van, }; await validateDTO(SaveClienteFavorecidoDTO, newItem); newItems.push(newItem); @@ -154,10 +124,7 @@ export class ClienteFavorecidoService { return newItems; } - private async saveFavorecidoFromUser( - user: User, - existingId_facorecido?: number, - ): Promise { + private async saveFavorecidoFromUser(user: User, existingId_facorecido?: number): Promise { const saveObject: SaveClienteFavorecidoDTO = { id: existingId_facorecido, nome: asString(user.fullName), @@ -175,22 +142,16 @@ export class ClienteFavorecidoService { cep: null, complementoCep: null, uf: null, - tipo: TipoFavorecidoEnum.operadora, + tipo: TipoFavorecidoEnum.van, }; await validateDTO(SaveClienteFavorecidoDTO, saveObject); await this.clienteFavorecidoRepository.save(saveObject); } - public async getOne( - fields: EntityCondition, - ): Promise { + public async getOne(fields: EntityCondition): Promise { const cliente = await this.clienteFavorecidoRepository.getOne(fields); if (!cliente) { - throw CommonHttpException.errorDetails( - 'cliente_favorecido.conta not found', - { pagadorConta: cliente }, - HttpStatus.NOT_FOUND, - ); + throw CommonHttpException.errorDetails('cliente_favorecido.conta not found', { pagadorConta: cliente }, HttpStatus.NOT_FOUND); } else { return cliente; } @@ -200,12 +161,10 @@ export class ClienteFavorecidoService { return await this.clienteFavorecidoRepository.findOneByNome(nome); } - public async findOne( - options: FindOneOptions, - ): Promise { + public async findOne(options: FindOneOptions): Promise { return await this.clienteFavorecidoRepository.findOne(options); } - + public async findOneRaw(where: IClienteFavorecidoRawWhere): Promise { const result = await this.clienteFavorecidoRepository.findManyRaw(where); return result?.[0] || null; diff --git a/src/cnab/service/extrato/extrato-header-arquivo.service.ts b/src/cnab/service/extrato/extrato-header-arquivo.service.ts index c844c3f9..64302d10 100644 --- a/src/cnab/service/extrato/extrato-header-arquivo.service.ts +++ b/src/cnab/service/extrato/extrato-header-arquivo.service.ts @@ -89,23 +89,23 @@ export class ExtratoHeaderArquivoService { : PagadorContaEnum.ContaBilhetagem; const query = ` - SELECT dthe."dataLancamento", - dthe.nsr AS processo, - 'Doc:'|| dthe."numeroInscricao"||'Ag.: '|| dthe.agencia || '-' || dthe."dvAgencia"|| - 'Conta: '|| dthe.conta ||'-'|| dthe."dvConta" AS lancamento, - 'Doc: '||dthe."numeroInscricao"||'Ag.: '||dthe.agencia||'-'||dthe."dvAgencia"|| - 'Conta: '||dthe.conta||'-'||dthe."dvConta" AS operacao, - dthe."tipoLancamento" AS tipo, - dthe."valorLancamento" AS valor + SELECT ede."dataLancamento", + ede.nsr AS processo, + 'Doc:'|| ede."numeroInscricao"||'Ag.: '|| ede.agencia || '-' || ede."dvAgencia"|| + 'Conta: '|| ede.conta ||'-'|| ede."dvConta" AS lancamento, + 'Doc: '||ede."numeroInscricao"||'Ag.: '||ede.agencia||'-'||ede."dvAgencia"|| + 'Conta: '||ede.conta||'-'||ede."dvConta" AS operacao, + ede."tipoLancamento" AS tipo, + ede."valorLancamento" AS valor FROM public.extrato_header_arquivo ha INNER JOIN public.extrato_header_lote ehl on ha.id = ehl."extratoHeaderArquivoId" - INNER JOIN public.extrato_detalhe_e dthe on ehl.id = dthe."extratoHeaderLoteId" + INNER JOIN public.extrato_detalhe_e ede on ehl.id = ede."extratoHeaderLoteId" WHERE ha."numeroConta" = '${_conta}' ${ _tipoLancamento - ? `\nAND dthe."tipoLancamento" = '${_tipoLancamento}'` + ? `\nAND ede."tipoLancamento" = '${_tipoLancamento}'` : '' } - AND dthe."dataLancamento" between '${_dt_inicio}' AND '${_dt_fim}'`; + AND ede."dataLancamento" between '${_dt_inicio}' AND '${_dt_fim}'`; return await this.entityManager.query(compactQuery(query)); } } diff --git a/src/cnab/service/pagamento/detalhe-a.service.ts b/src/cnab/service/pagamento/detalhe-a.service.ts index 02bfc595..2640fcf0 100644 --- a/src/cnab/service/pagamento/detalhe-a.service.ts +++ b/src/cnab/service/pagamento/detalhe-a.service.ts @@ -62,7 +62,7 @@ export class DetalheAService { numeroDocumentoEmpresa: r.detalheA.numeroDocumentoEmpresa.convertedValue, }); if (detalheA) { - if (detalheA.ocorrenciasCnab === undefined || detalheA.ocorrenciasCnab === '' || detalheA.ocorrenciasCnab !== r.detalheA.ocorrencias.value.trim()) { + if (detalheA.ocorrenciasCnab === undefined || detalheA.ocorrenciasCnab === '' || detalheA.ocorrenciasCnab !== r.detalheA.ocorrencias.value.trim() || !detalheA.dataEfetivacao) { const saveDetalheA = new DetalheADTO({ id: detalheA.id, loteServico: Number(r.detalheA.loteServico.value), diff --git a/src/cnab/service/pagamento/item-transacao.service.ts b/src/cnab/service/pagamento/item-transacao.service.ts index 03b58137..4e1ff42c 100644 --- a/src/cnab/service/pagamento/item-transacao.service.ts +++ b/src/cnab/service/pagamento/item-transacao.service.ts @@ -1,22 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { ItemTransacaoDTO } from 'src/cnab/dto/pagamento/item-transacao.dto'; -import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; import { ItemTransacao } from 'src/cnab/entity/pagamento/item-transacao.entity'; import { Transacao } from 'src/cnab/entity/pagamento/transacao.entity'; import { ItemTransacaoRepository } from 'src/cnab/repository/pagamento/item-transacao.repository'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; import { CustomLogger } from 'src/utils/custom-logger'; import { logDebug } from 'src/utils/log-utils'; -import { asObject } from 'src/utils/pipe-utils'; import { SaveIfNotExists } from 'src/utils/types/save-if-not-exists.type'; -import { - DeepPartial, - FindManyOptions, - FindOptionsWhere, - In, - Not, - QueryRunner, -} from 'typeorm'; +import { DeepPartial, FindManyOptions, FindOptionsWhere, In, Not, QueryRunner } from 'typeorm'; @Injectable() export class ItemTransacaoService { @@ -30,51 +20,6 @@ export class ItemTransacaoService { return await this.itemTransacaoRepository.update(id, dto); } - // #region generateDTOsFromLancamentos - - /** - * @param publicacoes Ready to save or saved Entity. Must contain valid Transacao - */ - public generateDTOsFromLancamentos( - lancamentos: LancamentoEntity[], - favorecidos: ClienteFavorecido[], - ): ItemTransacao[] { - /** Key: id ClienteFavorecido. Eficient way to find favorecido. */ - const favorecidosMap: Record = - favorecidos.reduce((map, i) => ({ ...map, [i.id]: i }), {}); - - const itens: ItemTransacao[] = []; - - // Mount DTOs - for (const lancamento of lancamentos) { - const favorecido = favorecidosMap[lancamento.id_cliente_favorecido.id]; - itens.push(this.generateDTOFromLancamento(lancamento, favorecido)); - } - return itens; - } - - /** - * A simple pipe thar converts BigqueryOrdemPagamento into ItemTransacaoDTO. - * - * **status** is Created. - */ - public generateDTOFromLancamento( - lancamento: LancamentoEntity, - favorecido: ClienteFavorecido, - ): ItemTransacao { - const transacao = asObject(lancamento.transacao); - /** detalheA = null, isRegistered = false */ - const itemTransacao = new ItemTransacao({ - clienteFavorecido: { id: favorecido.id }, - transacao: { id: transacao.id }, - valor: lancamento.valor_a_pagar, - dataOrdem: lancamento.data_ordem, - }); - return itemTransacao; - } - - // #endregion - /** * Bulk save Transacao. */ @@ -90,9 +35,7 @@ export class ItemTransacaoService { return newItens; } - public async findManyByIdTransacao( - id_transacao: number, - ): Promise { + public async findManyByIdTransacao(id_transacao: number): Promise { return await this.itemTransacaoRepository.findMany({ where: { transacao: { @@ -111,16 +54,14 @@ export class ItemTransacaoService { return many.pop() || null; } - public async save(dto: DeepPartial,queryRunner:QueryRunner): Promise { + public async save(dto: DeepPartial, queryRunner: QueryRunner): Promise { return await queryRunner.manager.getRepository(ItemTransacao).save(dto); } /** * Save if composite unique columns not exist. Otherwise, update. */ - public async saveManyIfNotExistsJae( - dtos: DeepPartial[], - ): Promise { + public async saveManyIfNotExistsJae(dtos: DeepPartial[]): Promise { // Existing const existing = await this.itemTransacaoRepository.findMany({ where: dtos.reduce( @@ -135,34 +76,22 @@ export class ItemTransacaoService { [], ), }); - const existingMap: Record = existing.reduce( - (m, i) => ({ ...m, [ItemTransacao.getUniqueIdJae(i)]: i }), - {}, - ); + const existingMap: Record = existing.reduce((m, i) => ({ ...m, [ItemTransacao.getUniqueIdJae(i)]: i }), {}); // Check if (existing.length === dtos.length) { - this.logger.warn( - `${existing.length}/${dtos.length} ItemTransacoes já existem, nada a fazer...`, - ); + this.logger.warn(`${existing.length}/${dtos.length} ItemTransacoes já existem, nada a fazer...`); } else if (existing.length) { - this.logger.warn( - `${existing.length}/${dtos.length} ItemTransacoes já existem, ignorando...`, - ); + this.logger.warn(`${existing.length}/${dtos.length} ItemTransacoes já existem, ignorando...`); return []; } // Save new - const newItems = dtos.filter( - (i) => !existingMap[ItemTransacao.getUniqueIdJae(i)], - ); + const newItems = dtos.filter((i) => !existingMap[ItemTransacao.getUniqueIdJae(i)]); const insert = await this.itemTransacaoRepository.insert(newItems); // Return saved - const insertIds = (insert.identifiers as { id: number }[]).reduce( - (l, i) => [...l, i.id], - [], - ); + const insertIds = (insert.identifiers as { id: number }[]).reduce((l, i) => [...l, i.id], []); const savedItems = await this.itemTransacaoRepository.findMany({ where: { id: In(insertIds) }, }); @@ -172,10 +101,7 @@ export class ItemTransacaoService { /** * Save if composite unique columns not exist. Otherwise, update. */ - public async saveIfNotExists( - dto: ItemTransacaoDTO, - updateIfExists?: boolean, - ): Promise> { + public async saveIfNotExists(dto: ItemTransacaoDTO, updateIfExists?: boolean): Promise> { // Find by composite unique columns const item = await this.itemTransacaoRepository.findOne({ where: { @@ -247,10 +173,6 @@ export class ItemTransacaoService { // Log const allIds = allFailed.reduce((l, i) => [...l, i.id], []).join(','); - logDebug( - this.logger, - `ItemTr. #${allIds} movidos para Transacao #${transacaoDest.id}`, - METHOD, - ); + logDebug(this.logger, `ItemTr. #${allIds} movidos para Transacao #${transacaoDest.id}`, METHOD); } } diff --git a/src/cnab/service/pagamento/remessa-retorno.service.ts b/src/cnab/service/pagamento/remessa-retorno.service.ts index 7ba8c919..46fb4e8a 100644 --- a/src/cnab/service/pagamento/remessa-retorno.service.ts +++ b/src/cnab/service/pagamento/remessa-retorno.service.ts @@ -18,7 +18,7 @@ import { CnabTrailerArquivo104 } from 'src/cnab/interfaces/cnab-240/104/cnab-tra import { CnabFile104Pgto } from 'src/cnab/interfaces/cnab-240/104/pagamento/cnab-file-104-pgto.interface'; import { Cnab104PgtoTemplates } from 'src/cnab/templates/cnab-240/104/pagamento/cnab-104-pgto-templates.const'; import { getCnabFieldConverted } from 'src/cnab/utils/cnab/cnab-field-utils'; -import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; +import { TransacaoViewService } from 'src/transacao-view/transacao-view.service'; import { CustomLogger } from 'src/utils/custom-logger'; import { asNumber, asString } from 'src/utils/pipe-utils'; import { Between, DataSource, DeepPartial, IsNull, Not, QueryRunner } from 'typeorm'; @@ -43,6 +43,8 @@ import { HeaderLoteService } from './header-lote.service'; import { ItemTransacaoAgrupadoService } from './item-transacao-agrupado.service'; import { ItemTransacaoService } from './item-transacao.service'; import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; +import { LancamentoService } from 'src/lancamento/lancamento.service'; +import { LancamentoStatus } from 'src/lancamento/enums/lancamento-status.enum'; const sc = structuredClone; const PgtoRegistros = Cnab104PgtoTemplates.file104.registros; @@ -53,7 +55,23 @@ export class RemessaRetornoService { timestamp: true, }); - constructor(private arquivoPublicacaoService: ArquivoPublicacaoService, private detalheAConfService: DetalheAConfService, private detalheAService: DetalheAService, private detalheBConfService: DetalheBConfService, private detalheBService: DetalheBService, private headerArquivoConfService: HeaderArquivoConfService, private headerArquivoService: HeaderArquivoService, private headerLoteConfService: HeaderLoteConfService, private headerLoteService: HeaderLoteService, private itemTransacaoAgService: ItemTransacaoAgrupadoService, private itemTransacaoService: ItemTransacaoService, private ocorrenciaService: OcorrenciaService, private transacaoViewService: TransacaoViewService, private dataSource: DataSource) {} + constructor( + private arquivoPublicacaoService: ArquivoPublicacaoService, // + private lancamentoService: LancamentoService, + private detalheAConfService: DetalheAConfService, + private detalheAService: DetalheAService, + private detalheBConfService: DetalheBConfService, + private detalheBService: DetalheBService, + private headerArquivoConfService: HeaderArquivoConfService, + private headerArquivoService: HeaderArquivoService, + private headerLoteConfService: HeaderLoteConfService, + private headerLoteService: HeaderLoteService, + private itemTransacaoAgService: ItemTransacaoAgrupadoService, + private itemTransacaoService: ItemTransacaoService, + private ocorrenciaService: OcorrenciaService, + private transacaoViewService: TransacaoViewService, + private dataSource: DataSource, + ) {} public async saveHeaderArquivoDTO(transacaoAg: TransacaoAgrupado, isConference: boolean): Promise { let headerArquivoDTO; @@ -91,8 +109,10 @@ export class RemessaRetornoService { const lotes: HeaderLoteDTO[] = []; let nsrTed = 0; let nsrCC = 0; - let loteTed; - let loteCC; + /** @type HeaderLoteDTO */ + let loteTed: any; + /** @type HeaderLoteDTO */ + let loteCC: any; for (const itemTransacaoAgrupado of itemTransacaoAgs) { const itemTransacao = await this.itemTransacaoService.findOne({ where: { @@ -331,20 +351,14 @@ export class RemessaRetornoService { * @param dataPgto O padrão é o dia de hoje. O valor será sempre >= hoje. * @returns null if failed ItemTransacao to CNAB */ public async saveDetalhes104(numeroDocumento: number, headerLote: HeaderLoteDTO, itemTransacaoAg: ItemTransacaoAgrupado, nsr: number, isConference: boolean, dataPgto?: Date, isCancelamento = false, detalheAC = new DetalheA()): Promise { - let favorecido; + /** @type ClienteFavorecido */ + let favorecido: any; if (itemTransacaoAg != undefined) { - const itemTransacao = await this.itemTransacaoService.findOne({ - where: { itemTransacaoAgrupado: { id: itemTransacaoAg.id } }, - }); + const itemTransacao = await this.itemTransacaoService.findOne({ where: { itemTransacaoAgrupado: { id: itemTransacaoAg.id } } }); favorecido = itemTransacao?.clienteFavorecido; } else { const itemTransacaoAg = detalheAC.headerLote.headerArquivo.transacaoAgrupado?.itemTransacoesAgrupado[0]; - const itemTransacao = await this.itemTransacaoService.findOne({ - where: { - itemTransacaoAgrupado: { id: itemTransacaoAg?.id }, - }, - }); - + const itemTransacao = await this.itemTransacaoService.findOne({ where: { itemTransacaoAgrupado: { id: itemTransacaoAg?.id } } }); favorecido = itemTransacao?.clienteFavorecido; } @@ -365,8 +379,9 @@ export class RemessaRetornoService { return null; } - if (dataPgto && dataPgto < new Date()) { - dataPgto = new Date(); + let _dataPgto = dataPgto || itemTransacaoAg.dataOrdem; + if (_dataPgto < new Date()) { + _dataPgto = new Date(); } // Save detalheA @@ -378,14 +393,7 @@ export class RemessaRetornoService { detalheA.dvContaDestino.value = favorecido.dvContaCorrente; detalheA.nomeTerceiro.value = favorecido.nome; detalheA.numeroDocumentoEmpresa.value = numeroDocumento; - - const fridayOrdem = itemTransacaoAg.dataOrdem; - detalheA.dataVencimento.value = fridayOrdem; - if (dataPgto === undefined) { - detalheA.dataVencimento.value = detalheA.dataVencimento.value; - } else { - detalheA.dataVencimento.value = dataPgto; - } + detalheA.dataVencimento.value = _dataPgto; if (!isCancelamento) { detalheA.valorLancamento.value = itemTransacaoAg.valor; @@ -491,7 +499,8 @@ export class RemessaRetornoService { */ public async compareRemessaToRetorno(detalheA: DetalheA, queryRunner: QueryRunner): Promise { await this.saveOcorrenciasDetalheA(detalheA, queryRunner); - await this.savePublicacaoRetorno(detalheA, queryRunner); + await this.saveRetornoPublicacao(detalheA, queryRunner); + await this.saveRetornoLancamento(detalheA, queryRunner); } async saveOcorrenciasDetalheA(detalheARetorno: DetalheA, queryRunner: QueryRunner) { @@ -511,7 +520,8 @@ export class RemessaRetornoService { await this.ocorrenciaService.saveMany(ocorrencias, queryRunner); } - async savePublicacaoRetorno(detalheARetorno: DetalheA, queryRunner: QueryRunner) { + /** Se o retorno for de Publicacao, atualiza */ + async saveRetornoPublicacao(detalheARetorno: DetalheA, queryRunner: QueryRunner) { const publicacoes = await this.arquivoPublicacaoService.findManyRaw({ itemTransacaoAgrupadoId: [detalheARetorno.itemTransacaoAgrupado.id], }); @@ -529,6 +539,22 @@ export class RemessaRetornoService { await this.arquivoPublicacaoService.updateManyRaw(publicacoes, 'savePublicacaoRetorno', queryRunner); } + /** Se o retorno for de Lancamento, atualiza */ + async saveRetornoLancamento(detalheARetorno: DetalheA, queryRunner: QueryRunner) { + const lancamentos = await this.lancamentoService.find({ detalheA: { id: [detalheARetorno.id] } }); + for (const lancamento of lancamentos) { + lancamento.is_pago = detalheARetorno.isPago(); + if (lancamento.is_pago) { + lancamento.data_pgto = detalheARetorno.dataEfetivacao; + lancamento.status = LancamentoStatus._4_pago; + } else { + lancamento.data_pgto = null; + lancamento.status = LancamentoStatus._5_erro; + } + } + await this.lancamentoService.updateManyRaw(lancamentos, ['is_pago', 'data_pgto', 'status'], queryRunner); + } + async compareTransacaoViewPublicacao(detalheA: DetalheA, queryRunner: QueryRunner) { const transacoesView = await this.getTransacoesViewWeek(subDays(detalheA.dataVencimento, 8), detalheA.dataVencimento); const publicacoesDetalhe = await this.arquivoPublicacaoService.getPublicacoesWeek(detalheA); diff --git a/src/cnab/service/pagamento/transacao-agrupado.service.ts b/src/cnab/service/pagamento/transacao-agrupado.service.ts index 254809f7..426d6a97 100644 --- a/src/cnab/service/pagamento/transacao-agrupado.service.ts +++ b/src/cnab/service/pagamento/transacao-agrupado.service.ts @@ -6,7 +6,7 @@ import { Pagador } from 'src/cnab/entity/pagamento/pagador.entity'; import { TransacaoAgrupado } from 'src/cnab/entity/pagamento/transacao-agrupado.entity'; import { PagadorContaEnum } from 'src/cnab/enums/pagamento/pagador.enum'; import { TransacaoAgrupadoRepository } from 'src/cnab/repository/pagamento/transacao-agrupado.repository'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { asNumber } from 'src/utils/pipe-utils'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; import { DeepPartial, UpdateResult } from 'typeorm'; @@ -36,10 +36,7 @@ export class TransacaoAgrupadoService { * * @param newLancamentos It must have at least 1 unused Lancamento */ - public generateDTOForLancamento( - pagador: Pagador, - newLancamentos: LancamentoEntity[], - ): Transacao { + public generateDTOForLancamento(pagador: Pagador, newLancamentos: Lancamento[]): Transacao { const today = new Date(); const friday = isFriday(today) ? today : nextFriday(today); const transacao = new Transacao({ @@ -58,22 +55,15 @@ export class TransacaoAgrupadoService { /** * Use first Transacao as set to update and all Transacoes to get ids. */ - public updateMany( - ids: number[], - set: DeepPartial, - ): Promise { + public updateMany(ids: number[], set: DeepPartial): Promise { return this.transacaoAgRepository.updateMany(ids, set); } - public saveManyIfNotExists( - transacoes: DeepPartial[], - ): Promise { + public saveManyIfNotExists(transacoes: DeepPartial[]): Promise { return this.transacaoAgRepository.saveManyIfNotExists(transacoes); } - public async saveMany( - transacoes: DeepPartial[], - ): Promise { + public async saveMany(transacoes: DeepPartial[]): Promise { const insertResult = await this.transacaoAgRepository.insert(transacoes); return await this.transacaoAgRepository.findMany({ where: insertResult.identifiers, @@ -83,9 +73,7 @@ export class TransacaoAgrupadoService { /** * Save Transacao if NSA not exists */ - public async save( - dto: DeepPartial, - ): Promise { + public async save(dto: DeepPartial): Promise { return await this.transacaoAgRepository.save(dto); } @@ -96,9 +84,7 @@ export class TransacaoAgrupadoService { /** * Get all transacao where id not exists in headerArquivo yet (new CNABS) */ - public async findAllNewTransacao( - tipo: PagadorContaEnum, - ): Promise { + public async findAllNewTransacao(tipo: PagadorContaEnum): Promise { return await this.transacaoAgRepository.findAllNewTransacao(tipo); } } diff --git a/src/cnab/service/pagamento/transacao.service.ts b/src/cnab/service/pagamento/transacao.service.ts index 6db86035..a4a16982 100644 --- a/src/cnab/service/pagamento/transacao.service.ts +++ b/src/cnab/service/pagamento/transacao.service.ts @@ -1,12 +1,10 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { TransacaoDTO } from '../../dto/pagamento/transacao.dto'; import { Transacao } from '../../entity/pagamento/transacao.entity'; import { TransacaoRepository } from '../../repository/pagamento/transacao.repository'; -import { isFriday, nextFriday } from 'date-fns'; -import { Pagador } from 'src/cnab/entity/pagamento/pagador.entity'; import { PagadorContaEnum } from 'src/cnab/enums/pagamento/pagador.enum'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { CustomLogger } from 'src/utils/custom-logger'; import { asNumber, asString } from 'src/utils/pipe-utils'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; import { SaveIfNotExists } from 'src/utils/types/save-if-not-exists.type'; @@ -15,9 +13,7 @@ import { DeepPartial, FindManyOptions, QueryRunner, UpdateResult } from 'typeorm @Injectable() export class TransacaoService { - private logger: Logger = new Logger(TransacaoService.name, { - timestamp: true, - }); + private logger = new CustomLogger(TransacaoService.name, { timestamp: true }); constructor(private transacaoRepository: TransacaoRepository) {} @@ -29,80 +25,14 @@ export class TransacaoService { return await this.transacaoRepository.findOne(fields); } - /** - * **status** is Created. - * - * It will automatically update Lancamentos via OneToMany - * - * @param newLancamentos It must have at least 1 unused Lancamento - */ - public generateDTOForLancamento( - pagador: Pagador, - newLancamentos: LancamentoEntity[], - ): Transacao { - const today = new Date(); - const friday = isFriday(today) ? today : nextFriday(today); - const transacao = new Transacao({ - dataOrdem: friday, - dataPagamento: null, - lancamentos: newLancamentos, // unique id for Lancamentos - pagador: { id: pagador.id } as Pagador, - }); - return transacao; - } - public update(dto: DeepPartial) { return this.transacaoRepository.update(asNumber(dto.id), dto); } - // #region generateDTOsFromPublicacoes - - // public generateDTOsFromPublicacoes( - // publicacoes: ArquivoPublicacao[], - // pagador: Pagador, - // ) { - // const transacoes: Transacao[] = []; - // /** key: idOrdemPagamento */ - // const transacaoMap: Record = {}; - // for (const publicacao of publicacoes) { - // const transacaoPK = publicacao.idOrdemPagamento; - // const newTransacao = this.generateDTOFromPublicacao(publicacao, pagador); - // if (!transacaoMap[transacaoPK]) { - // transacoes.push(newTransacao); - // transacaoMap[transacaoPK] = newTransacao; - // } - // } - // return transacoes; - // } - - /** - * getTransacaoFromOrdem() - * - * **status** is Created. - */ - // public generateDTOFromPublicacao( - // publicacao: ArquivoPublicacao, - // pagador: Pagador, - // ): Transacao { - // const transacao = new Transacao({ - // dataOrdem: publicacao.dataOrdem, - // dataPagamento: null, - // idOrdemPagamento: publicacao.idOrdemPagamento, // unique id for Ordem - // pagador: { id: pagador.id } as Pagador, - // status: new TransacaoStatus(TransacaoStatusEnum.created), - // ocorrencias: [], - // }); - // return transacao; - // } - - // #endregion - /** * Use first Transacao as set to update and all Transacoes to get ids. */ - public updateMany( - transacoes: DeepPartial[], - ): Promise { + public updateMany(transacoes: DeepPartial[]): Promise { const ids = transacoes.reduce((l, i) => [...l, asNumber(i.id)], []); const set = transacoes[0]; if ('id' in set) { @@ -111,48 +41,21 @@ export class TransacaoService { return this.transacaoRepository.updateMany(ids, set); } - public saveManyIfNotExists( - transacoes: DeepPartial[], - ): Promise { + public saveManyIfNotExists(transacoes: DeepPartial[]): Promise { return this.transacaoRepository.saveManyIfNotExists(transacoes); } - public async saveMany( - transacoes: DeepPartial[], - ): Promise { + public async saveMany(transacoes: DeepPartial[]): Promise { const insertResult = await this.transacaoRepository.insert(transacoes); return await this.transacaoRepository.findMany({ where: insertResult.identifiers, }); } - /** - * Save Transacao for Lancamento - */ - public async saveForLancamento(dto: TransacaoDTO): Promise { - await validateDTO(TransacaoDTO, dto); - const saved = await this.transacaoRepository.save(dto); - this.setLazyLancamentos([saved]); - return saved; - } - - /** - * Set lazy value (only id) to transacao.lancamentos - */ - private setLazyLancamentos(transacoes: Transacao[]) { - for (const transacao of transacoes) { - for (const lancamento of transacao.lancamentos || []) { - lancamento.transacao = { id: transacao.id } as Transacao; - } - } - } - /** * Save Transacao if Jae unique column not exists */ - public async saveForJaeIfNotExists( - dto: TransacaoDTO, - ): Promise> { + public async saveForJaeIfNotExists(dto: TransacaoDTO): Promise> { await validateDTO(TransacaoDTO, dto); const transacao = await this.transacaoRepository.findOne({ idOrdemPagamento: asString(dto.idOrdemPagamento), @@ -173,7 +76,7 @@ export class TransacaoService { /** * Save Transacao if NSA not exists */ - public async save(dto: TransacaoDTO,queryRunner: QueryRunner): Promise { + public async save(dto: TransacaoDTO, queryRunner: QueryRunner): Promise { await validateDTO(TransacaoDTO, dto); return await queryRunner.manager.getRepository(Transacao).save(dto); } @@ -185,9 +88,7 @@ export class TransacaoService { /** * Get all transacao where id not exists in headerArquivo yet (new CNABS) */ - public async findAllNewTransacao( - tipo: PagadorContaEnum, - ): Promise { + public async findAllNewTransacao(tipo: PagadorContaEnum): Promise { return await this.transacaoRepository.findAllNewTransacao(tipo); } } diff --git a/src/cnab/test/cnab-service/cnab.service.spec.ts b/src/cnab/test/cnab-service/cnab.service.spec.ts index ed4c15aa..cbf5ed32 100644 --- a/src/cnab/test/cnab-service/cnab.service.spec.ts +++ b/src/cnab/test/cnab-service/cnab.service.spec.ts @@ -17,9 +17,9 @@ // import { UsersService } from 'src/users/users.service'; // import { DeepPartial } from 'typeorm'; // import { CnabService } from '../../cnab.service'; -// import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; +// import { TransacaoViewService } from 'src/transacao-view/transacao-view.service'; // import { TestUtils } from '../../../utils/test-utils'; -// import { TransacaoView } from 'src/transacao-bq/transacao-view.entity'; +// import { TransacaoView } from 'src/transacao-view/transacao-view.entity'; // import { BigqueryTransacaoService } from 'src/bigquery/services/bigquery-transacao.service'; // import { BigqueryTransacao } from 'src/bigquery/entities/transacao.bigquery-entity'; diff --git a/src/cron-jobs/cron-jobs.service.ts b/src/cron-jobs/cron-jobs.service.ts index 83dbca5e..0aef1f3a 100644 --- a/src/cron-jobs/cron-jobs.service.ts +++ b/src/cron-jobs/cron-jobs.service.ts @@ -35,10 +35,11 @@ export enum CronJobsEnum { updateTransacaoViewVan = 'updateTransacaoViewVan', updateTransacaoViewVLT = 'updateTransacaoViewVLT', updateTransacaoViewValues = 'updateTransacaoViewValues', - syncTransacaoViewOrdemPgto = 'syncTransacaoViewOrdemPgto', + syncTransacaoViewOrdem = 'syncTransacaoViewOrdem', generateRemessaVLT = 'generateRemessaVLT', generateRemessaEmpresa = 'generateRemessaEmpresa', generateRemessaVan = 'generateRemessaVan', + generateRemessaLancamento = 'generateRemessaLancamento', } interface ICronjobDebug { today?: Date; @@ -72,12 +73,15 @@ export class CronJobsService { }); } - async onModuleLoad() { + async onModuleLoad() { const THIS_CLASS_WITH_METHOD = 'CronJobsService.onModuleLoad'; this.jobsConfig.push( { - /** NÃO REMOVER ESTE JOB, É ÚTIL PARA ALTERAR OS CRONJOBS EM CASO DE URGÊNCIA */ + /** + * Job interno. + * NÃO REMOVER ESTE JOB, É ÚTIL PARA ALTERAR OS CRONJOBS EM CASO DE URGÊNCIA + */ name: CronJobsEnum.pollDb, cronJobParameters: { // cronjob: * * * * - A cada minuto @@ -85,96 +89,130 @@ export class CronJobsService { onTick: async () => await this.pollDb(), }, }, - { - name: CronJobsEnum.bulkSendInvites, - cronJobParameters: { - cronTime: (await this.settingsService.getOneBySettingData(appSettings.any__mail_invite_cronjob, true, THIS_CLASS_WITH_METHOD)).getValueAsString(), - onTick: async () => await this.bulkSendInvites(), - }, - }, { - /** NÃO DESABILITAR ENVIO DE REPORT - Day 15, 14:45 GMT = 11:45 BRT (GMT-3) */ - name: CronJobsEnum.sendStatusReport, + /** Atualizar Retorno - Leitura dos Arquivos Retorno do Banco CEF para CCT - todo dia, a cada 30m */ + name: CronJobsEnum.updateRetorno, cronJobParameters: { - cronTime: (await this.settingsService.getOneBySettingData(appSettings.any__mail_report_cronjob, true, THIS_CLASS_WITH_METHOD)).getValueAsString(), - onTick: async () => await this.sendStatusReport(), + cronTime: '*/30 * * * *', // Every 30 min + onTick: async () => { + await this.updateRetorno(); + }, }, }, { - name: CronJobsEnum.bulkResendInvites, + /** Atualizar Transações Van - DLake para CCT - todo dia, a cada 30m */ + name: CronJobsEnum.updateTransacaoViewVan, cronJobParameters: { - cronTime: '45 14 15 * *', // Day 15, 14:45 GMT = 11:45 BRT (GMT-3) - onTick: async () => await this.bulkResendInvites(), + cronTime: '*/30 * * * *', // Every 30 min + onTick: async () => await this.updateTransacaoViewBigquery('Van'), }, }, { - name: CronJobsEnum.updateTransacaoViewVan, + /** + * Envio de Relatório Estatística Dados - todo dia, 08:00-08:01 + * NÃO DESABILITAR ENVIO DE REPORT - Day 15, 14:45 GMT = 11:45 BRT (GMT-3) + */ + name: CronJobsEnum.sendStatusReport, cronJobParameters: { - cronTime: '*/30 * * * *', // Every 30 min - onTick: async () => await this.updateTransacaoView('Van'), + cronTime: (await this.settingsService.getOneBySettingData(appSettings.any__mail_report_cronjob, true, THIS_CLASS_WITH_METHOD)).getValueAsString(), + onTick: async () => await this.sendStatusReport(), }, }, { - name: CronJobsEnum.updateTransacaoViewEmpresa, + /** + * Gerar arquivo remessa dos vanzeiros - toda 6a, 10:00, duração: 15 min + * + BD do CCT - Sincronizar Transações da Ordem Pagto com Trnas. VIEW + */ + name: CronJobsEnum.generateRemessaVan, cronJobParameters: { - cronTime: '0 9 * * *', // Every day, 12:00 GMT = 09:00 BRT (GMT-3) - onTick: async () => await this.updateTransacaoView('Empresa'), + cronTime: '0 13 * * *', // Every Friday (see method), 13:00 GMT = 10:00 BRT (GMT-3) + onTick: async () => { + await this.generateRemessaVan(); + await this.syncTransacaoViewOrdem('generateRemessaVan'); + }, }, }, { - name: CronJobsEnum.updateTransacaoViewVLT, + /** + * Gerar arquivo remessa do Consórcio VLT - 2a-6a, 08:00, duração: 15 min + * + BD do CCT - Sincronizar Transações da Ordem Pagto com Trnas. VIEW + */ + name: CronJobsEnum.generateRemessaVLT, cronJobParameters: { - cronTime: '0 9 * * *', // Every day, 12:00 GMT = 09:00 BRT (GMT-3) - onTick: async () => await this.updateTransacaoView('VLT'), + cronTime: '0 8 * * *', // Every day, 05:00 GMT = 8:00 BRT (GMT-3) + onTick: async () => { + const today = new Date(); + if (!isSaturday(today) && !isSunday(today)) { + await this.generateRemessaVLT(); + await this.syncTransacaoViewOrdem('generateRemessaVLT'); + } + }, }, }, { - name: CronJobsEnum.updateTransacaoViewValues, + /** + * Gerar arquivo Remessa dos Consórcios - toda 5a, 17:00, duração: 15 min + * + BD do CCT - Sincronizar Transações da Ordem Pagto com Trnas. VIEW + */ + name: CronJobsEnum.generateRemessaEmpresa, cronJobParameters: { - cronTime: '0 15 * * *', // Every day, 15:00 GMT = 12:00 BRT (GMT-3) - onTick: async () => await this.updateTransacaoViewValues(), + cronTime: '0 14 * * *', // Every Thursday (see method), 14:00 GMT = 17:00 BRT (GMT-3) + onTick: async () => { + await this.generateRemessaEmpresa(); + await this.syncTransacaoViewOrdem('generateRemessaEmpresa'); + }, }, }, + // { + // /** + // * Gerar arquivo Remessa do Lançamento - todo dia, duração: 15 min + // */ + // name: CronJobsEnum.generateRemessaLancamento, + // cronJobParameters: { + // cronTime: '0 15 * * *', // Every day, 15:00 GMT = 14:00 BRT (GMT-3) + // onTick: async () => { + // await this.generateRemessaLancamento(); + // }, + // }, + // }, { - name: CronJobsEnum.syncTransacaoViewOrdemPgto, + /** Atualizar Transações Empresa - DLake para CCT - todo dia, 09:00, duração: 20 min */ + name: CronJobsEnum.updateTransacaoViewEmpresa, cronJobParameters: { - cronTime: '0 9 * * *', // Every day, 06:00 GMT = 09:00 BRT (GMT-3) - onTick: async () => await this.syncTransacaoViewOrdemPgto(), + cronTime: '0 9 * * *', // Every day, 12:00 GMT = 09:00 BRT (GMT-3) + onTick: async () => await this.updateTransacaoViewBigquery('Empresa'), }, }, { - name: CronJobsEnum.updateRetorno, + /** Atualizar Transações VLT - DLake para CCT - todo dia, 09:00, duração: 20 min */ + name: CronJobsEnum.updateTransacaoViewVLT, cronJobParameters: { - cronTime: '*/30 * * * *', // Every 30 min - onTick: async () => await this.updateRetorno(), + cronTime: '0 9 * * *', // Every day, 12:00 GMT = 09:00 BRT (GMT-3) + onTick: async () => await this.updateTransacaoViewBigquery('VLT'), }, }, { - name: CronJobsEnum.generateRemessaEmpresa, + /** Reenvio de E-mail para Vanzeiros - 1 aceso ou Cadastro de Contas Bancárias - dia 15 de cada mês, 11:45, duração: 5 min */ + name: CronJobsEnum.bulkResendInvites, cronJobParameters: { - cronTime: '0 14 * * *', // Every Thursday (see method), 14:00 GMT = 17:00 BRT (GMT-3) - onTick: async () => { - await this.generateRemessaEmpresa(); - }, + cronTime: '45 14 15 * *', // Day 15, 14:45 GMT = 11:45 BRT (GMT-3) + onTick: async () => await this.bulkResendInvites(), }, }, { - name: CronJobsEnum.generateRemessaVan, + /** Atualizar Transações Campos Nulos CCT - DLake para CCT - todo dia, 12:00, duração: 10 min */ + name: CronJobsEnum.updateTransacaoViewValues, cronJobParameters: { - cronTime: '0 10 * * *', // Every Friday (see method), 10:00 GMT = 07:00 BRT (GMT-3) - onTick: async () => { - await this.generateRemessaVan(); - }, + cronTime: '0 15 * * *', // Every day, 15:00 GMT = 12:00 BRT (GMT-3) + onTick: async () => await this.updateTransacaoViewValues(), }, }, { - name: CronJobsEnum.generateRemessaVLT, + /** Envio do E-mail - Convite para o usuário realizar o 1o acesso no Sistema CCT - todo dia, 19:00, duração: 5 min */ + name: CronJobsEnum.bulkSendInvites, cronJobParameters: { - cronTime: '0 8 * * *', // Every day, 05:00 GMT = 8:00 BRT (GMT-3) - onTick: async () => { - const today = new Date(); - if (!isSaturday(today) && !isSunday(today)) await this.generateRemessaVLT(); - }, + cronTime: (await this.settingsService.getOneBySettingData(appSettings.any__mail_invite_cronjob, true, THIS_CLASS_WITH_METHOD)).getValueAsString(), + onTick: async () => await this.bulkSendInvites(), }, }, ); @@ -227,37 +265,41 @@ export class CronJobsService { */ async generateRemessaEmpresa(debug?: ICronjobDebug) { const METHOD = 'generateRemessaEmpresa'; - const today = debug?.today || new Date(); - if (!isThursday(today)) { - this.logger.log('Não implementado - Hoje não é quinta-feira. Abortando...', METHOD); - return; - } - if (!(await this.getIsCnabJobEnabled(METHOD)) && !debug?.force) { - return; - } - if (!isThursday(today)) { - this.logger.error('Não implementado - Hoje não é quinta-feira. Abortando...', undefined, METHOD); - return; + try { + const today = debug?.today || new Date(); + if (!isThursday(today)) { + this.logger.log('Não implementado - Hoje não é quinta-feira. Abortando...', METHOD); + return; + } + if (!(await this.getIsCnabJobEnabled(METHOD)) && !debug?.force) { + return; + } + if (!isThursday(today)) { + this.logger.error('Não implementado - Hoje não é quinta-feira. Abortando...', undefined, METHOD); + return; + } + this.logger.log('Tarefa iniciada', METHOD); + const startDate = new Date(); + const sex = subDays(today, 6); + const qui = today; + await this.cnabService.saveTransacoesJae(sex, qui, 0, 'Empresa'); + const listCnab = await this.cnabService.generateRemessa({ + tipo: PagadorContaEnum.ContaBilhetagem, + dataPgto: addDays(today, 1), + isConference: false, + isCancelamento: false, + }); + await this.cnabService.sendRemessa(listCnab); + this.logger.log(`Tarefa finalizada - ${formatDateInterval(new Date(), startDate)}`, METHOD); + } catch (error) { + this.logger.error('Erro ao executar tarefa.', error?.stack, METHOD); } - this.logger.log('Tarefa iniciada', METHOD); - const startDate = new Date(); - const sex = subDays(today, 6); - const qui = today; - await this.cnabService.saveTransacoesJae(sex, qui, 0, 'Empresa'); - const listCnab = await this.cnabService.generateRemessa({ - tipo: PagadorContaEnum.ContaBilhetagem, - dataPgto: addDays(today, 1), - isConference: false, - isCancelamento: false, - }); - await this.cnabService.sendRemessa(listCnab); - this.logger.log(`Tarefa finalizada - ${formatDateInterval(new Date(), startDate)}`, METHOD); } /** * Gera e envia remessa da semana atual, a ser pago numa sexta-feira. */ - async generateRemessaVan(debug?: ICronjobDebug) { + async generateRemessaVan(debug?: ICronjobDebug) { const METHOD = 'generateRemessaVan'; const today = debug?.today || new Date(); if (!isFriday(today)) { @@ -312,16 +354,41 @@ export class CronJobsService { this.logger.log(`Tarefa finalizada - ${formatDateInterval(new Date(), startDateLog)}`, METHOD); } - public async syncTransacaoViewOrdemPgto() { - const METHOD = 'syncTransacaoViewOrdemPgto'; + /** + * Regras de negócio: + * - Todo dia gera remessa de Lançamentos com dataOrdem = hoje + */ + public async generateRemessaLancamento(debug?: ICronjobDebug) { + const METHOD = 'generateRemessaLancamento'; + try { + this.logger.log('Tarefa iniciada', METHOD); + const today = debug?.today || new Date(); + const startDateLog = new Date(); + const startDate = today; + const endDate = today; + await this.cnabService.saveTransacoesLancamento(startDate, endDate); + const listCnab = await this.cnabService.generateRemessa({ + tipo: PagadorContaEnum.CETT, + dataPgto: undefined, // data programada no Lançamento + isConference: false, + isCancelamento: false, + }); + await this.cnabService.sendRemessa(listCnab); + this.logger.log(`Tarefa finalizada - ${formatDateInterval(new Date(), startDateLog)}`, METHOD); + } catch (error) { + this.logger.error('Erro ao executar tarefa.', error?.stack, METHOD); + } + } + + private async syncTransacaoViewOrdem(method = 'syncTransacaoViewOrdem') { try { const startDate = subDays(new Date(), 15); const today = new Date(); - this.logger.log(`Sincronizando TransacaoViews entre ${formatDateYMD(startDate)} e ${formatDateYMD(today)}`, METHOD); + this.logger.log(`Sincronizando TransacaoViews entre ${formatDateYMD(startDate)} e ${formatDateYMD(today)}`, method); await this.cnabService.syncTransacaoViewOrdemPgto({ dataOrdem_between: [startDate, today] }); - this.logger.log(`Trefa finalizada com sucesso.`, METHOD); + this.logger.log(`Trefa finalizada com sucesso.`, method); } catch (error) { - this.logger.error('Erro ao executar tarefa.', error?.stack, METHOD); + this.logger.error('Erro ao executar tarefa.', error?.stack, method); } } @@ -350,8 +417,8 @@ export class CronJobsService { * * `VLT`: Todo dia pega 1 dia antes. */ - async updateTransacaoView(consorcio: 'Van' | 'Empresa' | 'VLT', debug?: ICronjobDebug) { - const METHOD = this.updateTransacaoView.name; + async updateTransacaoViewBigquery(consorcio: 'Van' | 'Empresa' | 'VLT', debug?: ICronjobDebug) { + const METHOD = this.updateTransacaoViewBigquery.name; try { if (!(await this.getIsCnabJobEnabled(METHOD)) && !debug?.force) { return; diff --git a/src/database/migrations/1724957667914-Lancamento.ts b/src/database/migrations/1724957667914-Lancamento.ts new file mode 100644 index 00000000..682edf93 --- /dev/null +++ b/src/database/migrations/1724957667914-Lancamento.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Lancamento1724957667914 implements MigrationInterface { + name = 'Lancamento1724957667914' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "auth_usersIds"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "autorizado_por" character varying`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_user_ManyToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "algoritmo"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "algoritmo" numeric NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "userId" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_user_ManyToOne" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_user_ManyToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "userId" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "algoritmo"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "algoritmo" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_user_ManyToOne" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "autorizado_por"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "auth_usersIds" character varying`); + } + +} diff --git a/src/database/migrations/1724964086793-Lancamento_2v.ts b/src/database/migrations/1724964086793-Lancamento_2v.ts new file mode 100644 index 00000000..dbcc1836 --- /dev/null +++ b/src/database/migrations/1724964086793-Lancamento_2v.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Lancamento2v1724964086793 implements MigrationInterface { + name = 'Lancamento2v1724964086793' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_user_ManyToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "valor_a_pagar"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "descricao"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "autorId" integer`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "data_pgto" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "data_pgto" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "glosa" SET DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "recurso" SET DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "anexo" SET DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_autor_ManyToOne" FOREIGN KEY ("autorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_autor_ManyToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "anexo" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "recurso" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "glosa" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "data_pgto" SET DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "lancamento" ALTER COLUMN "data_pgto" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "autorId"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "descricao" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "valor_a_pagar" numeric NOT NULL`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "userId" integer`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_user_ManyToOne" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/src/database/migrations/1725038149559-LancamentoAutorizacao.ts b/src/database/migrations/1725038149559-LancamentoAutorizacao.ts new file mode 100644 index 00000000..a8a2256a --- /dev/null +++ b/src/database/migrations/1725038149559-LancamentoAutorizacao.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class LancamentoAutorizacao1725038149559 implements MigrationInterface { + name = 'LancamentoAutorizacao1725038149559' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" RENAME COLUMN "autorizado_por" TO "is_autorizado"`); + await queryRunner.query(`CREATE TABLE "lancamento_autorizacao" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lancamentoId" integer, "userId" integer, CONSTRAINT "PK_LancamentoAutorizacao_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_LancamentoAutorizacao_lancamento" ON "lancamento_autorizacao" ("lancamentoId") `); + await queryRunner.query(`CREATE INDEX "IDX_LancamentoAutorizacao_user" ON "lancamento_autorizacao" ("userId") `); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "is_autorizado"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "is_autorizado" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "lancamento_autorizacao" ADD CONSTRAINT "FK_LancamentoAutorizacao_lancamento" FOREIGN KEY ("lancamentoId") REFERENCES "lancamento"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "lancamento_autorizacao" ADD CONSTRAINT "FK_LancamentoAutorizacao_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento_autorizacao" DROP CONSTRAINT "FK_LancamentoAutorizacao_user"`); + await queryRunner.query(`ALTER TABLE "lancamento_autorizacao" DROP CONSTRAINT "FK_LancamentoAutorizacao_lancamento"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "is_autorizado"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD "is_autorizado" character varying`); + await queryRunner.query(`DROP INDEX "public"."IDX_LancamentoAutorizacao_user"`); + await queryRunner.query(`DROP INDEX "public"."IDX_LancamentoAutorizacao_lancamento"`); + await queryRunner.query(`DROP TABLE "lancamento_autorizacao"`); + await queryRunner.query(`ALTER TABLE "lancamento" RENAME COLUMN "is_autorizado" TO "autorizado_por"`); + } + +} diff --git a/src/database/migrations/1725545132372-PagamentosPendentes2v.ts b/src/database/migrations/1725545132372-PagamentosPendentes2v.ts new file mode 100644 index 00000000..7509a1e6 --- /dev/null +++ b/src/database/migrations/1725545132372-PagamentosPendentes2v.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class PagamentosPendentes2v1725545132372 implements MigrationInterface { + name = 'PagamentosPendentes2v1725545132372' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "pagamentos_pendentes" ADD "dataReferencia" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "pagamentos_pendentes" DROP COLUMN "dataReferencia"`); + } + +} diff --git a/src/database/migrations/1725545232586-LancamentoRemessa.ts b/src/database/migrations/1725545232586-LancamentoRemessa.ts new file mode 100644 index 00000000..97aa506b --- /dev/null +++ b/src/database/migrations/1725545232586-LancamentoRemessa.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class LancamentoRemessa1725545232586 implements MigrationInterface { + name = 'LancamentoRemessa1725545232586' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_transacao_ManyToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" RENAME COLUMN "transacaoId" TO "itemTransacaoId"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "UQ_Lancamento_itemTransacao" UNIQUE ("itemTransacaoId")`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_itemTransacao_OneToOne" FOREIGN KEY ("itemTransacaoId") REFERENCES "item_transacao"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "FK_Lancamento_itemTransacao_OneToOne"`); + await queryRunner.query(`ALTER TABLE "lancamento" DROP CONSTRAINT "UQ_Lancamento_itemTransacao"`); + await queryRunner.query(`ALTER TABLE "lancamento" RENAME COLUMN "itemTransacaoId" TO "transacaoId"`); + await queryRunner.query(`ALTER TABLE "lancamento" ADD CONSTRAINT "FK_Lancamento_transacao_ManyToOne" FOREIGN KEY ("transacaoId") REFERENCES "transacao"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/src/database/migrations/1725895301219-Lancamento3v.ts b/src/database/migrations/1725895301219-Lancamento3v.ts new file mode 100644 index 00000000..0088e4ed --- /dev/null +++ b/src/database/migrations/1725895301219-Lancamento3v.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Lancamento3v1725895301219 implements MigrationInterface { + name = 'Lancamento3v1725895301219' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" ADD "is_pago" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "is_pago"`); + } + +} diff --git a/src/database/migrations/1725912379556-LancamentoStatus.ts b/src/database/migrations/1725912379556-LancamentoStatus.ts new file mode 100644 index 00000000..ff3204b8 --- /dev/null +++ b/src/database/migrations/1725912379556-LancamentoStatus.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class LancamentoStatus1725912379556 implements MigrationInterface { + name = 'LancamentoStatus1725912379556' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" ADD "status" character varying NOT NULL DEFAULT 'criado'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "status"`); + } + +} diff --git a/src/database/migrations/1725986922881-LancamentoSoftDelete.ts b/src/database/migrations/1725986922881-LancamentoSoftDelete.ts new file mode 100644 index 00000000..bc664666 --- /dev/null +++ b/src/database/migrations/1725986922881-LancamentoSoftDelete.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class LancamentoSoftDelete1725986922881 implements MigrationInterface { + name = 'LancamentoSoftDelete1725986922881' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" ADD "deletedAt" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "lancamento" DROP COLUMN "deletedAt"`); + } + +} diff --git a/src/database/migrations/1714005101337-UpdateLancamento.ts b/src/database/migrations/2-cnab-jae/1714005101337-UpdateLancamento.ts similarity index 100% rename from src/database/migrations/1714005101337-UpdateLancamento.ts rename to src/database/migrations/2-cnab-jae/1714005101337-UpdateLancamento.ts diff --git a/src/database/migrations/1714151527859-UpdateLancamento2v.ts b/src/database/migrations/2-cnab-jae/1714151527859-UpdateLancamento2v.ts similarity index 100% rename from src/database/migrations/1714151527859-UpdateLancamento2v.ts rename to src/database/migrations/2-cnab-jae/1714151527859-UpdateLancamento2v.ts diff --git a/src/database/migrations/1714246457271-UpdateLancamento3v.ts b/src/database/migrations/2-cnab-jae/1714246457271-UpdateLancamento3v.ts similarity index 100% rename from src/database/migrations/1714246457271-UpdateLancamento3v.ts rename to src/database/migrations/2-cnab-jae/1714246457271-UpdateLancamento3v.ts diff --git a/src/database/migrations/1714409896493-UpdatePublicacao.ts b/src/database/migrations/3-cnab-refactor/1714409896493-UpdatePublicacao.ts similarity index 100% rename from src/database/migrations/1714409896493-UpdatePublicacao.ts rename to src/database/migrations/3-cnab-refactor/1714409896493-UpdatePublicacao.ts diff --git a/src/database/migrations/1714662784331-UpdatePublicacao2v.ts b/src/database/migrations/3-cnab-refactor/1714662784331-UpdatePublicacao2v.ts similarity index 100% rename from src/database/migrations/1714662784331-UpdatePublicacao2v.ts rename to src/database/migrations/3-cnab-refactor/1714662784331-UpdatePublicacao2v.ts diff --git a/src/database/migrations/1714686484049-CreateAgrupado.ts b/src/database/migrations/3-cnab-refactor/1714686484049-CreateAgrupado.ts similarity index 100% rename from src/database/migrations/1714686484049-CreateAgrupado.ts rename to src/database/migrations/3-cnab-refactor/1714686484049-CreateAgrupado.ts diff --git a/src/database/migrations/1714688980016-CreateAgrupado2v.ts b/src/database/migrations/3-cnab-refactor/1714688980016-CreateAgrupado2v.ts similarity index 100% rename from src/database/migrations/1714688980016-CreateAgrupado2v.ts rename to src/database/migrations/3-cnab-refactor/1714688980016-CreateAgrupado2v.ts diff --git a/src/database/migrations/1714764255864-CreateOcorrencia.ts b/src/database/migrations/3-cnab-refactor/1714764255864-CreateOcorrencia.ts similarity index 100% rename from src/database/migrations/1714764255864-CreateOcorrencia.ts rename to src/database/migrations/3-cnab-refactor/1714764255864-CreateOcorrencia.ts diff --git a/src/database/migrations/1715087636547-UpdateRetorno.ts b/src/database/migrations/3-cnab-refactor/1715087636547-UpdateRetorno.ts similarity index 100% rename from src/database/migrations/1715087636547-UpdateRetorno.ts rename to src/database/migrations/3-cnab-refactor/1715087636547-UpdateRetorno.ts diff --git a/src/database/migrations/1715114151403-OcorrenciaCode.ts b/src/database/migrations/3-cnab-refactor/1715114151403-OcorrenciaCode.ts similarity index 100% rename from src/database/migrations/1715114151403-OcorrenciaCode.ts rename to src/database/migrations/3-cnab-refactor/1715114151403-OcorrenciaCode.ts diff --git a/src/database/migrations/1715114882554-ExcludeDetalheARef.ts b/src/database/migrations/3-cnab-refactor/1715114882554-ExcludeDetalheARef.ts similarity index 100% rename from src/database/migrations/1715114882554-ExcludeDetalheARef.ts rename to src/database/migrations/3-cnab-refactor/1715114882554-ExcludeDetalheARef.ts diff --git a/src/database/migrations/1715188684545-UpdateItemTransacao.ts b/src/database/migrations/3-cnab-refactor/1715188684545-UpdateItemTransacao.ts similarity index 100% rename from src/database/migrations/1715188684545-UpdateItemTransacao.ts rename to src/database/migrations/3-cnab-refactor/1715188684545-UpdateItemTransacao.ts diff --git a/src/database/migrations/1715266251395-FixPublicacao.ts b/src/database/migrations/3-cnab-refactor/1715266251395-FixPublicacao.ts similarity index 100% rename from src/database/migrations/1715266251395-FixPublicacao.ts rename to src/database/migrations/3-cnab-refactor/1715266251395-FixPublicacao.ts diff --git a/src/database/migrations/1715693228210-CreatedAtLancamento.ts b/src/database/migrations/3-cnab-refactor/1715693228210-CreatedAtLancamento.ts similarity index 100% rename from src/database/migrations/1715693228210-CreatedAtLancamento.ts rename to src/database/migrations/3-cnab-refactor/1715693228210-CreatedAtLancamento.ts diff --git a/src/database/migrations/1715711970831-PublicacaoFKOcorrencia.ts b/src/database/migrations/3-cnab-refactor/1715711970831-PublicacaoFKOcorrencia.ts similarity index 100% rename from src/database/migrations/1715711970831-PublicacaoFKOcorrencia.ts rename to src/database/migrations/3-cnab-refactor/1715711970831-PublicacaoFKOcorrencia.ts diff --git a/src/database/migrations/1715882993008-RemoveHeaderArquivoStatus.ts b/src/database/migrations/3-cnab-refactor/1715882993008-RemoveHeaderArquivoStatus.ts similarity index 100% rename from src/database/migrations/1715882993008-RemoveHeaderArquivoStatus.ts rename to src/database/migrations/3-cnab-refactor/1715882993008-RemoveHeaderArquivoStatus.ts diff --git a/src/database/migrations/1715887343778-DetalheAFKAgrupado.ts b/src/database/migrations/3-cnab-refactor/1715887343778-DetalheAFKAgrupado.ts similarity index 100% rename from src/database/migrations/1715887343778-DetalheAFKAgrupado.ts rename to src/database/migrations/3-cnab-refactor/1715887343778-DetalheAFKAgrupado.ts diff --git a/src/database/migrations/1715954451933-TransacaoNoUniqueIdOrdem.ts b/src/database/migrations/3-cnab-refactor/1715954451933-TransacaoNoUniqueIdOrdem.ts similarity index 100% rename from src/database/migrations/1715954451933-TransacaoNoUniqueIdOrdem.ts rename to src/database/migrations/3-cnab-refactor/1715954451933-TransacaoNoUniqueIdOrdem.ts diff --git a/src/database/migrations/1716386948012-CreateTransacaoView.ts b/src/database/migrations/3-cnab-refactor/1716386948012-CreateTransacaoView.ts similarity index 100% rename from src/database/migrations/1716386948012-CreateTransacaoView.ts rename to src/database/migrations/3-cnab-refactor/1716386948012-CreateTransacaoView.ts diff --git a/src/database/migrations/1716406551459-TransacaoViewCpfCnpj.ts b/src/database/migrations/3-cnab-refactor/1716406551459-TransacaoViewCpfCnpj.ts similarity index 100% rename from src/database/migrations/1716406551459-TransacaoViewCpfCnpj.ts rename to src/database/migrations/3-cnab-refactor/1716406551459-TransacaoViewCpfCnpj.ts diff --git a/src/database/migrations/1716472421112-TransacaoViewNoUnique.ts b/src/database/migrations/3-cnab-refactor/1716472421112-TransacaoViewNoUnique.ts similarity index 100% rename from src/database/migrations/1716472421112-TransacaoViewNoUnique.ts rename to src/database/migrations/3-cnab-refactor/1716472421112-TransacaoViewNoUnique.ts diff --git a/src/database/migrations/1716576955307-OcorrenciasDetalheAOnly.ts b/src/database/migrations/3-cnab-refactor/1716576955307-OcorrenciasDetalheAOnly.ts similarity index 100% rename from src/database/migrations/1716576955307-OcorrenciasDetalheAOnly.ts rename to src/database/migrations/3-cnab-refactor/1716576955307-OcorrenciasDetalheAOnly.ts diff --git a/src/database/migrations/1717014260013-RemoveCnabStatus.ts b/src/database/migrations/3-cnab-refactor/1717014260013-RemoveCnabStatus.ts similarity index 100% rename from src/database/migrations/1717014260013-RemoveCnabStatus.ts rename to src/database/migrations/3-cnab-refactor/1717014260013-RemoveCnabStatus.ts diff --git a/src/database/migrations/1717442570102-TransacaoViewCreateUpdatedAt.ts b/src/database/migrations/3-cnab-refactor/1717442570102-TransacaoViewCreateUpdatedAt.ts similarity index 100% rename from src/database/migrations/1717442570102-TransacaoViewCreateUpdatedAt.ts rename to src/database/migrations/3-cnab-refactor/1717442570102-TransacaoViewCreateUpdatedAt.ts diff --git a/src/database/migrations/1718128591022-TransacaoViewValorPago.ts b/src/database/migrations/3-cnab-refactor/1718128591022-TransacaoViewValorPago.ts similarity index 100% rename from src/database/migrations/1718128591022-TransacaoViewValorPago.ts rename to src/database/migrations/3-cnab-refactor/1718128591022-TransacaoViewValorPago.ts diff --git a/src/database/migrations/1719431856725-HeaderLoteFormaLancamento.ts b/src/database/migrations/3-cnab-refactor/1719431856725-HeaderLoteFormaLancamento.ts similarity index 100% rename from src/database/migrations/1719431856725-HeaderLoteFormaLancamento.ts rename to src/database/migrations/3-cnab-refactor/1719431856725-HeaderLoteFormaLancamento.ts diff --git a/src/database/migrations/1719949229037-conference.ts b/src/database/migrations/3-cnab-refactor/1719949229037-conference.ts similarity index 100% rename from src/database/migrations/1719949229037-conference.ts rename to src/database/migrations/3-cnab-refactor/1719949229037-conference.ts diff --git a/src/database/migrations/1720214573088-transacaoView.ts b/src/database/migrations/3-cnab-refactor/1720214573088-transacaoView.ts similarity index 100% rename from src/database/migrations/1720214573088-transacaoView.ts rename to src/database/migrations/3-cnab-refactor/1720214573088-transacaoView.ts diff --git a/src/database/migrations/1721056266092-PagamentoPendente.ts b/src/database/migrations/3-cnab-refactor/1721056266092-PagamentoPendente.ts similarity index 100% rename from src/database/migrations/1721056266092-PagamentoPendente.ts rename to src/database/migrations/3-cnab-refactor/1721056266092-PagamentoPendente.ts diff --git a/src/database/migrations/1721170246216-RefactorMigration.ts b/src/database/migrations/3-cnab-refactor/1721170246216-RefactorMigration.ts similarity index 100% rename from src/database/migrations/1721170246216-RefactorMigration.ts rename to src/database/migrations/3-cnab-refactor/1721170246216-RefactorMigration.ts diff --git a/src/database/migrations/1721848566340-AdicionColunaIdTransacaoViewItemTransacao.ts b/src/database/migrations/3-cnab-refactor/1721848566340-AdicionColunaIdTransacaoViewItemTransacao.ts similarity index 100% rename from src/database/migrations/1721848566340-AdicionColunaIdTransacaoViewItemTransacao.ts rename to src/database/migrations/3-cnab-refactor/1721848566340-AdicionColunaIdTransacaoViewItemTransacao.ts diff --git a/src/database/migrations/1721853197274-AdicionColunaItemTransacaoAgrupadoIdTransacaoView.ts b/src/database/migrations/3-cnab-refactor/1721853197274-AdicionColunaItemTransacaoAgrupadoIdTransacaoView.ts similarity index 100% rename from src/database/migrations/1721853197274-AdicionColunaItemTransacaoAgrupadoIdTransacaoView.ts rename to src/database/migrations/3-cnab-refactor/1721853197274-AdicionColunaItemTransacaoAgrupadoIdTransacaoView.ts diff --git a/src/database/migrations/1722348299991-Lancamento.ts b/src/database/migrations/3-cnab-refactor/1722348299991-Lancamento.ts similarity index 100% rename from src/database/migrations/1722348299991-Lancamento.ts rename to src/database/migrations/3-cnab-refactor/1722348299991-Lancamento.ts diff --git a/src/database/migrations/1724436146343-DecouplePublicacao.ts b/src/database/migrations/3-cnab-refactor/1724436146343-DecouplePublicacao.ts similarity index 100% rename from src/database/migrations/1724436146343-DecouplePublicacao.ts rename to src/database/migrations/3-cnab-refactor/1724436146343-DecouplePublicacao.ts diff --git a/src/database/seeds/cliente-favorecido/cliente-favorecido-seed-data.ts b/src/database/seeds/cliente-favorecido/cliente-favorecido-seed-data.ts index 1a376e87..c2d3042a 100644 --- a/src/database/seeds/cliente-favorecido/cliente-favorecido-seed-data.ts +++ b/src/database/seeds/cliente-favorecido/cliente-favorecido-seed-data.ts @@ -1,79 +1,66 @@ -import { ClienteFavorecido } from "src/cnab/entity/cliente-favorecido.entity"; -import { TipoFavorecidoEnum } from "src/tipo-favorecido/tipo-favorecido.enum"; - -/** - * Favorecidos from - */ -export enum FavorecidoCpfCnpjEnum { - /** Pagador is CB */ - VLT = '18201378000119', - Intersul = '12464869000176', - Internorte = '12464539000180', - Transcarioca = '12464553000184', - SantaCruz = '12464577000133', - CMTC = '12464577000133', -} +import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; +import { FavorecidoEmpresaCpfCnpjEnum, FavorecidoEmpresaNomeEnum } from 'src/cnab/enums/favorecido-empresa.enum'; +import { TipoFavorecidoEnum } from 'src/cnab/enums/tipo-favorecido.enum'; export const ClienteFavorecidoSeedData: ClienteFavorecido[] = [ new ClienteFavorecido({ - nome: 'Concessionária do VLT Carioca S.A.', - cpfCnpj: '18201378000119', + nome: FavorecidoEmpresaNomeEnum.VLT, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.VLT, codigoBanco: '033', agencia: '2271', dvAgencia: '', contaCorrente: '13098785', dvContaCorrente: '7', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), new ClienteFavorecido({ - nome: 'Consórcio Intersul Transportes', - cpfCnpj: '12464869000176', + nome: FavorecidoEmpresaNomeEnum.Intersul, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.Intersul, codigoBanco: '033', agencia: '3003', dvAgencia: '', contaCorrente: '13080446', dvContaCorrente: '4', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), new ClienteFavorecido({ - nome: 'Consórcio Internorte de Transportes', - cpfCnpj: '12464539000180', + nome: FavorecidoEmpresaNomeEnum.Internorte, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.Internorte, codigoBanco: '033', agencia: '3403', dvAgencia: '', contaCorrente: '13004445', dvContaCorrente: '9', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), - + new ClienteFavorecido({ - nome: 'Consórcio Transcarioca de Transportes', - cpfCnpj: '12464553000184', + nome: FavorecidoEmpresaNomeEnum.Transcarioca, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.Transcarioca, codigoBanco: '033', agencia: '3403', dvAgencia: '', contaCorrente: '13010758', dvContaCorrente: '7', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), new ClienteFavorecido({ - nome: 'Consórcio Santa Cruz Transportes', - cpfCnpj: '12464577000133', - codigoBanco: '033', + nome: FavorecidoEmpresaNomeEnum.SantaCruz, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.SantaCruz, agencia: '3403', dvAgencia: '', contaCorrente: '13004442', dvContaCorrente: '8', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), new ClienteFavorecido({ - nome: 'Companhia Municipal de Transportes Coletivos CMTC Rio', - cpfCnpj: '44520687000161', + nome: FavorecidoEmpresaNomeEnum.SantaCruz, + cpfCnpj: FavorecidoEmpresaCpfCnpjEnum.SantaCruz, codigoBanco: '001', agencia: '2234', dvAgencia: '9', contaCorrente: '296001', dvContaCorrente: 'X', - tipo: TipoFavorecidoEnum.consorcio, + tipo: TipoFavorecidoEnum.empresa, }), ]; diff --git a/src/database/seeds/lancamento/lancamento-seed-data.service.ts b/src/database/seeds/lancamento/lancamento-seed-data.service.ts index 210a119a..11a8ec45 100644 --- a/src/database/seeds/lancamento/lancamento-seed-data.service.ts +++ b/src/database/seeds/lancamento/lancamento-seed-data.service.ts @@ -10,7 +10,6 @@ import { Repository } from 'typeorm'; @Injectable() export class LancamentoSeedDataService { - nodeEnv = (): string => ''; constructor( @@ -20,28 +19,25 @@ export class LancamentoSeedDataService { @InjectRepository(ClienteFavorecido) private favorecidosRepository: Repository, ) { - this.nodeEnv = () => - this.configService.getOrThrow('app.nodeEnv', { infer: true }); + this.nodeEnv = () => this.configService.getOrThrow('app.nodeEnv', { infer: true }); } async getData() { // Get user_ids const users = await this.usersRepository.find({ - take: 3 + take: 3, }); if (users.length < 3) { - throw new Exception(`Expected 3 users but got ${users.length}.` + - 'Did you seed this before seeding Users?'); + throw new Exception(`Expected 3 users but got ${users.length}.` + 'Did you seed this before seeding Users?'); } const userIds = users.reduce((l, i) => [...l, i.id], []); // Get id_favorecidos const favorecidos = await this.favorecidosRepository.find({ - take: 2 + take: 2, }); if (users.length < 2) { - throw new Exception(`Expected 2 Favorecidos but got ${favorecidos.length}.` + - 'Did you seed this before seeding ClienteFavorecidos?'); + throw new Exception(`Expected 2 Favorecidos but got ${favorecidos.length}.` + 'Did you seed this before seeding ClienteFavorecidos?'); } const today = getBRTFromUTC(new Date()); const seedTag = '[SEED] '; @@ -52,69 +48,61 @@ export class LancamentoSeedDataService { ? ([ { algoritmo: '1', - descricao: seedTag + 'Unapproved', - id_cliente_favorecido: { id: favorecidos[0].id }, + clienteFavorecido: { id: favorecidos[0].id }, data_lancamento: today, data_ordem: today, data_pgto: today, glosa: 1, - numero_processo: '1', + numero_processo: seedTag + 'Unapproved', recurso: 1, valor: 110, valor_a_pagar: 110, - userId: users[0].id, - user: { id: users[0].id }, + autor: { id: users[0].id }, anexo: 1, }, { algoritmo: '2', - descricao: seedTag + 'Approved 1', - id_cliente_favorecido: { id: favorecidos[0].id }, + clienteFavorecido: { id: favorecidos[0].id }, data_lancamento: today, data_ordem: today, data_pgto: today, glosa: 2, - numero_processo: '2', + numero_processo: seedTag + 'Approved 1', recurso: 2, valor: 951, valor_a_pagar: 951, - userId: users[0].id, - user: { id: users[0].id }, - auth_usersIds: userIds.slice(0, 1).join(','), + autor: { id: users[0].id }, + autorizado_por: userIds.slice(0, 1).join(','), anexo: 1, }, { algoritmo: 2, - descricao: seedTag + 'Approved 2', - id_cliente_favorecido: { id: favorecidos[0].id }, + clienteFavorecido: { id: favorecidos[0].id }, data_lancamento: today, data_ordem: today, data_pgto: today, glosa: 2, - numero_processo: '2', + numero_processo: seedTag + 'Approved 2', recurso: 2, valor: 951.12, valor_a_pagar: 951.12, - userId: users[0].id, - user: { id: users[0].id }, - auth_usersIds: userIds.slice(0, 2).join(','), + autor: { id: users[0].id }, + autorizado_por: userIds.slice(0, 2).join(','), anexo: 1, }, { algoritmo: 3, - descricao: seedTag + 'Approved 2 again', - id_cliente_favorecido: { id: favorecidos[0].id }, + clienteFavorecido: { id: favorecidos[0].id }, data_lancamento: today, data_ordem: today, data_pgto: today, glosa: 2, - numero_processo: '2', + numero_processo: seedTag + 'Approved 2 again', recurso: 2, valor: 1034.01, valor_a_pagar: 1034.01, - userId: users[0].id, - user: { id: users[0].id }, - auth_usersIds: userIds.slice(0, 2).join(','), + autor: { id: users[0].id }, + autorizado_por: userIds.slice(0, 2).join(','), anexo: 1, }, ] as LancamentoSeedData[]) diff --git a/src/database/seeds/lancamento/lancamento-seed.module.ts b/src/database/seeds/lancamento/lancamento-seed.module.ts index 6b1f1926..16b61c7b 100644 --- a/src/database/seeds/lancamento/lancamento-seed.module.ts +++ b/src/database/seeds/lancamento/lancamento-seed.module.ts @@ -1,15 +1,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { User } from 'src/users/entities/user.entity'; import { LancamentoSeedDataService } from './lancamento-seed-data.service'; import { LancamentoSeedService } from './lancamento-seed.service'; import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([User, ClienteFavorecido, LancamentoEntity]), ConfigModule], + imports: [TypeOrmModule.forFeature([User, ClienteFavorecido, Lancamento]), ConfigModule], providers: [LancamentoSeedService, LancamentoSeedDataService], exports: [LancamentoSeedService, LancamentoSeedDataService], }) -export class LancamentoSeedModule { } +export class LancamentoSeedModule {} diff --git a/src/database/seeds/lancamento/lancamento-seed.service.ts b/src/database/seeds/lancamento/lancamento-seed.service.ts index 05970903..09524fdf 100644 --- a/src/database/seeds/lancamento/lancamento-seed.service.ts +++ b/src/database/seeds/lancamento/lancamento-seed.service.ts @@ -1,16 +1,16 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { LancamentoEntity } from 'src/lancamento/lancamento.entity'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; import { In, Repository } from 'typeorm'; import { LancamentoSeedDataService } from './lancamento-seed-data.service'; @Injectable() export class LancamentoSeedService { constructor( - @InjectRepository(LancamentoEntity) - private lancamentoRepository: Repository, + @InjectRepository(Lancamento) + private lancamentoRepository: Repository, private lancamentoSeedDataService: LancamentoSeedDataService, - ) { } + ) {} async validateRun() { return Promise.resolve(global.force); @@ -18,12 +18,12 @@ export class LancamentoSeedService { async run() { const { fixtures } = await this.lancamentoSeedDataService.getData(); - const descricoes = fixtures.map(i => i.descricao); + const descricoes = fixtures.map((i) => i.numero_processo); // Remove existing seeds - await this.lancamentoRepository.delete({ descricao: In(descricoes) }); - const newItems: LancamentoEntity[] = []; + await this.lancamentoRepository.delete({ numero_processo: In(descricoes) }); + const newItems: Lancamento[] = []; for (const fixture of fixtures) { - newItems.push(new LancamentoEntity(fixture)); + newItems.push(new Lancamento(fixture)); } await this.lancamentoRepository.upsert(newItems, { conflictPaths: { id: true } }); } diff --git a/src/database/seeds/test/cnab/cnab-update-transacao-view-bq/cnab-update-transacao-view-bq-seed.module.ts b/src/database/seeds/test/cnab/cnab-update-transacao-view-bq/cnab-update-transacao-view-bq-seed.module.ts index 78a0a3e5..434bdd4b 100644 --- a/src/database/seeds/test/cnab/cnab-update-transacao-view-bq/cnab-update-transacao-view-bq-seed.module.ts +++ b/src/database/seeds/test/cnab/cnab-update-transacao-view-bq/cnab-update-transacao-view-bq-seed.module.ts @@ -10,7 +10,7 @@ // import { ItemTransacaoAgrupado } from 'src/cnab/entity/pagamento/item-transacao-agrupado.entity'; // import { Transacao } from 'src/cnab/entity/pagamento/transacao.entity'; // import { TransacaoAgrupado } from 'src/cnab/entity/pagamento/transacao-agrupado.entity'; -// import { TransacaoView } from 'src/transacao-bq/transacao-view.entity'; +// import { TransacaoView } from 'src/transacao-view/transacao-view.entity'; // import { ArquivoPublicacao } from 'src/cnab/entity/arquivo-publicacao.entity'; // import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; diff --git a/src/database/seeds/user/user-seed-data.service.ts b/src/database/seeds/user/user-seed-data.service.ts index 816433cc..664c670e 100644 --- a/src/database/seeds/user/user-seed-data.service.ts +++ b/src/database/seeds/user/user-seed-data.service.ts @@ -15,12 +15,8 @@ export class UserSeedDataService { cpfSamples: string[] = []; cnpjSamples: string[] = []; - constructor( - private configService: ConfigService, - private bigqueryService: BigqueryService, - ) { - this.nodeEnv = () => - this.configService.getOrThrow('app.nodeEnv', { infer: true }); + constructor(private configService: ConfigService, private bigqueryService: BigqueryService) { + this.nodeEnv = () => this.configService.getOrThrow('app.nodeEnv', { infer: true }); } async getData(): Promise { @@ -63,38 +59,48 @@ LIMIT 5 fullName: 'Alexander Rivail Ruiz', email: 'ruiz.smtr@gmail.com', password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), + role: new Role(RoleEnum.master), status: new Status(StatusEnum.active), }, { fullName: 'Bernardo Marcos', email: 'bernardo.marcos64@gmail.com', password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), + role: new Role(RoleEnum.master), status: new Status(StatusEnum.active), }, { fullName: 'Gabriel Fortes', email: 'glfg01092001@gmail.com', password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), + role: new Role(RoleEnum.master), status: new Status(StatusEnum.active), }, { fullName: 'Raphael Baptista Rivas de Araújo', email: 'raphaelrivasbra@gmail.com', password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), + role: new Role(RoleEnum.master), status: new Status(StatusEnum.active), }, { fullName: 'William FL 2007', email: 'williamfl2007@gmail.com', password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), + role: new Role(RoleEnum.master), + status: new Status(StatusEnum.active), + }, + + // Admin Master + { + fullName: 'Lauro Costa Silvestre', + email: 'laurosilvestre.smtr@gmail.com', + password: this.generateRandomPassword(), + role: new Role(RoleEnum.master), status: new Status(StatusEnum.active), }, - // Admin CCT + + // Admin Vanzeiro { fullName: 'Felipe Ribeiro', email: 'felipe.ribeiro@prefeitura.rio', @@ -109,8 +115,6 @@ LIMIT 5 role: new Role(RoleEnum.admin), status: new Status(StatusEnum.active), }, - - // Admins { fullName: 'Jéssica Venancio Teixeira Cardoso Simas', email: 'jessicasimas.smtr@gmail.com', @@ -139,13 +143,6 @@ LIMIT 5 role: new Role(RoleEnum.admin), status: new Status(StatusEnum.active), }, - { - fullName: 'Lauro Costa Silvestre', - email: 'laurosilvestre.smtr@gmail.com', - password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin), - status: new Status(StatusEnum.active), - }, { fullName: 'Admin bigquery', email: 'admin_bigquery@example.com', @@ -154,7 +151,23 @@ LIMIT 5 status: { id: StatusEnum.active } as Status, }, - // Usuários lançamento financeiro + // Admin lançamento + { + fullName: 'Simone Costa', + email: 'simonecosta.smtr@gmail.com', + password: this.generateRandomPassword(), + role: new Role(RoleEnum.admin_finan), + status: new Status(StatusEnum.active), + }, + { + fullName: 'Luciana Fernandes', + email: 'lucianafernandes.smtr@gmail.com', + password: this.generateRandomPassword(), + role: new Role(RoleEnum.admin_finan), + status: new Status(StatusEnum.active), + }, + + // Usuário lançamento { fullName: 'Usuário lançamento', email: 'ruizalexander@id.uff.br', @@ -190,20 +203,6 @@ LIMIT 5 role: new Role(RoleEnum.lancador_financeiro), status: new Status(StatusEnum.active), }, - { - fullName: 'Simone Costa', - email: 'simonecosta.smtr@gmail.com', - password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin_finan), - status: new Status(StatusEnum.active), - }, - { - fullName: 'Luciana Fernandes', - email: 'lucianafernandes.smtr@gmail.com', - password: this.generateRandomPassword(), - role: new Role(RoleEnum.admin_finan), - status: new Status(StatusEnum.active), - }, //apagar após teste { @@ -237,6 +236,7 @@ LIMIT 5 // Development only ...(this.nodeEnv() === 'local' || this.nodeEnv() === 'test' ? ([ + // Vanzeiros { fullName: 'Henrique Santos Template Cpf Van', email: 'henrique@example.com', @@ -257,6 +257,7 @@ LIMIT 5 role: { id: RoleEnum.user } as Role, status: { id: StatusEnum.active } as Status, }, + // Roles { fullName: 'Usuário Teste dos Santos Oliveira', email: 'user@example.com', @@ -267,79 +268,113 @@ LIMIT 5 status: { id: StatusEnum.active } as Status, }, { - fullName: 'Administrador Teste', - email: 'admin@example.com', - password: 'secret', + fullName: 'Administrador Financeiro Teste', + email: 'finan.admin@example.com', + password: 'ob>&+H%=&+H%=&+H%=&+H%=) { + if (dto) { + Object.assign(this, dto); + } + } + + @ApiProperty({ type: Number, example: 1.99 }) + @IsOptional() + @IsNumber() + valor: number; + + @ApiProperty({ type: 'DATE', example: '2024-08-01T19:54:56.299Z' }) + @IsDateString() + data_ordem: Date; + + @ApiProperty() + @ApiProperty({ type: 'DATE', example: '2024-08-01T19:54:56.299Z' }) + @IsDateString() + data_lancamento: Date; + + @ApiProperty({ example: 10 }) + @IsNumber() + algoritmo: number; + + @ApiProperty({ type: Number, required: false, description: 'Valor de pagamento bloqueado.', example: 10.99 }) + @IsOptional() + @IsNumber() + glosa?: number; + + @ApiProperty({ type: Number, required: false, example: 10.99 }) + @IsOptional() + @IsNumber() + recurso?: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsNumber() + anexo?: number; + + // TODO: Caso não precisemos disso, vamos deletar + // @ApiProperty({ type: Number, required: false, example: 1.99 }) + // @IsNumber() + // valor_a_pagar: number; + + @ApiProperty({ description: 'Não sabemos se será número, ser terá zeros a esquerda ou terá letras' }) + @IsNotEmpty() + @IsString() + numero_processo: string; + + @ApiProperty({ type: Number, example: 2 }) + @Min(1) + id_cliente_favorecido: number; + + /** userId - Used internally */ + author: DeepPartial; +} diff --git a/src/lancamento/dtos/lancamentoDto.ts b/src/lancamento/dtos/lancamentoDto.ts deleted file mode 100644 index 6820c985..00000000 --- a/src/lancamento/dtos/lancamentoDto.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsDate, - IsNotEmpty, - IsNumber, - IsNumberString, - IsString, - isNumber -} from 'class-validator'; -import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; -import { ValidateValue } from 'src/utils/validators/validate-value.validator'; -import { DeepPartial } from 'typeorm'; - -export class LancamentoDto { - @ApiProperty() - @IsNotEmpty() - @IsString() - descricao: string; - - @ApiProperty({ type: Number, example: 1.99 }) - @IsNumber() - valor: number; - - @ApiProperty() - @Transform((p) => new Date(p.value)) - @IsDate() - data_ordem: Date; - - @ApiProperty() - @Transform((p) => new Date(p.value)) - @IsDate() - data_pgto: Date; - - @ApiProperty() - @Transform((p) => new Date(p.value)) - @IsDate() - data_lancamento: Date; - - @ApiProperty({ example: 1 }) - @IsNumberString() - algoritmo: string; - - @ApiProperty({ example: 1 }) - @IsNumber() - glosa: number; - - @ApiProperty({ example: 1 }) - @IsNumber() - recurso: number; - - @ApiProperty() - @IsNotEmpty() - @IsNumber() - anexo: number; - - @ApiProperty({ type: Number, example: 1.99 }) - @IsNumber() - valor_a_pagar: number; - - @ApiProperty() - @IsNotEmpty() - @IsString() - numero_processo: string; - - @ApiProperty({ type: Number, example: 2 }) - @Transform((params) => ({ - id: params.value, - })) - @ValidateValue((v) => isNumber(v.id) && v.id > 0, { - message: (v) => - `Is not a number > 0 but ${v.value?.id} (type ${typeof v.value?.id})`, - }) - id_cliente_favorecido: DeepPartial; -} diff --git a/src/lancamento/entities/lancamento-autorizacao.entity.ts b/src/lancamento/entities/lancamento-autorizacao.entity.ts new file mode 100644 index 00000000..c537ae87 --- /dev/null +++ b/src/lancamento/entities/lancamento-autorizacao.entity.ts @@ -0,0 +1,53 @@ +import { Expose } from 'class-transformer'; +import { User } from 'src/users/entities/user.entity'; +import { EntityHelper } from 'src/utils/entity-helper'; +import { CreateDateColumn, DeepPartial, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { Lancamento } from './lancamento.entity'; + +export interface ILancamentoAutorizacao { + id: number; + lancamento: Lancamento; + user: User; + createdAt: Date; + updatedAt: Date; +} + +@Entity('lancamento_autorizacao') +export class LancamentoAutorizacao extends EntityHelper implements ILancamentoAutorizacao { + constructor(autorizado?: DeepPartial) { + super(); + if (autorizado !== undefined) { + Object.assign(this, autorizado); + } + } + + public static tableName = 'lancamento_autorizacao'; + + public static sqlFields: Record = { + id: `"id"`, + lancamento: `"lancamentoId"`, + user: `"userId"`, + createdAt: `"createdAt"`, + updatedAt: `"updatedAt"`, + }; + + @Expose() + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_LancamentoAutorizacao_id' }) + id: number; + + @ManyToOne(() => Lancamento, (lancamento) => lancamento.autorizacoes, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ foreignKeyConstraintName: 'FK_LancamentoAutorizacao_lancamento' }) + @Index('IDX_LancamentoAutorizacao_lancamento') + lancamento: Lancamento; + + @ManyToOne(() => User, (user) => user.lancamentos, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ foreignKeyConstraintName: 'FK_LancamentoAutorizacao_user' }) + @Index('IDX_LancamentoAutorizacao_user') + user: User; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/lancamento/entities/lancamento.entity.ts b/src/lancamento/entities/lancamento.entity.ts new file mode 100644 index 00000000..540f6235 --- /dev/null +++ b/src/lancamento/entities/lancamento.entity.ts @@ -0,0 +1,262 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Expose, Transform } from 'class-transformer'; +import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; +import { DetalheA } from 'src/cnab/entity/pagamento/detalhe-a.entity'; +import { ItemTransacao } from 'src/cnab/entity/pagamento/item-transacao.entity'; +import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; +import { User } from 'src/users/entities/user.entity'; +import { EntityHelper } from 'src/utils/entity-helper'; +import { asStringOrNumber } from 'src/utils/pipe-utils'; +import { AfterLoad, Column, CreateDateColumn, DeepPartial, DeleteDateColumn, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { LancamentoStatus } from '../enums/lancamento-status.enum'; +import { LancamentoInputDto } from '../dtos/lancamento-input.dto'; +import { LancamentoAutorizacao } from './lancamento-autorizacao.entity'; + +export interface ILancamento { + id: number; + valor: number; + data_ordem: Date; + data_pgto: Date | null; + data_lancamento: Date; + itemTransacao: ItemTransacao | null; + autorizacoes: User[]; + autor: User; + clienteFavorecido: ClienteFavorecido; + algoritmo: number; + glosa: number; + recurso: number; + anexo: number; + numero_processo: string; + is_autorizado: boolean; + is_pago: boolean; + status: LancamentoStatus; + createdAt: Date; + updatedAt: Date; + deletedAt: Date; +} + +@Entity('lancamento') +export class Lancamento extends EntityHelper implements ILancamento { + constructor(lancamento?: DeepPartial) { + super(); + if (lancamento !== undefined) { + Object.assign(this, lancamento); + } + } + + public static fromInputDto(dto: LancamentoInputDto) { + return new Lancamento({ + valor: dto.valor, + data_ordem: dto.data_ordem, + data_lancamento: dto.data_lancamento, + algoritmo: dto.algoritmo, + glosa: dto.glosa, + recurso: dto.recurso, + anexo: dto.anexo || 0, + numero_processo: dto.numero_processo, + clienteFavorecido: { id: dto.id_cliente_favorecido }, + autor: dto.author, + }); + } + + @Expose() + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_Lancamento_id' }) + id: number; + + @ApiProperty({ description: 'Valor a pagar' }) + @Column({ type: 'numeric' }) + valor: number; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + data_ordem: Date; + + @Column({ type: 'timestamp', nullable: true }) + data_pgto: Date | null; + + /** Data da criação do Lançamento */ + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + data_lancamento: Date; + + /** + * Geração de Remessa + * + * uniqueConstraintName: `UQ_Lancamento_itemTransacao` + */ + @Exclude() + @ApiProperty({ description: 'ItemTransação do CNAB remessa associado a este Lançamento' }) + @OneToOne(() => ItemTransacao, { nullable: true }) + @JoinColumn({ foreignKeyConstraintName: 'FK_Lancamento_itemTransacao_OneToOne' }) + @Transform(({ value }) => (!value ? null : ((it = value as ItemTransacao) => ({ id: it.id, itemTransacaoAgrupado: { id: it.itemTransacaoAgrupado.id } }))())) + itemTransacao: ItemTransacao | null; + + @ManyToMany(() => User, (user) => user) + @JoinTable({ + name: LancamentoAutorizacao.tableName, + joinColumn: { name: 'lancamentoId', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'userId', referencedColumnName: 'id' }, + synchronize: false, // We use LancamentoAutorizacao to generate migration + }) + @Expose({ name: 'autorizado_por' }) + @Transform(({ value }) => (value as User[]).map((u) => ({ id: u.id, nome: u.fullName }))) + autorizacoes: User[]; + + /** O autor mais recente da criação/modificação/autorização do Lançamento */ + @ManyToOne(() => User, { eager: true }) + @JoinColumn({ foreignKeyConstraintName: 'FK_Lancamento_autor_ManyToOne' }) + @Transform(({ value }) => ({ id: (value as User).id, fullName: (value as User).fullName } as DeepPartial)) + autor: User; + + @ManyToOne(() => ClienteFavorecido, { eager: true }) + @JoinColumn({ name: 'id_cliente_favorecido', foreignKeyConstraintName: 'FK_Lancamento_idClienteFavorecido_ManyToOne' }) + @Transform(({ value }) => ({ + id: (value as ClienteFavorecido).id, + nome: (value as ClienteFavorecido).nome, + })) + clienteFavorecido: ClienteFavorecido; + + @Column({ type: 'numeric', nullable: false }) + algoritmo: number; + + @Column({ type: 'numeric', nullable: false, default: 0 }) + glosa: number; + + @Column({ type: 'numeric', nullable: false, default: 0 }) + recurso: number; + + @Column({ type: 'numeric', nullable: false, default: 0 }) + anexo: number; + + @Column({ type: 'varchar', nullable: false }) + numero_processo: string; + + @Column({ type: Boolean, nullable: false, default: false }) + is_autorizado: boolean; + + @Column({ type: Boolean, nullable: false, default: false }) + is_pago: boolean; + + /** Uma forma de rastrear o estado atual do Lancamento */ + @Column({ enum: LancamentoStatus, nullable: false, default: LancamentoStatus._1_criado }) + status: LancamentoStatus; + + @Exclude() + @CreateDateColumn() + createdAt: Date; + + @Exclude() + @UpdateDateColumn() + updatedAt: Date; + + /** Como são dados de pagamento, caso se delete um Lançamento o dado permanece no banco para auditoria */ + @DeleteDateColumn() + @Exclude() + deletedAt: Date; + + /** Coluna virtual */ + @Exclude() + @Transform(({ value }) => (value === null ? null : ((da = value as DetalheA) => ({ id: da.id, ocorrenciasCnab: da.ocorrenciasCnab, numeroDocumentoEmpresa: da.numeroDocumentoEmpresa }))())) + detalheA: DetalheA | null = null; + + /** Coluna virtual - para consultar as ocorrências */ + ocorrencias: Ocorrencia[] = []; + + @AfterLoad() + setReadValues() { + this.glosa = asStringOrNumber(this.glosa); + this.algoritmo = asStringOrNumber(this.algoritmo); + this.recurso = asStringOrNumber(this.recurso); + this.anexo = asStringOrNumber(this.anexo); + this.valor = asStringOrNumber(this.valor); + this.autorizacoes = this.autorizacoes || []; + // if (this.itemTransacao?.itemTransacaoAgrupado.de) + } + + getIsAutorizado(): boolean { + return this.is_autorizado; + } + + addAutorizado(userId: number) { + this.autorizacoes.push({ id: userId } as User); + this.is_autorizado = this.autorizacoes.length >= 2; + if (this.status === LancamentoStatus._1_criado && this.is_autorizado) { + this.status = LancamentoStatus._2_aprovado; + } + } + + removeAutorizado(userId: number) { + this.autorizacoes = this.autorizacoes.filter((u) => u.id != userId); + this.is_autorizado = this.autorizacoes.length >= 2; + } + + hasAutorizadoPor(userId: number): boolean { + return Boolean(this.autorizacoes.find((u) => u.id == userId)); + } + + updateFromInputDto(dto: LancamentoInputDto) { + this.valor = dto.valor; + this.data_ordem = dto.data_ordem; + this.data_lancamento = dto.data_lancamento; + this.algoritmo = dto.algoritmo; + if (dto.glosa !== undefined) { + this.glosa = dto.glosa; + } + if (dto.recurso !== undefined) { + this.recurso = dto.recurso; + } + if (dto.anexo !== undefined) { + this.anexo = dto.anexo; + } + this.valor = dto.valor; + this.numero_processo = dto.numero_processo; + this.clienteFavorecido = new ClienteFavorecido({ id: dto.id_cliente_favorecido }); + this.autor = new User(dto.author); + } + + public static getSqlFields(table?: string, castType?: boolean): Record { + return { + id: `${table ? `${table}.` : ''}"id"`, + valor: `${table ? `${table}.` : ''}"valor"`, // number, + data_ordem: `${table ? `${table}.` : ''}"data_ordem"`, // Date, + data_pgto: `${table ? `${table}.` : ''}"data_pgto"`, // Date | null, + data_lancamento: `${table ? `${table}.` : ''}"data_lancamento"`, // Date, + itemTransacao: `${table ? `${table}.` : ''}"itemTransacao"`, // ItemTransacao, + autorizacoes: `${table ? `${table}.` : ''}"autorizacoes"`, // User[], + autor: `${table ? `${table}.` : ''}"autor"`, // User, + clienteFavorecido: `${table ? `${table}.` : ''}"clienteFavorecido"`, // ClienteFavorecido, + algoritmo: `${table ? `${table}.` : ''}"algoritmo"`, // number, + glosa: `${table ? `${table}.` : ''}"glosa"`, // number, + recurso: `${table ? `${table}.` : ''}"recurso"`, // number, + anexo: `${table ? `${table}.` : ''}"anexo"`, // number, + numero_processo: `${table ? `${table}.` : ''}"numero_processo"`, // string, + is_autorizado: `${table ? `${table}.` : ''}"is_autorizado"`, // boolean, + is_pago: `${table ? `${table}.` : ''}"is_pago"`, // boolean, + status: `${table ? `${table}.` : ''}"status"`, // string, + createdAt: `${table ? `${table}.` : ''}"createdAt"`, // Date, + updatedAt: `${table ? `${table}.` : ''}"updatedAt"`, // Date, + deletedAt: `${table ? `${table}.` : ''}"deletedAt"`, // Date, + }; + } + + public static sqlFieldTypes: Record = { + id: 'INT', + valor: 'NUMERIC', + data_ordem: 'TIMESTAMP', + data_pgto: 'TIMESTAMP', + data_lancamento: 'TIMESTAMP', + itemTransacao: 'INT', + autorizacoes: '', + autor: '', + clienteFavorecido: '', + algoritmo: 'NUMERIC', + glosa: 'NUMERIC', + recurso: 'NUMERIC', + anexo: 'NUMERIC', + numero_processo: 'VARCHAR', + is_pago: 'BOOLEAN', + is_autorizado: 'BOOLEAN', + status: 'VARCHAR', + createdAt: 'TIMESTAMP', + updatedAt: 'TIMESTAMP', + deletedAt: 'TIMESTAMP', + }; +} diff --git a/src/lancamento/enums/lancamento-status.enum.ts b/src/lancamento/enums/lancamento-status.enum.ts new file mode 100644 index 00000000..f62df159 --- /dev/null +++ b/src/lancamento/enums/lancamento-status.enum.ts @@ -0,0 +1,7 @@ +export enum LancamentoStatus { + _1_criado = 'criado', + _2_aprovado = 'aprovado', + _3_remessa = 'remessa', + _4_pago = 'pago', + _5_erro = 'erro', +} diff --git a/src/lancamento/interfaces/lancamento-seed-data.interface.ts b/src/lancamento/interfaces/lancamento-seed-data.interface.ts index e2377639..c08a1c21 100644 --- a/src/lancamento/interfaces/lancamento-seed-data.interface.ts +++ b/src/lancamento/interfaces/lancamento-seed-data.interface.ts @@ -1,24 +1,21 @@ -import { ClienteFavorecido } from "src/cnab/entity/cliente-favorecido.entity"; -import { User } from "src/users/entities/user.entity"; -import { DeepPartial } from "typeorm"; +import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; +import { User } from 'src/users/entities/user.entity'; +import { DeepPartial } from 'typeorm'; export interface LancamentoSeedData { id?: number; - descricao: string; valor: number; data_lancamento: Date; data_ordem: Date; data_pgto: Date; - algoritmo: string; + algoritmo: number; glosa: number; recurso: number; valor_a_pagar: number; numero_processo: string; - id_cliente_favorecido: DeepPartial; - userId: number; - /** Probably we dont need this field or userId. */ - user: DeepPartial; + clienteFavorecido: DeepPartial; + autor: DeepPartial; /** @example `1,2,3` */ - auth_usersIds?: string; + autorizado_por?: string; anexo: number; } diff --git a/src/lancamento/interfaces/lancamento.interface.ts b/src/lancamento/interfaces/lancamento.interface.ts deleted file mode 100644 index 5a98a04c..00000000 --- a/src/lancamento/interfaces/lancamento.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ClienteFavorecido } from "src/cnab/entity/cliente-favorecido.entity"; -import { DeepPartial } from "typeorm"; - -/** - * Used as output data to frontend - */ -export interface ItfLancamento { - id: number; - descricao: string; - valor: number; - data_ordem: Date; - data_pgto: Date; - data_lancamento: Date; - algoritmo: string; - glosa: number; - recurso: number; - anexo: number; - valor_a_pagar: number; - numero_processo: string; - id_cliente_favorecido: DeepPartial; - // Added fields - auth_usersIds: number[]; - autorizadopor: number[]; -} diff --git a/src/lancamento/lancamento.controller.ts b/src/lancamento/lancamento.controller.ts index 4bd1ca79..6bf27b2d 100644 --- a/src/lancamento/lancamento.controller.ts +++ b/src/lancamento/lancamento.controller.ts @@ -1,26 +1,20 @@ -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Param, - Post, - Put, - Query, - Request, - UseGuards, -} from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query, Request, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { ApiBearerAuth, ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { Roles } from 'src/roles/roles.decorator'; import { RoleEnum } from 'src/roles/roles.enum'; import { RolesGuard } from 'src/roles/roles.guard'; -import { LancamentoDto } from './dtos/lancamentoDto'; -import { ItfLancamento } from './interfaces/lancamento.interface'; -import { LancamentoService } from './lancamento.service'; +import { ApiDescription } from 'src/utils/api-param/description-api-param'; +import { ParseBooleanPipe } from 'src/utils/pipes/parse-boolean.pipe'; +import { ParseNumberPipe } from 'src/utils/pipes/parse-number.pipe'; import { AutorizaLancamentoDto } from './dtos/AutorizaLancamentoDto'; +import { LancamentoInputDto } from './dtos/lancamento-input.dto'; +import { Lancamento } from './entities/lancamento.entity'; +import { LancamentoService } from './lancamento.service'; +import { LancamentoStatus } from './enums/lancamento-status.enum'; +import { ParseEnumPipe } from 'src/utils/pipes/parse-enum.pipe'; +import { ParseArrayPipe } from 'src/utils/pipes/parse-array.pipe'; +import { IRequest } from 'src/utils/interfaces/request.interface'; @ApiTags('Lancamento') @Controller({ @@ -30,108 +24,71 @@ import { AutorizaLancamentoDto } from './dtos/AutorizaLancamentoDto'; export class LancamentoController { constructor(private readonly lancamentoService: LancamentoService) {} - @ApiBearerAuth() + @Get('/') + @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @Get('/') - @ApiQuery({ - name: 'mes', - type: Number, - required: false, - description: 'Mês do lançamento', - }) - @ApiQuery({ - name: 'periodo', - type: Number, - required: false, - description: - 'Período do lançamento. primeira quinzena ou segunda quinzena.', - }) - @ApiQuery({ - name: 'ano', - type: Number, - required: false, - description: 'Ano do lançamento.', - }) - @ApiQuery({ - name: 'autorizado', - type: Number, - required: false, - description: - 'use 1 ou 0 (ou deixe vazio) para filtrar por autorizado ou não autorizado.', - }) - @HttpCode(HttpStatus.OK) - async getLancamento( - @Request() request, - @Query('mes') mes: number, - @Query('periodo') periodo: number, - @Query('ano') ano: number, - @Query('autorizado') authorized: number, - ): Promise { - return await this.lancamentoService.findByPeriod( - mes, - periodo, - ano, - authorized, - ); + @ApiBearerAuth() + @ApiQuery({ name: 'periodo', type: Number, required: false, description: ApiDescription({ _: 'Período do lançamento. Primeira quinzena (dias 1-15) ou segunda quinzena (dias 16 até o fim do mês).', conditions: "periodo, mes, ano muest be filled. Otherwise it won't filter by date", min: 1, max: 2 }) }) + @ApiQuery({ name: 'mes', type: Number, required: false, description: ApiDescription({ _: 'Mês do lançamento', conditions: "periodo, mes, ano muest be filled. Otherwise it won't filter by date" }) }) + @ApiQuery({ name: 'ano', type: Number, required: false, description: ApiDescription({ _: 'Ano do lançamento.', conditions: "periodo, mes, ano muest be filled. Otherwise it won't filter by date" }) }) + @ApiQuery({ name: 'autorizado', type: Boolean, required: false, description: 'Fitra se foi autorizado ou não.' }) + @ApiQuery({ name: 'pago', type: Boolean, required: false, description: 'Fitra se foi autorizado ou não.' }) + @ApiQuery({ name: 'status', enum: LancamentoStatus, required: false, description: 'Fitra por status.' }) + async get( + @Request() request, // + @Query('periodo', new ParseNumberPipe({ min: 1, max: 2, optional: true })) periodo: number | undefined, + @Query('mes', new ParseNumberPipe({ min: 1, max: 12, optional: true })) mes: number | undefined, + @Query('ano') ano: number | undefined, + @Query('autorizado', new ParseBooleanPipe({ optional: true })) autorizado: boolean | undefined, + @Query('pago', new ParseBooleanPipe({ optional: true })) pago: boolean | undefined, + @Query('status', new ParseEnumPipe(LancamentoStatus, { optional: true })) status: LancamentoStatus | undefined, + ): Promise { + return await this.lancamentoService.find({ mes, periodo, ano, autorizado, pago, status }); } - - @ApiBearerAuth() + @Get('/getbystatus') + @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @Get('/getbystatus') - @ApiQuery({ - name: 'status', - required: false, - description: 'use 1 ou 0 para autorizado ou não autorizado.', - }) - @HttpCode(HttpStatus.OK) + @ApiOperation({ description: 'Pesquisar Lançamentos pelo status' }) + @ApiBearerAuth() + @ApiQuery({ name: 'autorizado', type: Boolean, required: true, description: 'Fitra se foi autorizado ou não.' }) async getByStatus( - @Request() request, - @Query('status') status: number, - ): Promise { - return await this.lancamentoService.findByStatus(status); + @Request() request, // + @Query('autorizado', new ParseBooleanPipe()) autorizado: boolean | undefined, + ): Promise { + const _autorizado = autorizado as boolean; + return await this.lancamentoService.findByStatus(_autorizado); } - @ApiBearerAuth() + @Get('/getValorAutorizado') + @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @Get('/getValorAutorizado') - @ApiQuery({ - name: 'mes', - required: true, - description: 'Mês do lançamento', - }) - @ApiQuery({ - name: 'periodo', - required: true, - description: - 'Período do lançamento. primeira quinzena ou segunda quinzena.', - }) - @ApiQuery({ - name: 'ano', - required: true, - description: 'Ano do lançamento.', - }) - @HttpCode(HttpStatus.OK) + @ApiOperation({ description: 'Obter a soma dos valores dos Lançamentos.' }) + @ApiBearerAuth() + @ApiQuery({ name: 'mes', required: true, description: 'Mês do lançamento' }) + @ApiQuery({ name: 'periodo', required: true, description: 'Período do lançamento. Primeira quinzena (dias 1-15) ou segunda quinzena (dias 16 até o fim do mês).' }) + @ApiQuery({ name: 'ano', required: true, description: 'Ano do lançamento.' }) + @ApiOperation({ description: `Inclui uma autorização do usuário autenticado para o Lançamento.` }) async getValorAutorizado( - @Request() request, + @Request() request, // @Query('mes') mes: number, @Query('periodo') periodo: number, @Query('ano') ano: number, @@ -139,95 +96,93 @@ export class LancamentoController { return await this.lancamentoService.getValorAutorizado(mes, periodo, ano); } - @ApiBearerAuth() + @Post('/create') + @HttpCode(HttpStatus.CREATED) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @ApiBody({ type: LancamentoDto }) - @HttpCode(HttpStatus.CREATED) - @Post('/create') + @ApiBearerAuth() + @ApiBody({ type: LancamentoInputDto }) async postCreateLancamento( - @Request() req: any, - @Body() lancamentoData: LancamentoDto, // It was ItfLancamento - ): Promise { - const userId = req.user.id; - const createdLancamento = await this.lancamentoService.create( - lancamentoData, - userId, - ); - return createdLancamento.toItfLancamento(); + @Request() req: any, // + @Body() lancamentoDto: LancamentoInputDto, + ): Promise { + lancamentoDto.author = { id: req.user.id }; + const createdLancamento = await this.lancamentoService.create(lancamentoDto); + return createdLancamento; } - @ApiBearerAuth() - @UseGuards(AuthGuard('jwt'), RolesGuard) - @Roles(RoleEnum.master, RoleEnum.admin_finan, RoleEnum.aprovador_financeiro) @Put('/authorize') - @ApiQuery({ - name: 'lancamentoId', - required: true, - description: 'Id do lançamento', - }) - @ApiBody({ type: AutorizaLancamentoDto }) @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('jwt'), RolesGuard) + @Roles( + RoleEnum.master, // + RoleEnum.admin_finan, + RoleEnum.aprovador_financeiro, + ) + @ApiOperation({ description: `Inclui uma autorização do usuário autenticado para o Lançamento.` }) + @ApiBearerAuth() + @ApiQuery({ name: 'lancamentoId', required: true, description: 'Id do lançamento' }) + @ApiBody({ type: AutorizaLancamentoDto }) async putAutorizarPagamento( - @Request() req, + @Request() req, // @Body() autorizaLancamentoDto: AutorizaLancamentoDto, ) { const userId = req.user.id; const lancamentoId = req.query.lancamentoId; - return await this.lancamentoService.autorizarPagamento( - userId, - lancamentoId, - autorizaLancamentoDto, - ); + return await this.lancamentoService.autorizarPagamento(userId, lancamentoId, autorizaLancamentoDto); } - @ApiBearerAuth() + @Put('/') + @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @Put('/') - @ApiBody({ type: LancamentoDto }) - @HttpCode(HttpStatus.OK) - @ApiQuery({ - name: 'lancamentoId', - required: true, - description: 'Id do lançamento', - }) - async atualizaLancamento( - @Request() req, - @Body() lancamentoData: LancamentoDto, // It was ItfLancamento + @ApiBearerAuth() + @ApiBody({ type: LancamentoInputDto }) + @ApiQuery({ name: 'lancamentoId', required: true, description: 'Id do lançamento' }) + async putLancamento( + @Request() req: IRequest, + @Query('lancamentoId', new ParseNumberPipe({ min: 1 })) lancamentoId: number, + @Body() lancamentoDto: LancamentoInputDto, // It was ItfLancamento ) { - const id = req.query.lancamentoId; - const userId = req.user.id; - return await this.lancamentoService.update(id, lancamentoData, userId); + lancamentoDto.author = { id: req.user.id }; + return await this.lancamentoService.updateDto(lancamentoId, lancamentoDto); } - @ApiBearerAuth() + @Get('/:id') + @HttpCode(HttpStatus.OK) @UseGuards(AuthGuard('jwt'), RolesGuard) @Roles( - RoleEnum.master, + RoleEnum.master, // RoleEnum.admin_finan, RoleEnum.lancador_financeiro, RoleEnum.aprovador_financeiro, ) - @Get('/:id') - @HttpCode(HttpStatus.OK) - async getById(@Param('id') id: number) { + @ApiBearerAuth() + async getId(@Param('id') id: number) { return await this.lancamentoService.getById(id); } @Delete('/:id') - @HttpCode(HttpStatus.NO_CONTENT) - async deleteById(@Param('id') id: number) { - return await this.lancamentoService.delete(id); + @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('jwt'), RolesGuard) + @Roles( + RoleEnum.master, // + RoleEnum.admin_finan, + RoleEnum.lancador_financeiro, + RoleEnum.aprovador_financeiro, + ) + @ApiBearerAuth() + async deleteId(@Param('id') id: number) { + return await this.lancamentoService.deleteId(id); } -} \ No newline at end of file +} diff --git a/src/lancamento/lancamento.entity.ts b/src/lancamento/lancamento.entity.ts deleted file mode 100644 index 2c0d0296..00000000 --- a/src/lancamento/lancamento.entity.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; -import { Transacao } from 'src/cnab/entity/pagamento/transacao.entity'; -import { User } from 'src/users/entities/user.entity'; -import { EntityHelper } from 'src/utils/entity-helper'; -import { - AfterLoad, - Column, - CreateDateColumn, - DeepPartial, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; -import { ItfLancamento } from './interfaces/lancamento.interface'; -import { asStringOrNumber } from 'src/utils/pipe-utils'; - -@Entity('lancamento') -export class LancamentoEntity extends EntityHelper { - constructor(lancamento?: DeepPartial) { - super(); - if (lancamento !== undefined) { - Object.assign(this, lancamento); - } - } - - @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_Lancamento_id' }) - id: number; - - @Column({ type: 'varchar' }) - descricao: string; - - /** Valor pago */ - @Column({ type: 'numeric' }) - valor: number; - - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) - data_ordem: Date; - - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) - data_pgto: Date; - - /** createdAt */ - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) - data_lancamento: Date; - - /** Remessa */ - @ManyToOne(() => Transacao, { nullable: true }) - @JoinColumn({ foreignKeyConstraintName: 'FK_Lancamento_transacao_ManyToOne' }) - transacao: Transacao; - - /** @example `1,2,3` */ - @Column({ type: 'varchar', nullable: true }) - auth_usersIds?: string; - - @Column({ type: 'int', nullable: false }) - userId?: number; - - /** Is userId the same as user? Why do we have two of them? */ - @ManyToOne(() => User) - @JoinColumn({ - name: 'userId', - foreignKeyConstraintName: 'FK_Lancamento_user_ManyToOne', - }) - user: User; - - @ManyToOne(() => ClienteFavorecido, { eager: true }) - @JoinColumn({ - name: 'id_cliente_favorecido', - foreignKeyConstraintName: 'FK_Lancamento_idClienteFavorecido_ManyToOne', - }) - id_cliente_favorecido: ClienteFavorecido; - - @Column({ type: 'varchar', nullable: false }) - algoritmo: string; - - @Column({ type: 'numeric', nullable: false }) - glosa: number; - - @Column({ type: 'numeric', nullable: false }) - recurso: number; - - @Column({ type: 'numeric', nullable: false }) - anexo: number; - - @Column({ type: 'numeric', nullable: false }) - valor_a_pagar: number; - - @Column({ type: 'varchar', nullable: false }) - numero_processo: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; - - toItfLancamento(): ItfLancamento { - return { - id: this.id, - descricao: this.descricao, - valor: this.valor, - data_ordem: this.data_ordem, - data_pgto: this.data_pgto, - data_lancamento: this.data_lancamento, - algoritmo: this.algoritmo, - glosa: this.glosa, - recurso: this.recurso, - anexo: this.anexo, - valor_a_pagar: this.valor_a_pagar, - numero_processo: this.numero_processo, - id_cliente_favorecido: this.id_cliente_favorecido, - auth_usersIds: this.auth_usersIds?.split(',')?.map(Number) || [], - autorizadopor: [], - }; - } - - @AfterLoad() - setReadValues() { - this.glosa = asStringOrNumber(this.glosa); - this.recurso = asStringOrNumber(this.recurso); - this.anexo = asStringOrNumber(this.anexo); - this.valor_a_pagar = asStringOrNumber(this.valor_a_pagar); - } -} diff --git a/src/lancamento/lancamento.module.ts b/src/lancamento/lancamento.module.ts index 09c174b5..dca16181 100644 --- a/src/lancamento/lancamento.module.ts +++ b/src/lancamento/lancamento.module.ts @@ -2,16 +2,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersModule } from 'src/users/users.module'; import { LancamentoController } from './lancamento.controller'; -import { LancamentoEntity } from './lancamento.entity'; +import { Lancamento } from './entities/lancamento.entity'; import { LancamentoService } from './lancamento.service'; import { LancamentoRepository } from './lancamento.repository'; +import { ClienteFavorecidoModule } from 'src/cnab/cliente-favorecido.module'; @Module({ - imports: [TypeOrmModule.forFeature([LancamentoEntity]), + imports: [ UsersModule, + ClienteFavorecidoModule, + TypeOrmModule.forFeature([Lancamento]), // ], controllers: [LancamentoController], providers: [LancamentoService, LancamentoRepository], exports: [LancamentoService, LancamentoRepository], }) -export class LancamentoModule { } +export class LancamentoModule {} diff --git a/src/lancamento/lancamento.repository.ts b/src/lancamento/lancamento.repository.ts index b75109cd..5d4106b2 100644 --- a/src/lancamento/lancamento.repository.ts +++ b/src/lancamento/lancamento.repository.ts @@ -1,50 +1,110 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - DeepPartial, - DeleteResult, - FindManyOptions, - FindOneOptions, - Repository, - SaveOptions, -} from 'typeorm'; -import { LancamentoEntity } from './lancamento.entity'; +import { Brackets, DeepPartial, DeleteResult, FindManyOptions, FindOneOptions, FindOptionsWhere, ObjectLiteral, QueryRunner, Repository, SaveOptions, UpdateResult } from 'typeorm'; +import { Lancamento } from './entities/lancamento.entity'; +import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; +import { LancamentoStatus } from './enums/lancamento-status.enum'; +import { compactQuery } from 'src/utils/console-utils'; + +export interface UpdateLancamentoWhere { + transacaoAgrupadoId: number; +} @Injectable() export class LancamentoRepository { constructor( - @InjectRepository(LancamentoEntity) - private readonly lancamentoRepository: Repository, + @InjectRepository(Lancamento) + private readonly lancamentoRepository: Repository, ) {} - create(entityLike: DeepPartial): LancamentoEntity { + create(entityLike: DeepPartial): Lancamento { return this.lancamentoRepository.create(entityLike); } - save( - entity: DeepPartial, - options?: SaveOptions, - ): Promise { + save(entity: DeepPartial, options?: SaveOptions): Promise { return this.lancamentoRepository.save(entity, options); } - findOne( - options: FindOneOptions, - ): Promise { - return this.findOne(options); + async updateRaw(set: DeepPartial, where: UpdateLancamentoWhere): Promise { + return await this.lancamentoRepository + .createQueryBuilder('lancamento') + .update() + .set(set) + .where( + compactQuery(` + id IN ( + SELECT l1.id FROM lancamento l1 + LEFT JOIN item_transacao it ON it.id = l1."itemTransacaoId" + LEFT JOIN item_transacao_agrupado ita ON ita.id = it."itemTransacaoAgrupadoId" + LEFT JOIN transacao_agrupado ta ON ta.id = ita."transacaoAgrupadoId" + WHERE ta.id = :transacaoAgrupadoId + ) + `), + where, + ) + .execute(); + } + + update(criteria: FindOptionsWhere, partialEntity: QueryDeepPartialEntity, queryRunner?: QueryRunner): Promise { + return (queryRunner?.manager?.getRepository(Lancamento) || this.lancamentoRepository).update(criteria, partialEntity); } - findMany( - options?: FindManyOptions | undefined, - ): Promise { - return this.lancamentoRepository.find(options); + async findOne(options: FindOneOptions): Promise { + let qb = this.lancamentoRepository + .createQueryBuilder('lancamento') // + .leftJoinAndSelect('lancamento.autorizacoes', 'autorizacoes') + .leftJoinAndSelect('lancamento.autor', 'autor') + .leftJoinAndSelect('lancamento.clienteFavorecido', 'clienteFavorecido') + .leftJoinAndSelect('lancamento.itemTransacao', 'itemTransacao') + .leftJoinAndSelect('itemTransacao.itemTransacaoAgrupado', 'itemTransacaoAgrupado') + .leftJoinAndMapOne('lancamento.detalheA', 'detalhe_a', 'detalheA', 'detalheA.itemTransacaoAgrupadoId = itemTransacaoAgrupado.id') + .leftJoinAndMapMany('lancamento.ocorrencias', 'ocorrencia', 'ocorrencia', 'ocorrencia.detalheAId = detalheA.id'); + + if (options?.where) { + qb = qb.where(options.where); + } + + return await qb.getOne(); + } + + async getOne(options: FindOneOptions): Promise { + const found = await this.findOne(options); + if (!found) { + throw new HttpException('Lancamento não encontrado', HttpStatus.NOT_FOUND); + } + return found; + } + + async findMany(options?: FindManyOptions | undefined, andWhere?: { detalheA: { id: number[] } }): Promise { + let whereCount = 0; + let qb = this.lancamentoRepository + .createQueryBuilder('lancamento') // + .leftJoinAndSelect('lancamento.autorizacoes', 'autorizacoes') + .leftJoinAndSelect('lancamento.autor', 'autor') + .leftJoinAndSelect('lancamento.clienteFavorecido', 'clienteFavorecido') + .leftJoinAndSelect('lancamento.itemTransacao', 'itemTransacao') + .leftJoinAndSelect('itemTransacao.itemTransacaoAgrupado', 'itemTransacaoAgrupado') + .leftJoinAndMapOne('lancamento.detalheA', 'detalhe_a', 'detalheA', 'detalheA.itemTransacaoAgrupadoId = itemTransacaoAgrupado.id') + .leftJoinAndMapMany('lancamento.ocorrencias', 'ocorrencia', 'ocorrencia', 'ocorrencia.detalheAId = detalheA.id'); + + if (options?.where) { + qb = qb[!whereCount ? 'where' : 'andWhere'](options?.where); + whereCount += 1; + } + if (andWhere) { + qb = qb[!whereCount ? 'where' : 'andWhere']('detalheA.id IN(:daId)', { daId: andWhere.detalheA.id.join(',') }); + whereCount += 1; + } + qb = qb.orderBy('lancamento.id', 'DESC'); + const ret = await qb.getMany(); + return ret; } - getAll(): Promise { - return this.lancamentoRepository.find(); + getAll(): Promise { + return this.findMany(); } - delete(id: number): Promise { - return this.lancamentoRepository.delete(id); + softDelete(id: number): Promise { + return this.lancamentoRepository.softDelete(id); } } diff --git a/src/lancamento/lancamento.service.ts b/src/lancamento/lancamento.service.ts index f9f07783..1530c0f1 100644 --- a/src/lancamento/lancamento.service.ts +++ b/src/lancamento/lancamento.service.ts @@ -1,313 +1,220 @@ -import { - HttpException, - HttpStatus, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; -import { endOfDay, isFriday, nextFriday, startOfDay, subDays } from 'date-fns'; +import { endOfDay, endOfMonth } from 'date-fns'; +import { ClienteFavorecido } from 'src/cnab/entity/cliente-favorecido.entity'; +import { FavorecidoEmpresaCpfCnpjEnum, FavorecidoEmpresaNomeEnum } from 'src/cnab/enums/favorecido-empresa.enum'; +import { ClienteFavorecidoService } from 'src/cnab/service/cliente-favorecido.service'; import { UsersService } from 'src/users/users.service'; +import { compactQuery } from 'src/utils/console-utils'; import { CustomLogger } from 'src/utils/custom-logger'; -import { Between, In, IsNull, Like } from 'typeorm'; +import { EntityHelper } from 'src/utils/entity-helper'; +import { CommonHttpException } from 'src/utils/http-exception/common-http-exception'; +import { Between, DeepPartial, IsNull, QueryRunner, UpdateResult } from 'typeorm'; import { AutorizaLancamentoDto } from './dtos/AutorizaLancamentoDto'; -import { LancamentoDto } from './dtos/lancamentoDto'; -import { ItfLancamento } from './interfaces/lancamento.interface'; -import { LancamentoEntity } from './lancamento.entity'; -import { LancamentoRepository } from './lancamento.repository'; +import { LancamentoInputDto } from './dtos/lancamento-input.dto'; +import { ILancamento, Lancamento } from './entities/lancamento.entity'; +import { LancamentoStatus } from './enums/lancamento-status.enum'; +import { LancamentoRepository, UpdateLancamentoWhere } from './lancamento.repository'; + +/** Usado para exibição no erro */ +const validFavorecidoNames = [ + String(FavorecidoEmpresaNomeEnum.CMTC), // + String(FavorecidoEmpresaNomeEnum.Internorte), + String(FavorecidoEmpresaNomeEnum.Intersul), + String(FavorecidoEmpresaNomeEnum.SantaCruz), + String(FavorecidoEmpresaNomeEnum.Transcarioca), + // ConcessionariaNomeEnum.VLT, // DESABILITADO ATÉ O MOMENTO +]; +const validFavorecidoCpfCnpjs = [ + String(FavorecidoEmpresaCpfCnpjEnum.CMTC), // + String(FavorecidoEmpresaCpfCnpjEnum.Internorte), + String(FavorecidoEmpresaCpfCnpjEnum.Intersul), + String(FavorecidoEmpresaCpfCnpjEnum.SantaCruz), + String(FavorecidoEmpresaCpfCnpjEnum.Transcarioca), + // ConcessionariaNomeEnum.VLT, // DESABILITADO ATÉ O MOMENTO +]; @Injectable() export class LancamentoService { - private readonly logger = new CustomLogger(LancamentoService.name, { - timestamp: true, - }); + private readonly logger = new CustomLogger(LancamentoService.name, { timestamp: true }); constructor( - private readonly lancamentoRepository: LancamentoRepository, + private readonly lancamentoRepository: LancamentoRepository, // private readonly usersService: UsersService, + private readonly clienteFavorecidoService: ClienteFavorecidoService, ) {} - // /** - // * Get unused data (no Transacao Id) from current payment week (thu-wed / qui-qua). - // */ - // async findByLancamentos(lancamentos: LancamentoEntity[]): Promise { - // const ids = lancamentos.map(i => i.id); - // const found = await this.lancamentoRepository.findMany({ - // where: { - // id: In(ids) - // } - // }); - // return found; - // } - /** - * Get unused data (no Transacao Id) from current payment week (sex-qui). + * Procura itens não usados ainda (sem Transacao Id) from current payment week (sex-qui). */ - findToPayWeek(): Promise { - const today = new Date(); - const friday = isFriday(today) ? today : nextFriday(today); - const sex = startOfDay(subDays(friday, 7)); - const qui = endOfDay(subDays(friday, 1)); - return this.lancamentoRepository.findMany({ + async findToPay(dataOrdemBetween?: [Date, Date]) { + const found = await this.lancamentoRepository.findMany({ where: { - data_lancamento: Between(sex, qui), - transacao: IsNull(), - auth_usersIds: Like('%,%'), // At least 2 approved (1 comma) + ...(dataOrdemBetween ? { data_lancamento: Between(...dataOrdemBetween) } : {}), + itemTransacao: IsNull(), + is_autorizado: true, }, }); + return { + /** Usado para Lançamento Financeiro */ + cett: found.filter((l) => l.clienteFavorecido.cpfCnpj == String(FavorecidoEmpresaCpfCnpjEnum.VLT)), + /** Usado majoritariamente para Jaé */ + contaBilhetagem: found.filter((l) => l.clienteFavorecido.cpfCnpj != String(FavorecidoEmpresaCpfCnpjEnum.VLT)), + }; } - async findByPeriod( - month: number | null = null, - period: number | null = null, - year: number | null = null, - authorized: number | null = null, - ): Promise { - let startDate: Date | undefined; - let endDate: Date | undefined; - - this.logger.debug(String(`Find Lancamento by period: ${period}`)); - if ( - month !== null && - period !== null && - year !== null && - !isNaN(month) && - !isNaN(period) && - !isNaN(year) && - typeof month === 'number' && - typeof period === 'number' && - typeof year === 'number' - ) { - [startDate, endDate] = this.getMonthDateRange(year, month, period); - this.logger.debug(String(`${startDate}, ${endDate}`)); - } - - const whereOptions: any = {}; - if (startDate && endDate) { - whereOptions.data_lancamento = Between(startDate, endDate); + async find(args?: { + mes?: number; // + periodo?: number; + ano?: number; + autorizado?: boolean; + pago?: boolean; + detalheA?: { id: number[] }; + status?: LancamentoStatus; + }): Promise { + /** [startDate, endDate] */ + let dateRange: [Date, Date] | null = null; + if (args?.mes && args?.periodo && args?.ano) { + dateRange = this.getMonthDateRange(args.ano, args.mes, args?.periodo); } - const lancamentos = await this.lancamentoRepository.findMany({ - where: whereOptions, - relations: ['user'], - }); - - const allUserIds = new Set(); - lancamentos.forEach((lancamento) => { - if (lancamento.auth_usersIds) { - lancamento.auth_usersIds - .split(',') - .forEach((id) => allUserIds.add(Number(id))); - } - }); - - let usersMap = new Map(); - if (allUserIds.size > 0) { - const users = await this.usersService.findMany({ + const lancamentos = await this.lancamentoRepository.findMany( + { where: { - id: In([...allUserIds]), + ...(dateRange ? { data_lancamento: Between(...dateRange) } : {}), + ...(args?.autorizado !== undefined ? { is_autorizado: args.autorizado } : {}), + ...(args?.pago !== undefined ? { is_pago: args.pago } : {}), + ...(args?.status ? { status: args.status } : {}), }, - }); - usersMap = new Map(users.map((user) => [user.id, user])); - } - - const lancamentosComUsuarios = lancamentos.map((lancamento) => { - const userIds = lancamento.auth_usersIds - ? lancamento.auth_usersIds.split(',').map(Number) - : []; - const autorizadopor: number[] = userIds - .map((id) => usersMap.get(id)) - .filter((user) => user !== undefined); - return { - ...lancamento.toItfLancamento(), - autorizadopor: autorizadopor, - } as ItfLancamento; - }); - - if (authorized === 1) { - this.logger.debug('AUTHORIZED 1'); - return lancamentosComUsuarios.filter( - (lancamento) => lancamento.autorizadopor.length === 2, - ); - } + relations: ['autorizacoes'] as (keyof ILancamento)[], + }, + args?.detalheA && { detalheA: args.detalheA }, + ); - if (authorized === 0) { - this.logger.debug('AUTHORIZED 0'); - return lancamentosComUsuarios.filter( - (lancamento) => lancamento.autorizadopor.length < 2, - ); - } + return lancamentos; + } - return lancamentosComUsuarios; + async findByStatus(isAutorizado: boolean): Promise { + const lancamentos = await this.lancamentoRepository.findMany({ where: { is_autorizado: isAutorizado } }); + return lancamentos; } - async findByStatus(status: number | null = null): Promise { - const lancamentos = await this.lancamentoRepository.findMany({ - relations: ['user'], - }); + async getValorAutorizado(month: number, period: number, year: number) { + const [startDate, endDate] = this.getMonthDateRange(year, month, period); + const autorizados = await this.lancamentoRepository.findMany({ where: { data_lancamento: Between(startDate, endDate) } }); + const autorizadoSum = autorizados.reduce((sum, lancamento) => sum + lancamento.valor, 0); + const resp = { valor_autorizado: autorizadoSum }; + return resp; + } - const allUserIds = new Set(); - lancamentos.forEach((lancamento) => { - lancamento.auth_usersIds - ?.split(',') - .forEach((id) => allUserIds.add(Number(id))); - }); + async create(dto: LancamentoInputDto): Promise { + const lancamento = await this.validateCreate(dto); + const created = await this.lancamentoRepository.save(this.lancamentoRepository.create(lancamento)); + const getCreated = await this.lancamentoRepository.getOne({ where: { id: created.id } }); + return getCreated; + } - let usersMap = new Map(); - if (allUserIds.size > 0) { - const users = await this.usersService.findMany({ - where: { - id: In([...allUserIds]), - }, - }); - usersMap = new Map(users.map((user) => [user.id, user])); + async validateCreate(dto: LancamentoInputDto): Promise { + const favorecido = await this.clienteFavorecidoService.findOne({ where: { id: dto.id_cliente_favorecido } }); + if (!favorecido) { + throw CommonHttpException.message(`id_cliente_favorecido: Favorecido não encontrado no sistema`); } - - const lancamentosComUsuarios = lancamentos.map((lancamento) => { - const userIds = lancamento.auth_usersIds - ? lancamento.auth_usersIds.split(',').map(Number) - : []; - const autorizadopor = userIds - .map((id) => usersMap.get(id)) - .filter((user) => user); - return { - ...lancamento.toItfLancamento(), - autorizadopor, - } as ItfLancamento; - }); - - if (status === 1) { - return lancamentosComUsuarios.filter( - (lancamento) => lancamento.autorizadopor.length === 2, - ); - } else { - return lancamentosComUsuarios.filter( - (lancamento) => lancamento.autorizadopor.length !== 2, - ); + if (!validFavorecidoCpfCnpjs.includes(favorecido.cpfCnpj)) { + throw CommonHttpException.messageArgs(`id_cliente_favorecido: Favorecido não permitido para Lançamento.`, { validFavorecidos: validFavorecidoNames }); } + const lancamento = Lancamento.fromInputDto(dto); + lancamento.clienteFavorecido = new ClienteFavorecido({ id: favorecido.id }); + return lancamento; } - async getValorAutorizado( - month: number, - period: number, - year: number, - ): Promise { - const [startDate, endDate] = this.getMonthDateRange(year, month, period); + async autorizarPagamento(userId: number, lancamentoId: string, AutorizaLancamentoDto: AutorizaLancamentoDto): Promise { + const lancamento = await this.lancamentoRepository.findOne({ where: { id: parseInt(lancamentoId) } }); + if (!lancamento) { + throw new HttpException('Lançamento não encontrado.', HttpStatus.NOT_FOUND); + } - const response = await this.lancamentoRepository.findMany({ - where: { - data_lancamento: Between(startDate, endDate), - }, - }); + if (lancamento.status !== LancamentoStatus._1_criado) { + throw new HttpException(`Apenas lançamentos com status '${LancamentoStatus._1_criado}' podem ser aprovados. Status encontrado: '${lancamento.status}'.)`, HttpStatus.PRECONDITION_FAILED); + } - const filteredResponse = response.filter( - (item) => item.auth_usersIds && item.auth_usersIds.split(',').length >= 2, - ); - this.logger.debug(`filteredResponse: ${filteredResponse}`); + const user = await this.usersService.findOne({ id: userId }); + if (!user) { + throw new HttpException('Usuário não encontrado', HttpStatus.UNAUTHORIZED); + } - const sumOfValues = filteredResponse.reduce( - (acc, curr) => acc + curr.valor, - 0, - ); + if (lancamento.hasAutorizadoPor(user.id)) { + throw new HttpException('Usuário já autorizou este Lançamento', HttpStatus.PRECONDITION_FAILED); + } - const formattedSum = sumOfValues.toLocaleString('pt-BR', { - style: 'currency', - currency: 'BRL', - }); + const isValidPassword = await bcrypt.compare(AutorizaLancamentoDto.password, user.password); + if (!isValidPassword) { + throw new HttpException('Senha inválida', HttpStatus.UNAUTHORIZED); + } - const resp = { - valor_autorizado: String(formattedSum), - }; + lancamento.addAutorizado(userId); - return resp; + return await this.lancamentoRepository.save(lancamento); } - async create( - lancamentoData: LancamentoDto, - userId: number, - ): Promise { - const newLancamento = this.lancamentoRepository.create(lancamentoData); - newLancamento.userId = userId; - return await this.lancamentoRepository.save(newLancamento); + updateRaw(set: DeepPartial, where: UpdateLancamentoWhere): Promise { + return this.lancamentoRepository.updateRaw(set, where); } - async autorizarPagamento( - userId: number, - lancamentoId: string, - AutorizaLancamentoDto: AutorizaLancamentoDto, - ): Promise { - const user = await this.usersService.findOne({ - id: userId, - }); - - if (!user) - throw new HttpException( - 'Usuário não encontrado', - HttpStatus.UNAUTHORIZED, - ); - - const isValidPassword = await bcrypt.compare( - AutorizaLancamentoDto.password, - user.password, - ); - - if (!isValidPassword) - throw new HttpException('Senha inválida', HttpStatus.UNAUTHORIZED); - - const lancamento = await this.lancamentoRepository.findOne({ - where: { id: parseInt(lancamentoId) }, - }); - - if (!lancamento) { - throw new Error('Lançamento não encontrado.'); - } - - const userIds = new Set( - lancamento.auth_usersIds - ? lancamento.auth_usersIds.split(',').map(Number) - : [], - ); - userIds.add(userId); - - lancamento.auth_usersIds = Array.from(userIds).join(','); - return (await this.lancamentoRepository.save(lancamento)).toItfLancamento(); + async updateDto(id: number, updateDto: LancamentoInputDto): Promise { + const lancamento = await this.validateUpdateDto(id, updateDto); + lancamento.updateFromInputDto(updateDto); + await this.lancamentoRepository.save(lancamento); + const updated = await this.lancamentoRepository.getOne({ where: { id: lancamento.id } }); + this.logger.log(`Lancamento #${updated.id} atualizado por ${updated.clienteFavorecido.nome}.`); + return updated; } - async update( - id: number, - updatedData: LancamentoDto, - userId: number, - ): Promise { - let lancamento = await this.lancamentoRepository.findOne({ where: { id } }); + async validateUpdateDto(id: number, updateDto: LancamentoInputDto): Promise { + const lancamento = await this.lancamentoRepository.findOne({ where: { id } }); if (!lancamento) { throw new NotFoundException(`Lançamento com ID ${id} não encontrado.`); } + if (lancamento.status !== LancamentoStatus._1_criado) { + throw new HttpException('Apenas é permitido alterar Lançamentos com status criado.', HttpStatus.NOT_ACCEPTABLE); + } + const favorecido = await this.clienteFavorecidoService.findOne({ where: { id: updateDto.id_cliente_favorecido } }); + if (!favorecido) { + throw CommonHttpException.message('id_cliente_favorecido: Favorecido não encontrado no sistema'); + } + if (!validFavorecidoNames.includes(favorecido.nome)) { + throw CommonHttpException.messageArgs('id_cliente_favorecido: Favorecido não permitido para Lançamento.', { validFavorecidos: validFavorecidoNames }); + } + return lancamento; + } - const { id_cliente_favorecido, ...restUpdatedData } = updatedData; - lancamento = new LancamentoEntity({ - ...lancamento, - ...restUpdatedData, - userId, - auth_usersIds: '', - }); - - await this.lancamentoRepository.save(lancamento); - this.logger.log(`Lancamento ${id_cliente_favorecido} atualizado`); - - return lancamento.toItfLancamento(); + async updateManyRaw(dtos: DeepPartial[], fields: (keyof ILancamento)[], queryRunner: QueryRunner): Promise { + if (!dtos.length) { + return []; + } + const id: keyof ILancamento = 'id'; + const updatedAt: keyof ILancamento = 'updatedAt'; + const query = EntityHelper.getQueryUpdate('lancamento', dtos, fields, Lancamento.sqlFieldTypes, id, updatedAt); + await queryRunner.manager.query(compactQuery(query)); + return dtos.map((dto) => new Lancamento(dto)); } - async getById(id: number): Promise { - const lancamento = await this.lancamentoRepository.findOne({ - where: { id }, - }); + async getById(id: number): Promise { + const lancamento = await this.lancamentoRepository.findOne({ where: { id } }); if (!lancamento) { throw new NotFoundException(`Lançamento com ID ${id} não encontrado.`); } - return lancamento.toItfLancamento(); + return lancamento; } - async delete(id: number): Promise { - await this.lancamentoRepository.delete(id); + async deleteId(id: number): Promise { + const toDelete = await this.lancamentoRepository.findOne({ where: { id } }); + if (!toDelete) { + throw new HttpException('Lançamento a ser deletado não existe.', HttpStatus.NOT_FOUND); + } + if (toDelete.status !== LancamentoStatus._1_criado) { + throw new HttpException('Apenas é permitido deletar Lançamentos com status criado.', HttpStatus.NOT_ACCEPTABLE); + } + await this.lancamentoRepository.softDelete(id); } getMonthDateRange(year: number, month: number, period: number): [Date, Date] { @@ -316,12 +223,10 @@ export class LancamentoService { if (period === 1) { startDate = new Date(year, month - 1, 1); - endDate = new Date(year, month - 1, 15); - endDate.setHours(23, 59, 59); + endDate = endOfDay(new Date(year, month - 1, 15)); } else if (period === 2) { startDate = new Date(year, month - 1, 16); - endDate = new Date(year, month, 0); - endDate.setHours(23, 59, 59); + endDate = endOfMonth(new Date(year, month, 0)); } else { throw new Error('Invalid period. Period should be 1 or 2.'); } diff --git a/src/relatorio/dtos/relatorio-sintetico.dto.ts b/src/relatorio/dtos/relatorio-sintetico.dto.ts index 90194ad7..f8c398ae 100644 --- a/src/relatorio/dtos/relatorio-sintetico.dto.ts +++ b/src/relatorio/dtos/relatorio-sintetico.dto.ts @@ -1,5 +1,4 @@ import { Exclude } from 'class-transformer'; -import { SetValue } from 'src/utils/decorators/set-value.decorator'; import { DeepPartial } from 'typeorm'; export class RelatorioSinteticoDto { diff --git a/src/relatorio/relatorio-consolidado.repository.ts b/src/relatorio/relatorio-consolidado.repository.ts index a9a955db..2b5e8890 100644 --- a/src/relatorio/relatorio-consolidado.repository.ts +++ b/src/relatorio/relatorio-consolidado.repository.ts @@ -4,8 +4,7 @@ import { DataSource } from 'typeorm'; import { RelatorioConsolidadoDto } from './dtos/relatorio-consolidado.dto'; import { IFindPublicacaoRelatorio } from './interfaces/find-publicacao-relatorio.interface'; import { CustomLogger } from 'src/utils/custom-logger'; -import { RelatorioAnaliticoDto } from './dtos/relatorio-analitico.dto'; -import { RelatorioSinteticoDto } from './dtos/relatorio-sintetico.dto'; + @Injectable() export class RelatorioConsolidadoRepository { @@ -29,7 +28,7 @@ export class RelatorioConsolidadoRepository { inner join item_transacao it on ita.id = it."itemTransacaoAgrupadoId" inner join arquivo_publicacao ap on ap."itemTransacaoId"=it.id inner join cliente_favorecido cf on cf.id=it."clienteFavorecidoId" - WHERE (1=1) `; + WHERE ita."id" not in(select tv."itemTransacaoAgrupadoId" from transacao_view tv) `; if(dataInicio!==undefined && dataFim!==undefined && (dataFim === dataInicio || new Date(dataFim)>new Date(dataInicio))) @@ -69,7 +68,7 @@ export class RelatorioConsolidadoRepository { inner join item_transacao it on ita.id = it."itemTransacaoAgrupadoId" inner join arquivo_publicacao ap on ap."itemTransacaoId"=it.id inner join cliente_favorecido cf on cf.id=it."clienteFavorecidoId" - WHERE (1=1) `; + WHERE ita."id" not in(select tv."itemTransacaoAgrupadoId" from transacao_view tv) `; if(dataInicio!==undefined && dataFim!==undefined && (dataFim === dataInicio || new Date(dataFim)>new Date(dataInicio))) @@ -167,7 +166,7 @@ export class RelatorioConsolidadoRepository { (dataFim === dataInicio || new Date(dataFim)>new Date(dataInicio))) query = query + ` and ita."dataOrdem" between '${dataInicio}' and '${dataFim}'`; - query = query +` and (ap."isPago"=false or ap."isPago" is null) `; + query = query +` and ap."isPago" is null `; if(favorecidoNome!==undefined && !(['Todos'].some(i=>favorecidoNome?.includes(i)))) query = query +` and cf.nome in('${favorecidoNome?.join("','")}')`; @@ -201,12 +200,13 @@ export class RelatorioConsolidadoRepository { inner join item_transacao it on ita.id = it."itemTransacaoAgrupadoId" inner join arquivo_publicacao ap on ap."itemTransacaoId"=it.id inner join cliente_favorecido cf on cf.id=it."clienteFavorecidoId" - WHERE ita."nomeConsorcio" in('STPC','STPL') `; + WHERE ita."id" not in(select tv."itemTransacaoAgrupadoId" from transacao_view tv) and + ita."nomeConsorcio" in('STPC','STPL') `; if(dataInicio!==undefined && dataFim!==undefined && (dataFim === dataInicio || new Date(dataFim)>new Date(dataInicio))) query = query +` and da."dataVencimento" between '${dataInicio}' and '${dataFim}'`; - query = query + ` and ap."isPago"=false `; + query = query + ` and ap."isPago" is null `; query = query + ` and da."ocorrenciasCnab" is null `; @@ -238,7 +238,9 @@ export class RelatorioConsolidadoRepository { tv."valorTransacao" AS valor_agrupado from transacao_view tv inner join cliente_favorecido cf on cf."cpfCnpj" =tv."operadoraCpfCnpj" - WHERE tv."nomeConsorcio" in('STPC','STPL') `; + WHERE tv."nomeConsorcio" in('STPC','STPL') + and tv."valorTransacao" >0 and + tv."itemTransacaoAgrupadoId" is null `; query = query +` and tv."datetimeTransacao" between '${dataInicio+' 00:00:00'}' and '${dataFim+' 23:59:59'}' `; diff --git a/src/relatorio/relatorio-sintetico.repository.ts b/src/relatorio/relatorio-sintetico.repository.ts index 9f463781..25174382 100644 --- a/src/relatorio/relatorio-sintetico.repository.ts +++ b/src/relatorio/relatorio-sintetico.repository.ts @@ -16,13 +16,39 @@ export class RelatorioSinteticoRepository { private getQuery(args:IFindPublicacaoRelatorio){ const dataInicio = args.dataInicio.toISOString().slice(0,10) const dataFim = args.dataFim.toISOString().slice(0,10) - let query = ` select distinct res.* from ( `; + let query = ` select distinct res.*, + (select sum(dta."valorLancamento")::float valor + from detalhe_a dta + inner join item_transacao_agrupado tt on dta."itemTransacaoAgrupadoId"=tt.id + left join item_transacao itt on itt."itemTransacaoAgrupadoId" = tt."id" + left join arquivo_publicacao app on app."itemTransacaoId"=itt.id + WHERE (1=1) `; + if(dataInicio!==undefined && dataFim!==undefined && + (dataFim === dataInicio || new Date(dataFim)>new Date(dataInicio))) + query = query + ` and dta."dataVencimento" between '${dataInicio}' and '${dataFim}'`; + if(args.pago !==undefined) + query = query +` and app."isPago"=${args.pago} `; + + query = query + ` and tt."nomeConsorcio"=res.consorcio `; + query = query + ` ) as subTotal `; + + query = query + ` from ( `; if(args.aPagar === undefined || args.aPagar === false){ query = query + ` - select distinct - case - when (it."nomeConsorcio" = 'VLT') THEN (da."dataVencimento" - INTERVAL '2 day')::varchar - else '' end as datatransacao, + select distinct + it.id, + case + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=1 THEN --segunda + (da."dataVencimento":: Date - INTERVAL '4 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=2 THEN --terça + (da."dataVencimento":: Date - INTERVAL '4 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=3 THEN --quarta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=4 THEN --quinta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=5 THEN --Sexta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + end as datatransacao, da."dataVencimento"::date::Varchar As datapagamento, it."nomeConsorcio" AS consorcio, cf.nome AS favorecido, @@ -30,7 +56,8 @@ export class RelatorioSinteticoRepository { case when (ap."isPago") then 'pago' when (not (ap."isPago")) then 'naopago' else 'apagar' end AS status, - case when (not (ap."isPago")) then oc."message" else '' end As mensagem_status + case when (not (ap."isPago")) then oc."message" else '' end As mensagem_status + from transacao_view tv inner join item_transacao_agrupado ita on tv."itemTransacaoAgrupadoId"=ita.id inner join detalhe_a da on da."itemTransacaoAgrupadoId"= ita.id @@ -63,9 +90,19 @@ export class RelatorioSinteticoRepository { query = query +` select distinct - case - when (it."nomeConsorcio" = 'VLT') THEN (da."dataVencimento" - INTERVAL '2 day')::varchar - else '' end as datatransacao, + it.id, + case + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=1 THEN --segunda + (da."dataVencimento":: Date - INTERVAL '4 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=2 THEN --terça + (da."dataVencimento":: Date - INTERVAL '4 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=3 THEN --quarta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=4 THEN --quinta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + when (it."nomeConsorcio" = 'VLT') and EXTRACT( DOW FROM da."dataVencimento")=5 THEN --Sexta + (da."dataVencimento":: Date - INTERVAL '2 day')::varchar + end as datatransacao, da."dataVencimento"::date::Varchar As datapagamento, it."nomeConsorcio" AS consorcio, cf.nome AS favorecido, @@ -73,7 +110,7 @@ export class RelatorioSinteticoRepository { case when (ap."isPago") then 'pago' when (not (ap."isPago")) then 'naopago' else 'apagar' end AS status, - case when (not (ap."isPago")) then oc."message" else '' end As mensagem_status + case when (not (ap."isPago")) then oc."message" else '' end As mensagem_status from item_transacao_agrupado ita inner join detalhe_a da on da."itemTransacaoAgrupadoId"= ita.id inner join item_transacao it on ita.id = it."itemTransacaoAgrupadoId" @@ -108,6 +145,7 @@ export class RelatorioSinteticoRepository { if(args.aPagar==true || (args.aPagar === undefined && args.pago === undefined)){ query = query +` select distinct + tv.id, (tv."datetimeTransacao":: Date)::Varchar As datatransacao, case when (tv."nomeConsorcio" = 'VLT') THEN (tv."datetimeTransacao":: Date + INTERVAL '2 day')::varchar @@ -123,7 +161,8 @@ export class RelatorioSinteticoRepository { cf.nome AS favorecido, round(tv."valorPago",2)::float as valor, 'a pagar' AS status, - '' As mensagem_status + '' As mensagem_status + from transacao_view tv inner join cliente_favorecido cf on tv."operadoraCpfCnpj"=cf."cpfCnpj" where tv."valorPago" > 0 `; @@ -145,14 +184,13 @@ export class RelatorioSinteticoRepository { } query = query + ` ) as res - order by res."datapagamento", res."consorcio", res."favorecido"`; + order by res."consorcio", res."favorecido",res."datapagamento" `; this.logger.debug(query); return query; } - public async findSintetico(args: IFindPublicacaoRelatorio): Promise { - + public async findSintetico(args: IFindPublicacaoRelatorio): Promise { const query = this.getQuery(args); this.logger.debug(query); const queryRunner = this.dataSource.createQueryRunner(); diff --git a/src/roles/roles.enum.ts b/src/roles/roles.enum.ts index 10fb9b93..2e860c5b 100644 --- a/src/roles/roles.enum.ts +++ b/src/roles/roles.enum.ts @@ -1,5 +1,6 @@ export enum RoleEnum { 'master' = 0, + /** Admin vanzeiros */ 'admin' = 1, 'user' = 2, 'lancador_financeiro' = 3, diff --git a/src/ticket-revenues/dtos/ticket-revenue.dto.ts b/src/ticket-revenues/dtos/ticket-revenue.dto.ts index 9c5558b6..1a5d7231 100644 --- a/src/ticket-revenues/dtos/ticket-revenue.dto.ts +++ b/src/ticket-revenues/dtos/ticket-revenue.dto.ts @@ -1,8 +1,7 @@ // @Exclude({ toPlainOnly: true }) import { Exclude } from 'class-transformer'; -import { ArquivoPublicacao } from 'src/cnab/entity/arquivo-publicacao.entity'; -import { ItemTransacaoAgrupado } from 'src/cnab/entity/pagamento/item-transacao-agrupado.entity'; +import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; /** * Internal representation of `IBqApiTicketRevenues` @@ -15,14 +14,17 @@ export class TicketRevenueDTO { constructor(dto?: TicketRevenueDTO) { if (dto) { Object.assign(this, dto); - this.isPago = Boolean(this.arquivoPublicacao?.isPago || this.isPago || this.paidValue === 0); + this.isPago = Boolean(this.isPago); + if (this.ocorrencias.length) { + this.ocorrencias = this.ocorrencias.map((o) => new Ocorrencia(o)); + } } } /** * Para o frontend exibir o número de passagens arrecadadas - individual é sempre 1 */ - count: number; + count: number = 1; /** * Represents `data` @@ -73,7 +75,7 @@ export class TicketRevenueDTO { * @description Timestamp de captura em GMT-3 (formato YYYY-MM-dd HH:mm:ssTZD) * @example '2023-09-12 14:49:00-03:00' */ - captureDateTime: string | null; + captureDateTime: string | null = null; /** * Represents `modo` @@ -81,43 +83,7 @@ export class TicketRevenueDTO { * @description Tipo de transporte * @options 'BRT', 'Ônibus', 'Van', 'VLT' */ - transportType: string | null; - - /** - * Represents `servico` - * - * @description Nome curto da linha operada pelo veículo com variação de serviço (ex: 010, 011SN, ...) - * @example '010', '011SN' - */ - vehicleService: string | null; - - /** - * Represents `sentido` - * - * GTFS `direction_id` - * - * @description Sentido de operação do serviço (0 = ida, 1 = volta) - * @example '0', '1' - */ - directionId: number | null; - - /** - * **Important field** - * - * Represents `id_veiculo` - * - * @description Identificador único do veículo - */ - vehicleId: string | null; - - /** - * Represents `id_cliente` - * - * @description Identificador único do cliente - * @example '3' - */ - clientId: string | null; - + transportType: string | null = null; /** * **Important field** * @@ -160,95 +126,34 @@ export class TicketRevenueDTO { */ transportIntegrationType: string | null; - /** - * [WIP] **Dont use it!** Currently in progress by bigquery team - * - * Represents `id_integracao` - * - * @description Tipo da integração realizada (identificador relacionado à matriz de integração) - * @type `string | null` - */ - integrationId: string | null; - - /** - * **Important field** - * - * Represents `latitude` - * - * @description Latitude da transação (WGS84) - * @type `float | null` - */ - transactionLat: number | null; - - /** - * **Important field** - * - * Represents `longitude` - * - * @description Longitude da transação (WGS84) - * @type `float | null` - */ - transactionLon: number | null; - - /** - * **Important field** - * - * Represents `stop_id` - * - * @description Código identificador do ponto de embarque (GTFS) - * @type `float | null` - */ - stopId: number | null; - - /** - * **Important field** - * - * Represents `stop_lat` - * - * @description Latitude do ponto de embarque (GTFS) - * @type `float | null` - */ - stopLat: number | null; - - /** - * **Important field** - * - * Represents `stop_lon` - * - * @description Longitude do ponto de embarque (GTFS) - * @type `float | null` - */ - stopLon: number | null; - - /** - * Valor bruto. - * - * Represents `valor_transacao` - * - * @description Valor debitado na transação atual (R$) - * @type `float | null` - */ + /** Valor bruto debitado na transação atual (R$) */ transactionValue: number | null; - /** Valor a ser pago - valor líquido calculado - * Não significa que foi pago + /** + * Valor a ser pago - valor líquido calculado. + * + * Se não houve pagamento o valor também é zero. */ paidValue: number; - /** - * Represents `versao` - * - * @description Código de controle de versão do dado (SHA Github) - * @example - */ - @Exclude() - bqDataVersion: string | null; - + /** arquivoPublicacao.isPago */ isPago = false; + /** arquivoPublicacao.dataEfetivacao */ + dataEfetivacao: Date; + + /** DetalheA.ocorrenciasCnab */ @Exclude() - arquivoPublicacao?: ArquivoPublicacao; + ocorrenciasCnab?: string; + /** DetalheA->Ocorrencias */ @Exclude() - itemTransacaoAgrupadoId?: number; + ocorrencias: Ocorrencia[] = []; + + /** + * Apenas soma se status = pago + */ + public static getAmountSum(data: T[]): number { + return +data.reduce((sum, i) => sum + (i.transactionValue || 0), 0).toFixed(2); + } } diff --git a/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts b/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts index 38c7c2fa..a049002d 100644 --- a/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts +++ b/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts @@ -2,7 +2,7 @@ import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; import { SetValue } from 'src/utils/decorators/set-value.decorator'; import { DeepPartial } from 'typeorm'; import { ITRCounts } from '../interfaces/tr-counts.interface'; -import { SetValueIf } from 'src/utils/decorators/set-value-if.decorator'; +import { TicketRevenueDTO } from './ticket-revenue.dto'; /** * This object represents a group of `IBqTicketRevenues` @@ -149,13 +149,74 @@ export class TicketRevenuesGroupDto { isPago = false; - /** - * CNAB retorno error message list. - */ + /** CNAB retorno error message list. */ @SetValue((v) => Ocorrencia.toUserValues(v)) errors: Ocorrencia[] = []; getIsEmpty() { return !this.count; } + + appendItem(newItem: TicketRevenueDTO) { + this.count += 1; + if (newItem.transactionValue) { + const value = this.transactionValueSum + newItem.transactionValue; + this.transactionValueSum = Number(value.toFixed(2)); + } + this.paidValueSum += newItem.paidValue; + const errors = Ocorrencia.getErrors(newItem.ocorrencias); + + for (const [_groupKey, groupValue] of Object.entries(this)) { + const groupKey = _groupKey as keyof TicketRevenuesGroupDto; + if (groupKey === 'isPago') { + if (!newItem?.isPago) { + this[groupKey] = false; + } + } else if (groupKey === 'errors') { + if (!newItem?.isPago) { + this[groupKey] = Ocorrencia.joinUniqueCode(this[groupKey], errors); + } + } else if (TicketRevenuesGroupDto.COUNT_PROPS.includes(groupKey)) { + const itemKey = groupKey.replace('Counts', '') as keyof TicketRevenueDTO; + this.appendCountGroup(groupKey, newItem, itemKey); + } else if (typeof groupValue === 'string') { + this[_groupKey] = newItem[groupKey]; + } + } + } + + appendCountGroup(groupKey: keyof TicketRevenuesGroupDto, newItem: TicketRevenueDTO, itemKey: string) { + const IGNORE_NULL_UNDEFINED = true; + const countsKey = newItem[itemKey]; + if ((countsKey !== null && countsKey !== undefined) || !IGNORE_NULL_UNDEFINED) { + const oldItem = this[groupKey][countsKey] as ITRCounts; + if (oldItem === undefined) { + (this[groupKey][countsKey] as ITRCounts) = { + count: 1, + transactionValue: newItem?.transactionValue || 0, + }; + } else { + (this[groupKey][countsKey] as ITRCounts) = { + count: oldItem.count + 1, + transactionValue: Number((oldItem.transactionValue + (newItem?.transactionValue || 0)).toFixed(2)), + }; + } + } + } + + public static COUNT_PROPS: (keyof TicketRevenuesGroupDto)[] = [ + 'transportTypeCounts', // + 'directionIdCounts', + 'paymentMediaTypeCounts', + 'transactionTypeCounts', + 'transportIntegrationTypeCounts', + 'stopIdCounts', + 'stopLatCounts', + 'stopLonCounts', + ]; + + /** Apenas soma se status = pago */ + public static getAmountSum(data: T[]): number { + return +data.reduce((sum, i) => sum + (i.transactionValueSum || 0), 0).toFixed(2); + } } diff --git a/src/ticket-revenues/interfaces/fetch-ticket-revenues.interface.ts b/src/ticket-revenues/interfaces/fetch-ticket-revenues.interface.ts deleted file mode 100644 index 189deec1..00000000 --- a/src/ticket-revenues/interfaces/fetch-ticket-revenues.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface IFetchTicketRevenues { - cpfCnpj?: string; - startDate?: Date; - endDate?: Date; - limit?: number; - offset?: number; - getToday?: boolean; - previousDays?: boolean; -} diff --git a/src/ticket-revenues/interfaces/tr-get-me-individual-args.interface.ts b/src/ticket-revenues/interfaces/tr-get-me-individual-args.interface.ts index 994d4ded..bed594f3 100644 --- a/src/ticket-revenues/interfaces/tr-get-me-individual-args.interface.ts +++ b/src/ticket-revenues/interfaces/tr-get-me-individual-args.interface.ts @@ -15,3 +15,4 @@ export interface ITRGetMeIndividualValidArgs { endDate?: string; timeInterval?: TimeIntervalEnum; } + diff --git a/src/ticket-revenues/ticket-revenues-repository.ts b/src/ticket-revenues/ticket-revenues-repository.ts deleted file mode 100644 index b9d174e2..00000000 --- a/src/ticket-revenues/ticket-revenues-repository.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { endOfDay, isToday, startOfDay } from 'date-fns'; -import { TransacaoView } from 'src/transacao-bq/transacao-view.entity'; -import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; -import { getPagination } from 'src/utils/get-pagination'; -import { getPaymentDates } from 'src/utils/payment-date-utils'; -import { PaginationOptions } from 'src/utils/types/pagination-options'; -import { Pagination } from 'src/utils/types/pagination.type'; -import { Between, FindOptionsWhere, In } from 'typeorm'; -import { TicketRevenueDTO } from './dtos/ticket-revenue.dto'; -import { TicketRevenuesGroupDto } from './dtos/ticket-revenues-group.dto'; -import { IFetchTicketRevenues } from './interfaces/fetch-ticket-revenues.interface'; -import { ITRGetMeIndividualValidArgs } from './interfaces/tr-get-me-individual-args.interface'; -import { ITRGetMeIndividualResponse } from './interfaces/tr-get-me-individual-response.interface'; -import { ArquivoPublicacaoService } from 'src/cnab/service/arquivo-publicacao.service'; - -@Injectable() -export class TicketRevenuesRepositoryService { - private logger: Logger = new Logger('TicketRevenuesRepository', { - timestamp: true, - }); - - constructor(private readonly transacaoViewService: TransacaoViewService, private arquivoPublicacaoService: ArquivoPublicacaoService) {} - - /** - * TODO: use it only for repository services - * - * Filter: used by: - * - ticket-revenues/get/me - */ - removeTodayData(data: T[], endDate: Date): T[] { - const mostRecentDate = startOfDay(new Date(data[0].date)); - if (mostRecentDate > endOfDay(endDate)) { - return data.filter((i) => !isToday(new Date(i.date))); - } else { - return data; - } - } - - public async getMeIndividual(validArgs: ITRGetMeIndividualValidArgs, paginationArgs: PaginationOptions): Promise> { - const { startDate, endDate } = getPaymentDates({ - endpoint: 'ticket-revenues', - startDateStr: validArgs.startDate, - endDateStr: validArgs.endDate, - timeInterval: validArgs.timeInterval, - }); - - const revenues = await this.findTransacaoView(startDate, endDate, validArgs); - const paidSum = +revenues - .filter((i) => i.isPago) - .reduce((s, i) => s + i.paidValue, 0) - .toFixed(2); - const countAll = revenues.length; - let ticketRevenuesResponse = revenues; - - if (ticketRevenuesResponse.length === 0) { - return getPagination( - { - amountSum: 0, - paidSum: 0, - data: [], - }, - { - dataLenght: 0, - maxCount: 0, - }, - paginationArgs, - ); - } - - ticketRevenuesResponse = this.removeTodayData(ticketRevenuesResponse, endDate); - - return getPagination( - { - paidSum, - amountSum: this.getAmountSum(ticketRevenuesResponse), - data: ticketRevenuesResponse, - }, - { - dataLenght: ticketRevenuesResponse.length, - maxCount: countAll, - }, - paginationArgs, - ); - } - - private async findTransacaoView(startDate: Date, endDate: Date, validArgs: ITRGetMeIndividualValidArgs) { - const fetchArgs: IFetchTicketRevenues = { - cpfCnpj: validArgs.user.getCpfCnpj(), - startDate, - endDate, - getToday: true, - }; - - const betweenDate: FindOptionsWhere = { - datetimeProcessamento: Between(fetchArgs.startDate as Date, fetchArgs.endDate as Date), - }; - const where: FindOptionsWhere[] = [ - { - ...betweenDate, - operadoraCpfCnpj: validArgs.user.getCpfCnpj(), - }, - { - ...betweenDate, - consorcioCnpj: validArgs.user.getCpfCnpj(), - }, - ]; - const today = new Date(); - if (fetchArgs.getToday) { - const isTodayDate: FindOptionsWhere = { - datetimeProcessamento: Between(startOfDay(today), endOfDay(today)), - }; - where.push({ - ...isTodayDate, - operadoraCpfCnpj: validArgs.user.getCpfCnpj(), - }); - where.push({ - ...isTodayDate, - consorcioCnpj: validArgs.user.getCpfCnpj(), - }); - } - - const transacaoViews = await this.transacaoViewService.find(where); - const publicacoes = await this.arquivoPublicacaoService.findMany({ where: { itemTransacao: { itemTransacaoAgrupado: { id: In(transacaoViews.map((t) => t.itemTransacaoAgrupadoId)) } } } }); - const revenues = transacaoViews.map((i) => i.toTicketRevenue(publicacoes)); - return revenues; - } - - /** - * Apenas soma se status = pago - */ - getAmountSum(data: T[]): number { - return +data.reduce((sum, i) => sum + this.getTransactionValue(i), 0).toFixed(2); - } - - private getTransactionValue(item: TicketRevenueDTO | TicketRevenuesGroupDto): number { - if ('transactionValue' in item) { - return item.transactionValue || 0; - } else if ('transactionValueSum' in item) { - return item.transactionValueSum || 0; - } else { - return 0; - } - } -} diff --git a/src/ticket-revenues/ticket-revenues.module.ts b/src/ticket-revenues/ticket-revenues.module.ts index 77e5a933..39c0079b 100644 --- a/src/ticket-revenues/ticket-revenues.module.ts +++ b/src/ticket-revenues/ticket-revenues.module.ts @@ -2,9 +2,9 @@ import { Module } from '@nestjs/common'; import { BigqueryModule } from 'src/bigquery/bigquery.module'; import { CnabModule } from 'src/cnab/cnab.module'; import { SettingsModule } from 'src/settings/settings.module'; -import { TransacaoViewModule } from 'src/transacao-bq/transacao-view.module'; +import { TransacaoViewModule } from 'src/transacao-view/transacao-view.module'; import { UsersModule } from 'src/users/users.module'; -import { TicketRevenuesRepositoryService } from './ticket-revenues-repository'; +import { TicketRevenuesRepositoryService } from './ticket-revenues.repository'; import { TicketRevenuesController } from './ticket-revenues.controller'; import { TicketRevenuesService } from './ticket-revenues.service'; diff --git a/src/ticket-revenues/ticket-revenues.repository.ts b/src/ticket-revenues/ticket-revenues.repository.ts new file mode 100644 index 00000000..6cf154de --- /dev/null +++ b/src/ticket-revenues/ticket-revenues.repository.ts @@ -0,0 +1,201 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { endOfDay, isToday, startOfDay } from 'date-fns'; +import { ArquivoPublicacaoService } from 'src/cnab/service/arquivo-publicacao.service'; +import { TransacaoView } from 'src/transacao-view/transacao-view.entity'; +import { TransacaoViewService } from 'src/transacao-view/transacao-view.service'; +import { compactQuery } from 'src/utils/console-utils'; +import { formatDateYMD } from 'src/utils/date-utils'; +import { getPagination } from 'src/utils/get-pagination'; +import { getPaymentDates } from 'src/utils/payment-date-utils'; +import { PaginationOptions } from 'src/utils/types/pagination-options'; +import { Pagination } from 'src/utils/types/pagination.type'; +import { DataSource } from 'typeorm'; +import { TicketRevenueDTO } from './dtos/ticket-revenue.dto'; +import { TicketRevenuesGroupDto } from './dtos/ticket-revenues-group.dto'; +import { ITRGetMeIndividualValidArgs } from './interfaces/tr-get-me-individual-args.interface'; +import { ITRGetMeIndividualResponse } from './interfaces/tr-get-me-individual-response.interface'; + +export interface TicketRevenuesIndividualOptions { + where: { + args?: { + startDate: Date; + endDate: Date; + validArgs: ITRGetMeIndividualValidArgs; + }; + transacaoView?: { + idTransacao?: string[]; + operadoraCpfCnpj?: string[]; + datetimeTransacao?: { between: [Date, Date][] }; + datetimeProcessamento?: { between: [Date, Date][] }; + /** Se a TransacaoView é apenas de dias anteriores */ + isPreviousDays?: boolean; + }; + }; + order?: { + datetimeProcessamento?: 'ASC' | 'DESC'; + id?: 'ASC' | 'DESC'; + }; + limit?: number; + offset?: number; +} + +@Injectable() +export class TicketRevenuesRepositoryService { + private logger: Logger = new Logger('TicketRevenuesRepository', { timestamp: true }); + + /** + * Regra de negócio: + * + * - Buscar todas as catracadas (transacao_view) no intervalo de data escolhido + * - transacao_view chama arquivo_publicacao via item_transacao_agrupado + * - Sabemos que a catracada foi paga através de `arquivo_publicacao.isPago` + * + * Observações: + * + * - Se `publicacao.isPago = true` mas `detalhe_a.ocorrenciasCnab` contém erro (ou vice-versa), os dados do banco estão inconsistentes. + */ + + constructor( + private readonly transacaoViewService: TransacaoViewService, + private arquivoPublicacaoService: ArquivoPublicacaoService, + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + /** + * TODO: use it only for repository services + * + * Filter: used by: + * - ticket-revenues/get/me + */ + removeTodayData(data: T[], endDate: Date): T[] { + const mostRecentDate = startOfDay(new Date(data[0].date)); + if (mostRecentDate > endOfDay(endDate)) { + return data.filter((i) => !isToday(new Date(i.date))); + } else { + return data; + } + } + + public async getMeIndividual(validArgs: ITRGetMeIndividualValidArgs, paginationArgs: PaginationOptions): Promise> { + const { startDate, endDate } = getPaymentDates({ + endpoint: 'ticket-revenues', + startDateStr: validArgs.startDate, + endDateStr: validArgs.endDate, + timeInterval: validArgs.timeInterval, + }); + + const revenues = await this.findManyIndividual({ where: { args: { startDate, endDate, validArgs } } }); + const paidSum = +revenues + .filter((i) => i.isPago) + .reduce((s, i) => s + i.paidValue, 0) + .toFixed(2); + const countAll = revenues.length; + + if (revenues.length === 0) { + return getPagination( + { + amountSum: 0, + paidSum: 0, + data: [], + }, + { + dataLenght: 0, + maxCount: 0, + }, + paginationArgs, + ); + } + + return getPagination( + { + paidSum, + amountSum: TicketRevenueDTO.getAmountSum(revenues), + data: revenues, + }, + { + dataLenght: revenues.length, + maxCount: countAll, + }, + paginationArgs, + ); + } + + async findManyIndividual(options: TicketRevenuesIndividualOptions): Promise { + const { args, transacaoView } = options.where; + if (!args && !transacaoView) { + throw new Error('É obrigatório filtrar o TransacaoView em TicketRevenueDto, para tornar a consulta rápida.'); + } + + const whereTransacaoView: string[] = []; + const tv1 = TransacaoView.getSqlFields('tv1'); + if (args) { + whereTransacaoView.push(`DATE(${tv1.datetimeProcessamento}) BETWEEN '${formatDateYMD(args.startDate)}' AND '${formatDateYMD(args.endDate)}'`); + if (args.validArgs.user.cpfCnpj) { + whereTransacaoView.push(`${tv1.operadoraCpfCnpj} = '${options.where.args?.validArgs.user.cpfCnpj}'`); + } + } + if (transacaoView) { + if (transacaoView?.idTransacao?.length) { + whereTransacaoView.push(`${tv1.idTransacao} IN ('${transacaoView?.idTransacao.join("','")}')`); + } + if (transacaoView?.operadoraCpfCnpj?.length) { + whereTransacaoView.push(`${tv1.operadoraCpfCnpj} IN ('${transacaoView?.operadoraCpfCnpj.join("','")}')`); + } + if (transacaoView?.datetimeTransacao) { + const betweenStr = transacaoView.datetimeTransacao.between.map(([start, end]) => `${tv1.datetimeTransacao}::DATE BETWEEN '${formatDateYMD(start)}' AND '${formatDateYMD(end)}'`).join(' OR '); + whereTransacaoView.push(`${betweenStr}`); + } + if (transacaoView?.datetimeProcessamento) { + const betweenStr = transacaoView.datetimeProcessamento.between.map(([start, end]) => `${tv1.datetimeProcessamento}::DATE BETWEEN '${formatDateYMD(start)}' AND '${formatDateYMD(end)}'`).join(' OR '); + whereTransacaoView.push(`${betweenStr}`); + } + if (transacaoView.isPreviousDays) { + whereTransacaoView.push(`${tv1.datetimeProcessamento}::DATE > ${tv1.datetimeTransacao}::DATE`); + } + } + + const tv = TransacaoView.getSqlFields('tv'); + const order: string[] = []; + if (options?.order?.datetimeProcessamento) { + order.push(`${tv1.datetimeProcessamento} ${options.order.datetimeProcessamento}`); + } + if (options?.order?.id) { + order.push(`${tv1.id} ${options.order.id}`); + } + + let query = ` + SELECT + 1 AS "count", + tv.id, + MIN(ita.id) AS ita_id, da.id AS da_id, + MIN(tv."datetimeProcessamento") AS "date", + EXTRACT(HOUR FROM MIN(tv."datetimeProcessamento"))::INT AS "processingHour", + MIN(tv."datetimeTransacao")::VARCHAR AS "transactionDateTime", + MIN(tv."datetimeProcessamento")::VARCHAR AS "processingDateTime", + MIN(tv."datetimeCaptura")::VARCHAR AS "captureDateTime", + MIN(tv."idTransacao") AS "transactionId", + MIN(tv."tipoPagamento") AS "paymentMediaType", + MIN(tv."tipoTransacao") AS "transactionType", + MIN(tv."valorTransacao")::FLOAT AS "transactionValue", + CASE WHEN MIN(tv."valorPago") IS NOT NULL THEN MIN(tv."valorPago")::FLOAT ELSE 0 END AS "paidValue", + BOOL_AND(ap."isPago") AS "isPago", + MIN(ap."dataEfetivacao") AS "dataEfetivacao", + CASE WHEN COUNT(o.id) > 0 THEN json_agg(json_build_object('id', o.id, 'code', o.code, 'message', o.message, 'detalheAId', o."detalheAId")) ELSE '[]' END AS ocorrencias + FROM (SELECT tv1.* FROM transacao_view tv1${whereTransacaoView ? ` WHERE (${whereTransacaoView.join(') AND (')}` : ''}) ORDER BY ${order.length ? order.join(', ') : 'tv1.id DESC'}) tv + LEFT JOIN item_transacao_agrupado ita ON ita.id = tv."itemTransacaoAgrupadoId" + LEFT JOIN detalhe_a da ON da."itemTransacaoAgrupadoId" = ita.id + LEFT JOIN ocorrencia o ON o."detalheAId" = da.id + LEFT JOIN item_transacao it ON it."itemTransacaoAgrupadoId" = ita.id + LEFT JOIN arquivo_publicacao ap ON ap."itemTransacaoId" = it.id + GROUP BY (tv.id, da.id) + `; + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + let result: any[] = await queryRunner.query(compactQuery(query)); + queryRunner.release(); + const revenueIndividuals = result.map((r) => new TicketRevenueDTO(r)); + return revenueIndividuals; + } +} diff --git a/src/ticket-revenues/ticket-revenues.service.spec.ts b/src/ticket-revenues/ticket-revenues.service.spec.ts index fa15f642..3fd203bc 100644 --- a/src/ticket-revenues/ticket-revenues.service.spec.ts +++ b/src/ticket-revenues/ticket-revenues.service.spec.ts @@ -4,7 +4,7 @@ import { BigqueryService } from 'src/bigquery/bigquery.service'; import { User } from 'src/users/entities/user.entity'; import { UsersService } from 'src/users/users.service'; import { ITicketRevenue } from './interfaces/ticket-revenue.interface'; -import { TicketRevenuesRepositoryService } from './ticket-revenues-repository'; +import { TicketRevenuesRepositoryService } from './ticket-revenues.repository'; import { TicketRevenuesService } from './ticket-revenues.service'; import { SettingsService } from 'src/settings/settings.service'; @@ -34,24 +34,12 @@ describe('TicketRevenuesService', () => { }, } as Provider; const module: TestingModule = await Test.createTestingModule({ - providers: [ - TicketRevenuesService, - TicketRevenuesRepositoryService, - usersServiceMock, - bigqueryServiceMock, - settingsServiceMock, - ], + providers: [TicketRevenuesService, TicketRevenuesRepositoryService, usersServiceMock, bigqueryServiceMock, settingsServiceMock], }).compile(); - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('2023-06-01T06:00:00.000Z').valueOf()); + jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2023-06-01T06:00:00.000Z').valueOf()); - ticketRevenuesService = module.get( - TicketRevenuesService, - ); - ticketRevenuesRepository = module.get( - TicketRevenuesRepositoryService, - ); + ticketRevenuesService = module.get(TicketRevenuesService); + ticketRevenuesRepository = module.get(TicketRevenuesRepositoryService); usersService = module.get(UsersService); }); @@ -133,21 +121,15 @@ describe('TicketRevenuesService', () => { bqDataVersion: i.toString(), }); } - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => - new Date('2023-06-03T03:30:00.000Z').valueOf(), - ); + jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2023-06-03T03:30:00.000Z').valueOf()); const user = new User(); user.id = 1; user.cpfCnpj = 'cpfCnpj_1'; jest.spyOn(usersService, 'getOne').mockResolvedValue(user); - jest - .spyOn(ticketRevenuesRepository as any, 'fetchTicketRevenues') - .mockResolvedValue({ - data: revenues, - countAll: revenues.length, - }); + jest.spyOn(ticketRevenuesRepository as any, 'fetchTicketRevenues').mockResolvedValue({ + data: revenues, + countAll: revenues.length, + }); // Act const result = await ticketRevenuesService.getMeGrouped({ @@ -157,9 +139,7 @@ describe('TicketRevenuesService', () => { }); // Assert - expect( - result.transactionTypeCounts?.['Gratuidade']['transactionValue'], - ).toEqual(0); + expect(result.transactionTypeCounts?.['Gratuidade']['transactionValue']).toEqual(0); }); it('should count and match transactionValueSum with sum of transactionType properties', /** @@ -225,21 +205,15 @@ describe('TicketRevenuesService', () => { bqDataVersion: i.toString(), }); } - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => - new Date('2023-06-03T03:30:00.000Z').valueOf(), - ); + jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2023-06-03T03:30:00.000Z').valueOf()); const user = new User(); user.id = 1; user.cpfCnpj = 'cpfCnpj_1'; jest.spyOn(usersService, 'getOne').mockResolvedValue(user); - jest - .spyOn(ticketRevenuesRepository as any, 'fetchTicketRevenues') - .mockResolvedValue({ - data: revenues, - countAll: revenues.length, - }); + jest.spyOn(ticketRevenuesRepository as any, 'fetchTicketRevenues').mockResolvedValue({ + data: revenues, + countAll: revenues.length, + }); // Act const result = await ticketRevenuesService.getMeGrouped({ @@ -249,12 +223,8 @@ describe('TicketRevenuesService', () => { }); // Assert - const transactionTypeCountsSum = Object.values( - result.transportTypeCounts, - ).reduce((sum, i) => sum + i.count, 0); - const expectedTransactionValueSum = Object.values( - result.transportTypeCounts, - ).reduce((sum, i) => sum + i.transactionValue, 0); + const transactionTypeCountsSum = Object.values(result.transportTypeCounts).reduce((sum, i) => sum + i.count, 0); + const expectedTransactionValueSum = Object.values(result.transportTypeCounts).reduce((sum, i) => sum + i.transactionValue, 0); expect(result.count).toEqual(transactionTypeCountsSum); expect(result.transactionValueSum).toEqual(expectedTransactionValueSum); }); diff --git a/src/ticket-revenues/ticket-revenues.service.ts b/src/ticket-revenues/ticket-revenues.service.ts index be711b01..b5082219 100644 --- a/src/ticket-revenues/ticket-revenues.service.ts +++ b/src/ticket-revenues/ticket-revenues.service.ts @@ -1,10 +1,6 @@ -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { endOfDay, isSameDay, isToday, nextFriday, startOfDay, subDays } from 'date-fns'; -import { DetalheA } from 'src/cnab/entity/pagamento/detalhe-a.entity'; -import { ArquivoPublicacaoService } from 'src/cnab/service/arquivo-publicacao.service'; -import { DetalheAService } from 'src/cnab/service/pagamento/detalhe-a.service'; -import { TransacaoView } from 'src/transacao-bq/transacao-view.entity'; -import { TransacaoViewService } from 'src/transacao-bq/transacao-view.service'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { isSameDay, isToday, nextFriday, startOfDay, subDays } from 'date-fns'; +import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; import { User } from 'src/users/entities/user.entity'; import { UsersService } from 'src/users/users.service'; import { CustomLogger } from 'src/utils/custom-logger'; @@ -15,32 +11,32 @@ import { logError } from 'src/utils/log-utils'; import { PAYMENT_START_WEEKDAY, PaymentEndpointType, getPaymentDates } from 'src/utils/payment-date-utils'; import { PaginationOptions } from 'src/utils/types/pagination-options'; import { Pagination } from 'src/utils/types/pagination.type'; -import { Between, FindOptionsWhere, In, MoreThan } from 'typeorm'; import { TicketRevenueDTO } from './dtos/ticket-revenue.dto'; import { TicketRevenuesGroupDto } from './dtos/ticket-revenues-group.dto'; -import { IFetchTicketRevenues } from './interfaces/fetch-ticket-revenues.interface'; import { ITRGetMeGroupedArgs } from './interfaces/tr-get-me-grouped-args.interface'; import { TRGetMeGroupedResponseDto } from './interfaces/tr-get-me-grouped-response.interface'; import { ITRGetMeIndividualArgs } from './interfaces/tr-get-me-individual-args.interface'; import { ITRGetMeIndividualResponse } from './interfaces/tr-get-me-individual-response.interface'; -import { TicketRevenuesRepositoryService as TicketRevenuesRepository } from './ticket-revenues-repository'; +import { TicketRevenuesRepositoryService as TicketRevenuesRepository } from './ticket-revenues.repository'; import { TicketRevenuesGroups } from './types/ticket-revenues-groups.type'; -import * as TicketRevenuesGroupList from './utils/ticket-revenues-groups.utils'; -import { ItemTransacaoAgrupadoService } from 'src/cnab/service/pagamento/item-transacao-agrupado.service'; + +export interface IFetchTicketRevenues { + cpfCnpj?: string[]; + startDate?: Date; + endDate?: Date; + limit?: number; + offset?: number; + getToday?: boolean; + previousDays?: boolean; +} @Injectable() export class TicketRevenuesService { - private logger: Logger = new CustomLogger(TicketRevenuesService.name, { - timestamp: true, - }); + private logger = new CustomLogger(TicketRevenuesService.name, { timestamp: true }); constructor( - private readonly usersService: UsersService, + private readonly usersService: UsersService, // private readonly ticketRevenuesRepository: TicketRevenuesRepository, - private readonly transacaoViewService: TransacaoViewService, - private readonly arquivoPublicacaoService: ArquivoPublicacaoService, - // private readonly itemTransacaoAgrupadoService: ItemTransacaoAgrupadoService, - private readonly detalheAService: DetalheAService, ) {} /** @@ -61,8 +57,8 @@ export class TicketRevenuesService { }); // Get data - const ticketRevenuesResponse: TicketRevenueDTO[] = await this.findTransacaoView({ - cpfCnpj: user.getCpfCnpj(), + const ticketRevenuesResponse: TicketRevenueDTO[] = await this.findManyIndividual({ + cpfCnpj: [user.getCpfCnpj()], startDate, endDate, }); @@ -70,12 +66,7 @@ export class TicketRevenuesService { if (ticketRevenuesResponse.length === 0) { return new TicketRevenuesGroupDto(); } - const detalhesA = await this.detalheAService.findMany({ - itemTransacaoAgrupado: { - id: In(ticketRevenuesResponse.map((i) => i.arquivoPublicacao?.itemTransacao.itemTransacaoAgrupado.id)), - }, - }); - const ticketRevenuesGroupSum = this.getGroupSum(ticketRevenuesResponse, detalhesA); + const ticketRevenuesGroupSum = this.getGroupSum(ticketRevenuesResponse); return ticketRevenuesGroupSum; } @@ -85,9 +76,6 @@ export class TicketRevenuesService { return user; } - /** - * - */ public async getMe(args: ITRGetMeGroupedArgs, pagination: PaginationOptions, endpoint: PaymentEndpointType): Promise { const METHOD = 'getMe'; // TODO: set groupBy as validation response @@ -101,8 +89,8 @@ export class TicketRevenuesService { const groupBy = args?.groupBy || 'day'; // Repository tasks - let ticketRevenuesResponse: TicketRevenueDTO[] = await this.findTransacaoView({ - cpfCnpj: user.getCpfCnpj(), + let ticketRevenuesResponse: TicketRevenueDTO[] = await this.findManyIndividual({ + cpfCnpj: [user.getCpfCnpj()], startDate, endDate, }); @@ -122,13 +110,7 @@ export class TicketRevenuesService { }); } - const itemAgrupadoIds = ticketRevenuesResponse.map((i) => i.itemTransacaoAgrupadoId); - const detalhesA = await this.detalheAService.findMany({ - itemTransacaoAgrupado: { - id: In(itemAgrupadoIds), - }, - }); - let ticketRevenuesGroups = this.getTicketRevenuesGroups(ticketRevenuesResponse, groupBy, detalhesA); + let ticketRevenuesGroups = this.getTicketRevenuesGroups(ticketRevenuesResponse, groupBy); ticketRevenuesGroups = this.fillDatesInGroups(ticketRevenuesGroups, groupBy, startDate, endDate); @@ -147,7 +129,7 @@ export class TicketRevenuesService { ticketRevenuesResponse = this.ticketRevenuesRepository.removeTodayData(ticketRevenuesResponse, endDate); ticketRevenuesGroups = this.ticketRevenuesRepository.removeTodayData(ticketRevenuesGroups, endDate); - const amountSum = this.ticketRevenuesRepository.getAmountSum(ticketRevenuesGroups); + const amountSum = TicketRevenuesGroupDto.getAmountSum(ticketRevenuesGroups); const ticketCount = ticketRevenuesGroups.reduce((sum, i) => sum + i.count, 0); @@ -186,75 +168,27 @@ export class TicketRevenuesService { return newGroups; } - public async findTransacaoView(args: IFetchTicketRevenues) { - const datetimeField: keyof TransacaoView = 'datetimeTransacao'; - const betweenDate: FindOptionsWhere = { - [datetimeField]: Between(startOfDay(args?.startDate || new Date(0)), endOfDay(args?.endDate || new Date())), - }; - const findOperadora: FindOptionsWhere = args?.cpfCnpj ? { operadoraCpfCnpj: args.cpfCnpj } : {}; - const findConsorcio: FindOptionsWhere = args?.cpfCnpj ? { consorcioCnpj: args.cpfCnpj } : {}; - const where: FindOptionsWhere[] = [ - { - ...betweenDate, - ...findOperadora, - }, - { - ...betweenDate, - ...findConsorcio, - }, - ]; + public async findManyIndividual(args: IFetchTicketRevenues): Promise { const today = new Date(); - if (args.getToday) { - const isTodayDate: FindOptionsWhere = { - [datetimeField]: Between(startOfDay(today), endOfDay(today)), - }; - where.push({ - ...isTodayDate, - ...findOperadora, - }); - where.push({ - ...isTodayDate, - ...findConsorcio, - }); - } - - let transacoes = await this.transacaoViewService.findCustom({ - where, + const revenues = await this.ticketRevenuesRepository.findManyIndividual({ + where: { + transacaoView: { + datetimeTransacao: { + between: [ + [args?.startDate || new Date(0), args?.endDate || new Date()], // + ...(args.getToday ? [[today, today] as [Date, Date]] : []), + ], + }, + ...(args?.cpfCnpj?.length ? { operadoraCpfCnpj: args.cpfCnpj } : {}), + isPreviousDays: args.previousDays, + }, + }, order: { datetimeProcessamento: 'DESC', }, ...(args?.offset ? { skip: args.offset } : {}), ...(args?.limit ? { take: args.limit } : {}), }); - - // Filtrar apenas dias anteriores (dataProcessamento > dataTransacao - dia) - if (args.previousDays) { - transacoes = transacoes.filter((i) => { - const notSameDay = !isSameDay(i.datetimeProcessamento, i.datetimeTransacao); - const processamentoGTtransacao = i.datetimeProcessamento > i.datetimeTransacao; - return notSameDay && processamentoGTtransacao; - }); - } - // const itemAgrupados = await this.itemTransacaoAgrupadoService.findMany({ - // where: { - // itemTransacao: { - // itemTransacaoAgrupado: { - // id: In(transacoes.map((t) => t.itemTransacaoAgrupadoId)), - // }, - // }, - // }, - // }); - const publicacoes = await this.arquivoPublicacaoService.findMany({ - where: { - itemTransacao: { - itemTransacaoAgrupado: { - id: In(transacoes.map((t) => t.itemTransacaoAgrupadoId)), - }, - }, - }, - }); - - const revenues = transacoes.map((i) => i.toTicketRevenue(publicacoes)); return revenues; } @@ -263,9 +197,9 @@ export class TicketRevenuesService { return user; } - private getGroupSum(data: TicketRevenueDTO[], detalhesA: DetalheA[]): TicketRevenuesGroupDto { + private getGroupSum(data: TicketRevenueDTO[]): TicketRevenuesGroupDto { const METHOD = this.getGroupSum.name; - const groupSums = this.getTicketRevenuesGroups(data, 'all', detalhesA); + const groupSums = this.getTicketRevenuesGroups(data, 'all'); if (groupSums.length >= 1) { if (groupSums.length > 1) { logError(this.logger, 'ticketRevenuesGroupSum should have 0-1 items, getting first one.', METHOD); @@ -288,16 +222,15 @@ export class TicketRevenuesService { * * Filter method: ticket-revenues/me */ - private getTicketRevenuesGroups(ticketRevenues: TicketRevenueDTO[], groupBy: 'day' | 'week' | 'month' | 'all' | string, detalhesA: DetalheA[]): TicketRevenuesGroupDto[] { - const result = ticketRevenues.reduce((group: TicketRevenuesGroups, item: TicketRevenueDTO) => { + private getTicketRevenuesGroups(ticketRevenues: TicketRevenueDTO[], groupBy: 'day' | 'week' | 'month' | 'all' | string): TicketRevenuesGroupDto[] { + const result = ticketRevenues.reduce((group: TicketRevenuesGroups, revenue: TicketRevenueDTO) => { const startWeekday: WeekdayEnum = PAYMENT_START_WEEKDAY; - const itemDate = new Date(item.processingDateTime); + const itemDate = new Date(revenue.processingDateTime); const nthWeek = getNthWeek(itemDate, startWeekday); - const foundDetalhesA = detalhesA.filter((i) => i.itemTransacaoAgrupado.id == item.itemTransacaoAgrupadoId); - const errors = DetalheA.getOcorrenciaErrors(foundDetalhesA); + const errors = Ocorrencia.getErrors(revenue.ocorrencias); // 'day', default, - let dateGroup = item.processingDateTime.slice(0, 10); + let dateGroup = revenue.processingDateTime.slice(0, 10); if (groupBy === 'week') { dateGroup = String(nthWeek); } @@ -309,8 +242,8 @@ export class TicketRevenuesService { } if (!group[dateGroup]) { - const friday = nextFriday(new Date(item.processingDateTime)).toISOString(); - const day = item.processingDateTime; + const friday = nextFriday(new Date(revenue.processingDateTime)).toISOString(); + const day = revenue.processingDateTime; const procsesingDate = groupBy === 'week' ? friday : day; const newGroup = new TicketRevenuesGroupDto({ count: 0, @@ -337,7 +270,7 @@ export class TicketRevenuesService { group[dateGroup].errors = [...new Set([...group[dateGroup].errors, ...errors])]; } - TicketRevenuesGroupList.appendItem(group[dateGroup], item, detalhesA); + group[dateGroup].appendItem(revenue); return group; }, {}); const resultList = Object.keys(result).map((dateGroup) => result[dateGroup]); diff --git a/src/ticket-revenues/utils/ticket-revenues-groups.utils.ts b/src/ticket-revenues/utils/ticket-revenues-groups.utils.ts deleted file mode 100644 index 72578245..00000000 --- a/src/ticket-revenues/utils/ticket-revenues-groups.utils.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { DetalheA } from 'src/cnab/entity/pagamento/detalhe-a.entity'; -import { TicketRevenuesGroupDto } from '../dtos/ticket-revenues-group.dto'; -import { ITRCounts } from '../interfaces/tr-counts.interface'; -import { TicketRevenueDTO } from '../dtos/ticket-revenue.dto'; -import { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; - -const COUNTS_KEYS = [ - 'transportTypeCounts', - 'directionIdCounts', - 'paymentMediaTypeCounts', - 'transactionTypeCounts', - 'transportIntegrationTypeCounts', - 'stopIdCounts', - 'stopLatCounts', - 'stopLonCounts', -]; - -export function appendCountsGroup( - group: TicketRevenuesGroupDto, - groupKey: keyof TicketRevenuesGroupDto, - newItem: TicketRevenueDTO, - itemKey: string, -) { - const IGNORE_NULL_UNDEFINED = true; - const countsKey = newItem[itemKey]; - if ( - (countsKey !== null && countsKey !== undefined) || - !IGNORE_NULL_UNDEFINED - ) { - const oldItem = group[groupKey][countsKey] as ITRCounts; - if (oldItem === undefined) { - (group[groupKey][countsKey] as ITRCounts) = { - count: 1, - transactionValue: newItem?.transactionValue || 0, - }; - } else { - (group[groupKey][countsKey] as ITRCounts) = { - count: oldItem.count + 1, - transactionValue: Number( - (oldItem.transactionValue + (newItem?.transactionValue || 0)).toFixed( - 2, - ), - ), - }; - } - } -} - -export function appendCountsValue( - group: TicketRevenuesGroupDto, - groupPropName: keyof TicketRevenuesGroupDto, - newItemKey: string | number, - ignoreNullUndefinedValue = true, -) { - if ( - (newItemKey !== null && newItemKey !== undefined) || - !ignoreNullUndefinedValue - ) { - const oldValue = group[groupPropName][newItemKey as any]; - if (oldValue === undefined) { - group[groupPropName][newItemKey as any] = 1; - } else { - group[groupPropName][newItemKey as any] += 1; - } - } -} - -export function appendItem( - group: TicketRevenuesGroupDto, - newItem: TicketRevenueDTO, - detalhesA: DetalheA[], -) { - group.count += 1; - if (newItem.transactionValue) { - const value = group.transactionValueSum + newItem.transactionValue; - group.transactionValueSum = Number(value.toFixed(2)); - } - group.paidValueSum += newItem.paidValue; - const foundDetalhesA = detalhesA.filter( - (i) => - i.itemTransacaoAgrupado.id === - newItem.arquivoPublicacao?.itemTransacao.itemTransacaoAgrupado.id, - ); - const errors = DetalheA.getOcorrenciaErrors(foundDetalhesA); - - for (const [groupKey, groupValue] of Object.entries(group)) { - if (groupKey === 'isPago') { - if (!newItem?.arquivoPublicacao?.isPago && !newItem?.isPago) { - group[groupKey] = false; - } - } else if (groupKey === 'errors') { - if (!newItem.arquivoPublicacao?.isPago && !newItem?.isPago) { - group[groupKey] = Ocorrencia.joinUniqueCode(group[groupKey], errors); - } - } else if (COUNTS_KEYS.includes(groupKey)) { - const itemKey = groupKey.replace('Counts', ''); - appendCountsGroup(group, groupKey as any, newItem, itemKey); - } else if (typeof groupValue === 'string') { - group[groupKey] = newItem[groupKey]; - } - } -} diff --git a/src/tipo-favorecido/tipo-favorecido.enum.ts b/src/tipo-favorecido/tipo-favorecido.enum.ts deleted file mode 100644 index 38097f5c..00000000 --- a/src/tipo-favorecido/tipo-favorecido.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum TipoFavorecidoEnum { - operadora = 'vanzeiro', - consorcio = 'consorcio', -} diff --git a/src/transacao-bq/interfaces/previous-days-args.ts b/src/transacao-view/interfaces/previous-days-args.ts similarity index 100% rename from src/transacao-bq/interfaces/previous-days-args.ts rename to src/transacao-view/interfaces/previous-days-args.ts diff --git a/src/transacao-bq/interfaces/sync-form-ordem.interface.ts b/src/transacao-view/interfaces/sync-form-ordem.interface.ts similarity index 100% rename from src/transacao-bq/interfaces/sync-form-ordem.interface.ts rename to src/transacao-view/interfaces/sync-form-ordem.interface.ts diff --git a/src/transacao-bq/transacao-view.entity.ts b/src/transacao-view/transacao-view.entity.ts similarity index 81% rename from src/transacao-bq/transacao-view.entity.ts rename to src/transacao-view/transacao-view.entity.ts index 3fcc7049..4f6a8a2d 100644 --- a/src/transacao-bq/transacao-view.entity.ts +++ b/src/transacao-view/transacao-view.entity.ts @@ -38,61 +38,11 @@ export class TransacaoView { if (transacao) { Object.assign(this, transacao); if (transacao.arquivoPublicacao) { - this.arquivoPublicacao = new ArquivoPublicacao(transacao.arquivoPublicacao) + this.arquivoPublicacao = new ArquivoPublicacao(transacao.arquivoPublicacao); } } } - public static getSqlFields(table?: string): Record { - return { - id: `${table ? `${table}.` : ''}"id"`, - datetimeTransacao: `${table ? `${table}.` : ''}"datetimeTransacao"`, - datetimeProcessamento: `${table ? `${table}.` : ''}"datetimeProcessamento"`, - datetimeCaptura: `${table ? `${table}.` : ''}"datetimeCaptura"`, - modo: `${table ? `${table}.` : ''}"modo"`, - idConsorcio: `${table ? `${table}.` : ''}"idConsorcio"`, - nomeConsorcio: `${table ? `${table}.` : ''}"nomeConsorcio"`, - idOperadora: `${table ? `${table}.` : ''}"idOperadora"`, - nomeOperadora: `${table ? `${table}.` : ''}"nomeOperadora"`, - idTransacao: `${table ? `${table}.` : ''}"idTransacao"`, - tipoPagamento: `${table ? `${table}.` : ''}"tipoPagamento"`, - tipoTransacao: `${table ? `${table}.` : ''}"tipoTransacao"`, - tipoGratuidade: `${table ? `${table}.` : ''}"tipoGratuidade"`, - valorTransacao: `${table ? `${table}.` : ''}"valorTransacao"`, - valorPago: `${table ? `${table}.` : ''}"valorPago"`, - operadoraCpfCnpj: `${table ? `${table}.` : ''}"operadoraCpfCnpj"`, - consorcioCnpj: `${table ? `${table}.` : ''}"consorcioCnpj"`, - arquivoPublicacao: `${table ? `${table}.` : ''}"arquivoPublicacaoId"`, - itemTransacaoAgrupadoId: `${table ? `${table}.` : ''}"itemTransacaoAgrupadoId"`, - createdAt: `${table ? `${table}.` : ''}"createdAt"`, - updatedAt: `${table ? `${table}.` : ''}"updatedAt"`, - }; - } - - public static sqlFieldTypes: Record = { - id: 'INT', - datetimeTransacao: 'TIMESTAMP', - datetimeProcessamento: 'TIMESTAMP', - datetimeCaptura: 'TIMESTAMP', - modo: 'VARCHAR', - idConsorcio: 'VARCHAR', - nomeConsorcio: 'VARCHAR', - idOperadora: 'VARCHAR', - nomeOperadora: 'VARCHAR', - idTransacao: 'VARCHAR', - tipoPagamento: 'VARCHAR', - tipoTransacao: 'VARCHAR', - tipoGratuidade: 'VARCHAR', - valorTransacao: 'NUMERIC', - valorPago: 'NUMERIC', - operadoraCpfCnpj: 'VARCHAR', - consorcioCnpj: 'VARCHAR', - arquivoPublicacao: 'INT', - itemTransacaoAgrupadoId: 'INT', - createdAt: 'TIMESTAMP', - updatedAt: 'TIMESTAMP', - }; - @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_TransacaoView_id', }) @@ -152,6 +102,7 @@ export class TransacaoView { @Column({ type: String, nullable: true }) consorcioCnpj: string | null; + /** @deprecated Replaced by `itemTransacaoAgrupadoId` */ @ManyToOne(() => ArquivoPublicacao, { eager: true, nullable: true }) @JoinColumn({ foreignKeyConstraintName: 'FK_TransacaoView_arquivoPublicacao_ManyToOne', @@ -176,6 +127,56 @@ export class TransacaoView { } } + public static getSqlFields(table?: string, castValues?: boolean): Record { + return { + id: `${table ? `${table}.` : ''}"id"`, + datetimeTransacao: `${table ? `${table}.` : ''}"datetimeTransacao"`, + datetimeProcessamento: `${table ? `${table}.` : ''}"datetimeProcessamento"`, + datetimeCaptura: `${table ? `${table}.` : ''}"datetimeCaptura"`, + modo: `${table ? `${table}.` : ''}"modo"`, + idConsorcio: `${table ? `${table}.` : ''}"idConsorcio"`, + nomeConsorcio: `${table ? `${table}.` : ''}"nomeConsorcio"`, + idOperadora: `${table ? `${table}.` : ''}"idOperadora"`, + nomeOperadora: `${table ? `${table}.` : ''}"nomeOperadora"`, + idTransacao: `${table ? `${table}.` : ''}"idTransacao"`, + tipoPagamento: `${table ? `${table}.` : ''}"tipoPagamento"`, + tipoTransacao: `${table ? `${table}.` : ''}"tipoTransacao"`, + tipoGratuidade: `${table ? `${table}.` : ''}"tipoGratuidade"`, + valorTransacao: `${table ? `${table}.` : ''}"valorTransacao"${castValues ? '::FLOAT' : ''}`, + valorPago: `${table ? `${table}.` : ''}"valorPago"${castValues ? '::FLOAT' : ''}`, + operadoraCpfCnpj: `${table ? `${table}.` : ''}"operadoraCpfCnpj"`, + consorcioCnpj: `${table ? `${table}.` : ''}"consorcioCnpj"`, + arquivoPublicacao: `${table ? `${table}.` : ''}"arquivoPublicacaoId"`, + itemTransacaoAgrupadoId: `${table ? `${table}.` : ''}"itemTransacaoAgrupadoId"`, + createdAt: `${table ? `${table}.` : ''}"createdAt"`, + updatedAt: `${table ? `${table}.` : ''}"updatedAt"`, + }; + } + + public static sqlFieldTypes: Record = { + id: 'INT', + datetimeTransacao: 'TIMESTAMP', + datetimeProcessamento: 'TIMESTAMP', + datetimeCaptura: 'TIMESTAMP', + modo: 'VARCHAR', + idConsorcio: 'VARCHAR', + nomeConsorcio: 'VARCHAR', + idOperadora: 'VARCHAR', + nomeOperadora: 'VARCHAR', + idTransacao: 'VARCHAR', + tipoPagamento: 'VARCHAR', + tipoTransacao: 'VARCHAR', + tipoGratuidade: 'VARCHAR', + valorTransacao: 'NUMERIC', + valorPago: 'NUMERIC', + operadoraCpfCnpj: 'VARCHAR', + consorcioCnpj: 'VARCHAR', + arquivoPublicacao: 'INT', + itemTransacaoAgrupadoId: 'INT', + createdAt: 'TIMESTAMP', + updatedAt: 'TIMESTAMP', + }; + public static fromBigqueryTransacao(bq: BigqueryTransacao) { return new TransacaoView({ datetimeCaptura: asStringDate(bq.datetime_captura), @@ -197,41 +198,31 @@ export class TransacaoView { }); } - toTicketRevenue(publicacoes: ArquivoPublicacao[]) { - const publicacoesTv = publicacoes.filter((p) => p.itemTransacao.itemTransacaoAgrupado.id == this.itemTransacaoAgrupadoId); - const publicacao: ArquivoPublicacao | undefined = publicacoesTv.filter((p) => p.isPago)[0] || publicacoesTv[0]; - const isPago = publicacao?.isPago == true; - const revenue = new TicketRevenueDTO({ - bqDataVersion: '', - captureDateTime: this.datetimeCaptura.toISOString(), - clientId: null, - directionId: null, - integrationId: null, - date: this.datetimeProcessamento.toISOString(), - paymentMediaType: this.tipoPagamento, - processingDateTime: this.datetimeProcessamento.toISOString(), - processingHour: this.datetimeProcessamento.getHours(), - stopId: null, - stopLat: null, - stopLon: null, - transactionDateTime: this.datetimeTransacao.toISOString(), - transactionId: this.idTransacao, - transactionLat: null, - transactionLon: null, - transactionType: this.tipoTransacao, - paidValue: this.valorPago || 0, - transactionValue: this.valorTransacao, - transportIntegrationType: null, - transportType: null, - vehicleId: null, - vehicleService: null, - arquivoPublicacao: this.arquivoPublicacao || undefined, - itemTransacaoAgrupadoId: this.itemTransacaoAgrupadoId || undefined, - isPago, - count: 1, - }); - return revenue; - } + // toTicketRevenue(publicacoes: ArquivoPublicacao[]) { + // const publicacoesTv = publicacoes.filter((p) => p.itemTransacao.itemTransacaoAgrupado.id == this.itemTransacaoAgrupadoId); + // const publicacao: ArquivoPublicacao | undefined = publicacoesTv.filter((p) => p.isPago)[0] || publicacoesTv[0]; + // const isPago = publicacao?.isPago == true; + // const revenue = new TicketRevenueDTO({ + // captureDateTime: this.datetimeCaptura.toISOString(), + // date: this.datetimeProcessamento.toISOString(), + // paymentMediaType: this.tipoPagamento, + // processingDateTime: this.datetimeProcessamento.toISOString(), + // processingHour: this.datetimeProcessamento.getHours(), + // transactionDateTime: this.datetimeTransacao.toISOString(), + // transactionId: this.idTransacao, + // transactionType: this.tipoTransacao, + // paidValue: this.valorPago || 0, + // transactionValue: this.valorTransacao, + // transportIntegrationType: null, + // transportType: null, + // arquivoPublicacao: this.arquivoPublicacao || undefined, + // itemTransacaoAgrupadoId: this.itemTransacaoAgrupadoId || undefined, + // isPago, + // count: 1, + // ocorrencias: [], + // }); + // return revenue; + // } getProperties() { return { diff --git a/src/transacao-bq/transacao-view.module.ts b/src/transacao-view/transacao-view.module.ts similarity index 100% rename from src/transacao-bq/transacao-view.module.ts rename to src/transacao-view/transacao-view.module.ts diff --git a/src/transacao-bq/transacao-view.repository.ts b/src/transacao-view/transacao-view.repository.ts similarity index 64% rename from src/transacao-bq/transacao-view.repository.ts rename to src/transacao-view/transacao-view.repository.ts index 9f487ab4..3b8cf573 100644 --- a/src/transacao-bq/transacao-view.repository.ts +++ b/src/transacao-view/transacao-view.repository.ts @@ -8,9 +8,28 @@ import { DataSource, DeepPartial, EntityManager, FindManyOptions, In, LessThanOr import { IPreviousDaysArgs } from './interfaces/previous-days-args'; import { ISyncOrdemPgto } from './interfaces/sync-form-ordem.interface'; import { ITransacaoView, TransacaoView } from './transacao-view.entity'; +import { formatDateYMD } from 'src/utils/date-utils'; -export interface IFindRawWhere { - idTransacao?: string[]; +export interface TransacaoViewFindRawOptions { + where: { + idTransacao?: string[]; + operadoraCpfCnpj?: string[]; + datetimeTransacao?: { between: [Date, Date][] }; + datetimeProcessamento?: { between: [Date, Date][] }; + }; + order?: { + datetimeProcessamento?: 'ASC' | 'DESC'; + id?: 'ASC' | 'DESC'; + }; + eager?: boolean; + limit?: number; + offset?: number; +} + +export interface TVFindUpdateValuesWhere { + diasAnteriores?: number; + idOperadora?: string[]; + valorPago_gt_zero?: boolean; } @Injectable() @@ -45,10 +64,11 @@ export class TransacaoViewRepository { } public async syncOrdemPgto(args?: ISyncOrdemPgto) { + const METHOD = 'syncOrdemPgto'; const where: string[] = []; if (args?.dataOrdem_between) { const [start, end] = args.dataOrdem_between.map((d) => d.toISOString()); - where.push(`DATE(tv."datetimeProcessamento") BETWEEN (DATE('${start}') - INTERVAL '1 DAY') AND '${end}'`); + where.push(`DATE(tv."datetimeTransacao") BETWEEN (DATE('${start}') - INTERVAL '1 DAY') AND '${end}'`); } if (args?.nomeFavorecido?.length) { where.push(`cf.nome ILIKE ANY(ARRAY['%${args.nomeFavorecido.join("%', '%")}%'])`); @@ -64,8 +84,7 @@ export class TransacaoViewRepository { ita.id AS ita_id, tv."datetimeTransacao", tv."datetimeProcessamento", - ita."dataOrdem", - ita."dataCaptura" + ita."dataOrdem" FROM item_transacao_agrupado ita INNER JOIN detalhe_a da ON da."itemTransacaoAgrupadoId" = ita.id INNER JOIN item_transacao it ON it."itemTransacaoAgrupadoId" = ita.id @@ -75,14 +94,14 @@ export class TransacaoViewRepository { AND tv."idOperadora" = ita."idOperadora" AND tv."operadoraCpfCnpj" = cf."cpfCnpj" AND tv."datetimeTransacao"::DATE BETWEEN - (da."dataVencimento"::DATE - (CASE WHEN ita."nomeConsorcio" = 'VLT' THEN INTERVAL '2 DAYS' ELSE INTERVAL '8 DAYS' END)) -- VENCIMENTO - 2 SE VLT; SENÃO QUINTA PGTO - AND (DATE(da."dataVencimento") - INTERVAL '2 DAYS') -- VENCIMENTO - 2 (OU QUARTA PGTO SE NÃO for VLT) - WHERE tv."valorPago" > 0 ${where.length ? `AND ${where.join(' AND ')}` : ''} + (ita."dataOrdem"::DATE - (CASE WHEN ita."nomeConsorcio" = 'VLT' THEN INTERVAL '2 DAYS' ELSE INTERVAL '8 DAYS' END)) -- VENCIMENTO - 2 SE VLT; SENÃO QUINTA PGTO + AND (DATE(ita."dataOrdem") - INTERVAL '2 DAYS') -- VENCIMENTO - 2 (OU QUARTA PGTO SE NÃO for VLT) + WHERE ${where.length ? `AND ${where.join(' AND ')}` : ''} ORDER BY tv.id ASC, ita.id DESC ) associados WHERE id = associados.tv_id `; - this.logger.debug(query); + this.logger.debug('query: ' + compactQuery(query), METHOD); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); let count = 0; @@ -111,7 +130,7 @@ export class TransacaoViewRepository { } const updatedAt: keyof ITransacaoView = 'updatedAt'; const query = EntityHelper.getQueryUpdate('transacao_view', dtos, fields, TransacaoView.sqlFieldTypes, reference, updatedAt); - this.logger.debug(query); + this.logger.debug('query: ' + compactQuery(query), METHOD); const [, count] = await (manager || this.transacaoViewRepository).query(compactQuery(query)); return count; } @@ -185,35 +204,79 @@ export class TransacaoViewRepository { return await this.transacaoViewRepository.find(options); } - public async findRaw(where?: IFindRawWhere): Promise { + public async findRaw(options?: TransacaoViewFindRawOptions): Promise { const tv = TransacaoView.getSqlFields('tv'); const qWhere: string[] = []; - if (where?.idTransacao?.length) { - qWhere.push(`${tv.idTransacao} IN ('${where.idTransacao.join("','")}')`); + const eager = options?.eager !== undefined ? options.eager : true; + + if (options?.where?.idTransacao?.length) { + qWhere.push(`${tv.idTransacao} IN ('${options.where?.idTransacao.join("','")}')`); + } + if (options?.where?.operadoraCpfCnpj?.length) { + qWhere.push(`${tv.operadoraCpfCnpj} IN ('${options.where?.operadoraCpfCnpj.join("','")}')`); + } + if (options?.where?.datetimeTransacao) { + const betweenStr = options.where.datetimeTransacao.between.map(([start, end]) => `${tv.datetimeTransacao}::DATE BETWEEN '${formatDateYMD(start)}' AND '${formatDateYMD(end)}'`).join(' OR '); + qWhere.push(`(${betweenStr})`); + } + if (options?.where?.datetimeProcessamento) { + const betweenStr = options.where.datetimeProcessamento.between.map(([start, end]) => `${tv.datetimeProcessamento}::DATE BETWEEN '${formatDateYMD(start)}' AND '${formatDateYMD(end)}'`).join(' OR '); + qWhere.push(`(${betweenStr})`); } + + const order: string[] = []; + if (options?.order?.datetimeProcessamento) { + order.push(`${tv.datetimeProcessamento} ${options.order.datetimeProcessamento}`); + } + if (options?.order?.id) { + order.push(`tv.id ${options.order.id}`); + } + const selectTv = - Object.values(tv) - .filter((i) => i != `tv.${tv.arquivoPublicacao}`) - .join(',') + `, json_build_object('id', ${tv.arquivoPublicacao}) AS "arquivoPublicacao"`; - const raw: any[] = await this.transacaoViewRepository.query( - compactQuery(` + Object.values(TransacaoView.getSqlFields('tv', true)) + .filter((i) => !i.startsWith(`tv.${tv.arquivoPublicacao}`)) + .join(',') + + `, json_build_object( + 'id', ${tv.arquivoPublicacao}, + 'isPago', ap."isPago", + ${eager ? `'itemTransacao', json_build_object('id', it.id, 'itemTransacaoAgrupado', json_build_object('id', ita.id))` : ''} + ) AS "arquivoPublicacao"`; + const query = ` SELECT ${selectTv} FROM transacao_view tv - ${where ? 'WHERE ' + qWhere.join(' AND ') : ''} - ORDER BY tv.id DESC - `), - ); + ${ + eager + ? `LEFT JOIN item_transacao_agrupado ita ON ita.id = tv."itemTransacaoAgrupadoId" + LEFT JOIN item_transacao it ON it."itemTransacaoAgrupadoId" = ita.id + LEFT JOIN arquivo_publicacao ap ON ap."itemTransacaoId" = it.id` + : '' + } + ${options ? 'WHERE ' + qWhere.join(' AND ') : ''} + ORDER BY ${order.length ? order.join(', ') : 'tv.id DESC'} + `; + const raw: any[] = await this.transacaoViewRepository.query(compactQuery(query)); const result = raw.map((i) => new TransacaoView(i)); return result; } - public async findUpdateValues(diasAnteriores?: number): Promise { + public async findUpdateValues(where?: TVFindUpdateValuesWhere): Promise { + const tv = TransacaoView.getSqlFields('tv'); + const qWhere: string[] = []; + if (where?.valorPago_gt_zero) { + qWhere.push(`NOT ${tv.valorPago} > 0`); + } + if (where?.diasAnteriores) { + qWhere.push(`${tv.datetimeProcessamento}::DATE >= NOW() - INTERVAL '${where?.diasAnteriores} DAYS'`); + } + if (where?.idOperadora?.length) { + qWhere.push(`${tv.idOperadora} IN('${where?.idOperadora.join("','")}')`); + } const raw: any[] = await this.transacaoViewRepository.query( compactQuery(` - SELECT tv.id, tv."idTransacao", tv."valorPago" + SELECT ${tv.id}, ${tv.idTransacao}, ${tv.valorPago}::FLOAT, ${tv.tipoTransacao}, ${tv.idOperadora} FROM transacao_view tv - WHERE NOT tv."valorPago" > 0 ${diasAnteriores ? `AND tv."datetimeProcessamento"::DATE >= NOW() - INTERVAL '${diasAnteriores} DAYS'` : ''} - ORDER BY tv.id DESC + ${qWhere.length ? `WHERE ${qWhere.join(' AND ')}` : ''} + ORDER BY ${tv.id} DESC `), ); const result = raw.map((i) => new TransacaoView(i)); diff --git a/src/transacao-bq/transacao-view.service.ts b/src/transacao-view/transacao-view.service.ts similarity index 91% rename from src/transacao-bq/transacao-view.service.ts rename to src/transacao-view/transacao-view.service.ts index c8d727df..d31e6cd1 100644 --- a/src/transacao-bq/transacao-view.service.ts +++ b/src/transacao-view/transacao-view.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@nestjs/common'; import { CustomLogger } from 'src/utils/custom-logger'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; -import { DataSource, DeepPartial, EntityManager, FindManyOptions, QueryRunner } from 'typeorm'; +import { DeepPartial, EntityManager, FindManyOptions, QueryRunner } from 'typeorm'; import { IPreviousDaysArgs } from './interfaces/previous-days-args'; import { ITransacaoView, TransacaoView } from './transacao-view.entity'; -import { IFindRawWhere, TransacaoViewRepository } from './transacao-view.repository'; +import { TVFindUpdateValuesWhere, TransacaoViewFindRawOptions, TransacaoViewRepository } from './transacao-view.repository'; import { ISyncOrdemPgto } from './interfaces/sync-form-ordem.interface'; @Injectable() export class TransacaoViewService { - private logger = new CustomLogger(TransacaoViewService.name, { - timestamp: true, - }); + private logger = new CustomLogger(TransacaoViewService.name, { timestamp: true }); constructor(private transacaoViewRepository: TransacaoViewRepository) {} @@ -68,11 +66,11 @@ export class TransacaoViewService { async findCustom(options: FindManyOptions) { return await this.transacaoViewRepository.find(options); } - async findRaw(where?: IFindRawWhere) { + async findRaw(where?: TransacaoViewFindRawOptions): Promise { return await this.transacaoViewRepository.findRaw(where); } - async findUpdateValues(diasAnteriores?: number) { - return await this.transacaoViewRepository.findUpdateValues(diasAnteriores); + async findUpdateValues(where?: TVFindUpdateValuesWhere) { + return await this.transacaoViewRepository.findUpdateValues(where); } /** diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index b7fc39d8..9e2303c0 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -6,26 +6,12 @@ import { Bank } from 'src/banks/entities/bank.entity'; import { InviteStatus } from 'src/mail-history-statuses/entities/mail-history-status.entity'; import { MailHistory } from 'src/mail-history/entities/mail-history.entity'; import { EntityHelper } from 'src/utils/entity-helper'; -import { - AfterLoad, - BeforeInsert, - BeforeUpdate, - Column, - CreateDateColumn, - DeepPartial, - DeleteDateColumn, - Entity, - Index, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { AfterLoad, BeforeInsert, BeforeUpdate, Column, CreateDateColumn, DeepPartial, DeleteDateColumn, Entity, Index, JoinColumn, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { FileEntity } from '../../files/entities/file.entity'; import { Role } from '../../roles/entities/role.entity'; import { Status } from '../../statuses/entities/status.entity'; import { UserHttpException } from 'src/utils/http-exception/user-http-exception'; +import { Lancamento } from 'src/lancamento/entities/lancamento.entity'; /** uniqueConstraintName: `UQ_User_email` */ @Entity() @@ -168,6 +154,9 @@ export class User extends EntityHelper { @Column({ type: String, nullable: true }) passValidatorId?: string; + @ManyToMany(() => Lancamento, (lancamento) => lancamento) + lancamentos: Lancamento[]; + @Expose({ name: 'aux_isRegistrationComplete' }) aux_isRegistrationComplete(): boolean { return ( @@ -200,10 +189,7 @@ export class User extends EntityHelper { 'bankAccountDigit', ]; - return requiredFields.filter( - (field) => - !(typeof this[field] === 'boolean' || Boolean(this[field]) === true), - ); + return requiredFields.filter((field) => !(typeof this[field] === 'boolean' || Boolean(this[field]) === true)); } @OneToMany(() => MailHistory, (mailHistory) => mailHistory.user.id, { @@ -269,10 +255,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankAgency(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getBankAgency(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.bankAgency) { throw UserHttpException.invalidField('bankAgency', { errorMessage: args?.errorMessage, @@ -286,10 +269,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankAgencyWithoutDigit(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getBankAgencyWithoutDigit(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { const agency = this.getBankAgency(args); return agency.substring(0, agency.length - 1); } @@ -298,10 +278,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankAgencyDigit(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getBankAgencyDigit(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { const agency = this.getBankAgency(args); return agency.substring(agency.length - 1); } @@ -310,10 +287,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankAccount(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getBankAccount(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.bankAccount) { throw UserHttpException.invalidField('bankAgency', { errorMessage: args?.errorMessage, @@ -327,10 +301,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankAccountDigit(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getBankAccountDigit(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.bankAccountDigit) { throw UserHttpException.invalidField('bankAgency', { errorMessage: args?.errorMessage, @@ -344,10 +315,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getBankCode(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): number { + getBankCode(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): number { if (!this.bankCode) { throw UserHttpException.invalidField('bankAgency', { errorMessage: args?.errorMessage, @@ -361,10 +329,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getFullName(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getFullName(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.fullName) { throw UserHttpException.invalidField('bankAgency', { errorMessage: args?.errorMessage, @@ -378,10 +343,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getCpfCnpj(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getCpfCnpj(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.cpfCnpj) { throw UserHttpException.invalidField('cpfCnpj', { errorMessage: args?.errorMessage, @@ -395,10 +357,7 @@ export class User extends EntityHelper { * Get field validated * @throws `HttpException` */ - getPermitCode(args?: { - errorMessage?: string; - httpStatusCode?: HttpStatus; - }): string { + getPermitCode(args?: { errorMessage?: string; httpStatusCode?: HttpStatus }): string { if (!this.permitCode) { throw UserHttpException.invalidField('permitCode', { errorMessage: args?.errorMessage, diff --git a/src/utils/all-exteptions-filter/custom-validation-options.ts b/src/utils/all-exteptions-filter/custom-validation-options.ts index dff8e802..3caa3d92 100644 --- a/src/utils/all-exteptions-filter/custom-validation-options.ts +++ b/src/utils/all-exteptions-filter/custom-validation-options.ts @@ -28,9 +28,7 @@ interface DecodedList { * A helper to pass extra error parameters from class-validator to a global errorFilter or similar. * @returns ValidationOptions with JSON encoding `message` and extra fields in `message` field. */ -export function customValidationOptions( - validationOptions: CustomValidationOptions, -): ValidationOptions { +export function customValidationOptions(validationOptions: CustomValidationOptions): ValidationOptions { const extraFields: CustomValidationOptions = { ...(validationOptions?.statusCode && { statusCode: validationOptions.statusCode, @@ -58,15 +56,15 @@ interface BehaviorOptions { * @returns The same HttpException but with decoded data for customValidation */ export function getCustomValidationOptions( - errorResponse: HttpExceptionResponse, + errorResponse: HttpExceptionResponse | string, behavior: BehaviorOptions = { setLowestStatus: true, }, ): Partial | null { - if (!errorResponse?.errors) { + if (typeof errorResponse !== 'string' && !errorResponse?.errors) { return null; } - const newResponse: Partial = { ...errorResponse }; + const newResponse: Partial = { ...(typeof errorResponse === 'string' ? { message: errorResponse } : errorResponse) }; newResponse.errors = {}; newResponse.details = {}; newResponse.statusCodes = {}; @@ -76,35 +74,34 @@ export function getCustomValidationOptions( statusCodes: [], }; let decodedLowestStatus: SerializedValidationOptions | undefined; - for (const [errorKey, errorValue] of Object.entries(errorResponse.errors)) { - const substrings = asJSONStrOrObj(errorValue) - .split(/,(?![^{}]*})/) - .map((substring) => substring.trim()); - const list: DecodedList = { - details: [], - errors: [], - statusCodes: [], - }; - for (const substring of substrings) { - try { - const decodedJson: SerializedValidationOptions = JSON.parse(substring); - const { statusCode, error, details } = decodedJson; - error && list.errors.push(error); - details && list.details.push(details); - statusCode && list.statusCodes.push(statusCode); - if ( - statusCode && - (!decodedLowestStatus || decodedLowestStatus.statusCode > statusCode) - ) { - decodedLowestStatus = decodedJson; + if (typeof errorResponse !== 'string' && errorResponse?.errors) { + for (const [errorKey, errorValue] of Object.entries(errorResponse.errors)) { + const substrings = asJSONStrOrObj(errorValue) + .split(/,(?![^{}]*})/) + .map((substring) => substring.trim()); + const list: DecodedList = { + details: [], + errors: [], + statusCodes: [], + }; + for (const substring of substrings) { + try { + const decodedJson: SerializedValidationOptions = JSON.parse(substring); + const { statusCode, error, details } = decodedJson; + error && list.errors.push(error); + details && list.details.push(details); + statusCode && list.statusCodes.push(statusCode); + if (statusCode && (!decodedLowestStatus || decodedLowestStatus.statusCode > statusCode)) { + decodedLowestStatus = decodedJson; + } + } catch (error) { + list.errors.push(substring); } - } catch (error) { - list.errors.push(substring); } - } - for (const listKey in list) { - if (list[listKey].length) { - newResponse[listKey][errorKey] = list[listKey].join(', '); + for (const listKey in list) { + if (list[listKey].length) { + newResponse[listKey][errorKey] = list[listKey].join(', '); + } } } } diff --git a/src/utils/all-exteptions-filter/filters/all-exceptions.filter.ts b/src/utils/all-exteptions-filter/filters/all-exceptions.filter.ts index 721c315a..30a85fb2 100644 --- a/src/utils/all-exteptions-filter/filters/all-exceptions.filter.ts +++ b/src/utils/all-exteptions-filter/filters/all-exceptions.filter.ts @@ -1,18 +1,9 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - HttpStatus -} from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; import { Request, Response } from 'express'; import { CustomLogger } from 'src/utils/custom-logger'; import { formatError } from 'src/utils/log-utils'; import { getCustomValidationOptions } from '../custom-validation-options'; -import { - CustomHttpExceptionResponse, - HttpExceptionResponse, -} from '../interfaces/http-exception-response.interface'; +import { CustomHttpExceptionResponse, HttpExceptionResponse } from '../interfaces/http-exception-response.interface'; @Catch() export class AllExceptionsFilter implements ExceptionFilter { @@ -31,29 +22,25 @@ export class AllExceptionsFilter implements ExceptionFilter { const error = exception instanceof Error ? (exception as Error) : undefined; this.logger.error(this.getErrorLogSummary(responseData), error?.stack); - response.status(responseData.statusCode).json( - responseData?.response || { - status: responseData.statusCode, - ...responseData.clientMessage, - timestamp: responseData.timestamp, - }, - ); + const responseJson = responseData?.response || { + status: responseData.statusCode, + ...responseData.clientMessage, + timestamp: responseData.timestamp, + }; + response.status(responseData.statusCode).json(responseJson); } - private getResponseData(kwargs: { - exception: unknown; - request: Request; - }): CustomHttpExceptionResponse { + private getResponseData(kwargs: { exception: unknown; request: Request }): CustomHttpExceptionResponse { const { exception, request } = kwargs; let responseData: CustomHttpExceptionResponse = { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + statusCode: (exception as any)?.status || HttpStatus.INTERNAL_SERVER_ERROR, uri: request.url, method: request.method, timestamp: new Date(), }; if (exception instanceof HttpException) { - const httpResponse = exception.getResponse() as HttpExceptionResponse; + const httpResponse = exception.getResponse() as HttpExceptionResponse | string; const customResponse = getCustomValidationOptions(httpResponse, { setLowestStatus: true, setMainMessage: true, @@ -62,7 +49,7 @@ export class AllExceptionsFilter implements ExceptionFilter { responseData = { ...responseData, statusCode: exception.getStatus(), - ...(customResponse || httpResponse), + ...(customResponse || (typeof httpResponse === 'string' ? { message: httpResponse } : httpResponse)), }; } if (responseData?.message) { @@ -83,18 +70,10 @@ export class AllExceptionsFilter implements ExceptionFilter { return responseData; } - private getErrorLogSummary = ( - errorResponse: CustomHttpExceptionResponse, - exception?: unknown, - ): string => { - const { statusCode, clientMessage, method, uri, internalMessage } = - errorResponse; + private getErrorLogSummary = (errorResponse: CustomHttpExceptionResponse, exception?: unknown): string => { + const { statusCode, clientMessage, method, uri, internalMessage } = errorResponse; - const errorLog = formatError( - `Code: ${statusCode} - ${method}: ${uri}`, - { ...clientMessage, ...internalMessage }, - exception instanceof Error ? (exception as Error) : undefined, - ); + const errorLog = formatError(`Code: ${statusCode} - ${method}: ${uri}`, { ...clientMessage, ...internalMessage }, exception instanceof Error ? (exception as Error) : undefined); return errorLog; }; diff --git a/src/utils/http-exception/common-http-exception.ts b/src/utils/http-exception/common-http-exception.ts index 4a89c734..b2631815 100644 --- a/src/utils/http-exception/common-http-exception.ts +++ b/src/utils/http-exception/common-http-exception.ts @@ -2,11 +2,7 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { getHttpStatusMessage } from './http-exception-utils'; export const CommonHttpException = { - detailField: ( - field: string, - message: string, - httpStatusCode: HttpStatus = 500, - ) => + detailField: (field: string, message: string, httpStatusCode: HttpStatus = 500) => new HttpException( { error: getHttpStatusMessage(httpStatusCode), @@ -25,6 +21,16 @@ export const CommonHttpException = { }, httpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, ), + messageArgs: (errorMessage: string, args: object, httpStatusCode?: HttpStatus) => + new HttpException( + { + error: { + message: errorMessage, + ...args, + }, + }, + httpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, + ), invalidField: ( entity: string, fieldName: string, @@ -44,11 +50,7 @@ export const CommonHttpException = { }, args?.httpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, ), - errorDetails: ( - error: string, - details: object | string, - httpStatusCode: HttpStatus = 500, - ) => + errorDetails: (error: string, details: object | string, httpStatusCode: HttpStatus = 500) => new HttpException( { error: error || getHttpStatusMessage(httpStatusCode), @@ -72,11 +74,7 @@ export const CommonHttpException = { }, HttpStatus.UNPROCESSABLE_ENTITY, ), - notFound: ( - notFoundProp: string, - httpStatusCode: HttpStatus = HttpStatus.NOT_FOUND, - error?: string, - ) => + notFound: (notFoundProp: string, httpStatusCode: HttpStatus = HttpStatus.NOT_FOUND, error?: string) => new HttpException( { error: error || getHttpStatusMessage(httpStatusCode), @@ -90,13 +88,7 @@ export const CommonHttpException = { }, httpStatusCode, ), - argNotType: ( - field: string, - expectedType: string, - value: any, - httpStatusCode: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY, - detailsOnly = true, - ) => + argNotType: (field: string, expectedType: string, value: any, httpStatusCode: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY, detailsOnly = true) => new HttpException( { code: 'arg-not-type', diff --git a/src/utils/pipes/parse-list.pipe.ts b/src/utils/pipes/parse-array.pipe.ts similarity index 67% rename from src/utils/pipes/parse-list.pipe.ts rename to src/utils/pipes/parse-array.pipe.ts index 13353c59..02022275 100644 --- a/src/utils/pipes/parse-list.pipe.ts +++ b/src/utils/pipes/parse-array.pipe.ts @@ -1,28 +1,34 @@ import { BadRequestException, Injectable, PipeTransform, ArgumentMetadata } from '@nestjs/common'; import { isString } from 'class-validator'; import { isArrayUnique } from '../array-utils'; +import { Enum } from '../enum'; /** * Valida se uma lista inserida contém apenas valores de outra lista * @param args.unique (opcional) Verifica se tem apenas valores únicos */ @Injectable() -export class ParseListPipe implements PipeTransform { +export class ParseArrayPipe implements PipeTransform { /** * * @param args.transform default: true */ constructor( private readonly args?: { + /** Check if list items are valid enum */ + enumType?: any; /** If must have unique values */ compareList?: any[]; unique?: boolean; transform?: boolean; + /** Return empty list if not value given */ + transformOptional?: boolean; optional?: boolean; }, ) {} transform(value: any | undefined, metadata: ArgumentMetadata): any { + const enumType = this.args?.enumType; const list = this.args?.compareList; const field = metadata.data; const unique = this.args?.unique; @@ -32,7 +38,7 @@ export class ParseListPipe implements PipeTransform { } if (this.args?.optional && value === undefined) { - return transform ? [] : value; + return this.args?.transformOptional ? [] : value; } if (!isString(value)) { @@ -41,6 +47,9 @@ export class ParseListPipe implements PipeTransform { const valueList = value.split(','); + if (enumType && !valueList.every((e) => this.isEnumValue(e, enumType))) { + throw new BadRequestException(`${field}: ${value} must have valid enum items (${Enum.getKeys(enumType).join(',')}).`); + } if (list && !valueList.every((i) => list.includes(i))) { throw new BadRequestException(`${field}: ${value} must have valid items.`); } @@ -52,4 +61,8 @@ export class ParseListPipe implements PipeTransform { return transformed; } + private isEnumValue(value: any, enumType: Record): boolean { + const enumValues = Object.values(enumType); + return enumValues.includes(value); + } } diff --git a/src/utils/pipes/parse-boolean.pipe.ts b/src/utils/pipes/parse-boolean.pipe.ts index 56ccc2d3..f07503b6 100644 --- a/src/utils/pipes/parse-boolean.pipe.ts +++ b/src/utils/pipes/parse-boolean.pipe.ts @@ -1,9 +1,4 @@ -import { - BadRequestException, - Injectable, - PipeTransform, - ArgumentMetadata, -} from '@nestjs/common'; +import { BadRequestException, Injectable, PipeTransform, ArgumentMetadata, HttpException, HttpStatus } from '@nestjs/common'; /** * @param required default is `false` @@ -21,6 +16,13 @@ export class ParseBooleanPipe implements PipeTransform { const defaultValue = this?.args?.defaultValue; const field = metadata.data; + if (!['true', 'false', true, false, undefined].includes(value)) { + let _value = value; + if (_value === '') { + _value = 'got empty string'; + } + throw new HttpException(`${field}: Invalid boolean value (${_value})`, HttpStatus.BAD_REQUEST); + } const booleanValue = value == 'true' || value == true; if (value === undefined || field === undefined) { diff --git a/src/utils/pipes/validate-enum.pipe.ts b/src/utils/pipes/parse-enum.pipe.ts similarity index 50% rename from src/utils/pipes/validate-enum.pipe.ts rename to src/utils/pipes/parse-enum.pipe.ts index 4833f4f7..075a74e7 100644 --- a/src/utils/pipes/validate-enum.pipe.ts +++ b/src/utils/pipes/parse-enum.pipe.ts @@ -1,29 +1,28 @@ -import { - PipeTransform, - Injectable, - ArgumentMetadata, - BadRequestException, -} from '@nestjs/common'; +import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; +export interface IValidateEnumPipe { + readonly optional: boolean; + readonly defaultValue?: any; +} @Injectable() -export class ValidateEnumPipe implements PipeTransform { +export class ParseEnumPipe implements PipeTransform { constructor( - private readonly enumType: Record, - private readonly required: boolean = false, - private readonly defaultValue?: any, + private readonly enumType: Record, // + private readonly options?: IValidateEnumPipe, ) {} transform(value: any, metadata: ArgumentMetadata) { + const field = metadata.data; if (value === undefined) { - return this.defaultValue; + if (this.options?.optional) { + return value; + } else if (this.options?.defaultValue) { + return this.options.defaultValue; + } } const enumValue = this.isEnumValue(value, this.enumType); if (!enumValue) { - throw new BadRequestException( - `Invalid ${metadata.type} value. It must be one of [${Object.values( - this.enumType, - )}].`, - ); + throw new BadRequestException(`${field}: Invalid value '${value}'. It must be one of [${Object.values(this.enumType)}].`); } return value; } diff --git a/src/utils/test-utils.ts b/src/utils/test-utils.ts index f76ee024..ff35a0f1 100644 --- a/src/utils/test-utils.ts +++ b/src/utils/test-utils.ts @@ -2,7 +2,7 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { DataSource, ObjectLiteral } from 'typeorm'; -import { TransacaoView } from '../../src/transacao-bq/transacao-view.entity'; +import { TransacaoView } from '../../src/transacao-view/transacao-view.entity'; export class TestUtils { private app: INestApplication; diff --git a/test/admin/lancamento.e2e-spec.ts b/test/admin/lancamento.e2e-spec.ts new file mode 100644 index 00000000..0f457bc7 --- /dev/null +++ b/test/admin/lancamento.e2e-spec.ts @@ -0,0 +1,430 @@ +import { HttpStatus } from '@nestjs/common'; +import { differenceInSeconds } from 'date-fns'; +import * as fs from 'fs'; +import { generate } from 'gerador-validador-cpf'; +import * as path from 'path'; +import * as request from 'supertest'; +import * as XLSX from 'xlsx'; +import { ADMIN_EMAIL, ADMIN_PASSWORD, APP_URL, LICENSEE_CASE_ACCENT, LICENSEE_CPF_PERMIT_CODE, MAILDEV_URL, TO_UPDATE_PERMIT_CODE } from '../utils/constants'; +import { parseStringUpperUnaccent } from 'src/utils/string-utils'; + +describe('Admin Financeiro managing Lancamento (e2e)', () => { + const app = APP_URL; + const tempFolder = path.join(__dirname, 'temp'); + let apiToken: any = {}; + + beforeAll(async () => { + await request(app) + .post('/api/v1/auth/admin/email/login') + .send({ email: ADMIN_EMAIL, password: ADMIN_PASSWORD }) + .then(({ body }) => { + apiToken = body.token; + }); + + if (!fs.existsSync(tempFolder)) { + fs.mkdirSync(tempFolder); + } + }); + + describe('Setup tests', () => { + it('should have UTC and local timezones', () => { + new Date().getTimezoneOffset(); + expect(process.env.TZ).toEqual('UTC'); + expect(global.__localTzOffset).toBeDefined(); + }); + + it('should have mailDev server', async () => { + await request(MAILDEV_URL).get('').expect(HttpStatus.OK); + }); + }); + + describe('Manage users', () => { + test('Filter users', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 7 - GitHub} + */ async () => { + // Arrange + await request(app).get('/api/v1/test/users/reset-testing-users').expect(200); + const licensee = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: LICENSEE_CPF_PERMIT_CODE }) + .expect(({ body }) => { + expect(body.data?.length).toBe(1); + }) + .then(({ body }) => body.data); + const licenseePartOfName = 'user'; + const args = [ + { + filter: { name: parseStringUpperUnaccent(LICENSEE_CASE_ACCENT) }, + expect: (body: any) => expect(body.data.some((i: any) => i.fullName === LICENSEE_CASE_ACCENT)).toBeTruthy(), + }, + { + filter: { permitCode: licensee.permitCode }, + expect: (body: any) => expect(body.data.some((i: any) => i.permitCode === LICENSEE_CPF_PERMIT_CODE)).toBeTruthy(), + }, + { + filter: { name: licensee.fullName }, + expect: (body: any) => expect(body.data.some((i: any) => i.permitCode === LICENSEE_CPF_PERMIT_CODE)).toBeTruthy(), + }, + { + filter: { email: licensee.email }, + expect: (body: any) => expect(body.data.some((i: any) => i.permitCode === LICENSEE_CPF_PERMIT_CODE)).toBeTruthy(), + }, + { + filter: { name: licenseePartOfName, inviteStatus: 'queued' }, + expect: (body: any) => expect(body.data.some((i: any) => i.fullName === 'Queued user')).toBeTruthy(), + }, + { + filter: { name: licenseePartOfName, inviteStatus: 'sent' }, + expect: (body: any) => expect(body.data.some((i: any) => i.fullName === 'Sent user')).toBeTruthy(), + }, + { + filter: { name: licenseePartOfName, inviteStatus: 'used' }, + expect: (body: any) => expect(body.data.some((i: any) => i.fullName === 'Used user')).toBeTruthy(), + }, + ]; + + // Assert + for (const arg of args) { + await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query(arg.filter) + .expect(HttpStatus.OK) + .then(({ body }) => { + arg.expect(body); + return body.data; + }); + } + }, 20000); + }); + + describe('Upload users', () => { + let users: any[] = []; + + test(`Upload users, status = 'queued'`, /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + // Arrange + const randomCode = Math.random().toString(36).slice(-8); + const uploadUsers = [ + { + codigo_permissionario: `permitCode_${randomCode}`, + nome: `Café_${randomCode}`, + email: `user.${randomCode}@test.com`, + telefone: `219${Math.random().toString().slice(2, 10)}`, + cpf: generate(), + }, + ]; + const excelFilePath = path.join(tempFolder, 'newUsers.xlsx'); + const workbook = XLSX.utils.book_new(); + const worksheet = XLSX.utils.json_to_sheet(uploadUsers); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); + XLSX.writeFile(workbook, excelFilePath); + + // Assert + await request(app) + .post('/api/v1/users/upload') + .auth(apiToken, { + type: 'bearer', + }) + .attach('file', excelFilePath) + .expect(HttpStatus.CREATED) + .expect(({ body }) => { + expect(body.uploadedUsers).toEqual(1); + }); + + users = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: uploadUsers[0].codigo_permissionario }) + .expect(({ body }) => { + expect(body.data?.length).toBe(1); + expect(body.data[0]?.fullName).toEqual(parseStringUpperUnaccent(uploadUsers[0].nome)); + expect(body.data[0]?.aux_inviteStatus?.name).toEqual('queued'); + }) + .then(({ body }) => body.data); + }); + + test(`Resend new user invite, status = 'sent'`, /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 4 - GitHub} + */ async () => { + const newUser = users[0]; + expect(newUser?.id).toBeDefined(); + + await request(APP_URL) + .post('/api/v1/auth/email/resend') + .auth(apiToken, { + type: 'bearer', + }) + .send({ + id: newUser.id, + }) + .expect(HttpStatus.NO_CONTENT); + const forgotLocalDate = new Date(); + forgotLocalDate.setMinutes(forgotLocalDate.getMinutes() + global.__localTzOffset); + + newUser.hash = await request(MAILDEV_URL) + .get('/email') + .then(({ body }) => + (body as any[]) + .filter((letter: any) => letter.to[0].address.toLowerCase() === newUser.email.toLowerCase() && /.*conclude\-registration\/(\w+).*/g.test(letter.text) && differenceInSeconds(forgotLocalDate, new Date(letter.date)) <= 10) + .pop() + ?.text.replace(/.*conclude\-registration\/(\w+).*/g, '$1'), + ); + + await request(APP_URL) + .post(`/api/v1/auth/licensee/invite/${newUser.hash}`) + .expect(HttpStatus.OK) + .expect(({ body }) => { + expect(body.email).toEqual(newUser.email); + expect(body?.inviteStatus?.name).toEqual('sent'); + }); + + users[0] = newUser; + }); + + test(`New user conclude registration, status = 'used'`, /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 5 - GitHub} + */ async () => { + const newUser = users[0]; + expect(newUser?.hash).toBeDefined(); + + const newPassword = Math.random().toString(36).slice(-8); + await request(APP_URL) + .post(`/api/v1/auth/licensee/register/${newUser.hash}`) + .send({ password: newPassword }) + .expect(HttpStatus.OK) + .expect(({ body }) => { + expect(body.user.aux_inviteStatus?.name).toEqual('used'); + expect(body.token).toBeDefined(); + }); + + newUser.password = newPassword; + users[0] = newUser; + }); + + test('New user login', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 6 - GitHub} + */ async () => { + const newUser = users[0]; + await request(APP_URL) + .post(`/api/v1/auth/licensee/login`) + .send({ permitCode: newUser.permitCode, password: newUser.password }) + .expect(HttpStatus.OK) + .expect(({ body }) => { + expect(body.token).toBeDefined(); + }); + }); + + test('Upload invalid cpf, block upload', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + // Arrange + const randomCode1 = Math.random().toString(36).slice(-8); + const randomCode2 = Math.random().toString(36).slice(-8); + const uploadUsers = [ + { + codigo_permissionario: `permitCode_${randomCode1}`, + nome: `Café_${randomCode1}`, + email: `user.${randomCode1}@mai.com`, + telefone: `219${Math.random().toString().slice(2, 10)}`, + cpf: `invalid_cpf_${randomCode1}`, + }, + { + codigo_permissionario: `permitCode_${randomCode2}`, + nome: `Café_${randomCode2}`, + email: `user.${randomCode2}@mai.com`, + telefone: `219${Math.random().toString().slice(2, 10)}`, + cpf: `invalid_cpf_${randomCode2}`, + }, + ]; + const excelFilePath = path.join(tempFolder, 'newUsers.xlsx'); + const workbook = XLSX.utils.book_new(); + const worksheet = XLSX.utils.json_to_sheet(uploadUsers); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); + XLSX.writeFile(workbook, excelFilePath); + + // Assert + await request(app) + .post('/api/v1/users/upload') + .auth(apiToken, { + type: 'bearer', + }) + .attach('file', excelFilePath) + .expect(HttpStatus.UNPROCESSABLE_ENTITY) + .expect(({ body }) => { + const invalidRows = body.error.file.invalidRows; + expect(invalidRows.length).toBeGreaterThan(0); + expect(invalidRows[0].errors).toMatchObject({ + cpf: 'CPF inválido', + }); + }); + }); + + test('Patch cpf, throw error', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + const user = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: TO_UPDATE_PERMIT_CODE }) + .expect(({ body }) => { + expect(body.data.length).toBe(1); + }) + .then(({ body }) => body.data[0]); + + // Assert + await request(app) + .patch(`/api/v1/users/${user.id}`) + .auth(apiToken, { + type: 'bearer', + }) + .send({ + cpfCnpj: generate(), + }) + .expect(HttpStatus.UNPROCESSABLE_ENTITY); + }); + + test('Patch bankAccount, success', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + // Arrange + const random4Nums = Math.floor(1000 + Math.random() * 9000); + const user = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: TO_UPDATE_PERMIT_CODE }) + .expect(({ body }) => { + expect(body.data.length).toBe(1); + }) + .then(({ body }) => body.data[0]); + + // Assert + await request(app) + .patch(`/api/v1/users/${user.id}`) + .auth(apiToken, { + type: 'bearer', + }) + .send({ + bankAccount: random4Nums, + }) + .expect(HttpStatus.OK) + .expect(({ body }) => { + expect(body.bankAccount).toEqual(String(random4Nums)); + }); + }); + + test('Upload invalid cpf, block upload', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + // Arrange + const randomCode1 = Math.random().toString(36).slice(-8); + const randomCode2 = Math.random().toString(36).slice(-8); + const uploadUsers = [ + { + codigo_permissionario: `permitCode_${randomCode1}`, + nome: `Café_${randomCode1}`, + email: `user.${randomCode1}@mai.com`, + telefone: `219${Math.random().toString().slice(2, 10)}`, + cpf: `invalid_cpf_${randomCode1}`, + }, + { + codigo_permissionario: `permitCode_${randomCode2}`, + nome: `Café_${randomCode2}`, + email: `user.${randomCode2}@mai.com`, + telefone: `219${Math.random().toString().slice(2, 10)}`, + cpf: `invalid_cpf_${randomCode2}`, + }, + ]; + const excelFilePath = path.join(tempFolder, 'newUsers.xlsx'); + const workbook = XLSX.utils.book_new(); + const worksheet = XLSX.utils.json_to_sheet(uploadUsers); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); + XLSX.writeFile(workbook, excelFilePath); + + // Assert + await request(app) + .post('/api/v1/users/upload') + .auth(apiToken, { + type: 'bearer', + }) + .attach('file', excelFilePath) + .expect(HttpStatus.UNPROCESSABLE_ENTITY) + .expect(({ body }) => { + const invalidRows = body.error.file.invalidRows; + expect(invalidRows.length).toBeGreaterThan(0); + expect(invalidRows[0].errors).toMatchObject({ + cpf: 'CPF inválido', + }); + }); + }); + + test('Patch cpf, throw error', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + const user = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: TO_UPDATE_PERMIT_CODE }) + .expect(({ body }) => { + expect(body.data.length).toBe(1); + }) + .then(({ body }) => body.data[0]); + + // Assert + await request(app) + .patch(`/api/v1/users/${user.id}`) + .auth(apiToken, { + type: 'bearer', + }) + .send({ + cpfCnpj: generate(), + }) + .expect(HttpStatus.UNPROCESSABLE_ENTITY); + }); + + test('Patch bankAccount, success', /** + * Requirement: 2023/11/16 {@link https://github.com/RJ-SMTR/api-cct/issues/94#issuecomment-1815016208 #94, item 3 - GitHub} + */ async () => { + // Arrange + const random4Nums = Math.floor(1000 + Math.random() * 9000); + const user = await request(app) + .get('/api/v1/users/') + .auth(apiToken, { + type: 'bearer', + }) + .query({ permitCode: TO_UPDATE_PERMIT_CODE }) + .expect(({ body }) => { + expect(body.data.length).toBe(1); + }) + .then(({ body }) => body.data[0]); + + // Assert + await request(app) + .patch(`/api/v1/users/${user.id}`) + .auth(apiToken, { + type: 'bearer', + }) + .send({ + bankAccount: random4Nums, + }) + .expect(HttpStatus.OK) + .expect(({ body }) => { + expect(body.bankAccount).toEqual(String(random4Nums)); + }); + }); + }); +});