Files
lukenyaMarathon_Frape/lukenya_marathon/lukenya_marathon/payments_management.py
T
KabundiNjuki 2217484648
CI / Server (push) Has been cancelled
Initial LuKenya Marathon application
2026-09-24 00:05:12 +03:00

449 lines
12 KiB
Python

import frappe
from frappe import _
from frappe.utils import flt
INACTIVE_REGISTRATION_STATUSES = (
"Cancelled",
"Transferred",
"Refunded",
)
CONFIRMED_PAYMENT_STATUSES = (
"Paid",
"Reconciled",
)
REFUND_STATUS = "Refunded"
def _edition_filter(alias, event_edition):
if event_edition:
return f" and {alias}.event_edition = %(event_edition)s"
return ""
def _date_filter(alias, from_date=None, to_date=None):
filters = ""
if from_date:
filters += f" and date({alias}.payment_date) >= %(from_date)s"
if to_date:
filters += f" and date({alias}.payment_date) <= %(to_date)s"
return filters
def _payment_params(event_edition=None, from_date=None, to_date=None):
return {
"event_edition": event_edition,
"from_date": from_date,
"to_date": to_date,
"inactive": INACTIVE_REGISTRATION_STATUSES,
"confirmed": CONFIRMED_PAYMENT_STATUSES,
}
def _single_value(sql, params):
rows = frappe.db.sql(sql, params, as_dict=True)
if not rows:
return 0
return rows[0].value or 0
@frappe.whitelist()
def get_payment_editions():
return frappe.get_all(
"Event Edition",
fields=[
"name",
"edition_name",
"edition_code",
"event_date",
"status",
],
order_by="event_date desc, modified desc",
limit_page_length=100,
)
@frappe.whitelist()
def get_payment_metrics(
event_edition=None,
from_date=None,
to_date=None,
gateway=None,
status=None,
):
params = _payment_params(event_edition, from_date, to_date)
registration_scope = """
r.registration_status not in %(inactive)s
"""
if event_edition:
registration_scope += " and r.event_edition = %(event_edition)s"
billed = _single_value(
f"""
select coalesce(sum(r.amount_due), 0) as value
from `tabRegistration` r
where {registration_scope}
""",
params,
)
transaction_scope = """
r.registration_status not in %(inactive)s
"""
if event_edition:
transaction_scope += " and r.event_edition = %(event_edition)s"
transaction_scope += _date_filter("p", from_date, to_date)
if gateway:
transaction_scope += " and p.gateway = %(gateway)s"
params["gateway"] = gateway
if status:
transaction_scope += " and p.status = %(status)s"
params["status"] = status
received = _single_value(
f"""
select coalesce(sum(p.amount), 0) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where {transaction_scope}
and p.status in %(confirmed)s
""",
params,
)
refunded = _single_value(
f"""
select coalesce(sum(p.amount), 0) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where {transaction_scope}
and p.status = 'Refunded'
""",
params,
)
paid_registrations = _single_value(
f"""
select count(*) as value
from `tabRegistration` r
where {registration_scope}
and r.payment_status = 'Paid'
""",
params,
)
unpaid_registrations = _single_value(
f"""
select count(*) as value
from `tabRegistration` r
where {registration_scope}
and r.payment_status in (
'Unpaid',
'Pending',
'Partially Paid',
'Failed'
)
""",
params,
)
pending_transactions = _single_value(
f"""
select count(*) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where {transaction_scope}
and p.status = 'Pending'
""",
params,
)
failed_transactions = _single_value(
f"""
select count(*) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where {transaction_scope}
and p.status = 'Failed'
""",
params,
)
refunded_transactions = _single_value(
f"""
select count(*) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where {transaction_scope}
and p.status = 'Refunded'
""",
params,
)
reconciliation_exceptions = _single_value(
f"""
select count(*) as value
from `tabRegistration` r
left join (
select registration, sum(amount) as paid
from `tabPayment Transaction`
where status in %(confirmed)s
group by registration
) p on p.registration = r.name
where {registration_scope}
and abs(coalesce(r.amount_due, 0) - coalesce(p.paid, 0)) > 0.01
""",
params,
)
total_received = flt(received)
total_refunded = flt(refunded)
return {
"total_billed": flt(billed),
"total_received": total_received,
"refunded_amount": total_refunded,
"net_received": total_received - total_refunded,
"outstanding": max(flt(billed) - total_received, 0),
"paid_registrations": int(paid_registrations),
"unpaid_registrations": int(unpaid_registrations),
"pending_transactions": int(pending_transactions),
"failed_transactions": int(failed_transactions),
"refunded_transactions": int(refunded_transactions),
"reconciliation_exceptions": int(reconciliation_exceptions),
}
@frappe.whitelist()
def get_payment_breakdown(
event_edition=None,
from_date=None,
to_date=None,
):
params = _payment_params(event_edition, from_date, to_date)
edition_sql = ""
if event_edition:
edition_sql = " and r.event_edition = %(event_edition)s"
date_sql = _date_filter("p", from_date, to_date)
statuses = frappe.db.sql(
f"""
select
p.status,
count(*) as transaction_count,
coalesce(sum(p.amount), 0) as amount
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where r.registration_status not in %(inactive)s
{edition_sql}
{date_sql}
group by p.status
order by p.status
""",
params,
as_dict=True,
)
gateways = frappe.db.sql(
f"""
select
p.gateway,
count(*) as transaction_count,
coalesce(sum(p.amount), 0) as amount,
sum(case when p.status in %(confirmed)s then 1 else 0 end) as paid_count,
sum(case when p.status = 'Pending' then 1 else 0 end) as pending_count,
sum(case when p.status = 'Failed' then 1 else 0 end) as failed_count
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
where r.registration_status not in %(inactive)s
{edition_sql}
{date_sql}
group by p.gateway
order by amount desc
""",
params,
as_dict=True,
)
return {
"statuses": statuses,
"gateways": gateways,
}
@frappe.whitelist()
def get_payment_transactions(
event_edition=None,
from_date=None,
to_date=None,
gateway=None,
status=None,
search=None,
page=1,
page_length=25,
):
page = max(int(page or 1), 1)
page_length = min(max(int(page_length or 25), 1), 100)
offset = (page - 1) * page_length
params = _payment_params(event_edition, from_date, to_date)
conditions = [
"r.registration_status not in %(inactive)s",
]
if event_edition:
conditions.append("r.event_edition = %(event_edition)s")
if from_date:
conditions.append("date(p.payment_date) >= %(from_date)s")
if to_date:
conditions.append("date(p.payment_date) <= %(to_date)s")
if gateway:
conditions.append("p.gateway = %(gateway)s")
params["gateway"] = gateway
if status:
conditions.append("p.status = %(status)s")
params["status"] = status
if search:
conditions.append(
"""
(
p.name like %(search)s
or p.gateway_transaction_id like %(search)s
or p.registration like %(search)s
or part.full_name like %(search)s
)
"""
)
params["search"] = f"%{search}%"
where_sql = " and ".join(conditions)
rows = frappe.db.sql(
f"""
select
p.name,
p.registration,
p.gateway,
p.gateway_transaction_id,
p.amount,
p.currency,
p.status,
p.payment_date,
p.failure_reason,
r.event_edition,
r.race_category,
r.amount_due,
r.payment_status,
part.full_name as participant_name,
e.edition_name,
c.category_name
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
left join `tabParticipant` part on part.name = r.participant
left join `tabEvent Edition` e on e.name = r.event_edition
left join `tabRace Category` c on c.name = r.race_category
where {where_sql}
order by p.payment_date desc, p.modified desc
limit %(limit)s offset %(offset)s
""",
{
**params,
"limit": page_length,
"offset": offset,
},
as_dict=True,
)
total = _single_value(
f"""
select count(*) as value
from `tabPayment Transaction` p
join `tabRegistration` r on r.name = p.registration
left join `tabParticipant` part on part.name = r.participant
where {where_sql}
""",
params,
)
return {
"rows": rows,
"total": int(total),
"page": page,
"page_length": page_length,
}
@frappe.whitelist()
def get_payment_exceptions(
event_edition=None,
page=1,
page_length=25,
):
page = max(int(page or 1), 1)
page_length = min(max(int(page_length or 25), 1), 100)
offset = (page - 1) * page_length
params = {
"event_edition": event_edition,
"inactive": INACTIVE_REGISTRATION_STATUSES,
"confirmed": CONFIRMED_PAYMENT_STATUSES,
"limit": page_length,
"offset": offset,
}
edition_sql = ""
if event_edition:
edition_sql = " and r.event_edition = %(event_edition)s"
rows = frappe.db.sql(
f"""
select
r.name as registration,
r.event_edition,
r.amount_due,
coalesce(pay.paid, 0) as paid,
(coalesce(r.amount_due, 0) - coalesce(pay.paid, 0)) as difference,
r.payment_status,
part.full_name as participant_name,
e.edition_name
from `tabRegistration` r
left join (
select registration, sum(amount) as paid
from `tabPayment Transaction`
where status in %(confirmed)s
group by registration
) pay on pay.registration = r.name
left join `tabParticipant` part on part.name = r.participant
left join `tabEvent Edition` e on e.name = r.event_edition
where r.registration_status not in %(inactive)s
{edition_sql}
and abs(coalesce(r.amount_due, 0) - coalesce(pay.paid, 0)) > 0.01
order by abs(coalesce(r.amount_due, 0) - coalesce(pay.paid, 0)) desc
limit %(limit)s offset %(offset)s
""",
params,
as_dict=True,
)
return rows