Skip to content

Commit fe743e9

Browse files
authored
added webhook support (#3)
1 parent 183c516 commit fe743e9

5 files changed

Lines changed: 171 additions & 0 deletions

File tree

src/pay_ccavenue/ccavenue.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from Crypto.Cipher import AES
99
from pay_ccavenue.models.config import CCavenueConfig
1010
from pay_ccavenue.models.form import CCavenueFormData
11+
from pay_ccavenue.models.webhook import CCavenueWebhookData
1112

1213

1314
class CCAvenue:
@@ -191,3 +192,16 @@ def decrypt(self, data: Dict[str, str]) -> Dict[str, str]:
191192
decrypted = cipher.decrypt(encrypted_text)
192193
self._unflatten_decrypted_data(decrypted)
193194
return self.decrypted_data
195+
196+
def process_webhook(self, response_body: Dict[str, str]) -> CCavenueWebhookData:
197+
"""
198+
Process and validate the webhook/notification data from CCAvenue.
199+
200+
Args:
201+
response_body (Dict[str, str]): The response body containing 'encResp'.
202+
203+
Returns:
204+
CCavenueWebhookData: The decrypted and parsed data object.
205+
"""
206+
decrypted_dict = self.decrypt(response_body)
207+
return CCavenueWebhookData.from_dict(decrypted_dict)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pay_ccavenue.models.config import CCavenueConfig
2+
from pay_ccavenue.models.form import CCavenueFormData
3+
from pay_ccavenue.models.webhook import CCavenueWebhookData
4+
5+
__all__ = ["CCavenueConfig", "CCavenueFormData", "CCavenueWebhookData"]

src/pay_ccavenue/models/webhook.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
5+
@dataclass
6+
class CCavenueWebhookData:
7+
"""
8+
Represents the data received from a CCAvenue webhook/notification.
9+
10+
Covers fields for:
11+
- Order Status (including Reconcilation and Echo)
12+
- Order Risk Status
13+
- Payment Type Status
14+
"""
15+
16+
# Core Fields (Order Status / generic)
17+
order_id: Optional[str] = None
18+
tracking_id: Optional[str] = None
19+
bank_ref_no: Optional[str] = None
20+
order_status: Optional[str] = None # Success, Failure, Aborted, Invalid
21+
failure_message: Optional[str] = None
22+
payment_mode: Optional[str] = None
23+
24+
# Card & Bank Details
25+
card_name: Optional[str] = None
26+
status_code: Optional[str] = None
27+
status_message: Optional[str] = None
28+
29+
# Transaction Details
30+
currency: Optional[str] = None
31+
amount: Optional[str] = None
32+
33+
# Billing Details
34+
billing_name: Optional[str] = None
35+
billing_address: Optional[str] = None
36+
billing_city: Optional[str] = None
37+
billing_state: Optional[str] = None
38+
billing_zip: Optional[str] = None
39+
billing_country: Optional[str] = None
40+
billing_tel: Optional[str] = None
41+
billing_email: Optional[str] = None
42+
43+
# Delivery Details
44+
delivery_name: Optional[str] = None
45+
delivery_address: Optional[str] = None
46+
delivery_city: Optional[str] = None
47+
delivery_state: Optional[str] = None
48+
delivery_zip: Optional[str] = None
49+
delivery_country: Optional[str] = None
50+
delivery_tel: Optional[str] = None
51+
52+
# Merchant Custom Fields
53+
merchant_param1: Optional[str] = None
54+
merchant_param2: Optional[str] = None
55+
merchant_param3: Optional[str] = None
56+
merchant_param4: Optional[str] = None
57+
merchant_param5: Optional[str] = None
58+
59+
# Offers & Vault
60+
vault: Optional[str] = None
61+
offer_type: Optional[str] = None
62+
offer_code: Optional[str] = None
63+
discount_value: Optional[str] = None
64+
65+
# Risk Status Specific
66+
risk_status: Optional[str] = None # High, Low, NR, GA
67+
risk_reason: Optional[str] = None
68+
69+
# Payment Type Status Specific
70+
payment_option: Optional[str] = None
71+
current_status: Optional[str] = None # ACTI, FLCT, INAC, DOWN, NEW
72+
73+
@classmethod
74+
def from_dict(cls, data: dict) -> "CCavenueWebhookData":
75+
"""
76+
Create a CCavenueWebhookData instance from a dictionary.
77+
Keys in the dictionary that don't match fields are ignored.
78+
"""
79+
# Filter data to only include keys that match field names
80+
known_fields = set(cls.__annotations__.keys())
81+
filtered_data = {k: v for k, v in data.items() if k in known_fields}
82+
return cls(**filtered_data)

tests/test_core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,13 @@ def mock_get_cipher():
108108
decrypted_data = {k: v.rstrip("\r") for k, v in decrypted_data.items()}
109109

110110
assert decrypted_data == {"key1": "value1", "key2": "value2", "key3": "value3"}
111+
112+
113+
def test_get_cipher(ccavenue_instance):
114+
"""Test _get_cipher logic directly without mocking."""
115+
cipher = ccavenue_instance._get_cipher()
116+
assert cipher is not None
117+
# We can't easily check the key inside the cipher object in PyCryptodome,
118+
# but successful creation is what we want to test.
119+
# The block size should be AES.block_size (16)
120+
assert cipher.block_size == 16

tests/test_webhook.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from pay_ccavenue.models.webhook import CCavenueWebhookData
2+
3+
4+
def test_webhook_data_from_dict():
5+
"""Test that CCavenueWebhookData correctly parses a dictionary."""
6+
data = {
7+
"order_id": "123",
8+
"order_status": "Success",
9+
"amount": "100.00",
10+
"unknown_field": "should_be_ignored",
11+
"merchant_param1": "custom_data",
12+
}
13+
14+
webhook_data = CCavenueWebhookData.from_dict(data)
15+
16+
assert webhook_data.order_id == "123"
17+
assert webhook_data.order_status == "Success"
18+
assert webhook_data.amount == "100.00"
19+
assert webhook_data.merchant_param1 == "custom_data"
20+
21+
# Check that unknown fields are ignored (not present as attributes)
22+
# Dataclasses only have defined fields
23+
assert not hasattr(webhook_data, "unknown_field")
24+
25+
26+
def test_process_webhook(ccavenue_instance, monkeypatch):
27+
"""Test the process_webhook method of CCAvenue class."""
28+
29+
# Mock decrypt to return a specific dictionary
30+
expected_dict = {
31+
"order_id": "ORDER123",
32+
"tracking_id": "TRACK123",
33+
"order_status": "Success",
34+
"amount": "500.50",
35+
"currency": "INR",
36+
"payment_mode": "Net Banking",
37+
"card_name": "HDFC",
38+
}
39+
40+
def mock_decrypt(data):
41+
return expected_dict
42+
43+
monkeypatch.setattr(ccavenue_instance, "decrypt", mock_decrypt)
44+
45+
# The input to process_webhook is a dict with 'encResp'
46+
# Since we mocked decrypt, the content doesn't matter much, but structure does
47+
dummy_input = {"encResp": "dummy_encrypted_string"}
48+
49+
result = ccavenue_instance.process_webhook(dummy_input)
50+
51+
assert isinstance(result, CCavenueWebhookData)
52+
assert result.order_id == "ORDER123"
53+
assert result.tracking_id == "TRACK123"
54+
assert result.order_status == "Success"
55+
assert result.amount == "500.50"
56+
assert result.currency == "INR"
57+
assert result.payment_mode == "Net Banking"
58+
assert result.card_name == "HDFC"
59+
# Ensure optional fields are None
60+
assert result.failure_message is None

0 commit comments

Comments
 (0)