diff --git a/src/bank-statements/dtos/bank-statement-previous-days.dto.ts b/src/bank-statements/dtos/bank-statement-previous-days.dto.ts index ead5a51e..320c5ae5 100644 --- a/src/bank-statements/dtos/bank-statement-previous-days.dto.ts +++ b/src/bank-statements/dtos/bank-statement-previous-days.dto.ts @@ -1,4 +1,5 @@ import { Ocorrencia } from "src/cnab/entity/pagamento/ocorrencia.entity"; +import { SetValue } from "src/utils/decorators/set-value.decorator"; export class BankStatementPreviousDaysDTO { constructor(dto?: BankStatementPreviousDaysDTO) { @@ -38,6 +39,7 @@ export class BankStatementPreviousDaysDTO { status: string | null; /** Bank error message */ + @SetValue((v) => Ocorrencia.toUserErrors(v)) errors: Ocorrencia[]; // Debug diff --git a/src/bank-statements/dtos/bank-statement.dto.ts b/src/bank-statements/dtos/bank-statement.dto.ts index 4dffcccd..8832b91f 100644 --- a/src/bank-statements/dtos/bank-statement.dto.ts +++ b/src/bank-statements/dtos/bank-statement.dto.ts @@ -1,4 +1,5 @@ import { Ocorrencia } from "src/cnab/entity/pagamento/ocorrencia.entity"; +import { SetValue } from "src/utils/decorators/set-value.decorator"; export class BankStatementDTO { constructor(dto?: BankStatementDTO) { @@ -15,7 +16,7 @@ export class BankStatementDTO { */ date: string; - + /** * Date when payment was made in bank. * @@ -28,13 +29,14 @@ export class BankStatementDTO { amount: number; paidAmount: number; - + /** Payment status */ status: string | null; - + /** Bank error message */ + @SetValue((v) => Ocorrencia.toUserErrors(v)) errors: Ocorrencia[]; - + // Debug ticketCount: number; } diff --git a/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts b/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts index 4c3f4ea3..069dee5a 100644 --- a/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts +++ b/src/bigquery/repositories/bigquery-ordem-pagamento.repository.ts @@ -89,7 +89,7 @@ export class BigqueryOrdemPagamentoRepository { SELECT CAST(t.data_ordem AS STRING) AS dataOrdem, t.id_consorcio AS idConsorcio, - CASE WHEN t.consorcio = 'STPL' THEN 'STPC' ELSE t.consorcio AS consorcio, + t.consorcio, t.id_operadora AS idOperadora, t.operadora AS operadora, t.id_ordem_pagamento AS idOrdemPagamento, diff --git a/src/bigquery/services/bigquery-ordem-pagamento.service.ts b/src/bigquery/services/bigquery-ordem-pagamento.service.ts index 2f73e733..fd8bfad7 100644 --- a/src/bigquery/services/bigquery-ordem-pagamento.service.ts +++ b/src/bigquery/services/bigquery-ordem-pagamento.service.ts @@ -22,7 +22,7 @@ export class BigqueryOrdemPagamentoService { const friday = isFriday(today) ? today : nextFriday(today); const sex = subDays(friday, 7 + daysBefore); - const qui = subDays(friday, 1 + daysBefore); + const qui = subDays(friday, 1); const ordemPgto = ( await this.bigqueryOrdemPagamentoRepository.findMany({ startDate: sex, diff --git a/src/cnab/cnab.service.ts b/src/cnab/cnab.service.ts index c31d524d..166494af 100644 --- a/src/cnab/cnab.service.ts +++ b/src/cnab/cnab.service.ts @@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common'; import { endOfDay, isFriday, + isSameDay, nextFriday, nextThursday, startOfDay, - subDays, + subDays } from 'date-fns'; import { BigqueryOrdemPagamentoDTO } from 'src/bigquery/dtos/bigquery-ordem-pagamento.dto'; import { BigqueryOrdemPagamentoService } from 'src/bigquery/services/bigquery-ordem-pagamento.service'; @@ -24,6 +25,7 @@ import { CustomLogger } from 'src/utils/custom-logger'; import { yearMonthDayToDate } from 'src/utils/date-utils'; import { asNumber } from 'src/utils/pipe-utils'; import { Between } from 'typeorm'; +import { ArquivoPublicacao } from './entity/arquivo-publicacao.entity'; import { ClienteFavorecido } from './entity/cliente-favorecido.entity'; import { ItemTransacaoAgrupado } from './entity/pagamento/item-transacao-agrupado.entity'; import { ItemTransacao } from './entity/pagamento/item-transacao.entity'; @@ -122,7 +124,13 @@ export class CnabService { * Atualiza a tabela TransacaoView */ async updateTransacaoViewBigquery(daysBack = 0) { - const transacoesBq = await this.bigqueryTransacaoService.getFromWeek(daysBack, false); + const transacoesBq = await this.bigqueryTransacaoService.getFromWeek( + daysBack, + false, + ); + // BigqueryTransacao.fromJson( + // `${__dirname}/test/cnab-service/data/data/update-transaca-view/bq-transacao-d0.json`, + // ); let chunkSize = 0; forChunk(transacoesBq, 1000, async (chunk) => { chunkSize += 1000; @@ -165,22 +173,57 @@ export class CnabService { async compareTransacaoViewPublicacao(daysBefore = 0) { const transacoesView = await this.getTransacoesViewWeek(daysBefore); - const publicacoes = await this.getPublicacoesWeek(daysBefore); + const publicacoes = this.getUniqueUpdatePublicacoes( + await this.getPublicacoesWeek(daysBefore), + ); for (const publicacao of publicacoes) { - const transacaoViewIds = transacoesView - .filter( - (transacaoView) => - transacaoView.idOperadora === - publicacao.itemTransacao.idOperadora && - transacaoView.idConsorcio === publicacao.itemTransacao.idConsorcio, - ) - .map((i) => i.id); - await this.transacaoViewService.updateMany(transacaoViewIds, { + const transacoes = transacoesView.filter( + (transacaoView) => + transacaoView.idOperadora === publicacao.itemTransacao.idOperadora && + transacaoView.idConsorcio === publicacao.itemTransacao.idConsorcio && + isSameDay( + // Se a data é a mesma (d+0 vs d+1) + transacaoView.datetimeProcessamento, // d+0 + subDays(publicacao.itemTransacao.dataOrdem, 1), // d+1 + ), + ); + const transacaoIds = transacoes.map((i) => i.id); + await this.transacaoViewService.updateMany(transacaoIds, { arquivoPublicacao: { id: publicacao.id }, }); } } + getUniqueUpdatePublicacoes(publicacoes: ArquivoPublicacao[]) { + const unique: ArquivoPublicacao[] = []; + publicacoes.forEach((publicacao) => { + const existing = ArquivoPublicacao.filterUnique(unique, publicacao)[0] as + | ArquivoPublicacao + | undefined; + const ocourences = ArquivoPublicacao.filterUnique( + publicacoes, + publicacao, + ).sort( + (a, b) => + b.itemTransacao.dataOrdem.getTime() - + a.itemTransacao.dataOrdem.getTime(), + ); + const paid = ocourences.filter((i) => i.isPago)[0] as + | ArquivoPublicacao + | undefined; + const noErrors = ocourences.filter((i) => !i.getIsError())[0] as + | ArquivoPublicacao + | undefined; + const recent = ocourences[0] as ArquivoPublicacao; + + if (!existing) { + const newPublicacao = paid || noErrors || recent; + unique.push(newPublicacao); + } + }); + return unique; + } + /** * Publicacao está associada com a ordem, portanto é sex-qui */ @@ -206,6 +249,13 @@ export class CnabService { return result; } + /** + * Salvar: + * - TransacaoAgrupado (CNAB) + * - ItemTransacaoAgrupado () + * - Transacao + * - + */ async saveAgrupamentos( ordem: BigqueryOrdemPagamentoDTO, pagador: Pagador, diff --git a/src/cnab/dto/pagamento/header-lote.dto.ts b/src/cnab/dto/pagamento/header-lote.dto.ts index be7bd72b..0ac34cee 100644 --- a/src/cnab/dto/pagamento/header-lote.dto.ts +++ b/src/cnab/dto/pagamento/header-lote.dto.ts @@ -1,4 +1,6 @@ import { IsNotEmpty, ValidateIf } from 'class-validator'; +import { Cnab104FormaLancamento } from 'src/cnab/enums/104/cnab-104-forma-lancamento.enum'; +import { CnabRegistros104Pgto } from 'src/cnab/interfaces/cnab-240/104/pagamento/cnab-registros-104-pgto.interface'; import { DeepPartial } from 'typeorm'; import { HeaderArquivo } from '../../entity/pagamento/header-arquivo.entity'; import { Pagador } from '../../entity/pagamento/pagador.entity'; @@ -49,4 +51,25 @@ export class HeaderLoteDTO { pagador?: DeepPartial; ocorrenciasCnab?: string; + + /** + * Usado apenas para geração do remessa, não é salvo no banco + * + * O formaLancamento depende do codigoBancoFavorecido. + * / + codigoBancoFavorecido: string; + + /** Usado apenas para geração do remessa, não é salvo no banco */ + formaLancamento: Cnab104FormaLancamento; + + /** Usado apenas para geração do remessa, não é salvo no banco */ + // itemTransacaoAgrupados: ItemTransacaoAgrupado[] = []; + + /** + * Usado apenas para geração do remessa, não é salvo no banco + * + * Após armazenar os itemTransacaoAgrupados, gera os respectivos detalhes + * e armazena neste DTO para gerar o CNAB. + */ + registros104: CnabRegistros104Pgto[] = []; } diff --git a/src/cnab/entity/arquivo-publicacao.entity.ts b/src/cnab/entity/arquivo-publicacao.entity.ts index 70650537..ee401238 100644 --- a/src/cnab/entity/arquivo-publicacao.entity.ts +++ b/src/cnab/entity/arquivo-publicacao.entity.ts @@ -14,6 +14,7 @@ import { UpdateDateColumn, } from 'typeorm'; import { ItemTransacao } from './pagamento/item-transacao.entity'; +import { isSameDay } from 'date-fns'; /** * Unique Jaé FK: idOrdemPagamento, idConsorcio, idOperadora @@ -76,4 +77,18 @@ export class ArquivoPublicacao extends EntityHelper { setReadValues() { this.valorRealEfetivado = asNullableStringOrNumber(this.valorRealEfetivado); } + + /** Evita acessar o itemTransacao.DetalheA.Ocorrencia para saber se teve erro. */ + getIsError() { + return !this.isPago && this.dataEfetivacao; + } + + public static filterUnique(publicacoes: ArquivoPublicacao[], compare: ArquivoPublicacao) { + return publicacoes.filter( + (p) => + p.itemTransacao.idConsorcio === compare.itemTransacao.idConsorcio && + p.itemTransacao.idOperadora === compare.itemTransacao.idOperadora && + isSameDay(p.itemTransacao.dataOrdem, compare.itemTransacao.dataOrdem), + ); + } } diff --git a/src/cnab/entity/pagamento/header-lote.entity.ts b/src/cnab/entity/pagamento/header-lote.entity.ts index df7c28bc..242ccbd9 100644 --- a/src/cnab/entity/pagamento/header-lote.entity.ts +++ b/src/cnab/entity/pagamento/header-lote.entity.ts @@ -42,11 +42,14 @@ export class HeaderLote extends EntityHelper { codigoConvenioBanco: string | null; @Column({ type: String, unique: false, nullable: true }) - tipoCompromisso: string | null; + tipoCompromisso: string; @Column({ type: String, unique: false, nullable: true }) parametroTransmissao: string; + @Column({ type: String, unique: false, nullable: true }) + formaLancamento: string; + @ManyToOne(() => Pagador, { eager: true }) @JoinColumn({ foreignKeyConstraintName: 'FK_HeaderLote_pagador_ManyToOne' }) pagador: Pagador; diff --git a/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts b/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts index ebd97b03..ef6d292a 100644 --- a/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts +++ b/src/cnab/entity/pagamento/item-transacao-agrupado.entity.ts @@ -14,6 +14,19 @@ import { import { ClienteFavorecido } from '../cliente-favorecido.entity'; import { TransacaoAgrupado } from './transacao-agrupado.entity'; +/** + * 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 { constructor(dto?: DeepPartial) { diff --git a/src/cnab/entity/pagamento/item-transacao.entity.ts b/src/cnab/entity/pagamento/item-transacao.entity.ts index dc24c9de..852f8eea 100644 --- a/src/cnab/entity/pagamento/item-transacao.entity.ts +++ b/src/cnab/entity/pagamento/item-transacao.entity.ts @@ -15,6 +15,18 @@ 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 + * - dataProcessamento + * - clienteFavorecido + */ @Entity() export class ItemTransacao extends EntityHelper { constructor(dto?: DeepPartial) { diff --git a/src/cnab/entity/pagamento/ocorrencia.entity.spec.ts b/src/cnab/entity/pagamento/ocorrencia.entity.spec.ts new file mode 100644 index 00000000..ffaef279 --- /dev/null +++ b/src/cnab/entity/pagamento/ocorrencia.entity.spec.ts @@ -0,0 +1,22 @@ +import { OcorrenciaEnum } from 'src/cnab/enums/ocorrencia.enum'; +import { Ocorrencia } from './ocorrencia.entity'; + +describe('Ocorrencia', () => { + describe('formatToUserErrors', () => { + it('Deve esconder 2 erros técnicos, e mostrar 1 erro genérico no lugar', () => { + const ocorrencias = [ + Ocorrencia.fromEnum(OcorrenciaEnum['AG']), + Ocorrencia.fromEnum(OcorrenciaEnum['02']), + Ocorrencia.fromEnum(OcorrenciaEnum['03']), + ]; + + const expectedOutput = [ + Ocorrencia.fromEnum(OcorrenciaEnum['AG']), + Ocorrencia.fromEnum(OcorrenciaEnum[' ']), + ]; + + const formattedErrors = Ocorrencia.toUserErrors(ocorrencias); + expect(formattedErrors).toEqual(expectedOutput); + }); + }); +}); diff --git a/src/cnab/entity/pagamento/ocorrencia.entity.ts b/src/cnab/entity/pagamento/ocorrencia.entity.ts index 88949e46..4452cc57 100644 --- a/src/cnab/entity/pagamento/ocorrencia.entity.ts +++ b/src/cnab/entity/pagamento/ocorrencia.entity.ts @@ -12,6 +12,20 @@ import { import { DetalheA } from './detalhe-a.entity'; import { Exclude } from 'class-transformer'; +const userErrors = [ + 'AG', + 'AL', + 'AM', + 'AN', + 'AO', + 'AS', + 'BG', + 'DA', + 'DB', + 'ZA', + 'ZY', +]; + @Entity() export class Ocorrencia extends EntityHelper { constructor(ocorrencias?: DeepPartial) { @@ -21,6 +35,13 @@ export class Ocorrencia extends EntityHelper { } } + public static fromEnum(value: OcorrenciaEnum) { + return new Ocorrencia({ + code: Enum.getKey(OcorrenciaEnum, value), + message: value, + }); + } + @Exclude() @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_Ocorrencia_id' }) id: number; @@ -85,4 +106,21 @@ export class Ocorrencia extends EntityHelper { } return joined; } + + /** + * Oculta erros técnicos do usuário, exibindo uma mensagem genérica no lugar + */ + public static toUserErrors(ocorrencias: Ocorrencia[]) { + let newOcorrencias = ocorrencias.map((j) => + !userErrors.includes(j.code) + ? new Ocorrencia({ ...j, code: ' ', message: OcorrenciaEnum[' '] }) + : j, + ); + newOcorrencias = newOcorrencias.reduce( + (l: Ocorrencia[], j) => + l.map((k) => k.code).includes(j.code) ? l : [...l, j], + [], + ); + return newOcorrencias; + } } diff --git a/src/cnab/entity/pagamento/transacao-agrupado.entity.ts b/src/cnab/entity/pagamento/transacao-agrupado.entity.ts index 4c617992..44225bac 100644 --- a/src/cnab/entity/pagamento/transacao-agrupado.entity.ts +++ b/src/cnab/entity/pagamento/transacao-agrupado.entity.ts @@ -33,6 +33,7 @@ export class TransacaoAgrupado extends EntityHelper { }) id: number; + /** sexta de pagamento (baseado no BigqueryOrdemPgto.dataOrdem) */ @Column({ type: Date, unique: false, nullable: true }) dataOrdem: Date; diff --git a/src/cnab/entity/pagamento/transacao.entity.ts b/src/cnab/entity/pagamento/transacao.entity.ts index 7bf2f702..d1b55a13 100644 --- a/src/cnab/entity/pagamento/transacao.entity.ts +++ b/src/cnab/entity/pagamento/transacao.entity.ts @@ -15,6 +15,22 @@ import { ItemTransacao } from './item-transacao.entity'; import { Pagador } from './pagador.entity'; import { TransacaoAgrupado } from './transacao-agrupado.entity'; +/** + * Representa um BigqueryOrdemPagamento (ou seja, um ArquivoPublicacao), + * associado a um pagador. + * + * Colunas: + * - dataOrdem: BqOrdem.dataOrdem + * + * Identificador: + * - transacaoAgrupado + * - idOrdemPagamento (diaPagamento) - BqOrdem + * + * Propósito dessa tabela: + * - Agrupar uma remessa CNAB por ordemPagamento + * - Não é usada para geração de remessa, mas é salva no banco mesmo assim + * - É usada no LancamentoFinanceiro + */ @Entity() export class Transacao extends EntityHelper { constructor(transacao?: Transacao | DeepPartial) { diff --git a/src/cnab/enums/ocorrencia.enum.ts b/src/cnab/enums/ocorrencia.enum.ts index 75a0b5ce..d61c669c 100644 --- a/src/cnab/enums/ocorrencia.enum.ts +++ b/src/cnab/enums/ocorrencia.enum.ts @@ -93,4 +93,6 @@ export enum OcorrenciaEnum { 'ZK' = 'Pagamento Rejeitado - Boleto Já Liquidado', 'ZY' = 'Pagamento Rejeitado - Beneficiário Divergente', 'ZW' = 'Dados do Pagador Incorretos', + /** Ocorrências X são erros customizados do CCT. Não existem no CNAB */ + ' ' = 'Ocorreu um erro! Por favor, aguarde a liberação do pagamento.', } diff --git a/src/cnab/interfaces/cnab-240/104/cnab-trailer-lote-104.interface.ts b/src/cnab/interfaces/cnab-240/104/cnab-trailer-lote-104.interface.ts index 4b3e4979..1219f13f 100644 --- a/src/cnab/interfaces/cnab-240/104/cnab-trailer-lote-104.interface.ts +++ b/src/cnab/interfaces/cnab-240/104/cnab-trailer-lote-104.interface.ts @@ -1,4 +1,7 @@ -import { CnabField, CnabFieldAs } from 'src/cnab/interfaces/cnab-all/cnab-field.interface'; +import { + CnabField, + CnabFieldAs, +} from 'src/cnab/interfaces/cnab-all/cnab-field.interface'; /** * @extends {CnabFields} @@ -9,6 +12,12 @@ export interface CnabTrailerLote104 { loteServico: CnabField; codigoRegistro: CnabFieldAs; usoExclusivoFebraban: CnabField; + /** + * Preencher com a quantidade de registros dentro do lote, + * considerar inclusive "HEADER" e "Trailer" do lote. + * + * Retornado conforme recebido. + */ quantidadeRegistrosLote: CnabField; /** Soma de todos os valores: detalhe A, I, O, N */ somatorioValores: CnabField; diff --git a/src/cnab/service/pagamento/header-lote.service.ts b/src/cnab/service/pagamento/header-lote.service.ts index 35701f72..5a91fdca 100644 --- a/src/cnab/service/pagamento/header-lote.service.ts +++ b/src/cnab/service/pagamento/header-lote.service.ts @@ -12,6 +12,7 @@ import { HeaderLoteDTO } from '../../dto/pagamento/header-lote.dto'; import { HeaderLote } from '../../entity/pagamento/header-lote.entity'; import { HeaderLoteRepository } from '../../repository/pagamento/header-lote.repository'; import { PagadorService } from './pagador.service'; +import { Cnab104FormaLancamento } from 'src/cnab/enums/104/cnab-104-forma-lancamento.enum'; const PgtoRegistros = Cnab104PgtoTemplates.file104.registros; @@ -27,11 +28,12 @@ export class HeaderLoteService { /** * From Transacao, HeaderArquivo transforms into HeaderLote. * - * `loteServico` should be set later before save! + * `loteServico` será atualizado com o valor gerado automaticamente em `updateHeaderLoteDTOFrom104()`! */ public getDTO( headerArquivo: HeaderArquivoDTO, pagador: Pagador, + formaLancamento: Cnab104FormaLancamento, ): HeaderLoteDTO { const dto = new HeaderLoteDTO({ codigoConvenioBanco: headerArquivo.codigoConvenio, @@ -42,6 +44,7 @@ export class HeaderLoteService { tipoInscricao: headerArquivo.tipoInscricao, headerArquivo: headerArquivo, loteServico: 1, + formaLancamento, }); return dto; } @@ -65,6 +68,7 @@ export class HeaderLoteService { numeroInscricao: lote.headerLote.numeroInscricao.stringValue, parametroTransmissao: lote.headerLote.parametroTransmissao.stringValue, tipoCompromisso: lote.headerLote.tipoCompromisso.stringValue, + formaLancamento: lote.headerLote.formaLancamento.stringValue, tipoInscricao: lote.headerLote.tipoInscricao.stringValue, pagador: { id: pagador.id }, }); @@ -88,6 +92,10 @@ export class HeaderLoteService { return await this.headerLoteRepository.save(dto); } + public async saveDto(dto: HeaderLoteDTO): Promise { + return await this.headerLoteRepository.save(dto); + } + public async findOne( fields: EntityCondition, ): Promise> { diff --git a/src/cnab/service/pagamento/item-transacao-agrupado.service.ts b/src/cnab/service/pagamento/item-transacao-agrupado.service.ts index 892b60a7..e17b35d6 100644 --- a/src/cnab/service/pagamento/item-transacao-agrupado.service.ts +++ b/src/cnab/service/pagamento/item-transacao-agrupado.service.ts @@ -60,7 +60,7 @@ export class ItemTransacaoAgrupadoService { return newItens; } - public async findManyByIdTransacao( + public async findManyByIdTransacaoAg( id_transacao: number, ): Promise { return await this.itemTransacaoAgRepository.findMany({ diff --git a/src/cnab/service/pagamento/remessa-retorno.service.spec.ts b/src/cnab/service/pagamento/remessa-retorno.service.spec.ts new file mode 100644 index 00000000..9778f527 --- /dev/null +++ b/src/cnab/service/pagamento/remessa-retorno.service.spec.ts @@ -0,0 +1,66 @@ +import { Provider } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { SettingEntity } from 'src/settings/entities/setting.entity'; +import { SettingsService } from 'src/settings/settings.service'; +import { User } from 'src/users/entities/user.entity'; +import { UsersService } from 'src/users/users.service'; +import { RemessaRetornoService } from './remessa-retorno.service'; + +xdescribe('RemessaRetornoService', () => { + let remessaRetornoService: RemessaRetornoService; + let settingsService: SettingsService; + let usersService: UsersService; + + beforeEach(async () => { + const configServiceMock = { + provide: ConfigService, + useValue: { + getOrThrow: jest.fn(), + }, + } as Provider; + const usersServiceMock = { + provide: UsersService, + useValue: { + findOne: jest.fn(), + }, + } as Provider; + + const module: TestingModule = await Test.createTestingModule({ + providers: [RemessaRetornoService, configServiceMock, usersServiceMock], + }).compile(); + + remessaRetornoService = module.get( + RemessaRetornoService, + ); + settingsService = module.get(SettingsService); + usersService = module.get(UsersService); + }); + + it('should be defined', () => { + expect(remessaRetornoService).toBeDefined(); + }); + + xdescribe('generateSaveRemessa', () => { + it('Deve gerar remessa com Transacoes da caixa e de outros bancos em lotes separados', async () => { + // Arrange + // const transacaoAg = new TransacaoAgrupado({ + // id: 1, + + // }) + jest + .spyOn(settingsService, 'findOneBySettingData') + .mockResolvedValue({ value: 'true' } as SettingEntity); + jest.spyOn(usersService, 'findOne').mockResolvedValue({ id: 1 } as User); + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2023-01-01').valueOf()); + + // Act + await remessaRetornoService.generateSaveRemessa(); + + // Assert + expect(usersService.findOne).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/cnab/service/pagamento/remessa-retorno.service.ts b/src/cnab/service/pagamento/remessa-retorno.service.ts index 461fdb5b..5d408053 100644 --- a/src/cnab/service/pagamento/remessa-retorno.service.ts +++ b/src/cnab/service/pagamento/remessa-retorno.service.ts @@ -7,6 +7,7 @@ import { ItemTransacaoAgrupado } from 'src/cnab/entity/pagamento/item-transacao- import { Pagador } from 'src/cnab/entity/pagamento/pagador.entity'; import { TransacaoAgrupado } from 'src/cnab/entity/pagamento/transacao-agrupado.entity'; import { TransacaoStatus } from 'src/cnab/entity/pagamento/transacao-status.entity'; +import { Cnab104FormaLancamento } from 'src/cnab/enums/104/cnab-104-forma-lancamento.enum'; import { TransacaoStatusEnum } from 'src/cnab/enums/pagamento/transacao-status.enum'; import { CnabTrailerArquivo104 } from 'src/cnab/interfaces/cnab-240/104/cnab-trailer-arquivo-104.interface'; import { CnabFile104Pgto } from 'src/cnab/interfaces/cnab-240/104/pagamento/cnab-file-104-pgto.interface'; @@ -15,7 +16,7 @@ import { getCnabFieldConverted } from 'src/cnab/utils/cnab/cnab-field-utils'; import { cnabSettings } from 'src/settings/cnab.settings'; import { SettingsService } from 'src/settings/settings.service'; import { CustomLogger } from 'src/utils/custom-logger'; -import { asString } from 'src/utils/pipe-utils'; +import { asNumber, asString } from 'src/utils/pipe-utils'; import { DeepPartial } from 'typeorm'; import { DetalheBDTO } from '../../dto/pagamento/detalhe-b.dto'; import { HeaderArquivoDTO } from '../../dto/pagamento/header-arquivo.dto'; @@ -35,7 +36,6 @@ import { HeaderLoteService } from './header-lote.service'; import { ItemTransacaoAgrupadoService } from './item-transacao-agrupado.service'; import { ItemTransacaoService } from './item-transacao.service'; import { TransacaoAgrupadoService } from './transacao-agrupado.service'; -import { TransacaoService } from './transacao.service'; const sc = structuredClone; const PgtoRegistros = Cnab104PgtoTemplates.file104.registros; @@ -52,20 +52,17 @@ export class RemessaRetornoService { private transacaoAgService: TransacaoAgrupadoService, private itemTransacaoService: ItemTransacaoService, private itemTransacaoAgService: ItemTransacaoAgrupadoService, - private transacaoService: TransacaoService, private detalheAService: DetalheAService, private detalheBService: DetalheBService, private settingsService: SettingsService, - private transacaoAgrupadoService: TransacaoAgrupadoService, - private itemTransacaoAgrupadoService: ItemTransacaoAgrupadoService, ) {} // #region generateSaveRemessa /** - * From Transacao generate: - * - Stringified Cnab to be exported - * - Cnab Tables DTO to be saved in database + * A partir de TransacaoAgrupado: + * - Gera string do CNAB para salvar no SFTP + * - Salva tabelas CNAB no banco */ public async generateSaveRemessa( transacaoAg: TransacaoAgrupado, @@ -77,24 +74,18 @@ export class RemessaRetornoService { ); // saveHeaderArquivo - await this.saveHeaderArquivo(headerArquivoDTO); + await this.headerArquivoService.save(headerArquivoDTO); const pagador = transacaoAg.pagador; - const headerLoteDTO = this.headerLoteService.getDTO( - headerArquivoDTO, + const savedHeaderLotes = await this.saveGetRemessaLotes( pagador, - ); - const savedHeaderLote = await this.saveHeaderLoteDTO(headerLoteDTO); - const detalhes = await this.saveListDetalhes( - savedHeaderLote.id, - transacaoAg, + headerArquivoDTO, ); // Generate Cnab const cnab104 = this.generateCnab104Pgto( headerArquivoDTO, - headerLoteDTO, - detalhes, + savedHeaderLotes, ); if (!cnab104) { @@ -113,10 +104,13 @@ export class RemessaRetornoService { headerArquivoDTO, processedCnab104.headerArquivo, ); - await this.updateHeaderLoteDTOFrom104( - headerLoteDTO, - processedCnab104.lotes[0].headerLote, - ); + for (const processedLote of processedCnab104.lotes) { + const savedLote = savedHeaderLotes.filter(i => i.formaLancamento === processedLote.headerLote.formaLancamento.value)[0]; + await this.updateHeaderLoteDTOFrom104( + savedLote, + processedLote.headerLote, + ); + } await this.transacaoAgService.save({ id: transacaoAg.id, status: new TransacaoStatus(TransacaoStatusEnum.remessa), @@ -125,6 +119,62 @@ export class RemessaRetornoService { return cnabString; } + /** + * Cada lote é separado por: + * - tipoCompromisso + * - formaLancamento + * + * Regras: + * + * tipoCompromisso: fixo + * formaLancamento: + * - se banco favorecido = Caixa, CreditoContaCorrente (01) + * (se banco do favorecido = banco do pagador - pagador é sempre Caixa) + * - senão, TED (41) + */ + async saveGetRemessaLotes( + pagador: Pagador, + headerArquivoDTO: HeaderArquivoDTO, + ) { + const transacaoAg = headerArquivoDTO.transacaoAgrupado as TransacaoAgrupado; + const itemTransacaoAgs = + await this.itemTransacaoAgService.findManyByIdTransacaoAg(transacaoAg.id); + + // Agrupar por Lotes + /** Agrupa por: formaLancamento */ + const lotes: HeaderLoteDTO[] = []; + for (const item of itemTransacaoAgs) { + const formaLancamento = + item.clienteFavorecido.codigoBanco === '104' + ? Cnab104FormaLancamento.CreditoContaCorrente + : Cnab104FormaLancamento.TED; + const loteUnique = lotes.filter( + (i) => i.formaLancamento === formaLancamento, + )[0] as HeaderLoteDTO | undefined; + + // Se existir, salva/insere os detalhes + if (loteUnique) { + const detalhes104 = await this.saveListDetalhes(loteUnique, [item]); + loteUnique.registros104.push(...detalhes104); + } + + // Senão, salva/gera novo Lote e salva/insere o primeiro detalhe + else { + const newLote = this.headerLoteService.getDTO( + headerArquivoDTO, + pagador, + formaLancamento, + ); + const savedHeaderLote = await this.headerLoteService.saveDto(newLote); + newLote.id = savedHeaderLote.id; + const detalhes104 = await this.saveListDetalhes(newLote, [item]); + newLote.registros104.push(...detalhes104); + lotes.push(newLote); + } + } + return lotes; + } + async convertCnabDetalheAToDTO( detalheA: CnabDetalheA_104, headerLoteId: number, @@ -183,21 +233,12 @@ export class RemessaRetornoService { }); } - async saveHeaderArquivo(headerArquivo: HeaderArquivoDTO) { - await this.headerArquivoService.save(headerArquivo); - } - - async saveHeaderLoteDTO(headerLote: HeaderLoteDTO) { - return await this.headerLoteService.save(headerLote); - } - /** - * Mount Cnab104 from tables + * Montar Cnab104 a partir dos DTOs de tabelas */ private generateCnab104Pgto( headerArquivo: HeaderArquivoDTO, - headerLoteDTO: HeaderLoteDTO, - detalhes: CnabRegistros104Pgto[], + headerLoteDTOs: HeaderLoteDTO[], ) { const headerArquivo104 = this.getHeaderArquivo104FromDTO(headerArquivo); const trailerArquivo104 = sc(PgtoRegistros.trailerArquivo); @@ -205,8 +246,7 @@ export class RemessaRetornoService { // Mount file104 const cnab104 = this.getCnabFilePgto( headerArquivo104, - headerLoteDTO, - detalhes, + headerLoteDTOs, trailerArquivo104, ); @@ -214,32 +254,30 @@ export class RemessaRetornoService { } /** - * Save ItemTransacao, save Detalhes - * Generate Detalhes + * Salva no banco e gera Detalhes104 para o lote + * + * Para cada ItemTransacaoAg: + * - Salva no banco + * - Gera Detalhes104 + * + * @returns Detalhes104 gerados a partir dos ItemTransacaoAg */ async saveListDetalhes( - headerLoteId: number, - transacaoAg: TransacaoAgrupado, + headerLoteDto: HeaderLoteDTO, + itemTransacoes: ItemTransacaoAgrupado[], ): Promise { let numeroDocumento = await this.detalheAService.getNextNumeroDocumento( new Date(), ); - - // Obter item transacao de transacoes - const itemTransacaoMany = - await this.itemTransacaoAgService.findManyByIdTransacao( - (transacaoAg as TransacaoAgrupado).id, - ); - // Para cada itemTransacao, cria detalhe const detalhes: CnabRegistros104Pgto[] = []; let itemTransacaoAgAux: ItemTransacaoAgrupado | undefined; - for (const itemTransacao of itemTransacaoMany) { + for (const itemTransacao of itemTransacoes) { // add valid itemTransacao itemTransacaoAgAux = itemTransacao as ItemTransacaoAgrupado; const detalhe = await this.saveDetalhes104( numeroDocumento, - headerLoteId, + headerLoteDto, itemTransacaoAgAux, ); if (detalhe) { @@ -293,19 +331,16 @@ export class RemessaRetornoService { getCnabFilePgto( headerArquivo104: CnabHeaderArquivo104, - headerLoteDTO: HeaderLoteDTO, - detalhes: CnabRegistros104Pgto[], + headerLoteDTOs: HeaderLoteDTO[], trailerArquivo104: CnabTrailerArquivo104, ) { const cnab104: CnabFile104Pgto = { headerArquivo: headerArquivo104, - lotes: [ - { - headerLote: this.getHeaderLoteFrom104(headerLoteDTO), - registros: detalhes, - trailerLote: sc(PgtoRegistros.trailerLote), - }, - ], + lotes: headerLoteDTOs.map((headerLote) => ({ + headerLote: this.getHeaderLoteFrom104(headerLote), + registros: headerLote.registros104, + trailerLote: sc(PgtoRegistros.trailerLote), + })), trailerArquivo: trailerArquivo104, }; cnab104.lotes = cnab104.lotes.filter((l) => l.registros.length > 0); @@ -372,6 +407,7 @@ export class RemessaRetornoService { headerLote104.parametroTransmissao.value = headerLoteDTO.parametroTransmissao; headerLote104.tipoInscricao.value = headerLoteDTO.tipoInscricao; + headerLote104.formaLancamento.value = headerLoteDTO.formaLancamento; // Pagador headerLote104.agenciaContaCorrente.value = headerArquivo.agencia; headerLote104.dvAgencia.value = headerArquivo.dvAgencia; @@ -403,7 +439,7 @@ export class RemessaRetornoService { * @returns null if failed ItemTransacao to CNAB */ public async saveDetalhes104( numeroDocumento: number, - headerLoteId: number, + headerLote: HeaderLoteDTO, itemTransacaoAg: ItemTransacaoAgrupado, ): Promise { const METHOD = 'getDetalhes104()'; @@ -441,7 +477,7 @@ export class RemessaRetornoService { const savedDetalheA = await this.saveDetalheA( detalheA, - headerLoteId, + asNumber(headerLote.id), itemTransacaoAg, ); diff --git a/src/cnab/templates/cnab-240/104/pagamento/cnab-header-lote-104-pgto-template.const.ts b/src/cnab/templates/cnab-240/104/pagamento/cnab-header-lote-104-pgto-template.const.ts index c4cc4680..6b13268e 100644 --- a/src/cnab/templates/cnab-240/104/pagamento/cnab-header-lote-104-pgto-template.const.ts +++ b/src/cnab/templates/cnab-240/104/pagamento/cnab-header-lote-104-pgto-template.const.ts @@ -12,6 +12,11 @@ import { CnabHeaderLote104Pgto } from 'src/cnab/interfaces/cnab-240/104/pagament * @version v032 micro - FEV/2024 * * Requirement: {@Link https://github.com/RJ-SMTR/api-cct/issues/233 #233, GitHub, 24/04/2023} + * + * Um lote por: + * - tipoCompromisso + * - formaLancamento + * */ export const cnabHeaderLote104PgtoTemplate: CnabHeaderLote104Pgto = { /** 1.01 */ @@ -29,7 +34,12 @@ export const cnabHeaderLote104PgtoTemplate: CnabHeaderLote104Pgto = { tipoOperacao: { pos: [9, 9], picture: 'X(001)', value: Cnab104TipoOperacao.Pagamento, ...Cnab.insert.d() }, /** 1.05 - 20 pagamento de fornecedor */ tipoServico: { pos: [10, 11], picture: '9(002)', value: Cnab104TipoServicoExtrato.PagamentoFornecedor, ...Cnab.insert.d() }, - /** 1.06 - Crédito em conta */ + /** + * 1.06 - Crédito em conta + * + * Regras: + * - Se o banco do favorecido + */ formaLancamento: { pos: [12, 13], picture: '9(002)', value: Cnab104FormaLancamento.TED, ...Cnab.insert.d() }, /** 1.07 - Fixo: 041 */ versaoLeiauteLote: { pos: [14, 16], picture: '9(003)', value: '041', ...Cnab.insert.d() }, diff --git a/src/cnab/utils/cnab/cnab-104-utils.ts b/src/cnab/utils/cnab/cnab-104-utils.ts index 71e5532e..12b350bb 100644 --- a/src/cnab/utils/cnab/cnab-104-utils.ts +++ b/src/cnab/utils/cnab/cnab-104-utils.ts @@ -51,6 +51,7 @@ export function stringifyCnab104File( ): [string, T] { const _cnab104 = process ? getProcessedCnab104(cnab104, cnabName) : cnab104; const cnab = getCnabFileFrom104(_cnab104); + // TODO: não está setando formaLancamento = 01 vs 41 no stringify CONTINUAR AQUI... const [cnabString, cnabFormatted] = stringifyCnabFile(cnab); const cnab104Formatted = getCnab104FromFile(cnabFormatted); return [cnabString, cnab104Formatted as T]; diff --git a/src/cnab/utils/cnab/cnab-utils.ts b/src/cnab/utils/cnab/cnab-utils.ts index 2b804172..7e4b8302 100644 --- a/src/cnab/utils/cnab/cnab-utils.ts +++ b/src/cnab/utils/cnab/cnab-utils.ts @@ -50,10 +50,9 @@ export function stringifyCnabFile(cnab: CnabFile): [string, CnabFile] { */ export function stringifyCnabRegistro(registro: CnabRegistro): string { validateCnabRegistro(registro); - return getSortedCnabFieldList(registro.fields).reduce( - (s, i) => s + stringifyCnabField(i), - '', - ); + const sorted = getSortedCnabFieldList(registro.fields); + const stringified = sorted.reduce((s, i) => s + stringifyCnabField(i), ''); + return stringified; } /** diff --git a/src/cron-jobs/cron-jobs.service.ts b/src/cron-jobs/cron-jobs.service.ts index 5efb73ef..1f30629a 100644 --- a/src/cron-jobs/cron-jobs.service.ts +++ b/src/cron-jobs/cron-jobs.service.ts @@ -96,8 +96,6 @@ export class CronJobsService implements OnModuleInit, OnModuleLoad { async onModuleLoad() { const THIS_CLASS_WITH_METHOD = 'CronJobsService.onModuleLoad'; - // await this.cnabService.updateTransacaoViewBigquery(35); - this.jobsConfig.push( { name: CrobJobsEnum.bulkSendInvites, diff --git a/src/database/migrations/1719431856725-HeaderLoteFormaLancamento.ts b/src/database/migrations/1719431856725-HeaderLoteFormaLancamento.ts new file mode 100644 index 00000000..c32399d7 --- /dev/null +++ b/src/database/migrations/1719431856725-HeaderLoteFormaLancamento.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class HeaderLoteFormaLancamento1719431856725 implements MigrationInterface { + name = 'HeaderLoteFormaLancamento1719431856725' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "header_lote" ADD "formaLancamento" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "header_lote" DROP COLUMN "formaLancamento"`); + } + +} diff --git a/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts b/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts index 3b0e3643..2f1b0e47 100644 --- a/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts +++ b/src/ticket-revenues/dtos/ticket-revenues-group.dto.ts @@ -1,6 +1,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 { Ocorrencia } from 'src/cnab/entity/pagamento/ocorrencia.entity'; /** * This object represents a group of `IBqTicketRevenues` @@ -150,5 +151,6 @@ export class TicketRevenuesGroupDto { /** * CNAB retorno error message list. */ + @SetValue((v) => Ocorrencia.toUserErrors(v)) errors: Ocorrencia[] = []; } diff --git a/src/utils/decorators/set-value.decorator.ts b/src/utils/decorators/set-value.decorator.ts new file mode 100644 index 00000000..6778735e --- /dev/null +++ b/src/utils/decorators/set-value.decorator.ts @@ -0,0 +1,12 @@ +import { Transform } from 'class-transformer'; + +/** + * @param setValue Callback to set value based on original value + */ +export function SetValue(setValue: (val: any, obj: any) => any) { + return function (target: any, propertyKey: string) { + Transform(({ obj }) => setValue(obj[propertyKey], obj), { + toPlainOnly: true, + })(target, propertyKey); + }; +}