Skip to content

refactor(qr): Enhance service design for dynamic qr code - #178

Open
eugengi wants to merge 13 commits into
Byte-Barn:masterfrom
eugengi:refactor-service-dynamic-qr
Open

eugengi wants to merge 13 commits into
Byte-Barn:masterfrom
eugengi:refactor-service-dynamic-qr

Conversation

@eugengi

@eugengi eugengi commented Sep 11, 2026

Copy link
Copy Markdown

Description

I initially had one question:

Is there a tested, authenticated HTTP client to handle the authentication/transport layer, allowing me to build upon and focus on the domain logic for the Mobile Number Validation service?

I found myself deep-diving into internals and infra plumbing to answer the above question. I noticed auth logic was leaked and duplicated across services and retested across various modules... I chose to fix this first to improve developer experience and reduce friction for the community when contributing and adding new services.

This led me to review at least one module to understand how this library works. That was Dynamic QR Code. I noted a few gaps and required fixes. This PR consists of patches to improve the module's design and other required updates.

Note

This PR is prerequisite work tied to the following issue #164. The next change will be creating an abstraction for a reusable auth contract and finally implementing the Mobile Number Validation service.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires documentation update
  • Refactor (code structure improvements, no new functionality)
  • Tests (addition or improvement of tests)
  • Chore (changes to tooling, CI/CD, or metadata)

How Has This Been Tested?

  • Run the project's aggregate interface to run checks and tests
(mpesakit) ➜  mpesakit git:(refactor-service-dynamic-qr) ✗ hatch run check
Click to expand terminal trace
(mpesakit) ➜  mpesakit git:(refactor-service-dynamic-qr) ✗
hatch run check
cmd [1] | ruff check .
All checks passed!
cmd [2] | ruff format --check .
207 files already formatted
cmd [3] | mypy mpesakit tests
Success: no issues found in 128 source files
cmd [4] | bandit -q -r mpesakit
cmd [5] | pytest -m 'not live'
=================== test session starts ===================
platform linux -- Python 3.12.11, pytest-8.4.2, pluggy-1.6.0 -- /home/apex-predator/.local/share/hatch/env/virtual/mpesakit/czZFqxpX/mpesakit/bin/python
cachedir: .pytest_cache
rootdir: /home/apex-predator/apexDev/opensource/mpesakit
configfile: pyproject.toml
testpaths: tests
plugins: anyio-4.14.2, cov-7.1.0, asyncio-0.26.0
asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 545 items / 14 deselected / 531 selected

