From d86e2ec737775f69682b6183f3440b17c62e14fa Mon Sep 17 00:00:00 2001 From: Breno Martinusso Date: Tue, 18 Jun 2019 18:25:04 -0300 Subject: [PATCH] Add remessa --- beneficiario.go | 14 +++ boleto/boleto.go | 2 - boleto/boleto_test.go | 8 +- boleto/codigo_barras.go | 8 +- boleto/conta.go | 12 +++ boleto/santander.go | 10 +- boleto/santander_test.go | 2 +- boleto/utils.go | 98 ----------------- boleto/utils_test.go | 160 --------------------------- conta.go | 84 ++++++++++++++- conta_test.go | 3 +- boleto/pagador.go => pagador.go | 22 +++- remessa/pagamento.go | 128 ++++++++++++++++++++++ remessa/pagamento_test.go | 17 +++ remessa/remessa.go | 84 ++++++++++----- remessa/santander.go | 17 --- remessa/santander_cnab400.go | 174 ++++++++++++++++++++++++++++++ remessa/santander_cnab400_test.go | 122 +++++++++++++++++++++ remessa/santander_test.go | 12 --- remessa/utils.go | 21 ++++ remessa/utils_test.go | 20 ++++ utils.go | 119 ++++++++++++++++++++ utils_test.go | 163 ++++++++++++++++++++++++++++ 23 files changed, 968 insertions(+), 332 deletions(-) create mode 100644 beneficiario.go create mode 100644 boleto/conta.go rename boleto/pagador.go => pagador.go (52%) create mode 100644 remessa/pagamento.go create mode 100644 remessa/pagamento_test.go delete mode 100644 remessa/santander.go create mode 100644 remessa/santander_cnab400.go create mode 100644 remessa/santander_cnab400_test.go delete mode 100644 remessa/santander_test.go create mode 100644 remessa/utils.go create mode 100644 remessa/utils_test.go create mode 100644 utils.go create mode 100644 utils_test.go diff --git a/beneficiario.go b/beneficiario.go new file mode 100644 index 0000000..c52239d --- /dev/null +++ b/beneficiario.go @@ -0,0 +1,14 @@ +package cobranca + +type Beneficiario struct { + Nome string + Documento string +} + +func (b Beneficiario) TipoInscricao() string { + return tipoInscricao(b.Documento) +} + +func (b Beneficiario) GetDocumento() string { + return SemMascara(b.Documento) +} diff --git a/boleto/boleto.go b/boleto/boleto.go index 74826d3..a0ddac6 100644 --- a/boleto/boleto.go +++ b/boleto/boleto.go @@ -21,8 +21,6 @@ type Boleto struct { nossoNumero string campoLivre string conta cobranca.Conta - pagador Pagador - avalista Avalista codigoBarras string linhaDigitavel string } diff --git a/boleto/boleto_test.go b/boleto/boleto_test.go index 16aa9f2..4f949ba 100644 --- a/boleto/boleto_test.go +++ b/boleto/boleto_test.go @@ -14,7 +14,7 @@ func TestNewBoleto(t *testing.T) { today := time.Now().Local().Format(dateFormat) valor := 273.71 - c := cobranca.NewConta(cobranca.CodigoSantander, "4042", "61900", "101", "0282033") + c := NewConta(cobranca.CodigoSantander, "4042", "61900", "101", "0282033") b, _ := NewBoleto(valor, venc, 1984, c) @@ -52,14 +52,14 @@ func TestNewBoleto(t *testing.T) { } func TestBoletoBancoNaoSuportado(t *testing.T) { - c := cobranca.NewConta("999", "4042", "61900", "101", "0282033") + c := NewConta("999", "4042", "61900", "101", "0282033") _, err := NewBoleto(1, time.Now(), 1984, c) if err == nil { t.Errorf("Should'n be nil") } - if err.Error() != bancoNaoSuportado { - t.Errorf("Expected '%s' got '%s'", bancoNaoSuportado, err.Error()) + if err.Error() != cobranca.BancoNaoSuportado { + t.Errorf("Expected '%s' got '%s'", cobranca.BancoNaoSuportado, err.Error()) } } diff --git a/boleto/codigo_barras.go b/boleto/codigo_barras.go index e96c586..e3ac1e0 100644 --- a/boleto/codigo_barras.go +++ b/boleto/codigo_barras.go @@ -4,6 +4,8 @@ import ( "math" "strconv" "time" + + "github.com/padmoney/cobranca" ) type CodigoBarras struct { @@ -15,10 +17,10 @@ type CodigoBarras struct { } func (cb CodigoBarras) String() string { - banco := StrPad(cb.Banco, 3, "0", StrPadLeft) + banco := cobranca.StrPad(cb.Banco, 3, "0", cobranca.StrPadLeft) val := math.Round(cb.Valor * 100) valStr := strconv.Itoa(int(val)) - valStr = Zeros(valStr, 10) + valStr = cobranca.Zeros(valStr, 10) fatorVencimento := FatorVencimento(cb.DataVencimento) s := banco + cb.Moeda() + fatorVencimento + valStr + cb.CampoLivre @@ -42,5 +44,5 @@ func FatorVencimento(d time.Time) string { delta := d.Sub(base) days := delta.Hours() / 24 s := strconv.Itoa(int(days)) - return Zeros(s, 4) + return cobranca.Zeros(s, 4) } diff --git a/boleto/conta.go b/boleto/conta.go new file mode 100644 index 0000000..788594e --- /dev/null +++ b/boleto/conta.go @@ -0,0 +1,12 @@ +package boleto + +import "github.com/padmoney/cobranca" + +func NewConta(banco string, agencia, contaCorrente, carteira, convenio string) cobranca.Conta { + return cobranca.Conta{ + Banco: banco, + Agencia: agencia, + ContaCorrente: contaCorrente, + Carteira: carteira, + Convenio: convenio} +} diff --git a/boleto/santander.go b/boleto/santander.go index 8f2d574..8dc38bf 100644 --- a/boleto/santander.go +++ b/boleto/santander.go @@ -2,6 +2,8 @@ package boleto import ( "strconv" + + "github.com/padmoney/cobranca" ) type Santander struct { @@ -13,9 +15,9 @@ func NewSantander(boleto Boleto) Santander { } func (s Santander) CampoLivre() string { - convenio := Zeros(s.boleto.Conta().Convenio, 7) - carteira := Zeros(s.boleto.Conta().Carteira, 3) - nossoNumero := OnlyNumbers(s.NossoNumero()) + convenio := cobranca.Zeros(s.boleto.Conta().Convenio, 7) + carteira := cobranca.Zeros(s.boleto.Conta().Carteira, 3) + nossoNumero := cobranca.OnlyNumbers(s.NossoNumero()) return "9" + convenio + nossoNumero + "0" + carteira } @@ -30,5 +32,5 @@ func (s Santander) NossoNumero() string { func (s Santander) codigoBarrasSemDV() string { n := strconv.FormatInt(s.boleto.Numero(), 10) - return Zeros(n, 12) + return cobranca.Zeros(n, 12) } diff --git a/boleto/santander_test.go b/boleto/santander_test.go index f06a0e5..2335c87 100644 --- a/boleto/santander_test.go +++ b/boleto/santander_test.go @@ -32,5 +32,5 @@ func TestBoletoSantanderSemRegistro(t *testing.T) { } func contaSantanderFixture(carteira, convenio string) cobranca.Conta { - return cobranca.NewConta(cobranca.CodigoSantander, "4042", "61900", carteira, convenio) + return NewConta(cobranca.CodigoSantander, "4042", "61900", carteira, convenio) } diff --git a/boleto/utils.go b/boleto/utils.go index 23050f0..defdd23 100644 --- a/boleto/utils.go +++ b/boleto/utils.go @@ -1,52 +1,11 @@ package boleto import ( - "fmt" "math" - "regexp" "strconv" - "strings" "time" ) -type PadType string - -const ( - StrPadLeft PadType = "str_pad_left" - StrPadRight PadType = "str_pad_right" -) - -var ( - transliterations = map[string]*regexp.Regexp{ - "A": regexp.MustCompile(`À|Á|Â|Ã|Ä|à|á|â|ã|ä`), - "AA": regexp.MustCompile(`Å|å`), - "AE": regexp.MustCompile(`Æ|æ`), - "C": regexp.MustCompile(`Ç|ç`), - "E": regexp.MustCompile(`È|É|Ê|Ë|è|é|ê|ë`), - "D": regexp.MustCompile(`Ð|ð`), - "I": regexp.MustCompile(`Ì|Í|Î|Ï|ì|í|î|ï`), - "L": regexp.MustCompile(`Ł|ł`), - "N": regexp.MustCompile(`Ñ|ñ|ń`), - "O": regexp.MustCompile(`Ò|Ó|Ô|Õ|Ö|ò|ó|ô|õ|ö|ō`), - "OE": regexp.MustCompile(`Œ|Ø|œ|ø`), - "Th": regexp.MustCompile(`Þ`), - "U": regexp.MustCompile(`Ù|Ú|Û|Ü|ù|ú|û|ü|ũ|ū|ŭ|ů|ű|ų`), - "Y": regexp.MustCompile(`Ý|ý|ÿ`), - "S": regexp.MustCompile(`ś`), - "SS": regexp.MustCompile(`ß`), - "Z": regexp.MustCompile(`ż`), - "TH": regexp.MustCompile(`þ`), - } -) - -func Brancos(s string, l int) string { - s = Sanitize(s) - if len(s) > l { - return s[len(s)-l:] - } - return StrPad(s, l, " ", StrPadRight) -} - func Date(year, month, day int) time.Time { return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) } @@ -63,11 +22,6 @@ func Explode(str string) ([]int, error) { return data, nil } -func OnlyNumbers(s string) string { - reg, _ := regexp.Compile("[^0-9]+") - return reg.ReplaceAllString(s, "") -} - func ParseDate(s string) time.Time { if s == "000000" { return time.Time{} @@ -100,55 +54,3 @@ func Round(v float64, places int) float64 { } return round / pow } - -// Sanitize replaces non-ASCII characters with an ASCII approximation, or if none exists, to “?”. -func Sanitize(word string) string { - for repl, regex := range transliterations { - word = regex.ReplaceAllString(word, repl) - } - - var safe string - for _, r := range word { - if isAscii(r) { - safe += string(r) - } else { - safe += "?" - } - } - return strings.ToUpper(safe) -} - -func isAscii(s rune) bool { - return int(s) >= 32 && int(s) <= 126 -} - -func StrPad(s string, padLen int, padStr string, padType PadType) string { - LenS := len(s) - if LenS == padLen { - return s - } - - if LenS > padLen { - if padType == StrPadLeft { - return s[LenS-padLen:] - } else { - return s[0:padLen] - } - } - - c := (padLen - len(s)) / len(padStr) - r := strings.Repeat(padStr, c) - if padType == StrPadLeft { - return r + s - } else { - return s + r - } -} - -func Zeros(s string, l int) string { - if len(s) > l { - return s[len(s)-l:] - } - f := "%0" + strconv.Itoa(l) + "s" - return fmt.Sprintf(f, s) -} diff --git a/boleto/utils_test.go b/boleto/utils_test.go index ec21828..9536f35 100644 --- a/boleto/utils_test.go +++ b/boleto/utils_test.go @@ -6,21 +6,6 @@ import ( "time" ) -func TestBrancos(t *testing.T) { - qote := "a long time ago" - got := Brancos(qote, 20) - expected := "A LONG TIME AGO " - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - got = Brancos("a long time ago", 8) - expected = "TIME AGO" - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } -} - func TestDate(t *testing.T) { now := time.Now() expected := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) @@ -44,14 +29,6 @@ func TestExplode(t *testing.T) { } } -func TestOnlyNumbers(t *testing.T) { - expected := "1234567890" - got := OnlyNumbers("1a2!3@4#5%6&7*8()9-0{}ç^;.,?/<>|") - if got != expected { - t.Errorf("Expected '%v' got '%v'", expected, got) - } -} - func TestParseDate(t *testing.T) { expected := time.Time{} got := ParseDate("000000") @@ -95,140 +72,3 @@ func TestRound(t *testing.T) { } } } - -func TestSanitize(t *testing.T) { - equivalents := map[string]string{ - "À": "A", - "Á": "A", - "Â": "A", - "Ã": "A", - "Ä": "A", - "Å": "AA", - "Æ": "AE", - "Ç": "C", - "È": "E", - "É": "E", - "Ê": "E", - "Ë": "E", - "Ì": "I", - "Í": "I", - "Î": "I", - "Ï": "I", - "Ð": "D", - "Ł": "L", - "Ñ": "N", - "Ò": "O", - "Ó": "O", - "Ô": "O", - "Õ": "O", - "Ö": "O", - "Ø": "OE", - "Ù": "U", - "Ú": "U", - "Ü": "U", - "Û": "U", - "Ý": "Y", - "Þ": "TH", - "ß": "SS", - "à": "A", - "á": "A", - "â": "A", - "ã": "A", - "ä": "A", - "å": "AA", - "æ": "AE", - "ç": "C", - "è": "E", - "é": "E", - "ê": "E", - "ë": "E", - "ì": "I", - "í": "I", - "î": "I", - "ï": "I", - "ð": "D", - "ł": "L", - "ñ": "N", - "ń": "N", - "ò": "O", - "ó": "O", - "ô": "O", - "õ": "O", - "ō": "O", - "ö": "O", - "ø": "OE", - "ś": "S", - "ù": "U", - "ú": "U", - "û": "U", - "ū": "U", - "ü": "U", - "ý": "Y", - "þ": "TH", - "ÿ": "Y", - "ż": "Z", - "Œ": "OE", - "œ": "OE", - "żż": "ZZ", - "Hello, 世界": "HELLO, ??", - } - - for k, expected := range equivalents { - got := Sanitize(k) - if got != expected { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - } -} - -func TestStrPad(t *testing.T) { - expected := "0000012345-6" - got := StrPad("0000012345-6", 12, "0", StrPadLeft) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - expected = "0000012345-6" - got = StrPad("12345-6", 12, "0", StrPadLeft) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - expected = "6789012345-6" - got = StrPad("0123456789012345-6", 12, "0", StrPadLeft) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - expected = "Padmoney " - got = StrPad("Padmoney", 10, " ", StrPadRight) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - expected = "Clap Clap Clap Clap " - got = StrPad("", 20, "Clap ", StrPadRight) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - expected = "Padmoney" - got = StrPad("Padmoney Payment Security", 8, " ", StrPadRight) - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } -} - -func TestZeros(t *testing.T) { - got := Zeros("12345-6", 12) - expected := "0000012345-6" - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } - - got = Zeros("1234567890", 5) - expected = "67890" - if expected != got { - t.Errorf("Expected '%s' got '%s'", expected, got) - } -} diff --git a/conta.go b/conta.go index adc7c35..a8369c9 100644 --- a/conta.go +++ b/conta.go @@ -1,5 +1,7 @@ package cobranca +import "strings" + type Conta struct { Banco string Agencia string @@ -7,13 +9,91 @@ type Conta struct { Carteira string Variacao string Convenio string + EspecieTitulo string + Aceite bool + Beneficiario Beneficiario } -func NewConta(banco string, agencia, contaCorrente, carteira, convenio string) Conta { +func NewConta(banco string, agencia, contaCorrente, carteira, convenio string, beneficiario Beneficiario) Conta { return Conta{ Banco: banco, Agencia: agencia, ContaCorrente: contaCorrente, Carteira: carteira, - Convenio: convenio} + Convenio: convenio, + Beneficiario: beneficiario} +} + +// Aceite do título +// N - Sem aceite +// A - Com aceite - Indica o reconhecimento formal (assinatura no documento) do sacado no título +func (c Conta) GetAceite() string { + if c.Aceite { + return "A" + } + return "N" +} + +// GetEspecieTitulo retorna a espécie de título de acordo com cada banco +// Banco do Brasil +// 01 - Duplicata Mercantil +// 02 - Nota Promissória +// 03 - Nota de Seguro +// 05 - Recibo +// 08 - Letra de Câmbio +// 09 - Warrant +// 10 - Cheque +// 12 - Duplicata de Serviço +// 13 - Nota de Débito +// 15 - Apólice de Seguro +// 25 - Dívida Ativa da União +// 26 - Dívida Ativa de Estado +// 27 - Dívida Ativa de Município +// +// Santander - 033 +// 01 - DUPLICATA +// 02 - NOTA PROMISSÓRIA +// 03 - APÓLICE / NOTA DE SEGURO +// 05 - RECIBO +// 06 - DUPLICATA DE SERVIÇO +// 07 - LETRA DE CAMBIO +// 08 - BDP - BOLETO DE PROPOSTA - ( NOTA 6) +// 19 - BCC – BOLETO CARTÃO DE CRÉDITO ( NOTA 8) +func (c Conta) GetEspecieTitulo() string { + if c.EspecieTitulo == "" { + return "01" + } + return c.EspecieTitulo +} + +func (c Conta) NumeroAgencia() string { + a := strings.Split(c.Agencia, "-") + if len(a) > 0 { + return a[0] + } + return "" +} + +func (c Conta) AgenciaDigito() string { + d := strings.Split(c.Agencia, "-") + if len(d) > 1 { + return d[1] + } + return "" +} + +func (c Conta) Numero() string { + n := strings.Split(c.ContaCorrente, "-") + if len(n) > 0 { + return n[0] + } + return "" +} + +func (c Conta) Digito() string { + d := strings.Split(c.ContaCorrente, "-") + if len(d) > 1 { + return d[1] + } + return "" } diff --git a/conta_test.go b/conta_test.go index 491e6e7..bd4af9a 100644 --- a/conta_test.go +++ b/conta_test.go @@ -9,8 +9,9 @@ func TestNewConta(t *testing.T) { contaCorrente := "567890" carteira := "01" convenio := "123456789" + beneficiario := Beneficiario{} - c := NewConta(CodigoBancoBrasil, agencia, contaCorrente, carteira, convenio) + c := NewConta(CodigoBancoBrasil, agencia, contaCorrente, carteira, convenio, beneficiario) if c.Banco != CodigoBancoBrasil { t.Errorf("Expected '%s' got '%s'", CodigoBancoBrasil, c.Banco) diff --git a/boleto/pagador.go b/pagador.go similarity index 52% rename from boleto/pagador.go rename to pagador.go index 4bf381b..5c41f5c 100644 --- a/boleto/pagador.go +++ b/pagador.go @@ -1,4 +1,4 @@ -package boleto +package cobranca type Avalista struct { Nome string @@ -23,3 +23,23 @@ func (p Pagador) PessoaFisica() bool { doc := p.GetDocumento() return len(doc) == 11 } + +func (p Pagador) TipoInscricao() string { + return tipoInscricao(p.Documento) +} + +// TipoInscricao retorna o tipo de inscrição +// 00 - ISENTO +// 01 - CPF +// 02 - CNPJ +func tipoInscricao(doc string) string { + doc = SemMascara(doc) + switch len(doc) { + case 11: + return "01" + case 14: + return "02" + default: + return "00" + } +} diff --git a/remessa/pagamento.go b/remessa/pagamento.go new file mode 100644 index 0000000..f143a3d --- /dev/null +++ b/remessa/pagamento.go @@ -0,0 +1,128 @@ +package remessa + +import ( + "fmt" + "time" + + "github.com/padmoney/cobranca" +) + +type Pagamento struct { + comando string + valor float64 + dataVencimento time.Time + numero string + nossoNumero string + diasProtesto int + percentualMoraAoMes float64 + percentualMulta float64 + pagador cobranca.Pagador + avalista cobranca.Avalista +} + +// NewPagamento retorna um pagamento +// comando: baixa, registro +func NewPagamento( + comando string, + valor float64, + dataVencimento time.Time, + numero string, + nossoNumero string, + diasProtesto int, + percentualMoraAoMes float64, + percentualMulta float64, + pagador cobranca.Pagador, + avalista cobranca.Avalista) Pagamento { + return Pagamento{ + comando: comando, + valor: valor, + dataVencimento: dataVencimento, + numero: numero, + nossoNumero: nossoNumero, + diasProtesto: diasProtesto, + percentualMoraAoMes: percentualMoraAoMes, + percentualMulta: percentualMulta, + pagador: pagador, + avalista: avalista, + } +} + +/** + * Mensagem Avalista + * Para CNPJ + * Posição 352 à 372 - Preencher com o nome do Sacador/Avalista. + * Posição 373 - Preencher com "espaço" + * Posição 374 à 377 - Preencher com o literal "CNPJ" + * Posição 378 à 391 - Preencher com o número do CNPJ do Sacador/Avalista + * Para CPF + * Posição 352 à 376 - Preencher com o nome do Sacador/Avalista + * Posição 377 - Preencher com "espaço" + * Posição 378 à 380 - Preencher com o literal "CPF" + * Posição 381 à 391 - Preencher com o número do CPF do Sacador/Avalista + * + * @return Pessoa Retorna o avalista + */ +func (p Pagamento) MensagemAvalista() string { + doc := cobranca.SemMascara(p.avalista.Documento) + var brancos int + var tipoDoc string + switch len(doc) { + case 11: + tipoDoc = "CPF" + brancos = 26 + case 14: + tipoDoc = "CNPJ" + brancos = 22 + default: + return p.avalista.Nome + } + return fmt.Sprintf("%s%s%s", + cobranca.Brancos(p.avalista.Nome, brancos), + tipoDoc, + doc) +} + +func (p Pagamento) JurosMoraPorDiaAtraso() float64 { + percJurosMoraDia := (p.percentualMoraAoMes / 100.0) / 30.0 + return percJurosMoraDia * p.valor +} + +func (p Pagamento) Comando() string { + return p.comando +} + +func (p Pagamento) Valor() float64 { + return p.valor +} + +func (p Pagamento) DataVencimento() time.Time { + return p.dataVencimento +} + +func (p Pagamento) Numero() string { + return p.numero +} + +func (p Pagamento) NossoNumero() string { + return p.nossoNumero +} + +func (p Pagamento) DiasProtesto() int { + return p.diasProtesto +} + +func (p Pagamento) PercentualMoraAoMes() float64 { + return p.percentualMoraAoMes +} + +func (p Pagamento) PercentualMulta() float64 { + return p.percentualMulta +} + +func (p Pagamento) Pagador() cobranca.Pagador { + return p.pagador +} + +func (p Pagamento) Avalista() cobranca.Avalista { + return p.avalista +} diff --git a/remessa/pagamento_test.go b/remessa/pagamento_test.go new file mode 100644 index 0000000..c577128 --- /dev/null +++ b/remessa/pagamento_test.go @@ -0,0 +1,17 @@ +package remessa + +import ( + "testing" + + "github.com/padmoney/cobranca" +) + +func TestMensagemAvalista(t *testing.T) { + a := cobranca.Avalista{Nome: "Nome do Avalista", + Documento: "280.834.810-09"} + p := Pagamento{avalista: a} + expected := "NOME DO AVALISTA CPF28083481009" + if got := p.MensagemAvalista(); got != expected { + t.Errorf("Expected '%s', got '%s'", expected, got) + } +} diff --git a/remessa/remessa.go b/remessa/remessa.go index 6db50de..e373c15 100644 --- a/remessa/remessa.go +++ b/remessa/remessa.go @@ -2,47 +2,77 @@ package remessa import ( "errors" + "fmt" + "strings" "github.com/padmoney/cobranca" ) -type RemessaGenerator interface { +const ( + CNAB400 = "cnab400" + + ComandoCancelamento = "cancelamento" + ComandoRegistro = "registro" + + layoutRemessaNaoSuportado = "Layout do arquivo de remessa não é suportado." +) + +type geradorRemessa interface { + Trailler() string + Header() string + Pagamentos(pagamentos []Pagamento) ([]string, error) } type Remessa struct { - Generator RemessaGenerator -} + layout string + conta cobranca.Conta + params Params + sequencial int64 + gerador geradorRemessa -func New(conta cobranca.Conta, params Params, sequential int64) (Remessa, error) { - remessa := Remessa{} + pagamentos []Pagamento +} +func New(layout string, conta cobranca.Conta, params Params, sequencial int64) (*Remessa, error) { + remessa := &Remessa{ + conta: conta, + params: params, + sequencial: sequencial} + if strings.ToLower(layout) != CNAB400 { + return remessa, errors.New(layoutRemessaNaoSuportado) + } switch conta.Banco { case cobranca.CodigoSantander: - remessa.Generator = NewSantander(conta, params, sequential) + remessa.gerador = NewSantanderCNAB400(conta, params) default: + return remessa, errors.New(cobranca.BancoNaoSuportado) } return remessa, nil } -/* - public function __construct($conta, array $params = []) - { - $this->conta = $conta; - $this->params = $params; - - $agencia = explode('-', $conta->agencia()); - $this->agencia = isset($agencia[0]) ? $agencia[0] : $conta->agencia(); - $this->agencia_dv = $this->digitoAgencia(); - - $conta_numero = explode('-', $conta->contaCorrente()); - $this->conta_numero = isset($conta_numero[0]) ? $conta_numero[0] : $conta->contaCorrente(); - $this->conta_dv = isset($conta_numero[1]) ? $conta_numero[1] : ''; - - $this->nome_cedente = $conta->beneficiario()->nome(); - $this->carteira = $conta->carteira(); - $this->convenio = $conta->convenio(); - $this->variacao = $conta->variacao(); - $this->sequencial_remessa = $conta->sequencialRemessa(); - } -*/ +func (r *Remessa) AddPagamento(p Pagamento) error { + r.pagamentos = append(r.pagamentos, p) + return nil +} + +func (r Remessa) NomeArquivo() string { + return fmt.Sprintf("REMESSA_%d.rem", r.sequencial) +} + +func (r Remessa) Linhas() (linhas []string, err error) { + header := r.gerador.Header() + linhas = append(linhas, header) + + pagamentos, err := r.gerador.Pagamentos(r.pagamentos) + if err != nil { + return + } + for _, p := range pagamentos { + linhas = append(linhas, cobranca.Sanitize(p)) + } + + trailler := r.gerador.Trailler() + linhas = append(linhas, trailler) + return +} diff --git a/remessa/santander.go b/remessa/santander.go deleted file mode 100644 index c91a77d..0000000 --- a/remessa/santander.go +++ /dev/null @@ -1,17 +0,0 @@ -package remessa - -import "github.com/padmoney/cobranca" - -type Santander struct { - conta cobranca.Conta - params Params - sequential int64 -} - -func NewSantander(conta cobranca.Conta, params Params, sequential int64) Santander { - return Santander{ - conta: conta, - params: params, - sequential: sequential, - } -} diff --git a/remessa/santander_cnab400.go b/remessa/santander_cnab400.go new file mode 100644 index 0000000..cac355e --- /dev/null +++ b/remessa/santander_cnab400.go @@ -0,0 +1,174 @@ +package remessa + +import ( + "fmt" + "strconv" + "time" + + "github.com/padmoney/cobranca" +) + +type SantanderCNAB400 struct { + conta cobranca.Conta + params Params + sequencialRegistro int + quantidadeDocumentos int + valorTotalDocumentos float64 +} + +func NewSantanderCNAB400(c cobranca.Conta, p Params) *SantanderCNAB400 { + return &SantanderCNAB400{conta: c, + params: p} +} + +func (s SantanderCNAB400) Header() string { + return fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + "0", // Código do registro = 0 + "1", // Código da remessa = 1 + "REMESSA", // Literal de transmissão = REMESSA + "01", // Código do serviço = 01 + cobranca.Brancos("COBRANCA", 15), // Literal de serviço = COBRANÇA + cobranca.Zeros(s.codigoTransmissao(), 20), // Código de Transmissão (nota 1) + cobranca.Brancos(s.conta.Beneficiario.Nome, 30), // Nome do Beneficiário + "033", // Código do Banco = 353/033 + "SANTANDER ", // Nome do Banco = SANTANDER + DataFormatada(time.Now()), // Data de Gravação + cobranca.Zeros("0", 16), // Zeros + cobranca.Brancos("", 47), // Mensagem 1 + cobranca.Brancos("", 47), // Mensagem 2 + cobranca.Brancos("", 47), // Mensagem 3 + cobranca.Brancos("", 47), // Mensagem 4 + cobranca.Brancos("", 47), // Mensagem 5 + cobranca.Brancos("", 34), // Brancos + cobranca.Brancos("", 6), // Brancos + cobranca.Zeros("0", 3), // Número da versão da remessa opcional, se informada, será controlada pelo sistema + "000001", // Número sequencial do registro no arquivo = 000001 + ) +} + +func (s *SantanderCNAB400) Pagamentos(pagamentos []Pagamento) (lines []string, err error) { + s.sequencialRegistro = 1 + s.quantidadeDocumentos = 0 + s.valorTotalDocumentos = 0.0 + for _, p := range pagamentos { + + s.quantidadeDocumentos += 1 + s.valorTotalDocumentos += p.Valor() + s.sequencialRegistro += 1 + + indMulta := "0" + if p.PercentualMulta() > 0.0 { + indMulta = "4" + } + + l := fmt.Sprintf("1%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + s.conta.Beneficiario.TipoInscricao(), + cobranca.Zeros(s.conta.Beneficiario.GetDocumento(), 14), + cobranca.Zeros(s.conta.NumeroAgencia(), 4), + cobranca.Zeros(s.conta.Convenio, 8), + s.formataNumeroConta(s.conta.Numero(), 8), + cobranca.Brancos(" ", 25), + cobranca.Zeros(cobranca.SemMascara(p.NossoNumero()), 8), + "000000", + " ", + indMulta, + ValorFormatado(p.PercentualMulta(), 2, 2), + "00", + cobranca.Zeros("0", 13), + " ", + "000000", // Data para cobrança de multa. (Nota 4) + "5", // Código da carteira + s.comando(p.Comando()), // Código da ocorrência + cobranca.Zeros(p.Numero(), 10), // Seu número + DataFormatada(p.DataVencimento()), + ValorFormatado(p.Valor(), 11, 2), // 24.7 127 a 139 9(011)v99 Valor do Título + "033", + "00000", // Código da agência cobradora do Banco Santander informar somente se carteira for igual a 5, caso contrário, informar zeros. + cobranca.Zeros(s.conta.GetEspecieTitulo(), 2), // Espécie de documento + cobranca.Brancos(s.conta.GetAceite(), 1), + DataFormatada(time.Now()), // Data da emissão do título + "00", // Primeira instrução cobrança + "00", // Segunda instrução cobrança + ValorFormatado(p.JurosMoraPorDiaAtraso(), 11, 2), // Valor de mora a ser cobrado por dia de atraso + "000000", // Data limite para concessão de desconto + ValorFormatado(0, 11, 2), // Valor de desconto a ser concedido + ValorFormatado(0, 8, 5), // Valor do IOF a ser recolhido pelo Banco para nota de seguro + ValorFormatado(0, 11, 2), // Valor do abatimento a ser concedido ou valor do segundo desconto. Vide posição 71. + p.Pagador().TipoInscricao(), + cobranca.Zeros(p.Pagador().GetDocumento(), 14), + cobranca.Brancos(p.Pagador().Nome, 40), + cobranca.Brancos(p.Pagador().Endereco, 40), + cobranca.Brancos(p.Pagador().Bairro, 12), + cobranca.Brancos(cobranca.SemMascara(p.Pagador().CEP), 8), + cobranca.Brancos(p.Pagador().Cidade, 15), + cobranca.Brancos(p.Pagador().UF, 2), + cobranca.Brancos(" ", 30), // Nome do sacador ou coobrigado + " ", + s.complemento(), + cobranca.Brancos("", 6), + "00", + " ", + cobranca.Zeros(strconv.Itoa(s.sequencialRegistro), 6), // Sequencial de Registro + ) + lines = append(lines, l) + } + return +} + +func (s SantanderCNAB400) Trailler() string { + q := strconv.Itoa(s.quantidadeDocumentos) + sr := strconv.Itoa(s.sequencialRegistro + 1) + return fmt.Sprintf("9%s%s%s%s", + cobranca.Zeros(q, 6), + ValorFormatado(s.valorTotalDocumentos, 11, 2), + cobranca.Zeros("0", 374), + cobranca.Zeros(sr, 6), + ) +} + +func (s SantanderCNAB400) comando(c string) string { + // Código da ocorrência: + // 01 = ENTRADA DE TÍTULO + // 02 = BAIXA DE TÍTULO + // 04 = CONCESSÃO DE ABATIMENTO + // 05 = CANCELAMENTO ABATIMENTO + // 06 = ALTERAÇÃO DE VENCIMENTO + // 07 = ALT. NÚMERO CONT.BENEFICIÁRIO + // 08 = ALTERAÇÃO DO SEU NÚMERO + // 09 = PROTESTAR + // 18 = SUSTAR PROTESTO (Após início do ciclo de protesto) + // 98 = NÃO PROTESTAR (Antes do início do ciclo de protesto) + switch c { + case ComandoCancelamento: + return "02" + default: + return "01" + } +} + +func (s SantanderCNAB400) contaPadraoNovo() bool { + return len(s.conta.Numero()) > 8 +} + +func (s SantanderCNAB400) codigoTransmissao() string { + return s.params.Get("codigo_transmissao") +} + +func (s SantanderCNAB400) complemento() string { + if s.contaPadraoNovo() { + n := s.conta.Numero() + n = n[len(n)-1:] + return fmt.Sprintf("I%s%s", + cobranca.Zeros(n, 1), + cobranca.Zeros(s.conta.Digito(), 1), + ) + } + return " " +} + +func (s SantanderCNAB400) formataNumeroConta(c string, q int) string { + if len(c) > q { + return c[0:q] + } + return cobranca.Zeros(c, q) +} diff --git a/remessa/santander_cnab400_test.go b/remessa/santander_cnab400_test.go new file mode 100644 index 0000000..218f6da --- /dev/null +++ b/remessa/santander_cnab400_test.go @@ -0,0 +1,122 @@ +package remessa + +import ( + "testing" + "time" + + "github.com/padmoney/cobranca" +) + +func TestCNAB400(t *testing.T) { + + expectedHeader := "01REMESSA01COBRANCA 17777751042700080112ACME CORPORATION 033SANTANDER " + + DataFormatada(time.Now()) + + "0000000000000000 " + + cobranca.Brancos("", 255) + + "000000001" + expectedDetalheAntesData := "1029999999900019140420123456713003758 00000123000000 40100000000000000000 000000501000000012317071900000000001990330000001A" + expectedDetalheDepoisData := "000000000000000000000000000000000000000000000000000000000000000200000000000191NOME DO SACADO ENDERECO DO SACADO BAIRRO 29315732CACHOEIRO DE ITES I90 00 000002" + + expectedDetalhe := expectedDetalheAntesData + DataFormatada(time.Now()) + expectedDetalheDepoisData + + expectedTrailer := "9000001000000000019900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003" + + conta := fixtureConta("18", "1234567") + params := fixtureParams() + remessa, err := New("CNAB400", conta, params, 1) + if err != nil { + t.Errorf("There should not be an error, error: %s", err) + } + p := fixturePagamento(1.99, time.Now().AddDate(0, 1, 0), "123", "123") + + remessa.AddPagamento(p) + + linhas, err := remessa.Strings() + if err != nil { + t.Errorf("There should not be an error, error: %s", err) + } + + if got := len(linhas); got != 3 { + t.Errorf("Expected '%d', got '%d'", 3, got) + } + + // header + if got := linhas[0]; got != expectedHeader { + t.Errorf("header: Expected '%s', got '%s'", expectedHeader, got) + } + if got := len(linhas[0]); got != 400 { + t.Errorf("header: Expected '%d', got '%d'", 400, got) + } + + // detalhe + if got := linhas[1]; got != expectedDetalhe { + t.Errorf("detail: Expected '%s', got '%s'", expectedDetalhe, got) + } + if got := len(linhas[1]); got != 400 { + t.Errorf("detail: Expected '%d', got '%d'", 400, got) + } + + // trailer + if got := linhas[2]; got != expectedTrailer { + t.Errorf("trailer: Expected '%s', got '%s'", expectedTrailer, got) + } + if got := len(linhas[2]); got != 400 { + t.Errorf("trailer: Expected '%d', got '%d'", 400, got) + } + // $this->assertEquals('REMESSA_1.rem', $remessa->nomeArquivo(), 'Nome do arquivo'); + +} + +func fixtureConta(carteira, convenio string) cobranca.Conta { + beneficiario := cobranca.Beneficiario{ + Nome: "ACME Corporation", + Documento: "99999999000191", + } + return cobranca.Conta{ + Banco: "033", + Agencia: "4042", + ContaCorrente: "130037589", + Carteira: carteira, + Convenio: convenio, + Beneficiario: beneficiario, + Aceite: true, + } +} + +func fixturePagamento(valor float64, dataVencimento time.Time, numero, nossoNumero string) Pagamento { + avalista := cobranca.Avalista{Nome: "Nome do Avalista", Documento: "335.176.000-08"} + pagador := cobranca.Pagador{ + Documento: "00000000000191", + Nome: "Nome do sacado", + Endereco: "Endereço do sacado", + Bairro: "Bairro", + CEP: "29315-732", + Cidade: "Cachoeiro de Itapemirim", + UF: "ES", + } + return NewPagamento("registro", valor, dataVencimento, numero, nossoNumero, 30, 1.0, 1.0, pagador, avalista) + /* + + private function novoPagamento($valor, $data_vencimento, $numero, $nosso_numero = null, + $dias_protesto = 30, + $percentual_mora_ao_mes = 1, + $percentual_multa = 1, + $avalista = [] + ) { + empty($nosso_numero) && $nosso_numero = $numero; + $pagador = [ + ]; + return new Pagamento('registro', $valor, $data_vencimento, $numero, $nosso_numero, + $dias_protesto, + $percentual_mora_ao_mes, + $percentual_multa, + $pagador); + } + */ +} + +func fixtureParams() Params { + p := NewParams() + p.Add("codigo_transmissao", "17777751042700080112") + return p +} diff --git a/remessa/santander_test.go b/remessa/santander_test.go deleted file mode 100644 index 36774ed..0000000 --- a/remessa/santander_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package remessa - -/* -func TestMethodName(t *testing.T) { - if err != nil { - t.Errorf("There should not be an error, error: %s", err) - } - if got != expected { - t.Errorf("Expected '%s', got '%s'", expected, got) - } -} -*/ diff --git a/remessa/utils.go b/remessa/utils.go new file mode 100644 index 0000000..e38361b --- /dev/null +++ b/remessa/utils.go @@ -0,0 +1,21 @@ +package remessa + +import ( + "math" + "strconv" + "time" + + "github.com/padmoney/cobranca" +) + +func DataFormatada(data time.Time) string { + return data.Format("020106") +} + +//protected function valorFormatado($valor, $quantidade = 11, $decimais = 2) +func ValorFormatado(valor float64, q, d int) string { + p := math.Pow(10, float64(d)) + valor = math.Round(valor * p) + s := strconv.FormatFloat(valor, 'f', -1, 64) + return cobranca.Zeros(s, q+d) +} diff --git a/remessa/utils_test.go b/remessa/utils_test.go new file mode 100644 index 0000000..7fd1926 --- /dev/null +++ b/remessa/utils_test.go @@ -0,0 +1,20 @@ +package remessa + +import ( + "testing" + "time" +) + +func TestDataFormatada(t *testing.T) { + layout := "2006-01-02" + str := "1984-11-09" + date, err := time.Parse(layout, str) + if err != nil { + t.Errorf("There should not be an error, error: %s", err) + } + + expected := "091184" + if got := DataFormatada(date); got != expected { + t.Errorf("Expected '%s', got '%s'", expected, got) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..7c2e3d3 --- /dev/null +++ b/utils.go @@ -0,0 +1,119 @@ +package cobranca + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +type PadType string + +const ( + StrPadLeft PadType = "str_pad_left" + StrPadRight PadType = "str_pad_right" +) + +var ( + transliterations = map[string]*regexp.Regexp{ + "A": regexp.MustCompile(`À|Á|Â|Ã|Ä|à|á|â|ã|ä`), + "AA": regexp.MustCompile(`Å|å`), + "AE": regexp.MustCompile(`Æ|æ`), + "C": regexp.MustCompile(`Ç|ç`), + "E": regexp.MustCompile(`È|É|Ê|Ë|è|é|ê|ë`), + "D": regexp.MustCompile(`Ð|ð`), + "I": regexp.MustCompile(`Ì|Í|Î|Ï|ì|í|î|ï`), + "L": regexp.MustCompile(`Ł|ł`), + "N": regexp.MustCompile(`Ñ|ñ|ń`), + "O": regexp.MustCompile(`Ò|Ó|Ô|Õ|Ö|ò|ó|ô|õ|ö|ō`), + "OE": regexp.MustCompile(`Œ|Ø|œ|ø`), + "Th": regexp.MustCompile(`Þ`), + "U": regexp.MustCompile(`Ù|Ú|Û|Ü|ù|ú|û|ü|ũ|ū|ŭ|ů|ű|ų`), + "Y": regexp.MustCompile(`Ý|ý|ÿ`), + "S": regexp.MustCompile(`ś`), + "SS": regexp.MustCompile(`ß`), + "Z": regexp.MustCompile(`ż`), + "TH": regexp.MustCompile(`þ`), + } +) + +func Brancos(s string, l int) string { + s = Sanitize(s) + if len(s) > l { + return s[0:l] + } + return StrPad(s, l, " ", StrPadRight) +} + +func BrancosLeft(s string, l int) string { + s = Sanitize(s) + if len(s) > l { + return s[len(s)-l:] + } + return StrPad(s, l, " ", StrPadLeft) +} + +func OnlyNumbers(s string) string { + reg, _ := regexp.Compile("[^0-9]+") + return reg.ReplaceAllString(s, "") +} + +// Sanitize replaces non-ASCII characters with an ASCII approximation, or if none exists, to “?”. +func Sanitize(word string) string { + for repl, regex := range transliterations { + word = regex.ReplaceAllString(word, repl) + } + + var safe string + for _, r := range word { + if isAscii(r) { + safe += string(r) + } else { + safe += "?" + } + } + return strings.ToUpper(safe) +} + +func isAscii(s rune) bool { + return int(s) >= 32 && int(s) <= 126 +} + +func SemMascara(s string) string { + s = strings.Replace(s, " ", "", -1) + s = strings.Replace(s, ".", "", -1) + s = strings.Replace(s, "-", "", -1) + s = strings.Replace(s, "/", "", -1) + return s +} + +func StrPad(s string, padLen int, padStr string, padType PadType) string { + LenS := len(s) + if LenS == padLen { + return s + } + + if LenS > padLen { + if padType == StrPadLeft { + return s[LenS-padLen:] + } else { + return s[0:padLen] + } + } + + c := (padLen - len(s)) / len(padStr) + r := strings.Repeat(padStr, c) + if padType == StrPadLeft { + return r + s + } else { + return s + r + } +} + +func Zeros(s string, l int) string { + if len(s) > l { + return s[len(s)-l:] + } + f := "%0" + strconv.Itoa(l) + "s" + return fmt.Sprintf(f, s) +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..4a3a3f1 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,163 @@ +package cobranca + +import "testing" + +func TestBrancos(t *testing.T) { + qote := "a long time ago" + got := Brancos(qote, 20) + expected := "A LONG TIME AGO " + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + got = Brancos("a long time ago", 8) + expected = "A LONG T" + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } +} + +func TestOnlyNumbers(t *testing.T) { + expected := "1234567890" + got := OnlyNumbers("1a2!3@4#5%6&7*8()9-0{}ç^;.,?/<>|") + if got != expected { + t.Errorf("Expected '%v' got '%v'", expected, got) + } +} + +func TestSanitize(t *testing.T) { + equivalents := map[string]string{ + "À": "A", + "Á": "A", + "Â": "A", + "Ã": "A", + "Ä": "A", + "Å": "AA", + "Æ": "AE", + "Ç": "C", + "È": "E", + "É": "E", + "Ê": "E", + "Ë": "E", + "Ì": "I", + "Í": "I", + "Î": "I", + "Ï": "I", + "Ð": "D", + "Ł": "L", + "Ñ": "N", + "Ò": "O", + "Ó": "O", + "Ô": "O", + "Õ": "O", + "Ö": "O", + "Ø": "OE", + "Ù": "U", + "Ú": "U", + "Ü": "U", + "Û": "U", + "Ý": "Y", + "Þ": "TH", + "ß": "SS", + "à": "A", + "á": "A", + "â": "A", + "ã": "A", + "ä": "A", + "å": "AA", + "æ": "AE", + "ç": "C", + "è": "E", + "é": "E", + "ê": "E", + "ë": "E", + "ì": "I", + "í": "I", + "î": "I", + "ï": "I", + "ð": "D", + "ł": "L", + "ñ": "N", + "ń": "N", + "ò": "O", + "ó": "O", + "ô": "O", + "õ": "O", + "ō": "O", + "ö": "O", + "ø": "OE", + "ś": "S", + "ù": "U", + "ú": "U", + "û": "U", + "ū": "U", + "ü": "U", + "ý": "Y", + "þ": "TH", + "ÿ": "Y", + "ż": "Z", + "Œ": "OE", + "œ": "OE", + "żż": "ZZ", + "Hello, 世界": "HELLO, ??", + } + + for k, expected := range equivalents { + got := Sanitize(k) + if got != expected { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + } +} + +func TestStrPad(t *testing.T) { + expected := "0000012345-6" + got := StrPad("0000012345-6", 12, "0", StrPadLeft) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + expected = "0000012345-6" + got = StrPad("12345-6", 12, "0", StrPadLeft) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + expected = "6789012345-6" + got = StrPad("0123456789012345-6", 12, "0", StrPadLeft) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + expected = "Padmoney " + got = StrPad("Padmoney", 10, " ", StrPadRight) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + expected = "Clap Clap Clap Clap " + got = StrPad("", 20, "Clap ", StrPadRight) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + expected = "Padmoney" + got = StrPad("Padmoney Payment Security", 8, " ", StrPadRight) + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } +} + +func TestZeros(t *testing.T) { + got := Zeros("12345-6", 12) + expected := "0000012345-6" + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } + + got = Zeros("1234567890", 5) + expected = "67890" + if expected != got { + t.Errorf("Expected '%s' got '%s'", expected, got) + } +}