Skip to content

Commit 0822842

Browse files
committed
fix: cache decorators and invoicing
1 parent 120ec95 commit 0822842

9 files changed

Lines changed: 421 additions & 17 deletions

File tree

apps/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@
7474
docs_url="/",
7575
throttle=[
7676
# Anonymous users: 5000 requests per minute
77-
AnonRateThrottle("5000/m"),
77+
AnonRateThrottle("10000/m"),
7878
# Authenticated users: 10000 requests per minute
79-
AuthRateThrottle("10000/m"),
79+
AuthRateThrottle("20000/m"),
8080
],
8181
)
8282

apps/common/cache/decorators.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,16 @@ async def cached_run(request, **kw):
7474
try:
7575
if operation.auth_callbacks:
7676
for auth_callback in operation.auth_callbacks:
77-
auth_result = await auth_callback(request)
78-
if auth_result:
79-
user_id = str(auth_result.id)
80-
request.auth = auth_result
81-
break
77+
try:
78+
if callable(auth_callback):
79+
auth_result = await auth_callback(request)
80+
if auth_result:
81+
user_id = str(auth_result.id)
82+
request.auth = auth_result
83+
break
84+
except TypeError:
85+
# auth_callback is None or not awaitable, skip
86+
pass
8287
elif hasattr(request, "auth") and request.auth:
8388
user_id = str(request.auth.id)
8489
except RequestError as e:
@@ -161,11 +166,16 @@ def cached_run(request, **kw):
161166
try:
162167
if operation.auth_callbacks:
163168
for auth_callback in operation.auth_callbacks:
164-
auth_result = auth_callback(request)
165-
if auth_result:
166-
user_id = str(auth_result.id)
167-
request.auth = auth_result
168-
break
169+
try:
170+
if callable(auth_callback):
171+
auth_result = auth_callback(request)
172+
if auth_result:
173+
user_id = str(auth_result.id)
174+
request.auth = auth_result
175+
break
176+
except TypeError:
177+
# auth_callback is None or not callable, skip
178+
pass
169179
elif hasattr(request, "auth") and request.auth:
170180
user_id = str(request.auth.id)
171181
except RequestError as e:

apps/payments/emails.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from django.template.loader import render_to_string
2+
from django.core.mail import EmailMessage
3+
from django.conf import settings
4+
import logging
5+
from datetime import datetime
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class PaymentEmailUtil:
11+
"""Email utilities for payment-related notifications"""
12+
13+
@classmethod
14+
def _send_email(cls, subject, template_name, context, recipient):
15+
"""Internal helper to render template and send email."""
16+
try:
17+
message = render_to_string(template_name, context)
18+
email_message = EmailMessage(subject=subject, body=message, to=[recipient])
19+
email_message.content_subtype = "html"
20+
email_message.send()
21+
logger.info(f"Email sent successfully to {recipient}: {subject}")
22+
except Exception as e:
23+
logger.error(f"Email sending failed for {recipient}: {e}", exc_info=True)
24+
25+
@classmethod
26+
def send_invoice_email(cls, invoice):
27+
"""Send invoice notification to customer"""
28+
try:
29+
# Get the frontend URL from settings or use default
30+
frontend_url = getattr(settings, 'FRONTEND_URL', 'http://localhost:5173')
31+
invoice_url = f"{frontend_url}/invoices/{invoice.invoice_number}"
32+
33+
context = {
34+
'customer_name': invoice.customer_name,
35+
'merchant_name': invoice.user.full_name,
36+
'invoice_number': invoice.invoice_number,
37+
'invoice_title': invoice.title,
38+
'invoice_description': invoice.description,
39+
'issue_date': invoice.issue_date.strftime('%B %d, %Y'),
40+
'due_date': invoice.due_date.strftime('%B %d, %Y'),
41+
'total_amount': f"{invoice.total_amount:,.2f}",
42+
'currency_symbol': invoice.wallet.currency.symbol,
43+
'invoice_url': invoice_url,
44+
'notes': invoice.notes,
45+
'current_year': datetime.now().year,
46+
}
47+
48+
cls._send_email(
49+
subject=f"Invoice {invoice.invoice_number} from {invoice.user.full_name}",
50+
template_name="invoice-created.html",
51+
context=context,
52+
recipient=invoice.customer_email,
53+
)
54+
except Exception as e:
55+
logger.error(f"Failed to send invoice email for {invoice.invoice_number}: {e}", exc_info=True)
56+
57+
@classmethod
58+
def send_payment_confirmation_email(cls, payment):
59+
"""Send payment confirmation email to payer"""
60+
try:
61+
# Get the frontend URL from settings or use default
62+
frontend_url = getattr(settings, 'FRONTEND_URL', 'http://localhost:5173')
63+
transaction_url = f"{frontend_url}/transactions"
64+
65+
# Determine payment description
66+
if payment.payment_link:
67+
payment_description = payment.payment_link.title
68+
elif payment.invoice:
69+
payment_description = f"Invoice {payment.invoice.invoice_number}"
70+
else:
71+
payment_description = "Payment"
72+
73+
context = {
74+
'payer_name': payment.payer_name,
75+
'merchant_name': payment.merchant_user.full_name,
76+
'reference': payment.reference,
77+
'payment_date': payment.created_at.strftime('%B %d, %Y at %I:%M %p'),
78+
'payment_description': payment_description,
79+
'amount': f"{payment.amount:,.2f}",
80+
'fee_amount': f"{payment.fee_amount:,.2f}",
81+
'net_amount': f"{payment.net_amount:,.2f}",
82+
'currency_symbol': payment.payer_wallet.currency.symbol,
83+
'new_balance': f"{payment.payer_wallet.balance:,.2f}",
84+
'invoice_number': payment.invoice.invoice_number if payment.invoice else None,
85+
'transaction_url': transaction_url,
86+
'current_year': datetime.now().year,
87+
'terms_url': f"{frontend_url}/terms",
88+
'privacy_url': f"{frontend_url}/privacy",
89+
}
90+
91+
cls._send_email(
92+
subject=f"Payment Confirmation - {payment.reference}",
93+
template_name="payment-confirmation.html",
94+
context=context,
95+
recipient=payment.payer_email,
96+
)
97+
except Exception as e:
98+
logger.error(f"Failed to send payment confirmation email for {payment.reference}: {e}", exc_info=True)