tests/integration/auth/test_token_manager_e2e.py::test_get_token_success PASSED
tests/integration/auth/test_token_manager_e2e.py::test_token_caching PASSED
tests/integration/auth/test_token_manager_e2e.py::test_force_refresh_token PASSED
tests/integration/auth/test_token_manager_e2e.py::test_invalid_credentials_raises PASSED
tests/integration/auth/test_token_manager_e2e.py::test_invalid_grant_type PASSED
tests/integration/auth/test_token_manager_e2e.py::test_invalid_auth_type PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_get_token_success PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_token_caching PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_invalid_credentials_raises PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_force_refresh_token PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_invalid_grant_type PASSED
tests/integration/auth/test_token_manager_e2e.py::test_async_invalid_auth_type PASSED
tests/unit/account_balance/test_account_balance.py::test_query_returns_acknowledgement PASSED
tests/unit/account_balance/test_account_balance.py::test_callback_result_parsing PASSED
tests/unit/account_balance/test_account_balance.py::test_account_balance_request_identifier_type_validation PASSED
tests/unit/account_balance/test_account_balance.py::test_account_balance_request_remarks_length_validation PASSED
tests/unit/account_balance/test_account_balance.py::test_timeout_callback_parsing PASSED
tests/unit/account_balance/test_account_balance.py::test_query_handles_string_response_code PASSED
tests/unit/account_balance/test_account_balance.py::test_async_query_returns_acknowledgement PASSED
tests/unit/account_balance/test_account_balance.py::test_async_query_handles_string_response_code PASSED
tests/unit/auth/test_token_manager.py::test_get_token_success PASSED
tests/unit/auth/test_token_manager.py::test_token_caching PASSED
tests/unit/auth/test_token_manager.py::test_force_refresh_token PASSED
tests/unit/auth/test_token_manager.py::test_invalid_credentials_raises PASSED
tests/unit/auth/test_token_manager.py::test_invalid_grant_type PASSED
tests/unit/auth/test_token_manager.py::test_invalid_auth_type PASSED
tests/unit/auth/test_token_manager.py::test_mpesa_api_exception_with_empty_error_message PASSED
tests/unit/auth/test_token_manager.py::test_token_missing_raises_exception PASSED
tests/unit/auth/test_token_manager.py::test_async_get_token_success PASSED
tests/unit/auth/test_token_manager.py::test_async_token_caching PASSED
tests/unit/auth/test_token_manager.py::test_async_force_refresh_token PASSED
tests/unit/auth/test_token_manager.py::test_async_expired_token_refresh PASSED
tests/unit/auth/test_token_manager.py::test_async_invalid_credentials_raises_custom_error PASSED
tests/unit/auth/test_token_manager.py::test_async_invalid_grant_type PASSED
tests/unit/auth/test_token_manager.py::test_async_invalid_auth_type PASSED
tests/unit/auth/test_token_manager.py::test_async_token_missing_raises_exception PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_ussd_push_acknowledged PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_ussd_push_http_error PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_b2b_express_checkout_success_callback PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_b2b_express_checkout_fail_callback PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_b2b_express_callback_response PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_b2b_express_callback_resultcode_as_string PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_async_ussd_push_acknowledged PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_async_ussd_push_http_error PASSED
tests/unit/b2b_express_checkout/test_b2b_express_checkout.py::test_async_ussd_push_token_manager_called PASSED
tests/unit/b2c/test_b2c.py::test_send_payment_success PASSED
tests/unit/b2c/test_b2c.py::test_send_payment_http_error PASSED
tests/unit/b2c/test_b2c.py::test_b2c_response_is_successful_zero_code PASSED
tests/unit/b2c/test_b2c.py::test_b2c_response_is_successful_all_zeros PASSED
tests/unit/b2c/test_b2c.py::test_b2c_response_is_successful_non_zero_code PASSED
tests/unit/b2c/test_b2c.py::test_b2c_response_is_successful_mixed_code PASSED
tests/unit/b2c/test_b2c.py::test_b2c_response_is_successful_empty_code PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_invalid_command_id_raises[InvalidCommand] PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_invalid_command_id_raises[] PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_invalid_command_id_raises[None] PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_invalid_partyb_raises PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_remarks_too_long_raises PASSED
tests/unit/b2c/test_b2c.py::test_b2c_request_occasion_too_long_raises PASSED
tests/unit/b2c/test_b2c.py::test_result_metadata_properties_all_present PASSED
tests/unit/b2c/test_b2c.py::test_result_metadata_properties_some_missing PASSED
tests/unit/b2c/test_b2c.py::test_result_metadata_properties_none_parameters PASSED
tests/unit/b2c/test_b2c.py::test_result_metadata_recipient_is_registered_none PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_schema PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_is_successful_zero_code PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_is_successful_all_zeros PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_is_successful_non_zero_code PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_is_successful_mixed_code PASSED
tests/unit/b2c/test_b2c.py::test_result_callback_is_successful_negative_code PASSED
tests/unit/b2c/test_b2c.py::test_async_send_payment_success PASSED
tests/unit/b2c/test_b2c.py::test_async_send_payment_token_error PASSED
tests/unit/b2c/test_b2c.py::test_async_send_payment_post_error PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_topup_success PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_topup_http_error PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_success_callback PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_fail_callback PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_callback_response PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_timeout_callback PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_timeout_callback_response PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_string_result_code_is_successful[0-True] PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_b2c_account_topup_string_result_code_is_successful[1-False] PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_async_topup_success PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_async_topup_http_error PASSED
tests/unit/b2c_account_top_up/test_b2c_account_top_up.py::test_async_topup_token_retrieval PASSED
tests/unit/bill_manager/test_bill_manager.py::test_opt_in_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_update_opt_in_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_send_single_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_send_bulk_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_cancel_single_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_cancel_bulk_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_bill_manager_http_error PASSED
tests/unit/bill_manager/test_bill_manager.py::test_app_key_required_for_invoice PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021-10-12-2021-10-12] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021/10/12-2021-10-12] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021-10-12 14:30-2021-10-12 14:30:00] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021-10-12 14:30:00-2021-10-12 14:30:00.00] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021/10/12 14:30:00-2021-10-12 14:30:00.00] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021-10-12 14:30:00.123-2021-10-12 14:30:00.12] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2021-10-12 14:30:00.10-2021-10-12 14:30:00.10] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_valid_formats[2025-08-19T15:33:15.376886-2025-08-19 15:33:15.37] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[2021-13-12] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[2021-10-32] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[20211012] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[2021-10-12 14:30:00:00] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[2021-10-12 14] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[12-10-2021] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_invalid_formats_raise[] PASSED
tests/unit/bill_manager/test_bill_manager.py::test_due_date_missing_raises PASSED
tests/unit/bill_manager/test_bill_manager.py::test_billed_period_invalid_raises PASSED
tests/unit/bill_manager/test_bill_manager.py::test_result_code_as_string_does_not_raise PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_opt_in_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_update_opt_in_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_send_single_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_send_bulk_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_cancel_single_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_cancel_bulk_invoice_success PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_bill_manager_http_error PASSED
tests/unit/bill_manager/test_bill_manager.py::test_async_app_key_required_for_invoice PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_buy_goods_request_acknowledged PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_buy_goods_http_error PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_business_buy_goods_result_callback_success PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_business_buy_goods_result_callback_response PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_business_buy_goods_timeout_callback PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_business_buy_goods_timeout_callback_response PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_business_buy_goods_result_callback_resultcode_string PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_async_buy_goods_request_acknowledged PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_async_buy_goods_http_error PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_async_buy_goods_token_manager_called PASSED
tests/unit/business_buy_goods/test_business_buy_goods.py::test_async_buy_goods_http_client_called_with_correct_params PASSED
tests/unit/business_paybill/test_business_paybill.py::test_paybill_request_acknowledged PASSED
tests/unit/business_paybill/test_business_paybill.py::test_paybill_http_error PASSED
tests/unit/business_paybill/test_business_paybill.py::test_business_paybill_result_callback_success PASSED
tests/unit/business_paybill/test_business_paybill.py::test_business_paybill_result_callback_response PASSED
tests/unit/business_paybill/test_business_paybill.py::test_business_paybill_timeout_callback PASSED
tests/unit/business_paybill/test_business_paybill.py::test_business_paybill_timeout_callback_response PASSED
tests/unit/business_paybill/test_business_paybill.py::test_business_paybill_result_callback_with_string_result_code PASSED
tests/unit/business_paybill/test_business_paybill.py::test_async_paybill_request_acknowledged PASSED
tests/unit/business_paybill/test_business_paybill.py::test_async_paybill_http_error PASSED
tests/unit/business_paybill/test_business_paybill.py::test_async_paybill_token_retrieval PASSED
tests/unit/c2b/test_c2b.py::test_register_url_success PASSED
tests/unit/c2b/test_c2b.py::test_register_url_handles_typo_field PASSED
tests/unit/c2b/test_c2b.py::test_register_url_handles_http_error PASSED
tests/unit/c2b/test_c2b.py::test_validate_payment_success PASSED
tests/unit/c2b/test_c2b.py::test_validate_payment_rejected PASSED
tests/unit/c2b/test_c2b.py::test_confirm_payment_success PASSED
tests/unit/c2b/test_c2b.py::test_warn_invalid_urls_triggers_warning PASSED
tests/unit/c2b/test_c2b.py::test_warn_invalid_urls_no_warning PASSED
tests/unit/c2b/test_c2b.py::test_warn_invalid_urls_multiple_keywords PASSED
tests/unit/c2b/test_c2b.py::test_register_url_invalid_response_type_raises_value_error PASSED
tests/unit/c2b/test_c2b.py::test_c2b_register_url_response_is_successful_zero_code PASSED
tests/unit/c2b/test_c2b.py::test_c2b_register_url_response_is_successful_all_zeros PASSED
tests/unit/c2b/test_c2b.py::test_c2b_register_url_response_is_successful_non_zero_code PASSED
tests/unit/c2b/test_c2b.py::test_c2b_register_url_response_is_successful_mixed_code PASSED
tests/unit/c2b/test_c2b.py::test_c2b_register_url_response_is_successful_empty_code PASSED
tests/unit/c2b/test_c2b.py::test_validate_result_code_valid PASSED
tests/unit/c2b/test_c2b.py::test_validate_result_code_invalid PASSED
tests/unit/c2b/test_c2b.py::test_warn_long_resultdesc_triggers_warning PASSED
tests/unit/c2b/test_c2b.py::test_warn_long_resultdesc_no_warning PASSED
tests/unit/c2b/test_c2b.py::test_warn_long_resultdesc_none PASSED
tests/unit/c2b/test_c2b.py::test_is_successful_with_mixed_string_response_code_no_type_error PASSED
tests/unit/c2b/test_c2b.py::test_async_register_url_success PASSED
tests/unit/c2b/test_c2b.py::test_async_register_url_handles_typo_field PASSED
tests/unit/c2b/test_c2b.py::test_async_register_url_handles_http_error PASSED
tests/unit/dynamic_qr_code/test_dynamic_qr_code_schemas.py::test_raises_validation_err_when_using_invalid_trx_code PASSED
tests/unit/dynamic_qr_code/test_dynamic_qr_code_schemas.py::test_generate_dynamic_qr_send_money_cpi_normalization[0712345678-254712345678] PASSED
tests/unit/dynamic_qr_code/test_dynamic_qr_code_schemas.py::test_generate_dynamic_qr_send_money_cpi_normalization[+254712345678-254712345678] PASSED
tests/unit/dynamic_qr_code/test_dynamic_qr_code_schemas.py::test_generate_dynamic_qr_send_money_cpi_normalization[254712345678-254712345678] PASSED
tests/unit/dynamic_qr_code/test_dynamic_qr_code_schemas.py::test_generate_dynamic_qr_send_money_invalid_cpi PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_base_url_sandbox PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_base_url_production PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_success PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_http_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_json_decode_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_timeout PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_connection_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_retries_and_succeeds PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_fails_after_max_retries PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_respects_custom_max_retries PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_post_generic_httpx_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_success PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_http_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_timeout PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_connection_error PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_retries_and_succeeds PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_fails_after_max_retries PASSED
tests/unit/http_client/test_mpesa_async_http_client.py::test_get_generic_httpx_error PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_base_url_sandbox PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_base_url_production PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_success[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_success[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_http_error[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_http_error[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_json_decode_error[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_json_decode_error[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_request_exception_is_not_retried_and_raises_api_exception[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_request_exception_is_not_retried_and_raises_api_exception[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_retries_and_succeeds[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_retries_and_succeeds[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_fails_after_max_retries[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_fails_after_max_retries[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_post_respects_custom_max_retries PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_success[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_success[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_http_error[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_http_error[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_json_decode_error[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_json_decode_error[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_request_exception_is_not_retried_and_raises_api_exception[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_request_exception_is_not_retried_and_raises_api_exception[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_retries_and_succeeds[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_retries_and_succeeds[False] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_fails_after_max_retries[True] PASSED
tests/unit/http_client/test_mpesa_http_client.py::test_get_fails_after_max_retries[False] PASSED
tests/unit/mpesa_express/test_schemas.py::test_transaction_type_enum PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_password_generation_and_phone_normalization PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_accepts_plus254_and_0_prefix PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_invalid_phone_raises PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_missing_password_and_passkey_raises PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_account_reference_length PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_transaction_desc_length PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_response_fields PASSED
tests/unit/mpesa_express/test_schemas.py::test_callback_metadata_item_balance_parsing PASSED
tests/unit/mpesa_express/test_schemas.py::test_callback_metadata_item_balance_parsing_invalid_float PASSED
tests/unit/mpesa_express/test_schemas.py::test_callback_metadata_item_non_balance_untouched PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_callback_properties PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_callback_missing_metadata_returns_none PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_query_request_password_generation PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_query_request_missing_password_and_passkey_raises PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_query_request_password_provided_without_timestamp_raises PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_query_response_fields PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_password_provided_without_timestamp_raises PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_request_paybill_requires_account_reference PASSED
tests/unit/mpesa_express/test_schemas.py::test_stkpush_simulate_callback_string_result_code_handling PASSED
tests/unit/mpesa_express/test_stk_push.py::test_push_success PASSED
tests/unit/mpesa_express/test_stk_push.py::test_query_success PASSED
tests/unit/mpesa_express/test_stk_push.py::test_push_handles_http_error PASSED
tests/unit/mpesa_express/test_stk_push.py::test_query_handles_http_error PASSED
tests/unit/mpesa_express/test_stk_push.py::test_stk_push_simulate_request_invalid_transaction_type PASSED
tests/unit/mpesa_express/test_stk_push.py::test_async_push_success PASSED
tests/unit/mpesa_express/test_stk_push.py::test_async_query_success PASSED
tests/unit/mpesa_express/test_stk_push.py::test_async_push_handles_http_error PASSED
tests/unit/mpesa_express/test_stk_push.py::test_async_query_handles_http_error PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_create_standing_order_success PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_create_standing_order_http_error PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_success_callback PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_fail_callback PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_callback_response PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_request_invalid_date_format PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_request_invalid_date_value PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_standing_order_request_other_date_value PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_invalid_phone_number PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_callback_resultcode_as_string_handled_gracefully PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_create_standing_order_success_async PASSED
tests/unit/mpesa_ratiba/test_mpesa_ratiba.py::test_create_standing_order_http_error_async PASSED
tests/unit/reversal/test_reversal.py::test_reverse_request_acknowledged PASSED
tests/unit/reversal/test_reversal.py::test_reverse_http_error PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_success PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_response PASSED
tests/unit/reversal/test_reversal.py::test_reversal_timeout_callback PASSED
tests/unit/reversal/test_reversal.py::test_reversal_timeout_callback_response PASSED
tests/unit/reversal/test_reversal.py::test_reversal_request_identifier_type_is_valid PASSED
tests/unit/reversal/test_reversal.py::test_reversal_request_remarks_too_long_raises PASSED
tests/unit/reversal/test_reversal.py::test_reversal_request_occasion_too_long_raises PASSED
tests/unit/reversal/test_reversal.py::test_reverse_responsecode_string_no_type_error PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_success_is_successful PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_failure_is_successful PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_success_code_is_successful PASSED
tests/unit/reversal/test_reversal.py::test_reversal_result_callback_failure_code_is_successful PASSED
tests/unit/reversal/test_reversal.py::test_async_reverse_request_acknowledged PASSED
tests/unit/reversal/test_reversal.py::test_async_reverse_http_error PASSED
tests/unit/reversal/test_reversal.py::test_async_reverse_token_manager_called PASSED
tests/unit/reversal/test_reversal.py::test_async_reverse_http_client_post_called PASSED
tests/unit/reversal/test_reversal.py::test_async_reverse_responsecode_string_no_type_error PASSED
tests/unit/security/test_get_credential.py::test_generate_security_credential_success_rsa PASSED
tests/unit/security/test_get_credential.py::test_generate_security_credential_missing_cert PASSED
tests/unit/security/test_get_credential.py::test_generate_security_credential_empty_initiator_password PASSED
tests/unit/security/test_get_credential.py::test_generate_security_credential_with_non_rsa_cert_raises_value_error PASSED
tests/unit/security/test_get_credential.py::test_generate_security_credential_with_invalid_cert_bytes PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.214.200-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.214.206-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.213.114-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.214.207-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.214.208-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.213.44-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.127-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.128-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.129-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.132-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.136-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.138-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.69-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.212.74-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[192.168.1.1-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[8.8.8.8-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[127.0.0.1-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[255.255.255.255-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[not.an.ip-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[None-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_default[196.201.214.999-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[10.0.0.1-allowed_ips0-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[10.0.0.2-allowed_ips1-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[10.0.0.3-allowed_ips2-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[196.201.214.200-allowed_ips3-True] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[196.201.214.200-allowed_ips4-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[196.201.214.200-allowed_ips5-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[196.201.214.200-allowed_ips6-False] PASSED
tests/unit/security/test_ip_whitelist.py::test_is_mpesa_ip_allowed_custom[196.201.214.200-None-True] PASSED
tests/unit/services/test_b2b_service.py::test_express_checkout_calls_ussd_push PASSED
tests/unit/services/test_b2b_service.py::test_paybill_calls_paybill PASSED
tests/unit/services/test_b2b_service.py::test_buygoods_calls_buy_goods PASSED
tests/unit/services/test_b2b_service.py::test_express_checkout_filters_kwargs PASSED
tests/unit/services/test_b2b_service.py::test_b2b_service_initializes_services_correctly PASSED
tests/unit/services/test_b2b_service.py::test_async_express_checkout_calls_ussd_push PASSED
tests/unit/services/test_b2b_service.py::test_async_paybill_calls_paybill PASSED
tests/unit/services/test_b2b_service.py::test_async_buygoods_calls_buy_goods PASSED
tests/unit/services/test_b2b_service.py::test_async_express_checkout_filters_kwargs PASSED
tests/unit/services/test_b2b_service.py::test_async_b2b_service_initializes_services_correctly PASSED
tests/unit/services/test_b2c_service.py::test_send_payment_calls_b2c_send_payment PASSED
tests/unit/services/test_b2c_service.py::test_account_topup_calls_account_topup PASSED
tests/unit/services/test_b2c_service.py::test_send_payment_filters_kwargs PASSED
tests/unit/services/test_b2c_service.py::test_account_topup_filters_kwargs PASSED
tests/unit/services/test_b2c_service.py::test_b2c_service_initializes_b2c_correctly PASSED
tests/unit/services/test_b2c_service.py::test_async_send_payment_calls_b2c_send_payment PASSED
tests/unit/services/test_b2c_service.py::test_async_account_topup_calls_account_topup PASSED
tests/unit/services/test_b2c_service.py::test_async_send_payment_filters_kwargs PASSED
tests/unit/services/test_b2c_service.py::test_async_account_topup_filters_kwargs PASSED
tests/unit/services/test_b2c_service.py::test_async_b2c_service_initializes_b2c_correctly PASSED
tests/unit/services/test_balance_service.py::test_init_sets_account_balance PASSED
tests/unit/services/test_balance_service.py::test_query_calls_account_balance_query PASSED
tests/unit/services/test_balance_service.py::test_query_filters_kwargs PASSED
tests/unit/services/test_balance_service.py::test_balance_service_initializes_account_balance_correctly PASSED
tests/unit/services/test_balance_service.py::test_async_init_sets_account_balance PASSED
tests/unit/services/test_balance_service.py::test_async_query_calls_account_balance_query PASSED
tests/unit/services/test_balance_service.py::test_async_query_filters_kwargs PASSED
tests/unit/services/test_balance_service.py::test_async_balance_service_initializes_account_balance_correctly PASSED
tests/unit/services/test_bill_service.py::test_opt_in_calls_bill_manager_opt_in PASSED
tests/unit/services/test_bill_service.py::test_bill_manager_update_opt_in PASSED
tests/unit/services/test_bill_service.py::test_bill_manager_send_single_invoice PASSED
tests/unit/services/test_bill_service.py::test_bill_manager_send_bulk_invoice PASSED
tests/unit/services/test_bill_service.py::test_bill_manager_cancel_single_invoice PASSED
tests/unit/services/test_bill_service.py::test_bill_manager_cancel_bulk_invoice PASSED
tests/unit/services/test_bill_service.py::test_bill_service_initializes_bill_manager_correctly PASSED
tests/unit/services/test_bill_service.py::test_async_opt_in_calls_bill_manager_opt_in PASSED
tests/unit/services/test_bill_service.py::test_async_bill_manager_update_opt_in PASSED
tests/unit/services/test_bill_service.py::test_async_bill_manager_send_single_invoice PASSED
tests/unit/services/test_bill_service.py::test_async_bill_manager_send_bulk_invoice PASSED
tests/unit/services/test_bill_service.py::test_async_bill_manager_cancel_single_invoice PASSED
tests/unit/services/test_bill_service.py::test_async_bill_manager_cancel_bulk_invoice PASSED
tests/unit/services/test_bill_service.py::test_async_bill_service_initializes_bill_manager_correctly PASSED
tests/unit/services/test_c2b_service.py::test_register_url_success PASSED
tests/unit/services/test_c2b_service.py::test_register_url_filters_kwargs PASSED
tests/unit/services/test_c2b_service.py::test_c2b_service_initializes_correctly PASSED
tests/unit/services/test_c2b_service.py::test_async_register_url_success PASSED
tests/unit/services/test_c2b_service.py::test_async_register_url_filters_kwargs PASSED
tests/unit/services/test_c2b_service.py::test_async_c2b_service_initializes_correctly PASSED
tests/unit/services/test_dynamic_qr_service.py::test_dynamic_qr_service_initializes_with_service_dependencies[DynamicQRCodeService] PASSED
tests/unit/services/test_dynamic_qr_service.py::test_dynamic_qr_service_initializes_with_service_dependencies[AsyncDynamicQRCodeService] PASSED
tests/unit/services/test_dynamic_qr_service.py::test_generate_success PASSED
tests/unit/services/test_dynamic_qr_service.py::test_async_generate_success PASSED
tests/unit/services/test_dynamic_qr_service.py::test_generate_dynamic_qr_handles_http_error PASSED
tests/unit/services/test_dynamic_qr_service.py::test_async_generate_dynamic_qr_handles_http_error PASSED
tests/unit/services/test_dynamic_qr_service.py::test_generate_dynamic_qr_string_response_code_no_type_error PASSED
tests/unit/services/test_dynamic_qr_service.py::test_async_generate_dynamic_qr_token_manager_called PASSED
tests/unit/services/test_express_service.py::test_push_success PASSED
tests/unit/services/test_express_service.py::test_push_filters_kwargs PASSED
tests/unit/services/test_express_service.py::test_query_success PASSED
tests/unit/services/test_express_service.py::test_query_filters_kwargs PASSED
tests/unit/services/test_express_service.py::test_stk_push_service_initializes_stk_push_correctly PASSED
tests/unit/services/test_express_service.py::test_async_push_success PASSED
tests/unit/services/test_express_service.py::test_async_push_filters_kwargs PASSED
tests/unit/services/test_express_service.py::test_async_query_success PASSED
tests/unit/services/test_express_service.py::test_async_query_filters_kwargs PASSED
tests/unit/services/test_express_service.py::test_async_stk_push_service_initializes_stk_push_correctly PASSED
tests/unit/services/test_ratiba_service.py::test_create_standing_order_success PASSED
tests/unit/services/test_ratiba_service.py::test_create_standing_order_filters_kwargs PASSED
tests/unit/services/test_ratiba_service.py::test_ratiba_service_initializes_ratiba_correctly PASSED
tests/unit/services/test_ratiba_service.py::test_async_create_standing_order_success PASSED
tests/unit/services/test_ratiba_service.py::test_async_create_standing_order_filters_kwargs PASSED
tests/unit/services/test_ratiba_service.py::test_async_ratiba_service_initializes_ratiba_correctly PASSED
tests/unit/services/test_reversal_service.py::test_reverse_calls_reversal PASSED
tests/unit/services/test_reversal_service.py::test_reverse_filters_kwargs PASSED
tests/unit/services/test_reversal_service.py::test_reversal_service_initializes_reversal_correctly PASSED
tests/unit/services/test_reversal_service.py::test_async_reverse_calls_reversal PASSED
tests/unit/services/test_reversal_service.py::test_async_reverse_filters_kwargs PASSED
tests/unit/services/test_reversal_service.py::test_async_reversal_service_initializes_reversal_correctly PASSED
tests/unit/services/test_tax_service.py::test_remittance_calls_tax_remittance PASSED
tests/unit/services/test_tax_service.py::test_remittance_filters_kwargs PASSED
tests/unit/services/test_tax_service.py::test_tax_service_initializes_tax_correctly PASSED
tests/unit/services/test_tax_service.py::test_async_remittance_calls_tax_remittance PASSED
tests/unit/services/test_tax_service.py::test_async_remittance_filters_kwargs PASSED
tests/unit/services/test_tax_service.py::test_async_tax_service_initializes_tax_correctly PASSED
tests/unit/services/test_transaction_service.py::test_query_status_calls_transaction_status PASSED
tests/unit/services/test_transaction_service.py::test_query_status_default_command_id PASSED
tests/unit/services/test_transaction_service.py::test_query_status_default_remarks PASSED
tests/unit/services/test_transaction_service.py::test_query_status_filters_kwargs PASSED
tests/unit/services/test_transaction_service.py::test_transaction_service_initializes_correctly PASSED
tests/unit/services/test_transaction_service.py::test_async_query_status_calls_transaction_status PASSED
tests/unit/services/test_transaction_service.py::test_async_query_status_default_command_id PASSED
tests/unit/services/test_transaction_service.py::test_async_query_status_default_remarks PASSED
tests/unit/services/test_transaction_service.py::test_async_query_status_filters_kwargs PASSED
tests/unit/services/test_transaction_service.py::test_async_transaction_service_initializes_correctly PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_remittance_request_acknowledged PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_remittance_http_error PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_tax_remittance_result_callback_success PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_tax_remittance_result_callback_response PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_tax_remittance_timeout_callback PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_tax_remittance_timeout_callback_response PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_tax_remittance_result_callback_with_string_resultcode PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_async_remittance_request_acknowledged PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_async_remittance_http_error PASSED
tests/unit/tax_remittance/test_tax_remittance.py::test_async_remittance_token_retrieval PASSED
tests/unit/test_async_mpesa_client.py::test_uses_callback_mixin PASSED
tests/unit/test_async_mpesa_client.py::test_http_client_instance PASSED
tests/unit/test_async_mpesa_client.py::test_default_max_retries_is_three PASSED
tests/unit/test_async_mpesa_client.py::test_custom_max_retries_is_passed_through PASSED
tests/unit/test_async_mpesa_client.py::test_token_manager_instance PASSED
tests/unit/test_async_mpesa_client.py::test_express_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_b2c_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_b2b_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_transactions_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_tax_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_balance_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_reversal_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_bill_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_dynamic_qr_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_c2b_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_ratiba_service_instance PASSED
tests/unit/test_async_mpesa_client.py::test_stk_push_alias PASSED
tests/unit/test_async_mpesa_client.py::test_stk_query_alias PASSED
tests/unit/test_async_mpesa_client.py::test_import_from_package PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_stk_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_stk_query_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_account_balance_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_account_balance_timeout PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_b2c_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_b2b_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_transactions_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_bill_manager_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_tax_remittance_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_dynamic_qr_code_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_ratiba_service_callback PASSED
tests/unit/test_async_mpesa_client.py::TestCallbackProcessing::test_process_reversal_callback PASSED
tests/unit/test_errors.py::test_mpesa_error_str_full_fields PASSED
tests/unit/test_errors.py::test_mpesa_error_str_partial_fields PASSED
tests/unit/test_errors.py::test_mpesa_error_str_no_fields PASSED
tests/unit/test_errors.py::test_mpesa_api_exception_message_and_properties PASSED
tests/unit/test_errors.py::test_mpesa_api_exception_with_minimal_error PASSED
tests/unit/test_errors.py::test_mpesa_error_raw_response_field PASSED
tests/unit/test_errors.py::test_mpesa_error_status_code_field PASSED
tests/unit/test_mpesa_client.py::test_uses_callback_mixin PASSED
tests/unit/test_mpesa_client.py::test_http_client_instance PASSED
tests/unit/test_mpesa_client.py::test_default_max_retries_is_three PASSED
tests/unit/test_mpesa_client.py::test_custom_max_retries_is_passed_through PASSED
tests/unit/test_mpesa_client.py::test_use_session_is_passed_through PASSED
tests/unit/test_mpesa_client.py::test_token_manager_instance PASSED
tests/unit/test_mpesa_client.py::test_express_service_instance PASSED
tests/unit/test_mpesa_client.py::test_b2c_service_instance PASSED
tests/unit/test_mpesa_client.py::test_b2b_service_instance PASSED
tests/unit/test_mpesa_client.py::test_transactions_service_instance PASSED
tests/unit/test_mpesa_client.py::test_tax_service_instance PASSED
tests/unit/test_mpesa_client.py::test_balance_service_instance PASSED
tests/unit/test_mpesa_client.py::test_reversal_service_instance PASSED
tests/unit/test_mpesa_client.py::test_bill_service_instance PASSED
tests/unit/test_mpesa_client.py::test_dynamic_qr_service_instance PASSED
tests/unit/test_mpesa_client.py::test_c2b_service_instance PASSED
tests/unit/test_mpesa_client.py::test_ratiba_service_instance PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_stk_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_stk_query_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_account_balance_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_account_balance_timeout PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_b2c_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_b2b_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_transactions_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_bill_manager_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_tax_remittance_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_dynamic_qr_code_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_ratiba_service_callback PASSED
tests/unit/test_mpesa_client.py::TestCallbackProcessing::test_process_reversal_callback PASSED
tests/unit/transaction_status/test_transaction_status.py::test_query_success PASSED
tests/unit/transaction_status/test_transaction_status.py::test_query_http_error PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_response_is_successful_zero_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_response_is_successful_all_zeros PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_response_is_successful_non_zero_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_response_is_successful_mixed_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_response_is_successful_empty_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_invalid_identifier_type_raises[0] PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_invalid_identifier_type_raises[3] PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_invalid_identifier_type_raises[5] PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_invalid_identifier_type_raises[invalid] PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_missing_transaction_id_and_originator_conversation_id_raises PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_remarks_too_long_raises PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_occasion_too_long_raises PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_msisdn_normalization PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_request_invalid_msisdn PASSED
tests/unit/transaction_status/test_transaction_status.py::test_result_metadata_properties_all_present PASSED
tests/unit/transaction_status/test_transaction_status.py::test_result_metadata_properties_some_missing PASSED
tests/unit/transaction_status/test_transaction_status.py::test_result_metadata_properties_none_parameters PASSED
tests/unit/transaction_status/test_transaction_status.py::test_result_callback_schema PASSED
tests/unit/transaction_status/test_transaction_status.py::test_query_response_code_type_variations PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_result_callback_is_successful_zero_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_result_callback_is_successful_all_zeros PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_result_callback_is_successful_non_zero_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_result_callback_is_successful_mixed_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_transaction_status_result_callback_is_successful_empty_code PASSED
tests/unit/transaction_status/test_transaction_status.py::test_async_query_success PASSED
tests/unit/transaction_status/test_transaction_status.py::test_async_query_http_error PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[0712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+254712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[254712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[ 0712345678 -254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[\t+254712345678\n-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[ 254712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+254 712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[0 712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[07 12345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[254 712345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+2547 12345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[0 712 345678-254712345678] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[712345678-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+25471234567-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+2547123456789-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[071234567-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[07123456789-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[25471234567-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[2547123456789-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[1234567890-None0] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[None-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[1234567890-None1] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[input_phone23-None] PASSED
tests/unit/utils/test_phone.py::test_normalize_phone_number[+2547a2345678-None] PASSED

===================== tests coverage ======================
____ coverage: platform linux, python 3.12.11-final-0 _____

Name                                                    Stmts   Miss  Cover
---------------------------------------------------------------------------
mpesakit/__init__.py                                        3      0   100%
mpesakit/account_balance/__init__.py                        3      0   100%
mpesakit/account_balance/account_balance.py                22      0   100%
mpesakit/account_balance/schemas.py                        80      0   100%
mpesakit/auth/__init__.py                                   3      0   100%
mpesakit/auth/access_token.py                              12      0   100%
mpesakit/auth/token_manager.py                             64      0   100%
mpesakit/b2b_express_checkout/__init__.py                   3      0   100%
mpesakit/b2b_express_checkout/b2b_express_checkout.py      22      0   100%
mpesakit/b2b_express_checkout/schemas.py                   38      0   100%
mpesakit/b2c/__init__.py                                    3      0   100%
mpesakit/b2c/b2c.py                                        22      0   100%
mpesakit/b2c/schemas.py                                   128      0   100%
mpesakit/b2c_account_top_up/__init__.py                     3      0   100%
mpesakit/b2c_account_top_up/b2c_account_top_up.py          23      0   100%
mpesakit/b2c_account_top_up/schemas.py                     71      0   100%
mpesakit/bill_manager/__init__.py                           3      0   100%
mpesakit/bill_manager/bill_manager.py                      92      0   100%
mpesakit/bill_manager/schemas.py                          156      0   100%
mpesakit/business_buy_goods/__init__.py                     3      0   100%
mpesakit/business_buy_goods/business_buy_goods.py          22      0   100%
mpesakit/business_buy_goods/schemas.py                     66      0   100%
mpesakit/business_paybill/__init__.py                       3      0   100%
mpesakit/business_paybill/business_paybill.py              22      0   100%
mpesakit/business_paybill/schemas.py                       64      0   100%
mpesakit/c2b/__init__.py                                    3      0   100%
mpesakit/c2b/c2b.py                                        26      0   100%
mpesakit/c2b/schemas.py                                    96      0   100%
mpesakit/dynamic_qr_code/__init__.py                        2      0   100%
mpesakit/dynamic_qr_code/schemas.py                        43      0   100%
mpesakit/errors.py                                         29      0   100%
mpesakit/http_client/__init__.py                            4      0   100%
mpesakit/http_client/_retry.py                             24      3    88%
mpesakit/http_client/http_client.py                        16      4    75%
mpesakit/http_client/mpesa_async_http_client.py            60      3    95%
mpesakit/http_client/mpesa_http_client.py                  71      2    97%
mpesakit/mpesa_client.py                                   82      4    95%
mpesakit/mpesa_express/__init__.py                          3      0   100%
mpesakit/mpesa_express/schemas.py                         207      0   100%
mpesakit/mpesa_express/stk_push.py                         32      0   100%
mpesakit/mpesa_ratiba/__init__.py                           3      0   100%
mpesakit/mpesa_ratiba/mpesa_ratiba.py                      22      0   100%
mpesakit/mpesa_ratiba/schemas.py                          103      0   100%
mpesakit/reversal/__init__.py                               3      0   100%
mpesakit/reversal/reversal.py                              22      0   100%
mpesakit/reversal/schemas.py                               81      0   100%
mpesakit/security/__init__.py                               3      0   100%
mpesakit/security/get_credential.py                        36      5    86%
mpesakit/security/ip_whitelist.py                          12      0   100%
mpesakit/services/__init__.py                              12      0   100%
mpesakit/services/b2b.py                                   40      0   100%
mpesakit/services/b2c.py                                   28      0   100%
mpesakit/services/balance.py                               19      0   100%
mpesakit/services/bill.py                                  52      0   100%
mpesakit/services/c2b.py                                   19      0   100%
mpesakit/services/dynamic_qr.py                            21      0   100%
mpesakit/services/express.py                               25      0   100%
mpesakit/services/ratiba.py                                19      0   100%
mpesakit/services/reversal.py                              20      0   100%
mpesakit/services/tax.py                                   19      0   100%
mpesakit/services/transaction.py                           27      0   100%
mpesakit/tax_remittance/__init__.py                         3      0   100%
mpesakit/tax_remittance/schemas.py                         64      0   100%
mpesakit/tax_remittance/tax_remittance.py                  22      0   100%
mpesakit/transaction_status/__init__.py                     3      0   100%
mpesakit/transaction_status/schemas.py                    121      0   100%
mpesakit/transaction_status/transaction_status.py          22      0   100%
mpesakit/utils/phone.py                                    14      0   100%
---------------------------------------------------------------------------
TOTAL                                                    2464     21    99%
Coverage HTML written to dir htmlcov
===== 531 passed, 14 deselected in 259.23s (0:04:19) ======
(mpesakit) ➜  mpesakit git:(refactor-service-dynamic-qr) ✗

Warning

The script hatch run ci was run but didn't pass. This is for live integration tests with the Daraja sandbox API.

Important

I tried to run live integration tests for the Dynamic QR Code service, but I've been getting a 503 Service Unavailable error from the M-Pesa Gateway for a couple of days now.

Checklist

  • My code follows the project's coding style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Additional Context

  • According to the docs, the response field ResponseCode should be in the following format AG_20191219_000043fdf61864fe9ff5, though we currently use 0. Is there a specific reason for this, or should we update it?
  • Currently, not all integration tests pass
  • Moving to a vertical slice layout for this library's tests can make test maintenance easier for contributors:
tests/
  ...
  c2b/
  dynamic_qr_code/
      conftest.py
      test_schemas
      test_unit
      test_integration
      test_service
      test_client
      ...

Then use pytest.markers to slice and drive tests: unit, integration, e2e, live, etc.

Note

This was after I noted some Dynamic QR Code tests were hidden in tests/unit/test_async_mpesa_client.py and tests/unit/test_mpesa_client.py. Centralizing all these tests under a single module would make things easier, imo.

[Style]
Multi-file edits on running `ruff format`

[Chore]
Ensure all local ci checks pass successfully.

[Commands]
➜ hatch run check
➜ hatch run ci

[Changes]
- Add `mypy` config to silence `import-not-found` errors on tests

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Tell git not to ignore UV's generated lockfile.

[Reason]
Ensure immutable snapshot for library dep tree for both
development and distribution.

[Commands]
➜ uv sync --extra dev --extra test

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Links to the Mpesa docs resulted in a 404 not found.
This was because of `APIS` instead of `apis` being used in the URL

[Changes]
- Update all broken links with correct URL

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Enhance module boundaries by moving functionality into service.

[Changes]
- Update broken documentation link in docstrings
- Rm base model `DynamicQRCode` with duplicate service behavior
- Move all tests requiring a `dynamic_qr_service` fixture to:
  tests/unit/services/test_dynamic_qr_service.py

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>

chore(!): Rebase into 0125945

Commit msg:
refactor(qr): Rm service responsibility from `pydantic` models

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>

chore(!): Rebase into 80d9436

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>

chore(!): Rebase into 0488068

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
[Issue]
The `DynamicQRGenerateResponse` represents the JSON response from Daraja
The current model misses `RequestID` which is a documented field
in the response body

[Fix]
- Add missing field to get exact parity with API response.
- Update all instances failing validation to include field across repo

[Note]
- The models docstring lists `RequestID`, but absent in the model
- Confirm if this is intentional from the maintainer

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Install and add `dotenv` to uv lockfile.

[Commands]
➜ uv add python-dotenv

[Note]
- Pkg is used in library but not present in project's lockfile.

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
[Issue]
Docs document an env var: `MPESA_ENVIRONMENT`
to set whether using the sandbox or prod APIs.
But tests read `MPESA_ENV`

[Fix]
Rename all occurrences of `MPESA_ENV` to `MPESA_ENVIRONMENT`

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Add dedicated directory to store and manage envs.

[Note]
- Tell git to ignore files that end in .env
- Prefer to split env files to avoid common pitfalls.
  (ie, reading only `sandbox.env` for test or local targets).

[Commands]
➜ cp .envs/sandbox.env.example .envs/sandbox.env
➜ cp .envs/production.env.example .envs/production.env

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Add shared fixtures for integration tests and the service: DynamicQRCode

Important: This pattern should be applied to all integration tests

[Changes]
- Fix wrong type hint assigned to request model
- Make clients session fixtures to trigger a single network call for an
  auth token and reuse for all integration tests.

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
[Problem]
QR services receive params only to reassemble internally.

[Rationale]
Multiple params can make APIs harder to reason and think about.

Param object patterns in this context centralize data structure and
validation, while making the contract and tests simpler and readable.

[Issue]
The `**kwargs` arg is a serious bug:
It silently drops all args not in the model `DynamicQRGenerateRequest`
This gives callers an illusion that we support other request args
when internally we actually drop them without warning.

[Fix]
Drop the `**kwargs` arg and all traces of it across this module.
Using a single param object is enough here.

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
Updates to enhance code semantics and design.

[Rationale]
- Parametrize test to use idiomatic patterns for variable inputs
- Rm TrxCode method validation:
  pydantic can handle validating fields assigned an enum type
- Update model configs to:
  Enforce runtime validation for updates to request DTO
  Enforce immutable read-only snapshot for response DTO
- Run ruff formatter
- Add fixtures for common, reused data and centralize fixtures
  in the `conftest.py` under the unit tests module

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
[Issue]
The DynamicQRCode module refers usage of a ResponseCode with a value: 00

[Fix]
Daraja 3.0 uses a string value `0` for the synchronous Gateway response

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>

@RafaelJohn9 RafaelJohn9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work!

Requested few changes.

Ran the full check suite locally against this branch (ruff, ruff format, mypy, bandit, pytest). Lint/format/types/security are all clean, and 525 unit tests pass — but 6 tests currently error out, which will fail CI once it runs.

Comment thread tests/unit/services/test_dynamic_qr_service.py
Ensure all hatch env scripts pass successfully and fix related issues.

[Command]
➜ hatch run check && hatch run ci

[Changes]
- Ignore `bandit` false positive with nosec pragma
- Avoid using `pytest_plugins` in non top-level `conftest.py` scripts
- Fix any issues raised from checks

Signed-off-by: Eugene Mwangi <mwangi.em37@gmail.com>
@eugengi
eugengi force-pushed the refactor-service-dynamic-qr branch from 2e87b4e to 6a836ac Compare September 13, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants