diff --git a/example/list_clients.py b/example/list_clients.py index ddfe04f..c12b04a 100644 --- a/example/list_clients.py +++ b/example/list_clients.py @@ -9,7 +9,7 @@ def main(): response = client.clients.list_clients() print(response.json()) - response = client.clients.create_client(name="nerd") + response = client.clients.create_client(name="dock") print(f"Created user with the ID of {response.data.client_id}") @@ -18,5 +18,7 @@ def main(): response = client.clients.list_clients() print(response.json()) + + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/myfinances/__pycache__/__init__.cpython-311.pyc b/myfinances/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index c818037..0000000 Binary files a/myfinances/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/myfinances/__pycache__/__init__.cpython-312.pyc b/myfinances/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index aefcbdc..0000000 Binary files a/myfinances/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/myfinances/__pycache__/base_service.cpython-312.pyc b/myfinances/__pycache__/base_service.cpython-312.pyc deleted file mode 100644 index f386527..0000000 Binary files a/myfinances/__pycache__/base_service.cpython-312.pyc and /dev/null differ diff --git a/myfinances/__pycache__/client.cpython-312.pyc b/myfinances/__pycache__/client.cpython-312.pyc deleted file mode 100644 index 498d590..0000000 Binary files a/myfinances/__pycache__/client.cpython-312.pyc and /dev/null differ diff --git a/myfinances/__pycache__/main.cpython-311.pyc b/myfinances/__pycache__/main.cpython-311.pyc deleted file mode 100644 index 67c0cc0..0000000 Binary files a/myfinances/__pycache__/main.cpython-311.pyc and /dev/null differ diff --git a/myfinances/__pycache__/models.cpython-312.pyc b/myfinances/__pycache__/models.cpython-312.pyc deleted file mode 100644 index ef821b8..0000000 Binary files a/myfinances/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/myfinances/client.py b/myfinances/client.py index 6389fd6..7f33b6b 100644 --- a/myfinances/client.py +++ b/myfinances/client.py @@ -32,5 +32,18 @@ def _post(self, endpoint: str, json: dict): response = self.session.post(url, json=json, headers=headers) return MyFinancesResponse.from_http_response(response) + def _patch(self, endpoint: str, json: dict): + url = f"{self.base_url}{endpoint}" + headers = {"Authorization": f"Bearer {self.api_key}"} + + response = self.session.patch(url, json=json, headers=headers) + return MyFinancesResponse.from_http_response(response) + + def _delete(self, endpoint: str): + url = f"{self.base_url}{endpoint}" + headers = {"Authorization": f"Bearer {self.api_key}"} + + response = self.session.delete(url, headers=headers) + return MyFinancesResponse.from_http_response(response) def close(self): - self.session.close() \ No newline at end of file + self.session.close() diff --git a/myfinances/clients/__pycache__/__init__.cpython-312.pyc b/myfinances/clients/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a3db444..0000000 Binary files a/myfinances/clients/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/myfinances/clients/__pycache__/models.cpython-312.pyc b/myfinances/clients/__pycache__/models.cpython-312.pyc deleted file mode 100644 index 724a36a..0000000 Binary files a/myfinances/clients/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/myfinances/clients/__pycache__/service.cpython-312.pyc b/myfinances/clients/__pycache__/service.cpython-312.pyc deleted file mode 100644 index 5d9f757..0000000 Binary files a/myfinances/clients/__pycache__/service.cpython-312.pyc and /dev/null differ diff --git a/myfinances/clients/service.py b/myfinances/clients/service.py index eca85da..a2c96d2 100644 --- a/myfinances/clients/service.py +++ b/myfinances/clients/service.py @@ -3,15 +3,15 @@ from pydantic import EmailStr from myfinances.base_service import BaseService -from myfinances.clients.models import ClientIdResponse, ClientData +from myfinances.clients.models import ClientIdResponse, ClientData, Client from myfinances.models import MyFinancesResponse class ClientsService(BaseService): def list_clients( - self, - order_by: Optional[str] = None, - search: Optional[str] = None + self, + order_by: Optional[str] = None, + search: Optional[str] = None ) -> MyFinancesResponse[ClientData]: """List clients under the specified team.""" params = {} @@ -24,16 +24,16 @@ def list_clients( return MyFinancesResponse(**response.dict()) def create_client( - self, - name: str, - phone_number: Optional[str] = None, - email: Optional[EmailStr] = None, - company: Optional[str] = None, - contact_method: Optional[str] = None, - is_representative: bool = False, - address: Optional[str] = None, - city: Optional[str] = None, - country: Optional[str] = None + self, + name: str, + phone_number: Optional[str] = None, + email: Optional[EmailStr] = None, + company: Optional[str] = None, + contact_method: Optional[str] = None, + is_representative: bool = False, + address: Optional[str] = None, + city: Optional[str] = None, + country: Optional[str] = None ) -> MyFinancesResponse[ClientIdResponse]: """List clients under the specified team.""" params = { @@ -48,6 +48,77 @@ def create_client( "country": country, } - response = self._client._post("/clients/create/", params) + response = self._client._post("/clients/create/", json=params) return MyFinancesResponse(**response.dict()) + + def get_client(self, client_id: int) -> MyFinancesResponse[Client]: + """ + Retrieving a singular client via their client_id. + + Args: + client_id (int): The ID of the client to retrieve. + + Returns: + MyFinancesResponse[Client]: Data of the client's details. + + """ + response = self._client._get(f"/clients/{client_id}") + return MyFinancesResponse(**response.dict()) + + def update_client(self, + name: str = None, + phone_number: Optional[str] = None, + email: Optional[EmailStr] = None, + company: Optional[str] = None, + contact_method: Optional[str] = None, + is_representative: Optional[bool] = None, + address: Optional[str] = None, + city: Optional[str] = None, + country: Optional[str] = None) -> MyFinancesResponse[Client]: + """ + Updating an existing client's details, with the provided parameters + + Args: + name (Optional[str]): name of the client to update + phone_number (Optional[str]): client's phone number to update + email (Optional[EmailStr]): client's email address to update + company (Optional[str]): client's company name to update + contact_method (Optional[str]): client's preferred contact to update + is_representative (Optional[bool]): to update the client if they are representative + address (Optional[str]): client's address to update + city (Optional[str]): client's location in the city to update + country (Optional[str]): client's country location to update + + Returns: + MyFinancesResponse[Client]: Data of the client's details is updated + """ + params = { + key: value for key, value in { + "name": name, + "phone_number": phone_number, + "email": email, + "company": company, + "contact_method": contact_method, + "is_representative": is_representative, + "address": address, + "city": city, + "country": country, + }.items() if value is not None + } + + response = self._client._patch("/clients/update/", json=params) + return MyFinancesResponse(**response.dict()) + + def delete_client(self, client_id: int) -> MyFinancesResponse[Client]: + """ + Deletion of a specific client + + Args: + client_id (int): client's id + + Returns: + MyFinancesResponse[Client]: Confirming the deletion of the client + """ + response = self._client._delete(f"/clients/{client_id}/delete") + return MyFinancesResponse(**response.dict()) diff --git a/myfinances/finance/__pycache__/__init__.cpython-312.pyc b/myfinances/finance/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e736336..0000000 Binary files a/myfinances/finance/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/myfinances/finance/invoices/__pycache__/__init__.cpython-312.pyc b/myfinances/finance/invoices/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5c0dab9..0000000 Binary files a/myfinances/finance/invoices/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/myfinances/finance/invoices/__pycache__/models.cpython-312.pyc b/myfinances/finance/invoices/__pycache__/models.cpython-312.pyc deleted file mode 100644 index b844c61..0000000 Binary files a/myfinances/finance/invoices/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/myfinances/finance/invoices/__pycache__/service.cpython-312.pyc b/myfinances/finance/invoices/__pycache__/service.cpython-312.pyc deleted file mode 100644 index 4607e69..0000000 Binary files a/myfinances/finance/invoices/__pycache__/service.cpython-312.pyc and /dev/null differ diff --git a/myfinances/finance/invoices/models.py b/myfinances/finance/invoices/models.py index d54a4bb..fb1dd81 100644 --- a/myfinances/finance/invoices/models.py +++ b/myfinances/finance/invoices/models.py @@ -10,11 +10,13 @@ class Invoice(BaseModel): due_date: Optional[str] = None description: Optional[str] = None + class CreateInvoiceResponse(BaseModel): customer_id: int amount: condecimal(gt=0) description: Optional[str] = None due_date: Optional[str] = None + class InvoiceList(BaseModel): - invoices: List[Invoice] \ No newline at end of file + invoices: List[Invoice] diff --git a/myfinances/finance/invoices/service.py b/myfinances/finance/invoices/service.py index 240e246..455f8e2 100644 --- a/myfinances/finance/invoices/service.py +++ b/myfinances/finance/invoices/service.py @@ -1,5 +1,5 @@ from myfinances.base_service import BaseService -from myfinances.finance.invoices.models import InvoiceList, CreateInvoiceResponse +from myfinances.finance.invoices.models import InvoiceList, CreateInvoiceResponse, Invoice from myfinances.models import MyFinancesResponse @@ -14,10 +14,82 @@ def create_invoice(self, } response = self._client._post("/invoices/create", json=payload) - return MyFinancesResponse(**response.dict()) - def list_invoices(self) -> MyFinancesResponse[InvoiceList]: response = self._client._get(f"/invoices/") return MyFinancesResponse(**response.dict()) + + def get_invoice(self, invoice_id: int) -> MyFinancesResponse[Invoice]: + """ + Gathering a specific Invoice by ID + + Args: + invoice_id (int): Invoice ID + + Returns: + MyFinancesResponse[Client]: Invoice details + """ + response = self._client._get(f"/invoices/{invoice_id}/") + return MyFinancesResponse(**response.dict()) + + def delete_invoice(self, invoice_id: int) -> MyFinancesResponse[Invoice]: + """ + Delete a specific Invoice by ID + + Args: + invoice_id (int): Invoice ID + + Returns: + MyFinancesResponse[Invoice]: Deleting the specified Invoice + """ + response = self._client._delete(f"/invoices/{invoice_id}/delete") + return MyFinancesResponse(**response.dict()) + + def search_invoices(self, customer_id: int = None, status: str = None) -> MyFinancesResponse[InvoiceList]: + """ + Search for a specific Invoice by use of filters + + Args: + customer_id (int): Customer ID + status (str): Invoice status + + Returns: + MyFinancesResponse[InvoiceList]: invoice list with the specified filters + """ + params = {} + + if customer_id is not None: + params["customer_id"] = customer_id + + if status is not None: + params["status"] = status + + response = self._client._get(f"/invoices/search", params=params) + return MyFinancesResponse(**response.dict()) + + def update_invoice(self, customer_id: int, amount: float = None, description: str = None, due_date: str = None, status: str = None ) -> MyFinancesResponse[Invoice]: + """ + Updating an existing Invoice + + Args: + customer_id (int): customer's ID + amount (float): Invoice total amount to be change + description (str): Invoice description to be change + due_date (str): due date of the invoice to be changed + status (str): Invoice status to be changed + + Returns: + MyFinancesResponse[Invoice]: updating an existing invoice's details + """ + params = { + key: value for key, value in{ + "amount": amount, + "description": description, + "due_date": due_date, + "status": status, + }.items() if value is not None + } + + response = self._client._patch(f"/invoices/{customer_id}/update", json=params) + return MyFinancesResponse(**response.dict()) diff --git a/myfinances/finance/receipts/__init__.py b/myfinances/finance/receipts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myfinances/finance/receipts/models.py b/myfinances/finance/receipts/models.py new file mode 100644 index 0000000..c5e989c --- /dev/null +++ b/myfinances/finance/receipts/models.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, FilePath, condecimal +from typing import Optional, List + + +class Receipt(BaseModel): + id: int + name: str + image: FilePath + date: Optional[str] = None + merchant_store: Optional[str] = None + purchase_category: Optional[str] = None + total_price: condecimal(gt=0) + owner: Optional[str] = None + + +class ReceiptList(BaseModel): + receipts: List[Receipt] + + +class ReceiptIDResponse(BaseModel): + receipt_id: int diff --git a/myfinances/finance/receipts/service.py b/myfinances/finance/receipts/service.py new file mode 100644 index 0000000..df1ce84 --- /dev/null +++ b/myfinances/finance/receipts/service.py @@ -0,0 +1,110 @@ +from typing import Optional + +from pydantic import FilePath + +from myfinances.base_service import BaseService +from myfinances.finance.receipts.models import Receipt, ReceiptList, ReceiptIDResponse +from myfinances.models import MyFinancesResponse + + +class ReceiptService(BaseService): + def create_receipt(self, + name: str, + image: Optional[FilePath] = None, + date: Optional[str] = None, + merchant_store: Optional[str] = None, + purchase_category: Optional[str] = None, + total_amount: float = None, + owner: Optional[str] = None + ) -> MyFinancesResponse[ReceiptIDResponse]: + """ + Creates a new receipt + + Args: + name(str): The name of the client + image(Optional[FilePath]): file path of the image of the receipt. + date(Optional[str]): date of the receipt (format: YYYY-MM-DD) + merchant_store(Optional[str]): The names store or merchant + purchase_category(Optional[str]): Category of the purchase + total_amount(float): The total of the purchase + owner(Optional[str]): Owner of the store + + Returns: + MyFinancesResponse[ReceiptIDResponse]: Creation of the receipt + """ + + params = { + "name": name, + "image": image, + "date": date, + "merchant_store": merchant_store, + "purchase_category": purchase_category, + "total_amount": total_amount, + "owner": owner + } + + response = self._client._post("/receipts/create/", json=params) + return MyFinancesResponse(**response.dict()) + + def list_receipts(self) -> MyFinancesResponse[ReceiptList]: + """ + Lists all the receipts + + Returns: + MyFinancesResponse[ReceiptList]: lists of all the receipts in the system + """ + response = self._client._get("/receipts/") + return MyFinancesResponse(**response.dict()) + + def delete_receipt(self, receipt_id: int) -> MyFinancesResponse[ReceiptIDResponse]: + """ + Deletes a receipt by its ID. + + Args: + receipt_id(int): Receipt ID's to delete the receipt + + Returns: + MyFinancesResponse[ReceiptIDResponse]: Deletion of the receipt + """ + response = self._client._delete(f"/receipts/{receipt_id}/delete") + return MyFinancesResponse(**response.dict()) + + def search_receipts(self, + receipt_id: int = None, + name: str = None, + merchant_store: str = None, + image: Optional[FilePath] = None, + date: Optional[str] = None, + purchase_category: Optional[str] = None, + total_amount: float = None, + owner: Optional[str] = None) -> MyFinancesResponse[ReceiptList]: + """ + Searching for the receipts for a specific receipt by using filters + + Args: + receipt_id(Optional[int]): Receipt ID's + name(str): The name of the client + image(Optional[FilePath]): file path of the image of the receipt. + date(Optional[str]): date of the receipt (format: YYYY-MM-DD) + merchant_store(Optional[str]): The names store or merchant + purchase_category(Optional[str]): Category of the purchase + total_amount(float): The total of the purchase + owner(Optional[str]): Owner of the store + + Returns: + MyFinancesResponse[ReceiptList]: List of receipts returning base the filter + """ + params = { + key: value for key, value in { + "id": receipt_id, + "name": name, + "merchant_store": merchant_store, + "image": image, + "date": date, + "purchase_category": purchase_category, + "total_amount": total_amount, + "owner": owner + }.items() if value is not None + } + response = self._client._get("/receipts/search/", params=params) + return MyFinancesResponse(**response.dict()) diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e88dd31..0000000 Binary files a/tests/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/tests/__pycache__/test_finance.cpython-312-pytest-8.3.3.pyc b/tests/__pycache__/test_finance.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index feb0cec..0000000 Binary files a/tests/__pycache__/test_finance.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_myfinances_client.cpython-312-pytest-8.3.3.pyc b/tests/__pycache__/test_myfinances_client.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index a885ecb..0000000 Binary files a/tests/__pycache__/test_myfinances_client.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/tests/test_clients.py b/tests/test_clients.py new file mode 100644 index 0000000..d06ae5f --- /dev/null +++ b/tests/test_clients.py @@ -0,0 +1,220 @@ +import pytest +from unittest.mock import Mock + +from myfinances import MyFinancesClient +from myfinances.clients.service import ClientsService + + +@pytest.fixture +def mock_client(): + mock = Mock(spec=MyFinancesClient) + mock.session = Mock() + return mock + + +@pytest.fixture +def clients_service(mock_client): + return ClientsService(mock_client) + + +def test_create_client(clients_service, mock_client): + clients_data = { + "name": "John Doe", + "phone_number": "1234567890", + "email": "john.doe@example.com", + "company": "Example Inc", + "contact_method": "email", + "is_representative": True, + "address": "123 Main St", + "city": "Sample City", + "country": "Sample Country" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": {"client_id": 123} + } + + mock_post = Mock() + mock_post.dict.return_value = mock_response_data + mock_client._post.return_value = mock_post + + response = clients_service.create_client(**clients_data) + + mock_client._post.assert_called_once_with("/clients/create/", json=clients_data) + assert response.data["client_id"] == 123 + assert response.meta.success is True + assert response.meta.status_code == 200 + + +def test_list_clients(clients_service, mock_client): + clients_data = [ + {"id": 1, "name": "Client 1", "company": "Company 1", "phone_number": "1234567890", + "email": "company1@company1.com", "contact_method": "phone", "is_representative": True, "address": "123 Main", + "city": "Sample City", "country": "Australia"}, + {"id": 2, "name": "Client 2", "company": "Company 2", "phone_number": "0987654321", + "email": "company2@company2.com", "contact_method": "email", "is_representative": True, + "address": "321 Main", "city": "Sample City", "country": "South America"}, + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": clients_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + response = clients_service.list_clients() + + mock_client._get.assert_called_once_with("/clients/", params={}) + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert isinstance(response.data, list) + assert len(response.data) == 2 + assert response.data[0]["id"] == clients_data[0]["id"] + + +def test_client_by_id(clients_service, mock_client): + clients_data = { + "id": 1, + "name": "Client 1", + "company": "Company 1", + "phone_number": "1234567890", + "email": "company1@company1.com", + "contact_method": "phone", + "is_representative": True, + "address": "123 Main", + "city": "Sample City", + "country": "Australia" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": clients_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + clients_id = 1 + response = clients_service.get_client(clients_id) + + mock_client._get.assert_called_once_with(f"/clients/{clients_id}") + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert response.data["id"] == clients_data["id"] + assert response.data["name"] == clients_data["name"] + assert response.data["company"] == clients_data["company"] + assert response.data == clients_data + + +def test_delete_clients(clients_service, mock_client): + clients_data = [ + {"id": 1, "name": "Client 1", "company": "Company 1", "phone_number": "1234567890", + "email": "company1@company1.com", "contact_method": "phone", "is_representative": True, "address": "123 Main", + "city": "Sample City", "country": "Australia"}, + {"id": 2, "name": "Client 2", "company": "Company 2", "phone_number": "0987654321", + "email": "company2@company2.com", "contact_method": "email", "is_representative": True, + "address": "321 Main", "city": "Sample City", "country": "South America"}, + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": clients_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._delete.return_value = mock_response + + Client_Id = 1 + response = clients_service.delete_client(Client_Id) + + mock_client._delete.assert_called_once_with(f"/clients/{Client_Id}/delete") + assert response.meta.success is True + assert response.meta.status_code == 200 + + remaining_clients = [clients for clients in clients_data if clients["id"] == Client_Id] + assert len(remaining_clients) == 1 + assert remaining_clients[0]["id"] == Client_Id + + +def test_update_clients_name(clients_service, mock_client): + clients_data = { + "id": 1, + "name": "Client 1", + "company": "Company 1", + "phone_number": "1234567890", + "email": "company1@company1.com", + "contact_method": "phone", + "is_representative": True, + "address": "123 Main", + "city": "Sample City", + "country": "Australia" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": clients_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._patch.return_value = mock_response + + new_name = "Client 3" + response = clients_service.update_client(name=new_name) + + mock_client._patch.assert_called_once_with(f"/clients/update/", json={"name": new_name}) + assert response.meta.success is True + assert response.meta.status_code == 200 + + # Updated Version + updated_clients_data = { + "id": 1, + "name": "Client 3", + "company": "Company 1", + "phone_number": "1234567890", + "email": "company1@company1.com", + "contact_method": "phone", + "is_representative": True, + "address": "123 Main", + "city": "Sample City", + "country": "Australia" + } + + mock_get_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": updated_clients_data + } + + mock_get_response = Mock() + mock_get_response.dict.return_value = mock_get_response_data + mock_client._get.return_value = mock_get_response + + get_response = clients_service.get_client(1) + mock_client._get.assert_called_once_with(f"/clients/1") + assert get_response.data["name"] == "Client 3" diff --git a/tests/test_finance.py b/tests/test_finance.py index f37d800..da9137c 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -1,8 +1,8 @@ - import pytest from unittest.mock import Mock from myfinances import MyFinancesClient -from myfinances.finance.invoices import InvoicesService, Invoice +from myfinances.finance.invoices import InvoicesService + @pytest.fixture def mock_client(): @@ -14,39 +14,224 @@ def mock_client(): def invoices_service(mock_client): return InvoicesService(mock_client) -def test_create_invoice(invoices_service): - mock_response = Mock() - mock_response.json.return_value = { - "id": 1, + +def test_create_invoice(invoices_service, mock_client): + new_invoice_data ={ "customer_id": 123, - "amount": 250.0, - "status": "created", - "due_date": None, - "description": "Service Fee" + "amount": 100, + "description": "Data Analytics service", + "due_date": "2020-05-21" } - mock_response.status_code = 200 - invoices_service._client.session.post.return_value = mock_response - invoice = invoices_service.create_invoice(customer_id=123, amount=250.0, description="Service Fee") + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": { + "invoice_id": 102, + } + } + + mock_post = Mock() + mock_post.dict.return_value = mock_response_data + mock_client._post.return_value = mock_post + + response = invoices_service.create_invoice(**new_invoice_data) + + mock_client._post.assert_called_once_with("/invoices/create", json=new_invoice_data) + assert response.data["invoice_id"] == 102 + assert response.meta.success is True + assert response.meta.status_code == 200 - assert isinstance(invoice, Invoice) - assert invoice.id == 1 - assert invoice.customer_id == 123 - assert invoice.amount == 250.0 - assert invoice.description == "Service Fee" -def test_list_invoices(invoices_service): +def test_list_invoices(invoices_service, mock_client): + + invoice_list = [ + {"id": 1, "customer_id": 123, "amount": 250.0, "description": "Service Fee", "status": "pending"}, + {"id": 2, "customer_id": 124, "amount": 300.0, "description": "Consulting Fee", "status": "pending"} + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": invoice_list + } + mock_response = Mock() - mock_response.json.return_value = [ + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + response = invoices_service.list_invoices() + + mock_client._get.assert_called_once_with("/invoices/") + + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert isinstance(response.data, list) + assert len(response.data) == 2 + assert response.data[0]["id"] == 1 + assert response.data[1]["amount"] == 300.0 + assert response.data[0]["status"] == "pending" + assert response.data[1]["status"] == "pending" + + +def test_delete_invoice(invoices_service, mock_client): + invoice_list = [ {"id": 1, "customer_id": 123, "amount": 250.0, "description": "Service Fee", "status": "pending"}, {"id": 2, "customer_id": 124, "amount": 300.0, "description": "Consulting Fee", "status": "pending"} ] - mock_response.status_code = 200 - invoices_service._client.session.get.return_value = mock_response - invoices = invoices_service.list_invoices() + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": invoice_list + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + + mock_client._delete.return_value = mock_response + + invoice_id = 1 + + response = invoices_service.delete_invoice(invoice_id) + + mock_client._delete.assert_called_once_with(f"/invoices/{invoice_id}/delete") + + assert response.meta.success is True + assert response.meta.status_code == 200 + + remaining_invoices = [invoice for invoice in invoice_list if invoice['id'] != invoice_id] + + assert len(remaining_invoices) == 1 + assert remaining_invoices[0]['id'] == 2 + + +def test_get_invoice(invoices_service, mock_client): + invoice_data = { + "id": 2, + "customer_id": 124, + "amount": 300.0, + "description": "Consulting Fee", + "status": "pending" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": invoice_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + invoice_id = 2 + response = invoices_service.get_invoice(invoice_id) + + mock_client._get.assert_called_once_with(f"/invoices/{invoice_id}/") + + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert response.data["id"] == invoice_data["id"] + assert response.data["customer_id"] == invoice_data["customer_id"] + assert response.data["amount"] == invoice_data["amount"] + assert response.data["description"] == invoice_data["description"] + assert response.data["status"] == invoice_data["status"] + + +def test_search_invoices_by_id(invoices_service, mock_client): + invoice_data = [ + {"id": 1, "customer_id": 123, "amount": 300.0, "description": "Analytics Consultant", "status": "pending"}, + {"id": 2, "customer_id": 124, "amount": 150.0, "description": "Service fee", "status": "paid"} + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": invoice_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + customer_id = 124 + + response = invoices_service.search_invoices(customer_id) + mock_client._get.assert_called_once_with(f"/invoices/search", params={"customer_id": customer_id}) + + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert response.data[1]["id"] == 2 + assert response.data[1]["customer_id"] == 124 + + +def test_update_invoice(invoices_service, mock_client): + invoice_data = { + "id": 1, + "customer_id": "125", + "amount": 100, + "description": "Service fees", + "due_date": "2024-12-31", + "status": "pending" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": invoice_data + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._patch.return_value = mock_response + + new_status = "paid" + + response = invoices_service.update_invoice(1, status=new_status) + + mock_client._patch.assert_called_once_with(f"/invoices/1/update", json={"status": new_status}) + + assert response.meta.success is True + assert response.meta.status_code == 200 + + # updated version + updated_invoice_data = { + "id": 1, + "customer_id": "125", + "amount": 100, + "description": "Service fees", + "due_date": "2024-12-31", + "status": "paid" + } + + mock_get_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": updated_invoice_data + } + + mock_get_response = Mock() + mock_get_response.dict.return_value = mock_get_response_data + mock_client._get.return_value = mock_get_response - assert len(invoices) == 2 - assert isinstance(invoices[0], Invoice) - assert invoices[0].id == 1 - assert invoices[1].amount == 300.0 \ No newline at end of file + get_response = invoices_service.get_invoice(1) + mock_client._get.assert_called_once_with(f"/invoices/1/") + assert get_response.data["status"] == "paid" diff --git a/tests/test_receipts.py b/tests/test_receipts.py new file mode 100644 index 0000000..df3aa4c --- /dev/null +++ b/tests/test_receipts.py @@ -0,0 +1,143 @@ +import pytest +from unittest.mock import Mock +from myfinances import MyFinancesClient +from myfinances.finance.receipts.service import ReceiptService + + +@pytest.fixture +def mock_client(): + mock = Mock(spec=MyFinancesClient) + mock.session = Mock() + return mock + + +@pytest.fixture +def receipts_service(mock_client): + return ReceiptService(mock_client) + + +def test_create_receipt(receipts_service, mock_client): + receipts_data = { + "name": "Client 1", + "image": "file_example", + "date": "2024-05-21", + "merchant_store": "Store 1", + "purchase_category": "Purchase 1", + "total_amount": 500, + "owner": "Client 2" + } + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": receipts_data + } + + mock_post = Mock() + mock_post.dict.return_value = mock_response_data + mock_client._post.return_value = mock_post + + response = receipts_service.create_receipt(**receipts_data) + + mock_client._post.assert_called_once_with("/receipts/create/", json=receipts_data) + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert response.data["name"] == receipts_data["name"] + assert response.data["image"] == receipts_data["image"] + + +def test_list_receipts(receipts_service, mock_client): + list_of_receipts = [ + {"id": 1, "name": "Client 1", "image": "file_example", "date": "2024-05-21", + "merchant_store": "Store 1", "purchase_category": "Purchase 1", "total_amount": 100, "owner": "Owner 1"}, + {"id": 2, "name": "Client 2", "image": "file1_example", "date": "2024-03-21", + "merchant_store": "Store 2", "purchase_category": "Purchase 2", "total_amount": 245, "owner": "Owner 2"} + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": list_of_receipts + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + response = receipts_service.list_receipts() + mock_client._get.assert_called_once_with("/receipts/") + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert isinstance(response.data, list) + assert len(response.data) == 2 + + +def test_delete_receipt(receipts_service, mock_client): + list_of_receipts = [ + {"id": 1, "name": "Client 1", "image": "file_example", "date": "2024-05-21", + "merchant_store": "Store 1", "purchase_category": "Purchase 1", "total_amount": 100, "owner": "Owner 1"}, + {"id": 2, "name": "Client 2", "image": "file1_example", "date": "2024-03-21", + "merchant_store": "Store 2", "purchase_category": "Purchase 2", "total_amount": 245, "owner": "Owner 2"} + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": list_of_receipts + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + + mock_client._delete = Mock(return_value=mock_response) + + receipt_id = 2 + + response = receipts_service.delete_receipt(receipt_id) + + mock_client._delete.assert_called_once_with(f"/receipts/{receipt_id}/delete") + + assert response.meta.success is True + assert response.meta.status_code == 200 + + remaining_receipts = [receipt for receipt in list_of_receipts if receipt["id"] != receipt_id] + assert len(remaining_receipts) == 1 + assert remaining_receipts[0]["id"] == 1 + + +def test_search_receipt(receipts_service, mock_client): + list_of_receipts = [ + {"id": 1, "name": "Client 1", "image": "file_example", "date": "2024-05-21", + "merchant_store": "Store 1", "purchase_category": "Purchase 1", "total_amount": 100, "owner": "Owner 1"}, + {"id": 2, "name": "Client 2", "image": "file1_example", "date": "2024-03-21", + "merchant_store": "Store 2", "purchase_category": "Purchase 2", "total_amount": 245, "owner": "Owner 2"} + ] + + mock_response_data = { + "meta": { + "success": True, + "status_code": 200, + }, + "data": list_of_receipts + } + + mock_response = Mock() + mock_response.dict.return_value = mock_response_data + mock_client._get.return_value = mock_response + + search_name = "Client 2" + response = receipts_service.search_receipts(name=search_name) + mock_client._get.assert_called_once_with("/receipts/search/", params={"name": search_name}) + + assert response.meta.success is True + assert response.meta.status_code == 200 + + assert response.data[1]["name"] == search_name