Skip to content

Commit

Permalink
wip: cnab module with no tests yet
Browse files Browse the repository at this point in the history
  • Loading branch information
yxuo committed Feb 20, 2024
1 parent c3f087b commit 6262362
Show file tree
Hide file tree
Showing 36 changed files with 984 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { MailCountModule } from './mail-count/mail-count.module';
import { CronJobsModule } from './cron-jobs/cron-jobs.module';
import { BigqueryModule } from './bigquery/bigquery.module';
import { TestModule } from './test/test.module';
import { CnabModule } from './cnab/cnab.module';

@Module({
imports: [
Expand Down Expand Up @@ -106,6 +107,7 @@ import { TestModule } from './test/test.module';
CronJobsModule,
BigqueryModule,
TestModule,
CnabModule,
],
})
export class AppModule {}
32 changes: 32 additions & 0 deletions src/cnab/cnab-remessa.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { CnabFile } from './types/cnab-file.type';
import { CnabRegistro } from './types/cnab-registro.type';
import { Cnab } from './cnab';

@Injectable()
export class CnabRemessaService {
/**
* Gerar arquivo remessa
*/
generateRemessaCnab(cnab: CnabFile) {
const plainCnab: CnabRegistro[] = [
cnab.headerArquivo,
...cnab.lotes.reduce(
(l: CnabRegistro[], i) => [
...l,
i.headerLote,
...i.registros,
i.trailerLote,
],
[],
),
cnab.trailerArquivo,
];
const cnabTextList: string[] = [];
for (const registro of plainCnab) {
cnabTextList.push(Cnab.getRegistroLine(registro));
}
const CNAB_EOL = '\r\n';
return cnabTextList.join(CNAB_EOL);
}
}
7 changes: 7 additions & 0 deletions src/cnab/cnab.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { CnabService } from './cnab.service';

@Module({
providers: [CnabService],
})
export class CnabModule {}
18 changes: 18 additions & 0 deletions src/cnab/cnab.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CnabService } from './cnab.service';

describe('CnabService', () => {
let service: CnabService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CnabService],
}).compile();

