Fix sales post-repair acceptance gaps
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user