From 67a0255c0d85212082aa5ba80abf072a80b9799f Mon Sep 17 00:00:00 2001 From: Magzhan Zhumabayev Date: Mon, 11 May 2026 17:33:08 +0500 Subject: [PATCH] Fix sales post-repair acceptance gaps --- services/sales_service/app.py | 45 +++++++++++++++------------- tests/test_sales_contract_routes.py | 1 + tests/test_sales_payment_webhooks.py | 27 +++++++++++++++++ 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/services/sales_service/app.py b/services/sales_service/app.py index 09ef3e7..67f492d 100644 --- a/services/sales_service/app.py +++ b/services/sales_service/app.py @@ -4108,6 +4108,7 @@ def close_deal( session.close() +@app.post("/api/v1/deals/{deal_id}/escalations", response_model=SalesEscalationOut) @app.post("/api/v1/deals/{deal_id}/escalate", response_model=SalesEscalationOut) def escalate_deal( deal_id: str, @@ -6613,28 +6614,32 @@ def reconcile_payment( else: adapter = get_payment_provider_adapter(provider) if adapter is None: - raise HTTPException(status_code=400, detail="Payment provider adapter is not configured") - if not row.external_payment_id: + if provider == "manual": + status = row.status + else: + raise HTTPException(status_code=400, detail="Payment provider adapter is not configured") + elif not row.external_payment_id: raise HTTPException(status_code=400, detail="Payment external_payment_id is required for reconcile") - provider_result = adapter.get_status( - row.external_payment_id, - context={ - "tenant_id": tenant_id, - "payment_id": row.payment_id, - "payment_provider": provider, - "integration_id": integration.integration_id if integration is not None else None, - }, - ) - if isinstance(provider_result, dict): - status = normalize_payment_status(provider, str(provider_result.get("status") or provider_result.get("provider_status") or "")) - failure_reason = str(provider_result.get("failure_reason") or failure_reason or "") or None - metadata.update(provider_result.get("metadata") if isinstance(provider_result.get("metadata"), dict) else {}) - if provider_result.get("amount") is not None: - row.amount = _coerce_payment_amount(provider_result.get("amount")) - if provider_result.get("currency"): - row.currency = str(provider_result.get("currency")).upper() else: - status = normalize_payment_status(provider, str(provider_result)) + provider_result = adapter.get_status( + row.external_payment_id, + context={ + "tenant_id": tenant_id, + "payment_id": row.payment_id, + "payment_provider": provider, + "integration_id": integration.integration_id if integration is not None else None, + }, + ) + if isinstance(provider_result, dict): + status = normalize_payment_status(provider, str(provider_result.get("status") or provider_result.get("provider_status") or "")) + failure_reason = str(provider_result.get("failure_reason") or failure_reason or "") or None + metadata.update(provider_result.get("metadata") if isinstance(provider_result.get("metadata"), dict) else {}) + if provider_result.get("amount") is not None: + row.amount = _coerce_payment_amount(provider_result.get("amount")) + if provider_result.get("currency"): + row.currency = str(provider_result.get("currency")).upper() + else: + status = normalize_payment_status(provider, str(provider_result)) updated = _apply_payment_update( session, diff --git a/tests/test_sales_contract_routes.py b/tests/test_sales_contract_routes.py index 81dbf3e..190544f 100644 --- a/tests/test_sales_contract_routes.py +++ b/tests/test_sales_contract_routes.py @@ -21,6 +21,7 @@ def test_sales_frontend_contract_routes_are_registered(): "/api/v1/deals/{deal_id}/invoices": {"GET", "POST"}, "/api/v1/deals/{deal_id}/conditions": {"GET", "POST", "PATCH", "PUT"}, "/api/v1/deals/{deal_id}/conditions/confirm": {"POST"}, + "/api/v1/deals/{deal_id}/escalations": {"GET", "POST"}, "/api/v1/deals/{deal_id}/automation-tasks": {"GET"}, "/api/v1/automation-tasks/{task_id}/cancel": {"POST"}, "/api/v1/automation-tasks/{task_id}/run-now": {"POST"}, diff --git a/tests/test_sales_payment_webhooks.py b/tests/test_sales_payment_webhooks.py index 8130656..794a050 100644 --- a/tests/test_sales_payment_webhooks.py +++ b/tests/test_sales_payment_webhooks.py @@ -480,6 +480,33 @@ def test_reconcile_uses_provider_adapter_and_safe_payment_flow(): session.close() +def test_reconcile_manual_payment_is_idempotent_without_adapter(): + tenant_id = "tenant_payment_manual_reconcile" + client = TestClient(sales_module.app) + _, deal = _create_lead_and_deal(client, tenant_id) + invoice = _create_invoice(client, deal["deal_id"], tenant_id, amount=50000) + created = client.post( + "/api/v1/payments/webhook", + json={ + "deal_id": deal["deal_id"], + "invoice_id": invoice["invoice_id"], + "payment_provider": "manual", + "external_payment_id": "manual-reconcile-1", + "amount": 50000, + "currency": "KZT", + "status": "success", + }, + headers=_headers(tenant_id), + ) + assert created.status_code == 200 + payment_id = created.json()["payment_id"] + + response = client.post(f"/api/v1/payments/{payment_id}/reconcile", headers=_headers(tenant_id)) + + assert response.status_code == 200 + assert response.json()["status"] == "success" + + def test_payment_webhook_publishes_payment_and_invoice_events_once_for_duplicate_payment(): tenant_id = "tenant_payment_events_once" client = TestClient(sales_module.app)