apps/payments/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class InvoiceListResponseSchema(ResponseSchema):
164164

165165

166166
class MakePaymentSchema(BaseSchema):
167-
wallet_id: UUID = Field(..., description="Payer's wallet")
167+
wallet_id: UUID = Field(..., description="Payer's wallet ID")
168168
amount: Optional[Decimal] = Field(
169169
None, gt=0, example=5000.00, description="Amount (required if not fixed)"
170170
)

apps/payments/services/payment_processor.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def process_payment_link_payment(
5454
"amount", f"Amount cannot exceed {link.max_amount}"
5555
)
5656

57+
# Get payer wallet
5758
payer_wallet = await Wallet.objects.select_related(
5859
"currency", "user"
5960
).aget_or_none(wallet_id=data.wallet_id)
@@ -114,8 +115,8 @@ async def process_payment_link_payment(
114115
# Create payment record
115116
payment = await Payment.objects.acreate(
116117
payment_link=link,
117-
payer_name=data.payer_name,
118-
payer_email=data.payer_email,
118+
payer_name=data.payer_name or f"{payer_wallet.user.first_name} {payer_wallet.user.last_name}",
119+
payer_email=data.payer_email or payer_wallet.user.email,
119120
payer_phone=data.payer_phone,
120121
payer_wallet=payer_wallet,
121122
merchant_user=link.user,
@@ -136,6 +137,17 @@ async def process_payment_link_payment(
136137
await link.asave(
137138
update_fields=["payments_count", "total_collected", "status", "updated_at"]
138139
)
140+
141+
# Send payment confirmation email asynchronously via Celery
142+
try:
143+
from apps.payments.tasks import send_payment_confirmation_email_async
144+
send_payment_confirmation_email_async.delay(str(payment.payment_id))
145+
except Exception as e:
146+
# Log error but don't fail payment
147+
import logging
148+
logger = logging.getLogger(__name__)
149+
logger.error(f"Failed to queue payment confirmation email: {e}")
150+
139151
return payment
140152

141153
@staticmethod
@@ -230,4 +242,15 @@ async def process_invoice_payment(
230242
)
231243

232244
await InvoiceManager.mark_invoice_paid(invoice, amount)
245+
246+
# Send payment confirmation email asynchronously via Celery
247+
try:
248+
from apps.payments.tasks import send_payment_confirmation_email_async
249+
send_payment_confirmation_email_async.delay(str(payment.payment_id))
250+
except Exception as e:
251+
# Log error but don't fail payment
252+
import logging
253+
logger = logging.getLogger(__name__)
254+
logger.error(f"Failed to queue payment confirmation email: {e}")
255+
233256
return payment

apps/payments/tasks.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Celery tasks for payments app
3+
"""
4+
5+
import logging
6+
from celery import shared_task
7+
from apps.payments.models import Invoice, Payment
8+
from apps.payments.emails import PaymentEmailUtil
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class PaymentEmailTasks:
14+
"""Email tasks for payment-related notifications"""
15+
16+
@staticmethod
17+
@shared_task(
18+
bind=True,
19+
autoretry_for=(Exception,),
20+
retry_kwargs={"max_retries": 3, "countdown": 60},
21+
name="payments.send_invoice_email",
22+
queue="emails",
23+
)
24+
def send_invoice_email(self, invoice_id: str):
25+
"""Send invoice notification email to customer"""
26+
try:
27+
invoice = Invoice.objects.select_related(
28+
'user', 'wallet', 'wallet__currency'
29+
).get_or_none(invoice_id=invoice_id)
30+
31+
if not invoice:
32+
logger.error(f"Invoice {invoice_id} not found")
33+
return {"status": "failed", "error": "Invoice not found"}
34+
35+
PaymentEmailUtil.send_invoice_email(invoice)
36+
logger.info(f"Invoice email sent for invoice {invoice.invoice_number}")
37+
return {"status": "success", "invoice_number": invoice.invoice_number}
38+
except Exception as exc:
39+
logger.error(f"Invoice email failed: {str(exc)}")
40+
raise self.retry(exc=exc)
41+
42+
@staticmethod
43+
@shared_task(
44+
bind=True,
45+
autoretry_for=(Exception,),
46+
retry_kwargs={"max_retries": 3, "countdown": 60},
47+
name="payments.send_payment_confirmation_email",
48+
queue="emails",
49+
)
50+
def send_payment_confirmation_email(self, payment_id: str):
51+
"""Send payment confirmation email to payer"""
52+
try:
53+
payment = Payment.objects.select_related(
54+
'payer_wallet',
55+
'payer_wallet__currency',
56+
'merchant_user',
57+
'payment_link',
58+
'invoice'
59+
).get_or_none(payment_id=payment_id)
60+
61+
if not payment:
62+
logger.error(f"Payment {payment_id} not found")
63+
return {"status": "failed", "error": "Payment not found"}
64+
65+
PaymentEmailUtil.send_payment_confirmation_email(payment)
66+
logger.info(f"Payment confirmation email sent for payment {payment.reference}")
67+
return {"status": "success", "reference": payment.reference}
68+
except Exception as exc:
69+
logger.error(f"Payment confirmation email failed: {str(exc)}")
70+
raise self.retry(exc=exc)
71+
72+
73+
# Expose task functions for imports
74+
send_invoice_email_async = PaymentEmailTasks.send_invoice_email
75+
send_payment_confirmation_email_async = PaymentEmailTasks.send_payment_confirmation_email

apps/payments/views.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from uuid import UUID
22
from typing import Optional
33
from ninja import Query, Router
4+
import logging
45

56
from apps.accounts.auth import AuthKycUser
67
from apps.common.exceptions import NotFoundError
@@ -24,8 +25,8 @@
2425
from apps.payments.services.invoice_manager import InvoiceManager
2526
from apps.payments.services.payment_processor import PaymentProcessor
2627
from apps.payments.models import Payment, PaymentLinkStatus
27-
28-
28+
from .tasks import PaymentEmailTasks
29+
logger = logging.getLogger(__name__)
2930
payment_router = Router(tags=["Payments (16)"])
3031

3132

@@ -113,6 +114,7 @@ async def get_payment_link_public(request, slug: str):
113114
"/pay/{slug}",
114115
summary="Make payment via payment link",
115116
response={201: PaymentDataResponseSchema},
117+
auth=AuthKycUser(),
116118
)
117119
async def pay_via_link(request, slug: str, data: MakePaymentSchema):
118120
payment = await PaymentProcessor.process_payment_link_payment(slug, data)
@@ -139,8 +141,16 @@ async def pay_via_link(request, slug: str, data: MakePaymentSchema):
139141
auth=AuthKycUser(),
140142
)
141143
async def create_invoice(request, data: CreateInvoiceSchema):
144+
142145
user = request.auth
143146
invoice = await InvoiceManager.create_invoice(user, data)
147+
148+
# Send invoice email to customer asynchronously via Celery
149+
try:
150+
PaymentEmailTasks.send_invoice_email.delay(str(invoice.invoice_id))
151+
except Exception as e:
152+
logger.error(f"Failed to queue invoice email: {e}")
153+
144154
invoice = await InvoiceManager.get_invoice(user, invoice.invoice_id)
145155
return CustomResponse.success("Invoice created successfully", invoice, 201)
146156

0 commit comments

Comments
 (0)