service = module.get<CnabService>(CnabService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions src/cnab/cnab.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class CnabService {}
3 changes: 3 additions & 0 deletions src/cnab/cnab.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
describe('cnab.ts', () => {
console.log('wip');
});
141 changes: 141 additions & 0 deletions src/cnab/cnab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { isDate } from 'date-fns';
import format from 'date-fns/format';
import { CnabFieldType } from './enums/cnab-field-type.enum';
import { CnabField } from './types/cnab-field.type';
import { CnabRegistro } from './types/cnab-registro.type';

export const Cnab = {
formatCnabNumber,
formatCnabText,
formatCurrency,
formatDate,
getFieldType,
getPictureValue,
getRegistroLine,
regexPicture,
validateFieldFormat,
};

/**
* Convert CNAB Registro into CNAB file line
*/
function getRegistroLine(registro: CnabRegistro) {
let line = '';
for (const value of Object.values(registro)) {
line += getPictureValue(value);
}
return line;
}

/**
* form CnabField get formatted value applying Picture
*/
function getPictureValue(item: CnabField) {
if (getFieldType(item) === CnabFieldType.Currency) {
return formatCurrency(item);
} else if (getFieldType(item) === CnabFieldType.Date) {
return formatDate(item);
} else if (getFieldType(item) === CnabFieldType.Number) {
return formatCnabNumber(item);
} else if (getFieldType(item) === CnabFieldType.Text) {
return formatCnabText(item);
}
}
function getFieldType(item: CnabField): CnabFieldType {
let result: CnabFieldType | undefined = undefined;
if (item.picture.indexOf('V9') > 0) {
result = CnabFieldType.Currency;
} else if (item.picture.startsWith('9')) {
if (item.dateFormat) {
result = CnabFieldType.Date;
} else {
result = CnabFieldType.Number;
}
} else if (item.picture.startsWith('X')) {
result = CnabFieldType.Text;
}

if (!result) {
throw new Error(`Cant recognize picture for ${item.picture}`);
}
validateFieldFormat(item);

return result;
}

function validateFieldFormat(item: CnabField) {
if (item.value === null) {
throw new Error('No formats allow null item value');
} else if (isNaN(Number(item.value))) {
throw new Error('Number format cant represent NaN value');
}
}

function regexPicture(exp: any, picture: any) {
const regex = new RegExp(exp);
const text = picture; // "9(10)V9(10)",
let result: RegExpExecArray | null = null;
const out: string[] = [];
// _tslint_:disable-next-line:no-conditional-assignment
while ((result = regex.exec(text))) {
out.push(result[1]);
}

return out;
}

/**
* Alfanumérico (picture X): alinhados à esquerda com brancos à direita. Preferencialmente,
* todos os caracteres devem ser maiúsculos. Aconselhase a não utilização de
* caracteres especiais (ex.: “Ç”, “?”,, etc) e
* acentuação gráfica (ex.: “Á”, “É”, “Ê”, etc) e os campos não utiliza dos deverão ser preenchidos com brancos.
*/
function formatCnabText(item: CnabField) {
const out = regexPicture(/X\((\w+?)\)/g, item.picture);
const size = Number(out[0]);
return String(item.value).slice(0, size).padEnd(size, ' ');
}

/** Numérico (picture 9): alinhado à direita com zeros à esquerda e os campos não utilizados deverão ser preenchidos
* com zeros. - Vírgula assumida (picture V): indica a posição da vírgula dentro de um campo numérico.
* Exemplo: num campo com picture “9(5)V9(2)”, o número “876,54” será representado por “0087654”
*/
function formatCnabNumber(item: CnabField): string {
const out = regexPicture(/9\((\w+?)\)/g, item.picture);
const size = Number(out[0]);
return String(Number(item.value).toFixed(0)).padStart(size, '0');
}

/**
* @param item for string data inputs use only numbers:
* - `HHMMSS` = "992359"
* - `ddMMyyyy` = "31121030"
* - `ddMMyy` = "311230"
* @returns
*/
function formatDate(item: CnabField) {
if (isDate(item.value) && item.dateFormat) {
return format(item.value, item.dateFormat);
} else {
const out = regexPicture(/9\((\w+?)\)/g, item.picture);
const itemValue = String(item.value);
const size = Number(out[0]);
return itemValue.slice(size).padStart(size, '0');
}
}

function formatCurrency(item: CnabField) {
const out = regexPicture(/9\((\w+?)\)/g, item.picture);
const integer = Number(out[0]);
const decimal = Number(out[1]);
const result = String(Number(item.value).toFixed(decimal))
.replace('.', '')
.padStart(integer + decimal, '0');
if (result.length > integer + decimal) {
throw new Error(
`Number "${result}" is too big to fit Currency Picture ` +
`${item.picture} (picture lenght: ${integer + decimal})`,
);
}
return result;
}
72 changes: 72 additions & 0 deletions src/cnab/consts/caixa/240/cnab-240-104-detalhe-a-template.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ICnab240_104DetalheA } from '../../../interfaces/cnab-240/104/cnab-240-104-detalhe-a.interface';

