Repository: andreroggeri/pynubank

Issue:
Adicionar endpoint para detalhes da fatura
Olá! Gostaria de obter os detalhes da fatura. Até já implementei isso, porém tenho dúvidas sobre a melhor forma de implementação, uma forma simples seria:

```
def get_bill_details(self, bill):
    request = requests.get(bill['_links']['self']['href'], headers=self.headers)
    return json.loads(request.content.decode('utf-8'))
```

`bill` seria um elemento do array retornado por `get_card_bills()` mas talvez fica estranho passar para a função elementos de um resultado de uma response externa, talvez o resultado poderia se tornar um array de elementos definidos na aplicação, algo como: 
```
class Bill
    (...)
```

Gostaria de umas dicas pois não tenho muita experiência com python profissionalmente, quais as melhores práticas, etc talvez links de projetos interessantes para seguir. #ajudaluciano

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/16376687/badge)](https://coveralls.io/builds/16376687)

Coverage remained the same at 100.0% when pulling **dd960eea1cfb0ddf803605c24d8ffe897f5665d7 on janjitsu:master** into **b315dd9b34064d16cc18fe91c4f96102d2a1444a on andreroggeri:master**.

janjitsu: Você tocou num ponto interessante @andreroggeri ... na minha aplicação eu criei um cash das responses por que acabei ficando bloqueando também... E se fizessemos esse cache no `pynubank`? claro que precisariamos ver uma forma mais inteligente de invalidá-lo. Mas pra mim faz sentido. Muita coisa não vai mudar e eu ainda não entendi muito bem qual a regra que bloqueia a máquina... Se soubessemos poderíamos antecipar o bloqueio na aplicação.
andreroggeri: @janjitsu criei a #15 para discutirmos isso ! 👍 

Patch:
diff --git a/.gitignore b/.gitignore
index 00099ec..8a3fedb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ nosetests.xml
 coverage.xml
 *.cover
 .hypothesis/
+.pytest_cache/
 
 # Translations
 *.mo
diff --git a/README.md b/README.md
index b0185e1..f7db56b 100644
--- a/README.md
+++ b/README.md
@@ -17,13 +17,20 @@ Disponível via pip
 from pynubank import Nubank
 
 # Utilize o CPF sem pontos ou traços
-nu = Nubank('123456789', 'senha') 
+nu = Nubank('123456789', 'senha')
 
 # Lista de dicionários contendo todas as transações de seu cartão de crédito
 card_statements = nu.get_card_statements()
 
 # Soma de todas as compras
-print(sum([t['amount'] for t in card_statements])) 
+print(sum([t['amount'] for t in card_statements]))
+
+# Lista de dicionários contendo todas as faturas do seu cartão de crédito
+bills = nu.get_bills()
+
+# Retorna um dicionário contendo os detalhes de uma fatura retornada por
+get_bills()
+bill_details = nu.get_bill_details(bills[1])
 ```
 
 ### NuConta
@@ -31,16 +38,16 @@ print(sum([t['amount'] for t in card_statements]))
 from pynubank import Nubank
 
 # Utilize o CPF sem pontos ou traços
-nu = Nubank('123456789', 'senha') 
+nu = Nubank('123456789', 'senha')
 
 # Lista de dicionários contendo todas as transações de seu cartão de crédito
-account_statements = nu.get_account_statements() 
+account_statements = nu.get_account_statements()
 
 # Soma de todas as transações na NuConta
 # Observacão: As transações de saída não possuem o valor negativo, então deve-se olhar a propiedade "__typename".
 # TransferInEvent = Entrada
 # TransferOutEvent = Saída
-print(sum([t['amount'] for t in account_statements])) 
+print(sum([t['amount'] for t in account_statements]))
 
 # Saldo atual
 print(nu.get_account_balance())
@@ -59,7 +66,7 @@ print(nu.get_account_balance())
 >>> df = pd.DataFrame(transactions, columns=['time', 'amount'])
 >>> df['time'] = pd.to_datetime(df['time'])
 >>> df.groupby([df.time.dt.year, df.time.dt.month]).sum() # Agrupado por Ano/Mês
-Year Month  Amount   
+Year Month  Amount
 2016 6      20000
      7      20000
      8      20000
@@ -67,14 +74,14 @@ Year Month  Amount
      10     20000
      11     40000
      12     40000
-     
+
 2017 1     100000
      2      20000
      3      30000
      4      35000
      5      12000
      6      22000
-     
+
 >>> df.groupby([df.title]).sum() # Agrupado por categoria
 title         amount
 casa           13000
@@ -105,4 +112,4 @@ $ pytest
 
 ## Contribuindo
 
-Envie sua PR para melhorar esse projeto ! 😋
\ No newline at end of file
+Envie sua PR para melhorar esse projeto ! 😋
diff --git a/pynubank/nubank.py b/pynubank/nubank.py
index ae31690..a429e4e 100644
--- a/pynubank/nubank.py
+++ b/pynubank/nubank.py
@@ -64,8 +64,12 @@ class Nubank:
         feed = self.get_card_feed()
         return list(filter(lambda x: x['category'] == 'transaction', feed['events']))
 
-    def get_card_bills(self):
+    def get_bills(self):
         request = requests.get(self.bills_url, headers=self.headers)
+        return json.loads(request.content.decode('utf-8'))['bills']
+
+    def get_bill_details(self, bill):
+        request = requests.get(bill['_links']['self']['href'], headers=self.headers)
         return json.loads(request.content.decode('utf-8'))
 
     def get_account_feed(self):


Test patch:
diff --git a/tests/test_nubank_client.py b/tests/test_nubank_client.py
index 787a858..1c1108a 100644
--- a/tests/test_nubank_client.py
+++ b/tests/test_nubank_client.py
@@ -231,6 +231,89 @@ def bills_return():
         ]
     }
 
+@pytest.fixture
+def bill_details_return():
+    return {
+        'bill': {
+            '_links': {
+                'barcode': {
+                    'href': 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/boleto/barcode'
+                },
+                'boleto_email': {
+                    'href': 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/boleto/email'
+                },
+                'invoice_email': {
+                    'href': 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/invoice/email'
+                },
+                'self': {
+                    'href': 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz'
+                }
+            },
+            'account_id': 'abcde-fghi-jklmn-opqrst-uvxz',
+            'auto_debit_failed': False,
+            'barcode': '',
+            'id': 'abcde-fghi-jklmn-opqrst-uvxz',
+            'line_items': [
+                {
+                    'amount': 2390,
+                    'category': 'Eletrônicos',
+                    'charges': 1,
+                    'href': 'nuapp://transaction/abcde-fghi-jklmn-opqrst-uvxz',
+                    'id': 'abcde-fghi-jklmn-opqrst-uvxz',
+                    'index': 0,
+                    'post_date': '2015-09-09',
+                    'title': 'Mercadopago Mlivre'
+                },
+                {
+                    'amount': 5490,
+                    'category': 'Eletrônicos',
+                    'charges': 1,
+                    'href': 'nuapp://transaction/abcde-fghi-jklmn-opqrst-uvxz',
+                    'id': 'abcde-fghi-jklmn-opqrst-uvxz',
+                    'index': 0,
+                    'post_date': '2015-09-09',
+                    'title': 'Mercadopago Mlivre'
+                }
+            ],
+            'linha_digitavel': '',
+            'payment_method': 'boleto',
+            'state': 'overdue',
+            'status': 'paid',
+            'summary': {
+                'adjustments': '0',
+                'close_date': '2015-09-25',
+                'due_date': '2015-10-10',
+                'effective_due_date': '2015-10-13',
+                'expenses': '78.8000',
+                'fees': '0',
+                'interest': 0,
+                'interest_charge': '0',
+                'interest_rate': '0.0775',
+                'interest_reversal': '0',
+                'international_tax': '0',
+                'late_fee': '0.02',
+                'late_interest_rate': '0.0875',
+                'minimum_payment': 7005,
+                'open_date': '2015-07-23',
+                'paid': 7880,
+                'past_balance': 0,
+                'payments': '0',
+                'precise_minimum_payment': '70.054500',
+                'precise_total_balance': '78.8000',
+                'previous_bill_balance': '0',
+                'tax': '0',
+                'total_accrued': '0',
+                'total_balance': 7880,
+                'total_credits': '0',
+                'total_cumulative': 7880,
+                'total_financed': '0',
+                'total_international': '0',
+                'total_national': '78.8000',
+                'total_payments': '0'
+            }
+        }
+    }
+
 @pytest.fixture
 def account_balance_return():
     return {'data': {'viewer': {'savingsAccount': {'currentSavingsBalance': {'netAmount': 127.33}}}}}
@@ -318,7 +401,7 @@ def test_get_card_feed(monkeypatch, authentication_return, events_return):
     assert events[0]['href'] == 'nuapp://transaction/abcde-fghi-jklmn-opqrst-uvxz'
     assert events[0]['_links']['self']['href'] == 'https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/_links_123'
 
-def test_get_card_bills(monkeypatch, authentication_return, bills_return):
+def test_get_bills(monkeypatch, authentication_return, bills_return):
     response = create_fake_response(authentication_return)
     monkeypatch.setattr('requests.post', MagicMock(return_value=response))
     nubank_client = Nubank('12345678909', '12345678')
@@ -326,11 +409,8 @@ def test_get_card_bills(monkeypatch, authentication_return, bills_return):
     response = create_fake_response(bills_return)
     monkeypatch.setattr('requests.get', MagicMock(return_value=response))
 
-    bills_response = nubank_client.get_card_bills()
-    assert bills_response['_links']['future']['href'] == 'https://prod-s0-billing.nubank.com.br/api/accounts/abcde-fghi-jklmn-opqrst-uvxz/bills/future'
-    assert bills_response['_links']['open']['href'] == 'https://prod-s0-billing.nubank.com.br/api/accounts/abcde-fghi-jklmn-opqrst-uvxz/bills/open'
+    bills = nubank_client.get_bills()
 
-    bills = bills_response['bills']
     assert len(bills) == 3
     assert bills[2]['_links']['self']['href'] == "https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz"
     assert bills[2]['href'] == 'nuapp://bill/abcde-fghi-jklmn-opqrst-uvxz'
@@ -368,6 +448,70 @@ def test_get_card_bills(monkeypatch, authentication_return, bills_return):
     assert summary["total_national"] == "364.32893934"
     assert summary["total_payments"] == "-960.47"
 
+def test_get_bill_details(monkeypatch, authentication_return, bill_details_return):
+    response = create_fake_response(authentication_return)
+    monkeypatch.setattr('requests.post', MagicMock(return_value=response))
+    nubank_client = Nubank('12345678909', '12345678')
+
+    response = create_fake_response(bill_details_return)
+    monkeypatch.setattr('requests.get', MagicMock(return_value=response))
+
+    bill_mock = {'_links':{'self':{'href':'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz'}}}
+    bill_response = nubank_client.get_bill_details(bill_mock)
+
+    bill = bill_response['bill']
+
+    assert bill['_links']['barcode']['href'] == 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/boleto/barcode'
+    assert bill['_links']['boleto_email']['href'] == 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/boleto/email'
+    assert bill['_links']['invoice_email']['href'] == 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz/invoice/email'
+    assert bill['_links']['self']['href'] == 'https://prod-s0-billing.nubank.com.br/api/bills/abcde-fghi-jklmn-opqrst-uvxz'
+    assert bill['account_id'] == 'abcde-fghi-jklmn-opqrst-uvxz'
+    assert bill['auto_debit_failed'] == False
+    assert bill['barcode'] == ''
+    assert bill['id'] == 'abcde-fghi-jklmn-opqrst-uvxz'
+    assert bill['line_items'][0]['amount'] == 2390
+    assert bill['line_items'][0]['category'] == 'Eletrônicos'
+    assert bill['line_items'][0]['charges'] == 1
+    assert bill['line_items'][0]['href'] == 'nuapp://transaction/abcde-fghi-jklmn-opqrst-uvxz'
+    assert bill['line_items'][0]['id'] == 'abcde-fghi-jklmn-opqrst-uvxz'
+    assert bill['line_items'][0]['index'] == 0
+    assert bill['line_items'][0]['post_date'] == '2015-09-09'
+    assert bill['line_items'][0]['title'] == 'Mercadopago Mlivre'
+    assert bill['linha_digitavel'] == ''
+    assert bill['payment_method'] == 'boleto'
+    assert bill['state'] == 'overdue'
+    assert bill['status'] == 'paid'
+    assert bill['summary']['adjustments'] == '0'
+    assert bill['summary']['close_date'] == '2015-09-25'
+    assert bill['summary']['due_date'] == '2015-10-10'
+    assert bill['summary']['effective_due_date'] == '2015-10-13'
+    assert bill['summary']['expenses'] == '78.8000'
+    assert bill['summary']['fees'] == '0'
+    assert bill['summary']['interest'] == 0
+    assert bill['summary']['interest_charge'] == '0'
+    assert bill['summary']['interest_rate'] == '0.0775'
+    assert bill['summary']['interest_reversal'] == '0'
+    assert bill['summary']['international_tax'] == '0'
+    assert bill['summary']['late_fee'] == '0.02'
+    assert bill['summary']['late_interest_rate'] == '0.0875'
+    assert bill['summary']['minimum_payment'] == 7005
+    assert bill['summary']['open_date'] == '2015-07-23'
+    assert bill['summary']['paid'] == 7880
+    assert bill['summary']['past_balance'] == 0
+    assert bill['summary']['payments'] == '0'
+    assert bill['summary']['precise_minimum_payment'] == '70.054500'
+    assert bill['summary']['precise_total_balance'] == '78.8000'
+    assert bill['summary']['previous_bill_balance'] == '0'
+    assert bill['summary']['tax'] == '0'
+    assert bill['summary']['total_accrued'] == '0'
+    assert bill['summary']['total_balance'] == 7880
+    assert bill['summary']['total_credits'] == '0'
+    assert bill['summary']['total_cumulative'] == 7880
+    assert bill['summary']['total_financed'] == '0'
+    assert bill['summary']['total_international'] == '0'
+    assert bill['summary']['total_national'] == '78.8000'
+    assert bill['summary']['total_payments'] == '0'
+
 def test_get_card_statements(monkeypatch, authentication_return, events_return):
     response = create_fake_response(authentication_return)
     monkeypatch.setattr('requests.post', MagicMock(return_value=response))