diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/paystack_client.py b/app/paystack_client.py index fe8cbc3..b1e975a 100644 --- a/app/paystack_client.py +++ b/app/paystack_client.py @@ -1,6 +1,6 @@ import os -import paystack from dotenv import load_dotenv +import httpx load_dotenv() @@ -17,61 +17,77 @@ def __init__(self, api_key: str | None = None): raise ValueError("Paystack API key not provided.") self.api_key = api_key - paystack.api_key = self.api_key + self.client = httpx.AsyncClient( + base_url=PAYSTACK_API_BASE, + headers={"Authorization": f"Bearer {self.api_key}"}, + ) - def get_balance(self): + async def get_balance(self): """Get the balance from the Paystack API.""" - return paystack.Balance.fetch() + response = await self.client.get("/balance") + return response.json() - def get_balance_ledger(self): + async def get_balance_ledger(self): """Get the balance ledger from the Paystack API.""" - return paystack.Balance.ledger() + response = await self.client.get("/balance/ledger") + return response.json() - def list_customers(self): + async def list_customers(self): """List customers from the Paystack API.""" - return paystack.Customer.list() + response = await self.client.get("/customer") + return response.json() - def create_customer( + async def create_customer( self, email: str, first_name: str, last_name: str, phone: str | None = None ): """Create a customer using the Paystack API.""" - return paystack.Customer.create( - email=email, first_name=first_name, last_name=last_name, phone=phone - ) - - def fetch_customer(self, customer_code: str): + payload = { + "email": email, + "first_name": first_name, + "last_name": last_name, + "phone": phone, + } + response = await self.client.post("/customer", json=payload) + return response.json() + + async def fetch_customer(self, customer_code: str): """Fetch a customer's details from the Paystack API.""" - return paystack.Customer.fetch(customer_code) + response = await self.client.get(f"/customer/{customer_code}") + return response.json() - def update_customer( + async def update_customer( self, code: str, first_name: str, last_name: str, phone: str | None = None ): """Update a customer's details using the Paystack API.""" - return paystack.Customer.update( - code=code, first_name=first_name, last_name=last_name, phone=phone - ) + payload = {"first_name": first_name, "last_name": last_name, "phone": phone} + response = await self.client.put(f"/customer/{code}", json=payload) + return response.json() - def list_products(self): + async def list_products(self): """List products from the Paystack API.""" - return paystack.Product.list() + response = await self.client.get("/product") + return response.json() - def create_product( + async def create_product( self, name: str, description: str, price: int, currency: str, quantity: int = 1 ): """Create a product using the Paystack API.""" - return paystack.Product.create( - name=name, - description=description, - price=price, - currency=currency, - quantity=quantity, - ) - - def fetch_product(self, product_code: str): + payload = { + "name": name, + "description": description, + "price": price, + "currency": currency, + "quantity": quantity, + } + response = await self.client.post("/product", json=payload) + return response.json() + + async def fetch_product(self, product_code: str): """Fetch a product's details from the Paystack API.""" - return paystack.Product.fetch(product_code) + response = await self.client.get(f"/product/{product_code}") + return response.json() - def update_product( + async def update_product( self, product_code: str, name: str | None, @@ -81,50 +97,59 @@ def update_product( quantity: int | None = None, ): """Update a product's details using the Paystack API.""" - return paystack.Product.update( - id=product_code, - name=name, - description=description, - price=price, - currency=currency, - quantity=quantity, - ) - - def delete_product(self, product_code: str): + payload = { + "name": name, + "description": description, + "price": price, + "currency": currency, + "quantity": quantity, + } + response = await self.client.put(f"/product/{product_code}", json=payload) + return response.json() + + async def delete_product(self, product_code: str): """Delete a product using the Paystack API.""" - return paystack.Product.delete(product_code) + response = await self.client.delete(f"/product/{product_code}") + return response.json() - def list_invoices(self): + async def list_invoices(self): """List invoices from the Paystack API.""" - return paystack.PaymentRequest.list() + response = await self.client.get("/paymentrequest") + return response.json() - def create_invoice(self, customer: str, amount: int): + async def create_invoice(self, customer: str, amount: int): """Create an invoice using the Paystack API.""" - return paystack.PaymentRequest.create(customer=customer, amount=amount) + payload = {"customer": customer, "amount": amount} + response = await self.client.post("/paymentrequest", json=payload) + return response.json() - def list_transactions(self): + async def list_transactions(self): """List transactions from the Paystack API.""" - return paystack.Transaction.list() + response = await self.client.get("/transaction") + return response.json() - def initialize_transaction(self, email: str, amount: int, currency: str): + async def initialize_transaction(self, email: str, amount: int, currency: str): """Initialize a transaction using the Paystack API.""" - return paystack.Transaction.initialize( - email=email, amount=amount, currency=currency - ) + payload = {"email": email, "amount": amount, "currency": currency} + response = await self.client.post("/transaction/initialize", json=payload) + return response.json() - def verify_transaction(self, reference: str): + async def verify_transaction(self, reference: str): """Verify a transaction using the Paystack API.""" - return paystack.Transaction.verify(reference=reference) + response = await self.client.get(f"/transaction/verify/{reference}") + return response.json() - def fetch_transaction(self, transaction_id: str): + async def fetch_transaction(self, transaction_id: str): """Fetch a transaction's details from the Paystack API.""" - return paystack.Transaction.fetch(transaction_id) + response = await self.client.get(f"/transaction/{transaction_id}") + return response.json() - def get_transaction_timeline(self, id_or_reference: str): + async def get_transaction_timeline(self, id_or_reference: str): """Get a transaction's timeline from the Paystack API.""" - return paystack.Transaction.timeline(id_or_reference) + response = await self.client.get(f"/transaction/timeline/{id_or_reference}") + return response.json() - def download_transactions( + async def download_transactions( self, per_page: int | None = 50, page: int | None = 1, @@ -132,27 +157,33 @@ def download_transactions( to_date: str | None = None, ): """Download a transactions receipt from the Paystack API.""" - return paystack.Transaction.download( - per_page=per_page, page=page, _from=from_date, to=to_date - ) + params = {"perPage": per_page, "page": page, "from": from_date, "to": to_date} + response = await self.client.get("/transaction/export", params=params) + return response.json() - def create_refund(self, transaction: str, amount: int | None = None): + async def create_refund(self, transaction: str, amount: int | None = None): """Create a refund using the Paystack API.""" - return paystack.Refund.create(transaction=transaction, amount=amount) + payload = {"transaction": transaction, "amount": amount} + response = await self.client.post("/refund", json=payload) + return response.json() - def list_subscriptions(self): + async def list_subscriptions(self): """List subscriptions from the Paystack API.""" - return paystack.Subscription.list() + response = await self.client.get("/subscription") + return response.json() - def disable_subscription(self, code: str, token: str): + async def disable_subscription(self, code: str, token: str): """Disable a subscription using the Paystack API.""" - return paystack.Subscription.disable(code=code, token=token) + payload = {"code": code, "token": token} + response = await self.client.post("/subscription/disable", json=payload) + return response.json() - def list_disputes(self): + async def list_disputes(self): """List disputes from the Paystack API.""" - return paystack.Dispute.list() + response = await self.client.get("/dispute") + return response.json() - def add_evidence_to_dispute( + async def add_evidence_to_dispute( self, dispute_id: str, customer_email: str, @@ -161,19 +192,21 @@ def add_evidence_to_dispute( service_details: str, ): """Add evidence to a dispute using the Paystack API.""" - return paystack.Dispute.add_evidence( - id=dispute_id, - customer_email=customer_email, - customer_name=customer_name, - customer_phone=customer_phone, - service_details=service_details, - ) - - def fetch_dispute(self, dispute_id: str): + payload = { + "customer_email": customer_email, + "customer_name": customer_name, + "customer_phone": customer_phone, + "service_details": service_details, + } + response = await self.client.post(f"/dispute/{dispute_id}/evidence", json=payload) + return response.json() + + async def fetch_dispute(self, dispute_id: str): """Fetch a dispute's details from the Paystack API.""" - return paystack.Dispute.fetch(dispute_id) + response = await self.client.get(f"/dispute/{dispute_id}") + return response.json() - def download_dispute( + async def download_dispute( self, per_page: int | None = 50, page: int | None = 1, @@ -181,11 +214,11 @@ def download_dispute( to_date: str | None = None, ): """Download a dispute receipt from the Paystack API.""" - return paystack.Dispute.download( - per_page=per_page, page=page, _from=from_date, to=to_date - ) + params = {"perPage": per_page, "page": page, "from": from_date, "to": to_date} + response = await self.client.get("/dispute/export", params=params) + return response.json() - def resolve_dispute( + async def resolve_dispute( self, dispute_id: str, resolution: str, @@ -195,25 +228,35 @@ def resolve_dispute( evidence: str | None = None, ): """Resolve a dispute using the Paystack API.""" - return paystack.Dispute.resolve( - dispute_id, resolution, message, refund_amount, uploaded_filename, evidence - ) - - def create_payment_page( + payload = { + "resolution": resolution, + "message": message, + "refund_amount": refund_amount, + "uploaded_filename": uploaded_filename, + "evidence": evidence, + } + response = await self.client.put(f"/dispute/{dispute_id}/resolve", json=payload) + return response.json() + + async def create_payment_page( self, name: str, amount: int, description: str | None = None ): """Create a payment page using the Paystack API.""" - return paystack.Page.create(name=name, amount=amount, description=description) + payload = {"name": name, "amount": amount, "description": description} + response = await self.client.post("/page", json=payload) + return response.json() - def list_payment_pages(self): + async def list_payment_pages(self): """List payment pages from the Paystack API.""" - return paystack.Page.list() + response = await self.client.get("/page") + return response.json() - def fetch_payment_page(self, id: str): + async def fetch_payment_page(self, id: str): """Fetch a payment page's details from the Paystack API.""" - return paystack.Page.fetch(id) + response = await self.client.get(f"/page/{id}") + return response.json() - def update_payment_page( + async def update_payment_page( self, id: str, name: str | None = None, @@ -221,50 +264,62 @@ def update_payment_page( amount: int | None = None, ): """Update a payment page using the Paystack API.""" - return paystack.Page.update( - id, name=name, description=description, amount=amount - ) + payload = {"name": name, "description": description, "amount": amount} + response = await self.client.put(f"/page/{id}", json=payload) + return response.json() - def disable_payment_page(self, id: str): + async def disable_payment_page(self, id: str): """Disable a payment page using the Paystack API.""" - return paystack.Page.update(id, active=False) + payload = {"active": False} + response = await self.client.put(f"/page/{id}", json=payload) + return response.json() - def enable_payment_page(self, id: str): + async def enable_payment_page(self, id: str): """Enable a payment page using the Paystack API.""" - return paystack.Page.update(id, active=True) + payload = {"active": True} + response = await self.client.put(f"/page/{id}", json=payload) + return response.json() - def add_products_to_payment_page(self, id: str, products: list[str]): + async def add_products_to_payment_page(self, id: str, products: list[str]): """Add products to a payment page using the Paystack API.""" - return paystack.Page.add_products(id=id, product=products) + payload = {"product": products} + response = await self.client.post(f"/page/{id}/product", json=payload) + return response.json() - def create_plan(self, name: str, amount: int, interval: str): + async def create_plan(self, name: str, amount: int, interval: str): """Create a plan using the Paystack API.""" - return paystack.Plan.create(name=name, amount=amount, interval=interval) + payload = {"name": name, "amount": amount, "interval": interval} + response = await self.client.post("/plan", json=payload) + return response.json() - def list_plans(self): + async def list_plans(self): """List plans from the Paystack API.""" - return paystack.Plan.list() + response = await self.client.get("/plan") + return response.json() - def fetch_plan(self, plan_code: str): + async def fetch_plan(self, plan_code: str): """Fetch a plan's details from the Paystack API.""" - return paystack.Plan.fetch(plan_code) + response = await self.client.get(f"/plan/{plan_code}") + return response.json() - def resolve_account_number(self, account_number: str, bank_code: str): + async def resolve_account_number(self, account_number: str, bank_code: str): """Resolve an account number using the Paystack API.""" - return paystack.Verification.resolve_account_number( - account_number=account_number, bank_code=bank_code - ) + params = {"account_number": account_number, "bank_code": bank_code} + response = await self.client.get("/bank/resolve", params=params) + return response.json() - def list_avs( + async def list_avs( self, country: str, type: str | None = None, currency: str | None = None, ): """List states for address_verification the Paystack API.""" - return paystack.Verification.avs(type=type, country=country, currency=currency) + params = {"type": type, "country": country, "currency": currency} + response = await self.client.get("/address_verification/states", params=params) + return response.json() - def fetch_banks( + async def fetch_banks( self, country: str | None = None, pay_with_bank_transfer: bool | None = None, @@ -275,24 +330,37 @@ def fetch_banks( gateway: str | None = None, ): """Fetch a bank's details from the Paystack API.""" - return paystack.Verification.fetch_banks( - country=country, - pay_with_bank_transfer=pay_with_bank_transfer, - use_cursor=use_cursor, - per_page=per_page, - next=next, - previous=previous, - gateway=gateway, - ) - - def list_countries(self): + params = { + "country": country, + "pay_with_bank_transfer": pay_with_bank_transfer, + "use_cursor": use_cursor, + "per_page": per_page, + "next": next, + "previous": previous, + "gateway": gateway, + } + response = await self.client.get("/bank", params=params) + return response.json() + + async def list_countries(self): """List countries from the Paystack API.""" - return paystack.Verification.list_countries() + response = await self.client.get("/country") + return response.json() - def resolve_card_bin(self, card_bin: str): + async def resolve_card_bin(self, card_bin: str): """Resolve a card bin using the Paystack API.""" - return paystack.Verification.resolve_card_bin(card_bin) + response = await self.client.get(f"/decision/bin/{card_bin}") + return response.json() + + +class _LazyPaystackClient: + _instance = None + + def __getattr__(self, name): + if self._instance is None: + self._instance = PaystackClient() + return getattr(self._instance, name) # A single client instance to be used by the tools -paystack_client = PaystackClient() +paystack_client = _LazyPaystackClient() \ No newline at end of file diff --git a/app/tools.py b/app/tools.py index 4c87c8b..1507251 100644 --- a/app/tools.py +++ b/app/tools.py @@ -3,31 +3,31 @@ @mcp.tool(name="balance.read") -def get_balance(): +async def get_balance(): """ Retrieves the balance from a Paystack account. """ - return paystack_client.get_balance() + return await paystack_client.get_balance() @mcp.tool(name="balance.ledger") -def get_balance_ledger(): +async def get_balance_ledger(): """ Retrieves the balance ledger from a Paystack account. """ - return paystack_client.get_balance_ledger() + return await paystack_client.get_balance_ledger() @mcp.tool(name="customer.list") -def list_customers(): +async def list_customers(): """ Retrieves a list of all customers. """ - return paystack_client.list_customers() + return await paystack_client.list_customers() @mcp.tool(name="customer.create") -def create_customer( +async def create_customer( email: str, first_name: str, last_name: str, phone: str | None = None ): """ @@ -39,22 +39,22 @@ def create_customer( last_name: The customer's last name. phone: The customer's phone number (optional). """ - return paystack_client.create_customer(email, first_name, last_name, phone) + return await paystack_client.create_customer(email, first_name, last_name, phone) @mcp.tool(name="customer.read") -def fetch_customer(customer_code: str): +async def fetch_customer(customer_code: str): """ Fetches the details of a specific customer. Args: customer_code: The code of the customer to fetch. """ - return paystack_client.fetch_customer(customer_code) + return await paystack_client.fetch_customer(customer_code) @mcp.tool(name="customer.update") -def update_customer( +async def update_customer( code: str, first_name: str, last_name: str, phone: str | None = None ): """ @@ -66,19 +66,19 @@ def update_customer( last_name: The customer's new last name. phone: The customer's new phone number (optional). """ - return paystack_client.update_customer(code, first_name, last_name, phone) + return await paystack_client.update_customer(code, first_name, last_name, phone) @mcp.tool(name="product.list") -def list_products(): +async def list_products(): """ Retrieves a list of all products. """ - return paystack_client.list_products() + return await paystack_client.list_products() @mcp.tool(name="product.create") -def create_product( +async def create_product( name: str, description: str, price: int, currency: str, quantity: int = 1 ): """ @@ -91,22 +91,24 @@ def create_product( currency: The currency of the price (e.g., NGN). quantity: The available quantity of the product (default is 1). """ - return paystack_client.create_product(name, description, price, currency, quantity) + return await paystack_client.create_product( + name, description, price, currency, quantity + ) @mcp.tool(name="product.read") -def fetch_product(product_code: str): +async def fetch_product(product_code: str): """ Fetches the details of a specific product. Args: product_code: The code of the product to fetch. """ - return paystack_client.fetch_product(product_code) + return await paystack_client.fetch_product(product_code) @mcp.tool(name="product.update") -def update_product( +async def update_product( product_code: str, name: str | None = None, description: str | None = None, @@ -124,31 +126,31 @@ def update_product( currency: The new currency of the price (e.g., NGN) (optional). quantity: The new available quantity of the product (optional). """ - return paystack_client.update_product( + return await paystack_client.update_product( product_code, name, description, price, currency, quantity ) @mcp.tool(name="product.delete") -def delete_product(product_code: str): +async def delete_product(product_code: str): """ Deletes a specific product. Args: product_code: The code of the product to delete. """ - return paystack_client.delete_product(product_code) + return await paystack_client.delete_product(product_code) @mcp.tool(name="invoice.list") -def list_invoices(): +async def list_invoices(): """ Retrieves a list of all invoices. """ - return paystack_client.list_invoices() + return await paystack_client.list_invoices() @mcp.tool(name="invoice.create") -def create_invoice(customer: str, amount: int): +async def create_invoice(customer: str, amount: int): """ Creates a new invoice. @@ -156,19 +158,19 @@ def create_invoice(customer: str, amount: int): customer: The customer's code or email address. amount: The amount of the invoice in the smallest currency unit (e.g., kobo). """ - return paystack_client.create_invoice(customer, amount) + return await paystack_client.create_invoice(customer, amount) @mcp.tool(name="transaction.list") -def list_transactions(): +async def list_transactions(): """ Retrieves a list of all transactions. """ - return paystack_client.list_transactions() + return await paystack_client.list_transactions() @mcp.tool(name="transaction.initialize") -def initialize_transaction(email: str, amount: int, currency: str): +async def initialize_transaction(email: str, amount: int, currency: str): """ Initializes a new transaction. @@ -177,44 +179,44 @@ def initialize_transaction(email: str, amount: int, currency: str): amount: The amount of the transaction in the smallest currency unit (e.g., kobo). currency: The currency of the transaction (e.g., NGN). """ - return paystack_client.initialize_transaction(email, amount, currency) + return await paystack_client.initialize_transaction(email, amount, currency) @mcp.tool(name="transaction.verify") -def verify_transaction(reference: str): +async def verify_transaction(reference: str): """ Verifies the status of a transaction. Args: reference: The reference of the transaction to verify. """ - return paystack_client.verify_transaction(reference) + return await paystack_client.verify_transaction(reference) @mcp.tool(name="transaction.read") -def fetch_transaction(transaction_id: str): +async def fetch_transaction(transaction_id: str): """ Fetches the details of a specific transaction. Args: transaction_id: The ID of the transaction to fetch. """ - return paystack_client.fetch_transaction(transaction_id) + return await paystack_client.fetch_transaction(transaction_id) @mcp.tool(name="transaction.timeline") -def get_transaction_timeline(transaction_id_or_reference: str): +async def get_transaction_timeline(transaction_id_or_reference: str): """ Retrieves the timeline of a specific transaction. Args: transaction_id_or_reference: The ID/Reference of the transaction to get the timeline for. """ - return paystack_client.get_transaction_timeline(transaction_id_or_reference) + return await paystack_client.get_transaction_timeline(transaction_id_or_reference) @mcp.tool(name="transaction.download") -def download_transactions( +async def download_transactions( per_page: int | None = 50, page: int | None = 1, from_date: str | None = None, @@ -229,11 +231,13 @@ def download_transactions( from_date: The start date for filtering transactions (optional, format: 'YYYY-MM-DD'). to_date: The end date for filtering transactions (optional, format: 'YYYY-MM-DD'). """ - return paystack_client.download_transactions(per_page, page, from_date, to_date) + return await paystack_client.download_transactions( + per_page, page, from_date, to_date + ) @mcp.tool(name="refund.create") -def create_refund(transaction: str, amount: int | None = None): +async def create_refund(transaction: str, amount: int | None = None): """ Creates a new refund. @@ -242,19 +246,19 @@ def create_refund(transaction: str, amount: int | None = None): amount: The amount to refund in the smallest currency unit (e.g., kobo). If not provided, a full refund will be issued. """ - return paystack_client.create_refund(transaction, amount) + return await paystack_client.create_refund(transaction, amount) @mcp.tool(name="subscription.list") -def list_subscriptions(): +async def list_subscriptions(): """ Retrieves a list of all subscriptions. """ - return paystack_client.list_subscriptions() + return await paystack_client.list_subscriptions() @mcp.tool(name="subscription.disable") -def disable_subscription(code: str, token: str): +async def disable_subscription(code: str, token: str): """ Disables a subscription. @@ -262,30 +266,30 @@ def disable_subscription(code: str, token: str): code: The subscription code. token: The email token of the customer. """ - return paystack_client.disable_subscription(code, token) + return await paystack_client.disable_subscription(code, token) @mcp.tool(name="dispute.list") -def list_disputes(): +async def list_disputes(): """ Retrieves a list of all disputes. """ - return paystack_client.list_disputes() + return await paystack_client.list_disputes() @mcp.tool(name="dispute.read") -def fetch_dispute(dispute_id: str): +async def fetch_dispute(dispute_id: str): """ Fetches the details of a specific dispute. Args: dispute_id: The ID of the dispute to fetch. """ - return paystack_client.fetch_dispute(dispute_id) + return await paystack_client.fetch_dispute(dispute_id) @mcp.tool(name="dispute.download") -def download_dispute( +async def download_dispute( per_page: int | None = 50, page: int | None = 1, from_date: str | None = None, @@ -300,11 +304,11 @@ def download_dispute( from_date: The start date for filtering dispute (optional, format: 'YYYY-MM-DD'). to_date: The end date for filtering dispute (optional, format: 'YYYY-MM-DD'). """ - return paystack_client.download_dispute(per_page, page, from_date, to_date) + return await paystack_client.download_dispute(per_page, page, from_date, to_date) @mcp.tool(name="dispute.resolve") -def resolve_dispute( +async def resolve_dispute( dispute_id: str, resolution: str, message: str, @@ -324,13 +328,13 @@ def resolve_dispute( evidence: 'evidence_example' # str | Evidence Id for fraud claims (optional) """ - return paystack_client.resolve_dispute( + return await paystack_client.resolve_dispute( dispute_id, resolution, message, refund_amount, uploaded_filename, evidence ) @mcp.tool(name="dispute.add_evidence") -def add_evidence_to_dispute( +async def add_evidence_to_dispute( dispute_id: str, customer_email: str, customer_name: str, @@ -347,44 +351,45 @@ def add_evidence_to_dispute( customer_phone: The phone number of the customer. service_details: Details of the service provided. """ - return paystack_client.add_evidence_to_dispute( + return await paystack_client.add_evidence_to_dispute( dispute_id, customer_email, customer_name, customer_phone, service_details ) @mcp.tool(name="payment_page.create") -def create_payment_page(name: str, amount: int): +async def create_payment_page(name: str, amount: int, description: str | None = None): """ Creates a new payment page. Args: name: The name of the payment page. amount: The amount for the payment page in the smallest currency unit (e.g., kobo). + description: A description for the payment page (optional). """ - return paystack_client.create_payment_page(name, amount) + return await paystack_client.create_payment_page(name, amount, description) @mcp.tool(name="payment_page.list") -def list_payment_pages(): +async def list_payment_pages(): """ Retrieves a list of all payment pages. """ - return paystack_client.list_payment_pages() + return await paystack_client.list_payment_pages() @mcp.tool(name="payment_page.read") -def fetch_payment_page(id: str): +async def fetch_payment_page(id: str): """ Fetches the details of a specific payment page. Args: id: The id of the payment page to fetch. """ - return paystack_client.fetch_payment_page(id) + return await paystack_client.fetch_payment_page(id) @mcp.tool(name="payment_page.update") -def update_payment_page( +async def update_payment_page( id: str, name: str | None = None, description: str | None = None, @@ -398,42 +403,42 @@ def update_payment_page( description: The new description of the payment page (optional). amount: The new amount for the payment page in the smallest currency unit (e.g., kobo) (optional). """ - return paystack_client.update_payment_page(id, name, description, amount) + return await paystack_client.update_payment_page(id, name, description, amount) @mcp.tool(name="payment_page.disable") -def disable_payment_page(id: str): +async def disable_payment_page(id: str): """ Disables a specific payment page. Args: id: The id of the payment page to disable. """ - return paystack_client.disable_payment_page(id) + return await paystack_client.disable_payment_page(id) @mcp.tool(name="payment_page.enable") -def enable_payment_page(id: str): +async def enable_payment_page(id: str): """ Enables a specific payment page. Args: id: The id of the payment page to enable. """ - return paystack_client.enable_payment_page(id) + return await paystack_client.enable_payment_page(id) @mcp.tool(name="payment_page.add_products") -def add_products_to_payment_page(id: str, products: list[str]): +async def add_products_to_payment_page(id: str, products: list[str]): """ Adds products to a specific payment page. Args: id: The id of the payment page to add products to. products: A list of product codes to add to the payment page. """ - return paystack_client.add_products_to_payment_page(id, products) + return await paystack_client.add_products_to_payment_page(id, products) @mcp.tool(name="plan.create") -def create_plan(name: str, amount: int, interval: str): +async def create_plan(name: str, amount: int, interval: str): """ Creates a new subscription plan. @@ -442,30 +447,30 @@ def create_plan(name: str, amount: int, interval: str): amount: The amount for the plan in the smallest currency unit (e.g., kobo). interval: The frequency of the plan (e.g., 'daily', 'weekly', 'monthly'). """ - return paystack_client.create_plan(name, amount, interval) + return await paystack_client.create_plan(name, amount, interval) @mcp.tool(name="plan.list") -def list_plans(): +async def list_plans(): """ Retrieves a list of all subscription plans. """ - return paystack_client.list_plans() + return await paystack_client.list_plans() @mcp.tool(name="plan.read") -def fetch_plan(plan_code: str): +async def fetch_plan(plan_code: str): """ Fetches the details of a specific subscription plan. Args: plan_code: The code of the plan to fetch. """ - return paystack_client.fetch_plan(plan_code) + return await paystack_client.fetch_plan(plan_code) @mcp.tool(name="verification.fetch_banks") -def fetch_banks( +async def fetch_banks( country: str | None = None, pay_with_bank_transfer: bool | None = None, use_cursor: bool | None = None, @@ -486,13 +491,13 @@ def fetch_banks( previous: The cursor for the previous page (optional). gateway: Filter banks by payment gateway (optional). """ - return paystack_client.fetch_banks( + return await paystack_client.fetch_banks( country, pay_with_bank_transfer, use_cursor, per_page, next, previous, gateway ) @mcp.tool(name="verification.list_avs") -def list_avs(country: str, type: str | None = None, currency: str | None = None): +async def list_avs(country: str, type: str | None = None, currency: str | None = None): """ Lists all available account verification services. Args: @@ -500,19 +505,19 @@ def list_avs(country: str, type: str | None = None, currency: str | None = None) type: The type of verification service to filter by (optional). currency: The currency code to filter by (optional). """ - return paystack_client.list_avs(type, country, currency) + return await paystack_client.list_avs(country, type, currency) @mcp.tool(name="verification.list_countries") -def list_countries(): +async def list_countries(): """ Retrieves a list of all countries. """ - return paystack_client.list_countries() + return await paystack_client.list_countries() @mcp.tool(name="verification.resolve_account_number") -def resolve_account_number(account_number: str, bank_code: str): +async def resolve_account_number(account_number: str, bank_code: str): """ Resolves an account number to get the account holder's name. @@ -520,15 +525,15 @@ def resolve_account_number(account_number: str, bank_code: str): account_number: The account number to resolve. bank_code: The bank code of the account's bank. """ - return paystack_client.resolve_account_number(account_number, bank_code) + return await paystack_client.resolve_account_number(account_number, bank_code) @mcp.tool(name="verification.resolve_card_bin") -def resolve_card_bin(card_bin: str): +async def resolve_card_bin(card_bin: str): """ Resolves a card BIN to get the associated card details. Args: card_bin: The card BIN to resolve. """ - return paystack_client.resolve_card_bin(card_bin) + return await paystack_client.resolve_card_bin(card_bin) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3426c6c..c4f6188 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,13 +23,14 @@ classifiers = [ dependencies = [ "httpx>=0.28.1", "mcp[cli]>=1.15.0", - "paystack-sdk>=0.0.10", + "python-dotenv>=1.0.0", ] -[dependency-groups] +[project.optional-dependencies] dev = [ "pytest>=8.4.2", "ruff>=0.13.2", + "pytest-asyncio>=0.23.8", ] [project.urls] diff --git a/tests/test_tools.py b/tests/test_tools.py index e1d0a20..cd565c9 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,5 +1,11 @@ import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, AsyncMock + +# Import app.server before app.tools to avoid circular dependency +from app import server +from app import tools + +pytestmark = pytest.mark.asyncio @pytest.fixture(autouse=True) @@ -16,301 +22,249 @@ def mock_paystack_client(): """ Mock the paystack_client to avoid actual API calls during tests. """ - with patch("app.tools.paystack_client", MagicMock()) as mock_client: + with patch.object( + tools.paystack_client, "_instance", new_callable=AsyncMock + ) as mock_client: yield mock_client -def test_get_balance(mock_paystack_client): - from app.tools import get_balance - - get_balance() - mock_paystack_client.get_balance.assert_called_once() - - -def test_get_balance_ledger(mock_paystack_client): - from app.tools import get_balance_ledger - - get_balance_ledger() - mock_paystack_client.get_balance_ledger.assert_called_once() - - -def test_list_customers(mock_paystack_client): - from app.tools import list_customers - - list_customers() - mock_paystack_client.list_customers.assert_called_once() - - -def test_create_customer(mock_paystack_client): - from app.tools import create_customer - - create_customer("test@example.com", "John", "Doe") - mock_paystack_client.create_customer.assert_called_once() - - -def test_fetch_customer(mock_paystack_client): - from app.tools import fetch_customer - - fetch_customer("CUS_123") - mock_paystack_client.fetch_customer.assert_called_once() +async def test_get_balance(mock_paystack_client): + await tools.get_balance() + mock_paystack_client.get_balance.assert_awaited_once() -def test_update_customer(mock_paystack_client): - from app.tools import update_customer +async def test_get_balance_ledger(mock_paystack_client): + await tools.get_balance_ledger() + mock_paystack_client.get_balance_ledger.assert_awaited_once() - update_customer("CUS_123", "John", "Doe") - mock_paystack_client.update_customer.assert_called_once() +async def test_list_customers(mock_paystack_client): + await tools.list_customers() + mock_paystack_client.list_customers.assert_awaited_once() -def test_list_products(mock_paystack_client): - from app.tools import list_products - list_products() - mock_paystack_client.list_products.assert_called_once() - - -def test_create_product(mock_paystack_client): - from app.tools import create_product - - create_product("Test Product", "A product for testing", 1000, "NGN") - mock_paystack_client.create_product.assert_called_once() - - -def test_fetch_product(mock_paystack_client): - from app.tools import fetch_product - - fetch_product("PROD_123") - mock_paystack_client.fetch_product.assert_called_once() - - -def test_update_product(mock_paystack_client): - from app.tools import update_product - - update_product("PROD_123", name="New Name") - mock_paystack_client.update_product.assert_called_once() - - -def test_delete_product(mock_paystack_client): - from app.tools import delete_product - - delete_product("PROD_123") - mock_paystack_client.delete_product.assert_called_once() - - -def test_list_invoices(mock_paystack_client): - from app.tools import list_invoices +async def test_create_customer(mock_paystack_client): + await tools.create_customer("test@example.com", "John", "Doe") + mock_paystack_client.create_customer.assert_awaited_once_with( + "test@example.com", "John", "Doe", None + ) - list_invoices() - mock_paystack_client.list_invoices.assert_called_once() +async def test_fetch_customer(mock_paystack_client): + await tools.fetch_customer("CUS_123") + mock_paystack_client.fetch_customer.assert_awaited_once_with("CUS_123") -def test_create_invoice(mock_paystack_client): - from app.tools import create_invoice - create_invoice("CUS_123", 5000) - mock_paystack_client.create_invoice.assert_called_once() +async def test_update_customer(mock_paystack_client): + await tools.update_customer("CUS_123", "John", "Doe") + mock_paystack_client.update_customer.assert_awaited_once_with( + "CUS_123", "John", "Doe", None + ) -def test_list_transactions(mock_paystack_client): - from app.tools import list_transactions +async def test_list_products(mock_paystack_client): + await tools.list_products() + mock_paystack_client.list_products.assert_awaited_once() - list_transactions() - mock_paystack_client.list_transactions.assert_called_once() +async def test_create_product(mock_paystack_client): + await tools.create_product("Test Product", "A product for testing", 1000, "NGN") + mock_paystack_client.create_product.assert_awaited_once_with( + "Test Product", "A product for testing", 1000, "NGN", 1 + ) -def test_initialize_transaction(mock_paystack_client): - from app.tools import initialize_transaction - initialize_transaction("test@example.com", 2500, "NGN") - mock_paystack_client.initialize_transaction.assert_called_once() +async def test_fetch_product(mock_paystack_client): + await tools.fetch_product("PROD_123") + mock_paystack_client.fetch_product.assert_awaited_once_with("PROD_123") -def test_verify_transaction(mock_paystack_client): - from app.tools import verify_transaction +async def test_update_product(mock_paystack_client): + await tools.update_product("PROD_123", name="New Name") + mock_paystack_client.update_product.assert_awaited_once_with( + "PROD_123", "New Name", None, None, None, None + ) - verify_transaction("REF_123") - mock_paystack_client.verify_transaction.assert_called_once() +async def test_delete_product(mock_paystack_client): + await tools.delete_product("PROD_123") + mock_paystack_client.delete_product.assert_awaited_once_with("PROD_123") -def test_fetch_transaction(mock_paystack_client): - from app.tools import fetch_transaction - fetch_transaction("TRANS_123") - mock_paystack_client.fetch_transaction.assert_called_once() +async def test_list_invoices(mock_paystack_client): + await tools.list_invoices() + mock_paystack_client.list_invoices.assert_awaited_once() -def test_get_transaction_timeline(mock_paystack_client): - from app.tools import get_transaction_timeline +async def test_create_invoice(mock_paystack_client): + await tools.create_invoice("CUS_123", 5000) + mock_paystack_client.create_invoice.assert_awaited_once_with("CUS_123", 5000) - get_transaction_timeline("TRANS_123") - mock_paystack_client.get_transaction_timeline.assert_called_once() +async def test_list_transactions(mock_paystack_client): + await tools.list_transactions() + mock_paystack_client.list_transactions.assert_awaited_once() -def test_download_transactions(mock_paystack_client): - from app.tools import download_transactions - download_transactions() - mock_paystack_client.download_transactions.assert_called_once() +async def test_initialize_transaction(mock_paystack_client): + await tools.initialize_transaction("test@example.com", 2500, "NGN") + mock_paystack_client.initialize_transaction.assert_awaited_once_with( + "test@example.com", 2500, "NGN" + ) -def test_create_refund(mock_paystack_client): - from app.tools import create_refund +async def test_verify_transaction(mock_paystack_client): + await tools.verify_transaction("REF_123") + mock_paystack_client.verify_transaction.assert_awaited_once_with("REF_123") - create_refund("TRANS_123") - mock_paystack_client.create_refund.assert_called_once() +async def test_fetch_transaction(mock_paystack_client): + await tools.fetch_transaction("TRANS_123") + mock_paystack_client.fetch_transaction.assert_awaited_once_with("TRANS_123") -def test_list_subscriptions(mock_paystack_client): - from app.tools import list_subscriptions - list_subscriptions() - mock_paystack_client.list_subscriptions.assert_called_once() +async def test_get_transaction_timeline(mock_paystack_client): + await tools.get_transaction_timeline("TRANS_123") + mock_paystack_client.get_transaction_timeline.assert_awaited_once_with("TRANS_123") -def test_disable_subscription(mock_paystack_client): - from app.tools import disable_subscription +async def test_download_transactions(mock_paystack_client): + await tools.download_transactions() + mock_paystack_client.download_transactions.assert_awaited_once_with( + 50, 1, None, None + ) - disable_subscription("SUB_123", "TOKEN_123") - mock_paystack_client.disable_subscription.assert_called_once() +async def test_create_refund(mock_paystack_client): + await tools.create_refund("TRANS_123") + mock_paystack_client.create_refund.assert_awaited_once_with("TRANS_123", None) -def test_list_disputes(mock_paystack_client): - from app.tools import list_disputes - list_disputes() - mock_paystack_client.list_disputes.assert_called_once() +async def test_list_subscriptions(mock_paystack_client): + await tools.list_subscriptions() + mock_paystack_client.list_subscriptions.assert_awaited_once() -def test_fetch_dispute(mock_paystack_client): - from app.tools import fetch_dispute +async def test_disable_subscription(mock_paystack_client): + await tools.disable_subscription("SUB_123", "TOKEN_123") + mock_paystack_client.disable_subscription.assert_awaited_once_with( + "SUB_123", "TOKEN_123" + ) - fetch_dispute("DIS_123") - mock_paystack_client.fetch_dispute.assert_called_once() +async def test_list_disputes(mock_paystack_client): + await tools.list_disputes() + mock_paystack_client.list_disputes.assert_awaited_once() -def test_download_dispute(mock_paystack_client): - from app.tools import download_dispute - download_dispute() - mock_paystack_client.download_dispute.assert_called_once() +async def test_fetch_dispute(mock_paystack_client): + await tools.fetch_dispute("DIS_123") + mock_paystack_client.fetch_dispute.assert_awaited_once_with("DIS_123") -def test_resolve_dispute(mock_paystack_client): - from app.tools import resolve_dispute +async def test_download_dispute(mock_paystack_client): + await tools.download_dispute() + mock_paystack_client.download_dispute.assert_awaited_once_with(50, 1, None, None) - resolve_dispute("DIS_123", "resolved", "Message", "1000", "file.pdf") - mock_paystack_client.resolve_dispute.assert_called_once() +async def test_resolve_dispute(mock_paystack_client): + await tools.resolve_dispute("DIS_123", "resolved", "Message", "1000", "file.pdf") + mock_paystack_client.resolve_dispute.assert_awaited_once_with( + "DIS_123", "resolved", "Message", "1000", "file.pdf", None + ) -def test_add_evidence_to_dispute(mock_paystack_client): - from app.tools import add_evidence_to_dispute - add_evidence_to_dispute( +async def test_add_evidence_to_dispute(mock_paystack_client): + await tools.add_evidence_to_dispute( + "DIS_123", "test@example.com", "John Doe", "12345", "Details" + ) + mock_paystack_client.add_evidence_to_dispute.assert_awaited_once_with( "DIS_123", "test@example.com", "John Doe", "12345", "Details" ) - mock_paystack_client.add_evidence_to_dispute.assert_called_once() - - -def test_create_payment_page(mock_paystack_client): - from app.tools import create_payment_page - - create_payment_page("Test Page", 1000) - mock_paystack_client.create_payment_page.assert_called_once() - - -def test_list_payment_pages(mock_paystack_client): - from app.tools import list_payment_pages - - list_payment_pages() - mock_paystack_client.list_payment_pages.assert_called_once() - - -def test_fetch_payment_page(mock_paystack_client): - from app.tools import fetch_payment_page - - fetch_payment_page("PAGE_123") - mock_paystack_client.fetch_payment_page.assert_called_once() - - -def test_update_payment_page(mock_paystack_client): - from app.tools import update_payment_page - - update_payment_page("PAGE_123", name="New Page Name") - mock_paystack_client.update_payment_page.assert_called_once() - - -def test_disable_payment_page(mock_paystack_client): - from app.tools import disable_payment_page - - disable_payment_page("PAGE_123") - mock_paystack_client.disable_payment_page.assert_called_once() -def test_enable_payment_page(mock_paystack_client): - from app.tools import enable_payment_page +async def test_create_payment_page(mock_paystack_client): + await tools.create_payment_page("Test Page", 1000, "A test page") + mock_paystack_client.create_payment_page.assert_awaited_once_with( + "Test Page", 1000, "A test page" + ) - enable_payment_page("PAGE_123") - mock_paystack_client.enable_payment_page.assert_called_once() +async def test_list_payment_pages(mock_paystack_client): + await tools.list_payment_pages() + mock_paystack_client.list_payment_pages.assert_awaited_once() -def test_add_products_to_payment_page(mock_paystack_client): - from app.tools import add_products_to_payment_page - add_products_to_payment_page("PAGE_123", ["PROD_123"]) - mock_paystack_client.add_products_to_payment_page.assert_called_once() +async def test_fetch_payment_page(mock_paystack_client): + await tools.fetch_payment_page("PAGE_123") + mock_paystack_client.fetch_payment_page.assert_awaited_once_with("PAGE_123") -def test_create_plan(mock_paystack_client): - from app.tools import create_plan +async def test_update_payment_page(mock_paystack_client): + await tools.update_payment_page("PAGE_123", name="New Page Name") + mock_paystack_client.update_payment_page.assert_awaited_once_with( + "PAGE_123", "New Page Name", None, None + ) - create_plan("Test Plan", 1000, "monthly") - mock_paystack_client.create_plan.assert_called_once() +async def test_disable_payment_page(mock_paystack_client): + await tools.disable_payment_page("PAGE_123") + mock_paystack_client.disable_payment_page.assert_awaited_once_with("PAGE_123") -def test_list_plans(mock_paystack_client): - from app.tools import list_plans - list_plans() - mock_paystack_client.list_plans.assert_called_once() +async def test_enable_payment_page(mock_paystack_client): + await tools.enable_payment_page("PAGE_123") + mock_paystack_client.enable_payment_page.assert_awaited_once_with("PAGE_123") -def test_fetch_plan(mock_paystack_client): - from app.tools import fetch_plan +async def test_add_products_to_payment_page(mock_paystack_client): + await tools.add_products_to_payment_page("PAGE_123", ["PROD_123"]) + mock_paystack_client.add_products_to_payment_page.assert_awaited_once_with( + "PAGE_123", ["PROD_123"] + ) - fetch_plan("PLAN_123") - mock_paystack_client.fetch_plan.assert_called_once() +async def test_create_plan(mock_paystack_client): + await tools.create_plan("Test Plan", 1000, "monthly") + mock_paystack_client.create_plan.assert_awaited_once_with( + "Test Plan", 1000, "monthly" + ) -def test_fetch_banks(mock_paystack_client): - from app.tools import fetch_banks - fetch_banks(country="NG") - mock_paystack_client.fetch_banks.assert_called_once() +async def test_list_plans(mock_paystack_client): + await tools.list_plans() + mock_paystack_client.list_plans.assert_awaited_once() -def test_list_avs(mock_paystack_client): - from app.tools import list_avs +async def test_fetch_plan(mock_paystack_client): + await tools.fetch_plan("PLAN_123") + mock_paystack_client.fetch_plan.assert_awaited_once_with("PLAN_123") - list_avs(country="Nigeria") - mock_paystack_client.list_avs.assert_called_once() +async def test_fetch_banks(mock_paystack_client): + await tools.fetch_banks(country="NG") + mock_paystack_client.fetch_banks.assert_awaited_once_with( + "NG", None, None, None, None, None, None + ) -def test_list_countries(mock_paystack_client): - from app.tools import list_countries - list_countries() - mock_paystack_client.list_countries.assert_called_once() +async def test_list_avs(mock_paystack_client): + await tools.list_avs(country="Nigeria") + mock_paystack_client.list_avs.assert_awaited_once_with("Nigeria", None, None) -def test_resolve_account_number(mock_paystack_client): - from app.tools import resolve_account_number +async def test_list_countries(mock_paystack_client): + await tools.list_countries() + mock_paystack_client.list_countries.assert_awaited_once() - resolve_account_number("1234567890", "058") - mock_paystack_client.resolve_account_number.assert_called_once() +async def test_resolve_account_number(mock_paystack_client): + await tools.resolve_account_number("1234567890", "058") + mock_paystack_client.resolve_account_number.assert_awaited_once_with( + "1234567890", "058" + ) -def test_resolve_card_bin(mock_paystack_client): - from app.tools import resolve_card_bin - resolve_card_bin("539983") - mock_paystack_client.resolve_card_bin.assert_called_once() +async def test_resolve_card_bin(mock_paystack_client): + await tools.resolve_card_bin("539983") + mock_paystack_client.resolve_card_bin.assert_awaited_once_with("539983") \ No newline at end of file