export const cnab240_104DetalheATemplate: ICnab240_104DetalheA = {
codigoBanco: { pos: [1, 3], picture: '9(003)', value: '104' },
loteServico: { pos: [4, 7], picture: '9(004)', value: ' ' },
codigoRegistro: { pos: [8, 8], picture: '9(001)', value: '3' },
nsr: { pos: [9, 13], picture: '9(005)', value: ' ' },
codigoSegmento: { pos: [14, 14], picture: 'X(001)', value: 'A' },
tipoMovimento: { pos: [15, 15], picture: '9(001)', value: ' ' },
codigoInstrucaoMovimento: { pos: [16, 17], picture: '9(002)', value: ' ' },
camaraCompensacao: { pos: [18, 20], picture: '9(003)', value: ' ' },
codigoBancoDestino: { pos: [21, 23], picture: '9(003)', value: ' ' },
codigoAgenciaDestino: { pos: [24, 28], picture: '9(005)', value: ' ' },
dvAgenciaDestino: { pos: [29, 29], picture: 'X(001)', value: ' ' },
contaCorrenteDestino: {
pos: [30, 41],
picture: '9(012)',
value: ' ',
},
dvContaDestino: { pos: [42, 42], picture: 'X(001)', value: ' ' },
dvAgenciaContaDestino: { pos: [43, 43], picture: 'X(001)', value: ' ' },
nomeTerceiro: {
pos: [44, 73],
picture: 'X(030)',
value: ' ',
},
numeroDocumento: { pos: [74, 79], picture: '9(006)', value: ' ' },
filler: { pos: [80, 92], picture: 'X(013)', value: ' ' },
tipoContaFinalidadeTed: { pos: [93, 93], picture: 'X(001)', value: ' ' },
dataVencimento: { pos: [94, 101], picture: '9(008)', value: ' ' },
tipoMoeda: { pos: [102, 104], picture: 'X(003)', value: ' ' },
quantidadeMoeda: {
pos: [105, 119],
picture: '9(010)V99999',
value: ' ',
},
valorLancamento: {
pos: [120, 134],
picture: '9(013)V99',
value: ' ',
},
numeroDocumentoBanco: {
pos: [135, 143],
picture: '9(009)',
value: ' ',
},
filler2: { pos: [144, 146], picture: 'X(003)', value: ' ' },
quantidadeParcelas: { pos: [147, 148], picture: '9(002)', value: ' ' },
indicadorBloqueio: { pos: [149, 149], picture: 'X(001)', value: ' ' },
indicadorFormaParcelamento: {
pos: [150, 150],
picture: '9(001)',
value: ' ',
},
periodoDiaVencimento: { pos: [151, 152], picture: 'X(002)', value: ' ' },
numeroParcela: { pos: [153, 154], picture: '9(002)', value: ' ' },
dataEfetivacao: { pos: [155, 162], picture: '9(008)', value: ' ' },
valorRealEfetivado: {
pos: [163, 177],
picture: '9(013)V99',
value: ' ',
},
informacao2: {
pos: [178, 217],
picture: 'X(040)',
value: ' ',
},
finalidadeDOC: { pos: [218, 219], picture: '9(002)', value: ' ' },
usoFebraban: { pos: [220, 229], picture: 'X(010)', value: ' ' },
avisoAoFavorecido: { pos: [230, 230], picture: '9(001)', value: ' ' },
ocorrencias: { pos: [231, 240], picture: 'X(010)', value: ' ' },
};
60 changes: 60 additions & 0 deletions src/cnab/consts/caixa/240/cnab-240-104-detalhe-b-template.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ICnab240CaixaDetalheB } from 'src/cnab/interfaces/cnab-240/104/cnab-240-104-detalhe-b.interface';

export const cnab240_104DetalheBTemplate: ICnab240CaixaDetalheB = {
codigoBanco: { pos: [1, 3], picture: '9(003)', value: '104' },
loteServico: { pos: [4, 7], picture: '9(004)', value: ' ' },
codigoRegistro: { pos: [8, 8], picture: '9(001)', value: ' ' },
nsr: { pos: [9, 13], picture: '9(005)', value: ' ' },
codigoSegmento: { pos: [14, 14], picture: 'X(001)', value: ' ' },
usoExclusivoFebraban: { pos: [15, 17], picture: 'X(003)', value: ' ' },
tipoInscricao: { pos: [18, 18], picture: '9(001)', value: ' ' },
numeroInscricao: {
pos: [19, 32],
picture: '9(014)',
value: ' ',
},
logradouro: {
pos: [33, 62],
picture: 'X(030)',
value: ' ',
},
numeroLocal: { pos: [63, 67], picture: '9(005)', value: ' ' },
complemento: { pos: [68, 82], picture: 'X(015)', value: ' ' },
bairro: { pos: [83, 97], picture: 'X(015)', value: ' ' },
cidade: { pos: [98, 117], picture: 'X(020)', value: ' ' },
cep: { pos: [118, 122], picture: '9(005)', value: ' ' },
complementoCep: { pos: [123, 125], picture: 'X(003)', value: ' ' },
siglaEstado: { pos: [126, 127], picture: 'X(002)', value: ' ' },
dataVencimento: { pos: [128, 135], picture: '9(008)', value: ' ' },
valorDocumento: {
pos: [136, 150],
picture: '9(013)V99',
value: ' ',
},
valorAbatimento: {
pos: [151, 165],
picture: '9(013)V99',
value: ' ',
},
valorDesconto: {
pos: [166, 180],
picture: '9(013)V99',
value: ' ',
},
valorMora: { pos: [181, 195], picture: '9(013)V99', value: ' ' },
valorMulta: {
pos: [196, 210],
picture: '9(013)V99',
value: ' ',
},
codigoDocumentoFavorecido: {
pos: [211, 225],
picture: 'X(015)',
value: ' ',
},
usoExclusivoFebraban2: {
pos: [226, 240],
picture: 'X(015)',
value: ' ',
},
};
Loading

0 comments on commit 6262362

Please sign in to comment.