2034 lines
62 KiB
Python
2034 lines
62 KiB
Python
# Copyright (c) 2020, JINGROW
|
||
# For license information, please see license.txt
|
||
from __future__ import annotations
|
||
|
||
from itertools import groupby
|
||
import urllib.parse
|
||
import json
|
||
import segno
|
||
import io
|
||
import base64
|
||
import jingrow
|
||
from jingrow import _ # Import this for translation functionality
|
||
from jingrow.core.utils import find
|
||
from jingrow.utils import fmt_money, get_request_site_address, getdate, add_months
|
||
import random
|
||
from datetime import datetime
|
||
|
||
from jcloud.api.regional_payments.mpesa.utils import (
|
||
create_invoice_partner_site,
|
||
create_payment_partner_transaction,
|
||
fetch_param_value,
|
||
get_details_from_request_log,
|
||
get_mpesa_setup_for_team,
|
||
get_payment_gateway,
|
||
sanitize_mobile_number,
|
||
update_tax_id_or_phone_no,
|
||
)
|
||
from jcloud.jcloud.pagetype.mpesa_setup.mpesa_connector import MpesaConnector
|
||
from jcloud.jcloud.pagetype.team.team import (
|
||
has_unsettled_invoices,
|
||
)
|
||
from jcloud.utils import get_current_team
|
||
from jcloud.utils.billing import (
|
||
GSTIN_FORMAT,
|
||
clear_setup_intent,
|
||
get_publishable_key,
|
||
get_razorpay_client,
|
||
get_setup_intent,
|
||
get_stripe,
|
||
make_formatted_pg,
|
||
states_with_tin,
|
||
validate_gstin_check_digit,
|
||
)
|
||
from jcloud.utils.mpesa_utils import create_mpesa_request_log
|
||
from jcloud.api.payment.wechatpay import WeChatPayAPI
|
||
from jcloud.api.payment.alipay import AlipayAPI
|
||
|
||
@jingrow.whitelist()
|
||
def get_publishable_key_and_setup_intent():
|
||
team = get_current_team()
|
||
return {
|
||
"publishable_key": get_publishable_key(),
|
||
"setup_intent": get_setup_intent(team),
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def upcoming_invoice():
|
||
team = get_current_team(True)
|
||
invoice = team.get_upcoming_invoice()
|
||
|
||
if invoice:
|
||
upcoming_invoice = invoice.as_dict()
|
||
upcoming_invoice.formatted = make_formatted_pg(invoice, ["Currency"])
|
||
else:
|
||
upcoming_invoice = None
|
||
|
||
return {
|
||
"upcoming_invoice": upcoming_invoice,
|
||
"available_credits": fmt_money(team.get_balance(), 2, team.currency),
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_balance_credit():
|
||
team = get_current_team(True)
|
||
return team.get_balance()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def past_invoices():
|
||
return get_current_team(True).get_past_invoices()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def invoices_and_payments():
|
||
team = get_current_team(True)
|
||
return team.get_past_invoices()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def refresh_invoice_link(invoice):
|
||
pg = jingrow.get_pg("Invoice", invoice)
|
||
return pg.refresh_stripe_payment_link()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def balances():
|
||
team = get_current_team()
|
||
has_bought_credits = jingrow.db.get_all(
|
||
"Balance Transaction",
|
||
filters={
|
||
"source": ("in", ("Prepaid Credits", "Transferred Credits", "Free Credits")),
|
||
"team": team,
|
||
"pagestatus": 1,
|
||
"type": ("!=", "Partnership Fee"),
|
||
},
|
||
limit=1,
|
||
)
|
||
if not has_bought_credits:
|
||
return []
|
||
|
||
bt = jingrow.qb.PageType("Balance Transaction")
|
||
inv = jingrow.qb.PageType("Invoice")
|
||
query = (
|
||
jingrow.qb.from_(bt)
|
||
.left_join(inv)
|
||
.on(bt.invoice == inv.name)
|
||
.select(
|
||
bt.name,
|
||
bt.creation,
|
||
bt.amount,
|
||
bt.currency,
|
||
bt.source,
|
||
bt.type,
|
||
bt.ending_balance,
|
||
bt.description,
|
||
inv.period_start,
|
||
)
|
||
.where((bt.pagestatus == 1) & (bt.team == team))
|
||
.orderby(bt.creation, order=jingrow.qb.desc)
|
||
)
|
||
|
||
data = query.run(as_dict=True)
|
||
for d in data:
|
||
d.formatted = dict(
|
||
amount=fmt_money(d.amount, 2, d.currency),
|
||
ending_balance=fmt_money(d.ending_balance, 2, d.currency),
|
||
)
|
||
|
||
if d.period_start:
|
||
d.formatted["invoice_for"] = d.period_start.strftime("%B %Y")
|
||
return data
|
||
|
||
|
||
def get_processed_balance_transactions(transactions: list[dict]):
|
||
"""Cleans up transactions and adjusts ending balances accordingly"""
|
||
|
||
cleaned_up_transations = get_cleaned_up_transactions(transactions)
|
||
processed_balance_transactions = []
|
||
for bt in reversed(cleaned_up_transations):
|
||
if is_added_credits_bt(bt) and len(processed_balance_transactions) < 1:
|
||
processed_balance_transactions.append(bt)
|
||
elif is_added_credits_bt(bt):
|
||
bt.ending_balance += processed_balance_transactions[
|
||
-1
|
||
].ending_balance # Adjust the ending balance
|
||
processed_balance_transactions.append(bt)
|
||
elif bt.type == "Applied To Invoice":
|
||
processed_balance_transactions.append(bt)
|
||
|
||
return list(reversed(processed_balance_transactions))
|
||
|
||
|
||
def get_cleaned_up_transactions(transactions: list[dict]):
|
||
"""Only picks Balance transactions that the users care about"""
|
||
|
||
cleaned_up_transations = []
|
||
for bt in transactions:
|
||
if is_added_credits_bt(bt):
|
||
cleaned_up_transations.append(bt)
|
||
continue
|
||
|
||
if bt.type == "Applied To Invoice" and not find(
|
||
cleaned_up_transations, lambda x: x.invoice == bt.invoice
|
||
):
|
||
cleaned_up_transations.append(bt)
|
||
continue
|
||
return cleaned_up_transations
|
||
|
||
|
||
def is_added_credits_bt(bt):
|
||
"""Returns `true` if credits were added and not some reverse transaction"""
|
||
if not (
|
||
bt.type == "Adjustment"
|
||
and bt.source
|
||
in (
|
||
"Prepaid Credits",
|
||
"Free Credits",
|
||
"Transferred Credits",
|
||
) # Might need to re-think this
|
||
):
|
||
return False
|
||
|
||
# Is not a reverse of a previous balance transaction
|
||
bt.description = bt.description or ""
|
||
return not bt.description.startswith("Reverse")
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def details():
|
||
team = get_current_team(True)
|
||
address = None
|
||
if team.billing_address:
|
||
address = jingrow.get_pg("Address", team.billing_address)
|
||
address_parts = [
|
||
address.address_line1,
|
||
address.city,
|
||
address.state,
|
||
address.country,
|
||
address.pincode,
|
||
]
|
||
billing_address = ", ".join([d for d in address_parts if d])
|
||
else:
|
||
billing_address = ""
|
||
|
||
return {
|
||
"billing_name": team.billing_name,
|
||
"billing_address": billing_address,
|
||
"gstin": address.gstin if address else None,
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_customer_details(team):
|
||
"""This method is called by jingrow.com for creating Customer and Address"""
|
||
team_pg = jingrow.db.get_value("Team", team, "*")
|
||
return {
|
||
"team": team_pg,
|
||
"address": jingrow.get_pg("Address", team_pg.billing_address),
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_payment_intent_for_micro_debit(payment_method_name):
|
||
team = get_current_team(True)
|
||
stripe = get_stripe()
|
||
|
||
micro_debit_charge_field = (
|
||
"micro_debit_charge_usd" if team.currency == "USD" else "micro_debit_charge_cny"
|
||
)
|
||
amount = jingrow.db.get_single_value("Jcloud Settings", micro_debit_charge_field)
|
||
|
||
intent = stripe.PaymentIntent.create(
|
||
amount=int(amount * 100),
|
||
currency=team.currency.lower(),
|
||
customer=team.stripe_customer_id,
|
||
description="Micro-Debit Card Test Charge",
|
||
metadata={
|
||
"payment_for": "micro_debit_test_charge",
|
||
"payment_method_name": payment_method_name,
|
||
},
|
||
)
|
||
return {"client_secret": intent["client_secret"]}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_payment_intent_for_partnership_fees():
|
||
team = get_current_team(True)
|
||
jcloud_settings = jingrow.get_cached_pg("Jcloud Settings")
|
||
metadata = {"payment_for": "partnership_fee"}
|
||
fee_amount = jcloud_settings.partnership_fee_usd
|
||
|
||
if team.currency == "CNY":
|
||
fee_amount = jcloud_settings.partnership_fee_cny
|
||
gst_amount = fee_amount * jcloud_settings.gst_percentage
|
||
fee_amount += gst_amount
|
||
metadata.update({"gst": round(gst_amount, 2)})
|
||
|
||
stripe = get_stripe()
|
||
intent = stripe.PaymentIntent.create(
|
||
amount=int(fee_amount * 100),
|
||
currency=team.currency.lower(),
|
||
customer=team.stripe_customer_id,
|
||
description="Partnership Fee",
|
||
metadata=metadata,
|
||
)
|
||
return {
|
||
"client_secret": intent["client_secret"],
|
||
"publishable_key": get_publishable_key(),
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_payment_intent_for_buying_credits(amount):
|
||
team = get_current_team(True)
|
||
metadata = {"payment_for": "prepaid_credits"}
|
||
total_unpaid = total_unpaid_amount()
|
||
|
||
if amount < total_unpaid and not team.jerp_partner:
|
||
jingrow.throw(f"Amount {amount} is less than the total unpaid amount {total_unpaid}.")
|
||
|
||
if team.currency == "CNY":
|
||
gst_amount = amount * jingrow.db.get_single_value("Jcloud Settings", "gst_percentage")
|
||
amount += gst_amount
|
||
metadata.update({"gst": round(gst_amount, 2)})
|
||
|
||
amount = round(amount, 2)
|
||
stripe = get_stripe()
|
||
intent = stripe.PaymentIntent.create(
|
||
amount=int(amount * 100),
|
||
currency=team.currency.lower(),
|
||
customer=team.stripe_customer_id,
|
||
description="Prepaid Credits",
|
||
metadata=metadata,
|
||
)
|
||
return {
|
||
"client_secret": intent["client_secret"],
|
||
"publishable_key": get_publishable_key(),
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_payment_intent_for_prepaid_app(amount, metadata):
|
||
stripe = get_stripe()
|
||
team = get_current_team(True)
|
||
payment_method = jingrow.get_value(
|
||
"Stripe Payment Method", team.default_payment_method, "stripe_payment_method_id"
|
||
)
|
||
try:
|
||
if not payment_method:
|
||
intent = stripe.PaymentIntent.create(
|
||
amount=amount * 100,
|
||
currency=team.currency.lower(),
|
||
customer=team.stripe_customer_id,
|
||
description="Prepaid App Purchase",
|
||
metadata=metadata,
|
||
)
|
||
else:
|
||
intent = stripe.PaymentIntent.create(
|
||
amount=amount * 100,
|
||
currency=team.currency.lower(),
|
||
customer=team.stripe_customer_id,
|
||
description="Prepaid App Purchase",
|
||
off_session=True,
|
||
confirm=True,
|
||
metadata=metadata,
|
||
payment_method=payment_method,
|
||
payment_method_options={"card": {"request_three_d_secure": "any"}},
|
||
)
|
||
|
||
return {
|
||
"payment_method": payment_method,
|
||
"client_secret": intent["client_secret"],
|
||
"publishable_key": get_publishable_key(),
|
||
}
|
||
except stripe.error.CardError as e:
|
||
err = e.error
|
||
if err.code == "authentication_required":
|
||
# Bring the customer back on-session to authenticate the purchase
|
||
return {
|
||
"error": "authentication_required",
|
||
"payment_method": err.payment_method.id,
|
||
"amount": amount,
|
||
"card": err.payment_method.card,
|
||
"publishable_key": get_publishable_key(),
|
||
"client_secret": err.payment_intent.client_secret,
|
||
}
|
||
if err.code:
|
||
# The card was declined for other reasons (e.g. insufficient funds)
|
||
# Bring the customer back on-session to ask them for a new payment method
|
||
return {
|
||
"error": err.code,
|
||
"payment_method": err.payment_method.id,
|
||
"publishable_key": get_publishable_key(),
|
||
"client_secret": err.payment_intent.client_secret,
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_payment_methods():
|
||
team = get_current_team()
|
||
return jingrow.get_pg("Team", team).get_payment_methods()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def set_as_default(name):
|
||
payment_method = jingrow.get_pg("Stripe Payment Method", {"name": name, "team": get_current_team()})
|
||
payment_method.set_default()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def remove_payment_method(name):
|
||
team = get_current_team()
|
||
payment_method_count = jingrow.db.count("Stripe Payment Method", {"team": team})
|
||
|
||
if has_unsettled_invoices(team) and payment_method_count == 1:
|
||
return "Unpaid Invoices"
|
||
|
||
payment_method = jingrow.get_pg("Stripe Payment Method", {"name": name, "team": team})
|
||
payment_method.delete()
|
||
return None
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def finalize_invoices():
|
||
unsettled_invoices = jingrow.get_all(
|
||
"Invoice",
|
||
{"team": get_current_team(), "status": ("in", ("Draft", "Unpaid"))},
|
||
pluck="name",
|
||
)
|
||
|
||
for inv in unsettled_invoices:
|
||
inv_pg = jingrow.get_pg("Invoice", inv)
|
||
inv_pg.finalize_invoice()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def unpaid_invoices():
|
||
team = get_current_team()
|
||
return jingrow.db.get_all(
|
||
"Invoice",
|
||
{
|
||
"team": team,
|
||
"status": ("in", ["Draft", "Unpaid", "Invoice Created"]),
|
||
"type": "Subscription",
|
||
},
|
||
["name", "status", "period_end", "currency", "amount_due", "total"],
|
||
order_by="creation asc",
|
||
)
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_unpaid_invoices():
|
||
team = get_current_team()
|
||
unpaid_invoices = jingrow.db.get_all(
|
||
"Invoice",
|
||
{
|
||
"team": team,
|
||
"status": "Unpaid",
|
||
"type": "Subscription",
|
||
},
|
||
["name", "status", "period_end", "currency", "amount_due", "total"],
|
||
order_by="creation asc",
|
||
)
|
||
|
||
if len(unpaid_invoices) == 1:
|
||
return jingrow.get_pg("Invoice", unpaid_invoices[0].name)
|
||
return unpaid_invoices
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def change_payment_mode(mode):
|
||
team = get_current_team(get_pg=True)
|
||
|
||
team.payment_mode = mode
|
||
if team.partner_email and mode == "Paid By Partner" and not team.billing_team:
|
||
team.billing_team = jingrow.db.get_value(
|
||
"Team",
|
||
{"enabled": 1, "jerp_partner": 1, "partner_email": team.partner_email},
|
||
"name",
|
||
)
|
||
if team.billing_team and mode != "Paid By Partner":
|
||
team.billing_team = ""
|
||
team.save()
|
||
return None
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def prepaid_credits_via_onboarding():
|
||
"""When prepaid credits are bought, the balance is not immediately reflected.
|
||
This method will check balance every second and then set payment_mode"""
|
||
from time import sleep
|
||
|
||
team = get_current_team(get_pg=True)
|
||
|
||
seconds = 0
|
||
# block until balance is updated
|
||
while team.get_balance() == 0 or seconds > 20:
|
||
seconds += 1
|
||
sleep(1)
|
||
jingrow.db.rollback()
|
||
|
||
team.payment_mode = "Prepaid Credits"
|
||
team.save()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_invoice_usage(invoice):
|
||
team = get_current_team()
|
||
# apply team filter for safety
|
||
pg = jingrow.get_pg("Invoice", {"name": invoice, "team": team})
|
||
out = pg.as_dict()
|
||
# a dict with formatted currency values for display
|
||
out.formatted = make_formatted_pg(pg)
|
||
out.invoice_pdf = pg.invoice_pdf or (pg.currency == "USD" and pg.get_pdf())
|
||
return out
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_summary():
|
||
team = get_current_team()
|
||
invoices = jingrow.get_all(
|
||
"Invoice",
|
||
filters={"team": team, "status": ("in", ["Paid", "Unpaid"])},
|
||
fields=[
|
||
"name",
|
||
"status",
|
||
"period_end",
|
||
"payment_mode",
|
||
"type",
|
||
"currency",
|
||
"amount_paid",
|
||
],
|
||
order_by="creation desc",
|
||
)
|
||
|
||
invoice_names = [x.name for x in invoices]
|
||
grouped_invoice_items = get_grouped_invoice_items(invoice_names)
|
||
|
||
for invoice in invoices:
|
||
invoice.items = grouped_invoice_items.get(invoice.name, [])
|
||
|
||
return invoices
|
||
|
||
|
||
def get_grouped_invoice_items(invoices: list[str]) -> dict:
|
||
"""Takes a list of invoices (invoice names) and returns a dict of the form:
|
||
{ "<invoice_name1>": [<invoice_items>], "<invoice_name2>": [<invoice_items>], }
|
||
"""
|
||
invoice_items = jingrow.get_all(
|
||
"Invoice Item",
|
||
filters={"parent": ("in", invoices)},
|
||
fields=[
|
||
"amount",
|
||
"document_name AS name",
|
||
"document_type AS type",
|
||
"parent",
|
||
"quantity",
|
||
"rate",
|
||
"plan",
|
||
],
|
||
)
|
||
|
||
grouped_items = groupby(invoice_items, key=lambda x: x["parent"])
|
||
invoice_items_map = {}
|
||
for invoice_name, items in grouped_items:
|
||
invoice_items_map[invoice_name] = list(items)
|
||
|
||
return invoice_items_map
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def after_card_add():
|
||
clear_setup_intent()
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def setup_intent_success(setup_intent, address=None):
|
||
setup_intent = jingrow._dict(setup_intent)
|
||
|
||
# refetching the setup intent to get mandate_id from stripe
|
||
stripe = get_stripe()
|
||
setup_intent = stripe.SetupIntent.retrieve(setup_intent.id)
|
||
|
||
team = get_current_team(True)
|
||
clear_setup_intent()
|
||
mandate_reference = setup_intent.payment_method_options.card.mandate_options.reference
|
||
payment_method = team.create_payment_method(
|
||
setup_intent.payment_method,
|
||
setup_intent.id,
|
||
setup_intent.mandate,
|
||
mandate_reference,
|
||
set_default=True,
|
||
)
|
||
if address:
|
||
address = jingrow._dict(address)
|
||
team.update_billing_details(address)
|
||
|
||
return {"payment_method_name": payment_method.name}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def validate_gst(address, method=None):
|
||
# 保留函数以维持代码结构
|
||
return
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def get_latest_unpaid_invoice():
|
||
team = get_current_team()
|
||
unpaid_invoices = jingrow.get_all(
|
||
"Invoice",
|
||
{"team": team, "status": "Unpaid", "payment_attempt_count": (">", 0)},
|
||
pluck="name",
|
||
order_by="creation desc",
|
||
limit=1,
|
||
)
|
||
|
||
if unpaid_invoices:
|
||
unpaid_invoice = jingrow.db.get_value(
|
||
"Invoice",
|
||
unpaid_invoices[0],
|
||
["amount_due", "payment_mode", "amount_due", "currency"],
|
||
as_dict=True,
|
||
)
|
||
if unpaid_invoice.payment_mode == "Prepaid Credits" and team_has_balance_for_invoice(unpaid_invoice):
|
||
return None
|
||
|
||
return unpaid_invoice
|
||
return None
|
||
|
||
|
||
def team_has_balance_for_invoice(prepaid_mode_invoice):
|
||
team = get_current_team(get_pg=True)
|
||
return team.get_balance() >= prepaid_mode_invoice.amount_due
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_razorpay_order(amount, type=None):
|
||
client = get_razorpay_client()
|
||
team = get_current_team(get_pg=True)
|
||
|
||
if team.currency == "CNY":
|
||
gst_amount = amount * jingrow.db.get_single_value("Jcloud Settings", "gst_percentage")
|
||
amount += gst_amount
|
||
|
||
amount = round(amount, 2)
|
||
data = {
|
||
"amount": int(amount * 100),
|
||
"currency": team.currency,
|
||
"notes": {
|
||
"Description": "Order for Jingrow Prepaid Credits",
|
||
"Team (Jingrow ID)": team.name,
|
||
"gst": gst_amount if team.currency == "CNY" else 0,
|
||
},
|
||
}
|
||
if type and type == "Partnership Fee":
|
||
data.get("notes").update({"Type": type})
|
||
order = client.order.create(data=data)
|
||
|
||
payment_record = jingrow.get_pg(
|
||
{"pagetype": "Razorpay Payment Record", "order_id": order.get("id"), "team": team.name, "type": type}
|
||
).insert(ignore_permissions=True)
|
||
|
||
return {
|
||
"order_id": order.get("id"),
|
||
"key_id": client.auth[0],
|
||
"payment_record": payment_record.name,
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def handle_razorpay_payment_success(response):
|
||
client = get_razorpay_client()
|
||
client.utility.verify_payment_signature(response)
|
||
|
||
payment_record = jingrow.get_pg(
|
||
"Razorpay Payment Record",
|
||
{"order_id": response.get("razorpay_order_id")},
|
||
for_update=True,
|
||
)
|
||
payment_record.update(
|
||
{
|
||
"payment_id": response.get("razorpay_payment_id"),
|
||
"signature": response.get("razorpay_signature"),
|
||
"status": "Captured",
|
||
}
|
||
)
|
||
payment_record.save(ignore_permissions=True)
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def handle_razorpay_payment_failed(response):
|
||
payment_record = jingrow.get_pg(
|
||
"Razorpay Payment Record",
|
||
{"order_id": response["error"]["metadata"].get("order_id")},
|
||
for_update=True,
|
||
)
|
||
|
||
payment_record.status = "Failed"
|
||
payment_record.failure_reason = response["error"]["description"]
|
||
payment_record.save(ignore_permissions=True)
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def total_unpaid_amount():
|
||
team = get_current_team(get_pg=True)
|
||
balance = team.get_balance()
|
||
negative_balance = -1 * balance if balance < 0 else 0
|
||
|
||
return (
|
||
jingrow.get_all(
|
||
"Invoice",
|
||
{"status": "Unpaid", "team": team.name, "type": "Subscription", "pagestatus": ("!=", 2)},
|
||
["sum(amount_due) as total"],
|
||
pluck="total",
|
||
)[0]
|
||
or 0
|
||
) + negative_balance
|
||
|
||
|
||
# Mpesa integrations, mpesa exjcloud
|
||
"""Send stk push to the user"""
|
||
|
||
|
||
def generate_stk_push(**kwargs):
|
||
"""Generate stk push by making a API call to the stk push API."""
|
||
args = jingrow._dict(kwargs)
|
||
partner_value = args.partner
|
||
|
||
# Fetch the team document based on the extracted partner value
|
||
partner = jingrow.get_all("Team", filters={"user": partner_value, "jerp_partner": 1}, pluck="name")
|
||
if not partner:
|
||
jingrow.throw(_(f"Partner team {partner_value} not found"), title=_("Mpesa Exjcloud Error"))
|
||
|
||
# Get Mpesa settings for the partner's team
|
||
mpesa_setup = get_mpesa_setup_for_team(partner[0])
|
||
try:
|
||
callback_url = (
|
||
get_request_site_address(True) + "/api/action/jcloud.api.billing.verify_m_pesa_transaction"
|
||
)
|
||
env = "production" if not mpesa_setup.sandbox else "sandbox"
|
||
# for sandbox, business shortcode is same as till number
|
||
business_shortcode = (
|
||
mpesa_setup.business_shortcode if env == "production" else mpesa_setup.till_number
|
||
)
|
||
connector = MpesaConnector(
|
||
env=env,
|
||
app_key=mpesa_setup.consumer_key,
|
||
app_secret=mpesa_setup.get_password("consumer_secret"),
|
||
)
|
||
|
||
mobile_number = sanitize_mobile_number(args.sender)
|
||
response = connector.stk_push(
|
||
business_shortcode=business_shortcode,
|
||
amount=args.amount_with_tax,
|
||
passcode=mpesa_setup.get_password("pass_key"),
|
||
callback_url=callback_url,
|
||
reference_code=mpesa_setup.till_number,
|
||
phone_number=mobile_number,
|
||
description="Jingrow Payment",
|
||
)
|
||
return response # noqa: RET504
|
||
except Exception as e:
|
||
jingrow.log_error(f"Mpesa Exjcloud Transaction Error")
|
||
jingrow.throw(
|
||
_("Issue detected with Mpesa configuration, check the error logs for more details"),
|
||
title=_("Mpesa Exjcloud Error"),
|
||
)
|
||
|
||
|
||
@jingrow.whitelist(allow_guest=True)
|
||
def verify_m_pesa_transaction(**kwargs):
|
||
"""Verify the transaction result received via callback from STK."""
|
||
transaction_response, request_id = parse_transaction_response(kwargs)
|
||
status = handle_transaction_result(transaction_response, request_id)
|
||
|
||
return {"status": status, "ResultDesc": transaction_response.get("ResultDesc")}
|
||
|
||
|
||
def parse_transaction_response(kwargs):
|
||
"""Parse and validate the transaction response."""
|
||
|
||
if "Body" not in kwargs or "stkCallback" not in kwargs["Body"]:
|
||
jingrow.log_error(title="Invalid transaction response format", message=kwargs)
|
||
jingrow.throw(_("Invalid transaction response format"))
|
||
|
||
transaction_response = jingrow._dict(kwargs["Body"]["stkCallback"])
|
||
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||
if not isinstance(checkout_id, str):
|
||
jingrow.throw(_("Invalid Checkout Request ID"))
|
||
|
||
return transaction_response, checkout_id
|
||
|
||
|
||
def handle_transaction_result(transaction_response, integration_request):
|
||
"""Handle the logic based on ResultCode in the transaction response."""
|
||
|
||
result_code = transaction_response.get("ResultCode")
|
||
status = None
|
||
|
||
if result_code == 0:
|
||
try:
|
||
status = "Completed"
|
||
create_mpesa_request_log(
|
||
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
|
||
)
|
||
|
||
create_mpesa_payment_record(transaction_response)
|
||
except Exception as e:
|
||
status = "Failed"
|
||
create_mpesa_request_log(
|
||
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
|
||
)
|
||
|
||
elif result_code == 1037: # User unreachable (Phone off or timeout)
|
||
status = "Failed"
|
||
create_mpesa_request_log(
|
||
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
|
||
)
|
||
|
||
elif result_code == 1032: # User cancelled the request
|
||
status = "Cancelled"
|
||
create_mpesa_request_log(
|
||
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
|
||
)
|
||
|
||
else: # Other failure codes
|
||
status = "Failed"
|
||
create_mpesa_request_log(
|
||
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
|
||
)
|
||
return status
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def request_for_payment(**kwargs):
|
||
"""request for payments"""
|
||
team = get_current_team()
|
||
|
||
kwargs.setdefault("team", team)
|
||
args = jingrow._dict(kwargs)
|
||
update_tax_id_or_phone_no(team, args.tax_id, args.phone_number)
|
||
|
||
amount = args.request_amount
|
||
args.request_amount = jingrow.utils.rounded(amount, 2)
|
||
response = jingrow._dict(generate_stk_push(**args))
|
||
handle_api_mpesa_response("CheckoutRequestID", args, response)
|
||
|
||
return response
|
||
|
||
|
||
def handle_api_mpesa_response(global_id, request_dict, response):
|
||
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
|
||
# check error response
|
||
if response.requestId:
|
||
req_name = response.requestId
|
||
error = response
|
||
else:
|
||
# global checkout id used as request name
|
||
req_name = getattr(response, global_id)
|
||
error = None
|
||
|
||
create_mpesa_request_log(request_dict, "Host", "Mpesa Exjcloud", req_name, error, output=response)
|
||
|
||
if error:
|
||
jingrow.throw(_(response.errorMessage), title=_("Transaction Error"))
|
||
|
||
|
||
def create_mpesa_payment_record(transaction_response):
|
||
"""Create a new entry in the Mpesa Payment Record for a successful transaction."""
|
||
item_response = transaction_response.get("CallbackMetadata", {}).get("Item", [])
|
||
mpesa_receipt_number = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
||
transaction_time = fetch_param_value(item_response, "TransactionDate", "Name")
|
||
phone_number = fetch_param_value(item_response, "PhoneNumber", "Name")
|
||
transaction_id = transaction_response.get("CheckoutRequestID")
|
||
amount = fetch_param_value(item_response, "Amount", "Name")
|
||
merchant_request_id = transaction_response.get("MerchantRequestID")
|
||
info = get_details_from_request_log(transaction_id)
|
||
gateway_name = get_payment_gateway(info.partner)
|
||
# Create a new entry in M-Pesa Payment Record
|
||
data = {
|
||
"transaction_id": transaction_id,
|
||
"amount": amount,
|
||
"team": jingrow.get_value("Team", info.team, "user"),
|
||
"default_currency": "KES",
|
||
"rate": info.requested_amount,
|
||
}
|
||
mpesa_invoice, invoice_name = create_invoice_partner_site(data, gateway_name)
|
||
payment_record = jingrow.get_pg(
|
||
{
|
||
"pagetype": "Mpesa Payment Record",
|
||
"transaction_id": transaction_id,
|
||
"transaction_time": parse_datetime(transaction_time),
|
||
"transaction_type": "Mpesa Exjcloud",
|
||
"team": info.team,
|
||
"phone_number": str(phone_number),
|
||
"amount": info.requested_amount,
|
||
"grand_total": amount,
|
||
"merchant_request_id": merchant_request_id,
|
||
"payment_partner": info.partner,
|
||
"amount_usd": info.amount_usd,
|
||
"exchange_rate": info.exchange_rate,
|
||
"local_invoice": mpesa_invoice,
|
||
"mpesa_receipt_number": mpesa_receipt_number,
|
||
}
|
||
)
|
||
payment_record.insert(ignore_permissions=True)
|
||
payment_record.submit()
|
||
"""create payment partner transaction which will then create balance transaction"""
|
||
create_payment_partner_transaction(
|
||
info.team, info.partner, info.exchange_rate, info.amount_usd, info.requested_amount, gateway_name
|
||
)
|
||
mpesa_details = {
|
||
"mpesa_receipt_number": mpesa_receipt_number,
|
||
"mpesa_merchant_id": merchant_request_id,
|
||
"mpesa_payment_record": payment_record.name,
|
||
"mpesa_request_id": transaction_id,
|
||
"mpesa_invoice": invoice_name,
|
||
}
|
||
create_balance_transaction_and_invoice(info.team, info.amount_usd, mpesa_details)
|
||
|
||
jingrow.msgprint(_("Mpesa Payment Record entry created successfully"))
|
||
|
||
|
||
def create_balance_transaction_and_invoice(team, amount, mpesa_details):
|
||
balance_transaction = jingrow.get_pg(
|
||
pagetype="Balance Transaction",
|
||
team=team,
|
||
source="Prepaid Credits",
|
||
type="Adjustment",
|
||
amount=amount,
|
||
description=mpesa_details.get("mpesa_payment_record"),
|
||
paid_via_local_pg=1,
|
||
)
|
||
balance_transaction.insert(ignore_permissions=True)
|
||
balance_transaction.submit()
|
||
|
||
invoice = jingrow.get_pg(
|
||
pagetype="Invoice",
|
||
team=team,
|
||
type="Prepaid Credits",
|
||
status="Paid",
|
||
total=amount,
|
||
amount_due=amount,
|
||
amount_paid=amount,
|
||
amount_due_with_tax=amount,
|
||
due_date=jingrow.utils.nowdate(),
|
||
mpesa_merchant_id=mpesa_details.get("mpesa_merchant_id", ""),
|
||
mpesa_receipt_number=mpesa_details.get("mpesa_receipt_number", ""),
|
||
mpesa_request_id=mpesa_details.get("mpesa_request_id", ""),
|
||
mpesa_payment_record=mpesa_details.get("mpesa_payment_record", ""),
|
||
mpesa_invoice=mpesa_details.get("mpesa_invoice", ""),
|
||
)
|
||
invoice.append(
|
||
"items",
|
||
{
|
||
"description": "Prepaid Credits",
|
||
"document_type": "Balance Transaction",
|
||
"document_name": balance_transaction.name,
|
||
"quantity": 1,
|
||
"rate": amount,
|
||
},
|
||
)
|
||
invoice.insert(ignore_permissions=True)
|
||
invoice.submit()
|
||
|
||
|
||
def parse_datetime(date):
|
||
from datetime import datetime
|
||
|
||
return datetime.strptime(str(date), "%Y%m%d%H%M%S")
|
||
|
||
@jingrow.whitelist(allow_guest=True)
|
||
def handle_alipay_notification():
|
||
"""处理支付宝支付通知回调"""
|
||
try:
|
||
# 获取支付宝通知数据
|
||
alipay_data = jingrow.request.form.to_dict()
|
||
|
||
# 获取必要参数
|
||
order_id = alipay_data.get("out_trade_no")
|
||
trade_no = alipay_data.get("trade_no")
|
||
trade_status = alipay_data.get("trade_status")
|
||
team_name = urllib.parse.unquote(alipay_data.get("passback_params", ""))
|
||
|
||
# 检查订单号是否存在
|
||
if not order_id:
|
||
jingrow.log_error("订单号为空", "支付宝错误")
|
||
return "fail"
|
||
|
||
# 查找支付记录
|
||
payment_record = jingrow.db.get_value(
|
||
"Order",
|
||
{"order_id": order_id},
|
||
["name", "total_amount", "status", "team"],
|
||
as_dict=True
|
||
)
|
||
|
||
if not payment_record:
|
||
jingrow.log_error(f"未找到支付记录: {order_id}", "支付宝错误")
|
||
return "fail"
|
||
|
||
# 检查记录状态,避免重复处理
|
||
if payment_record.status == "交易成功":
|
||
return "success"
|
||
|
||
# 只有交易成功时才更新状态
|
||
if trade_status == "TRADE_SUCCESS":
|
||
# 更新订单记录状态
|
||
jingrow.db.set_value(
|
||
"Order",
|
||
payment_record.name,
|
||
{
|
||
"status": "已支付",
|
||
"trade_no": trade_no,
|
||
"payment_method": "支付宝"
|
||
}
|
||
)
|
||
|
||
# 执行支付完成后的业务逻辑
|
||
handle_order_payment_complete(order_id)
|
||
|
||
# 立即提交数据库事务
|
||
jingrow.db.commit()
|
||
|
||
return "success"
|
||
|
||
return "success"
|
||
except Exception as e:
|
||
jingrow.log_error("支付宝错误", f"处理失败: {str(e)}")
|
||
return "fail"
|
||
|
||
@jingrow.whitelist(allow_guest=True)
|
||
def handle_wechatpay_notification():
|
||
"""处理微信支付通知回调"""
|
||
try:
|
||
# 获取请求数据
|
||
headers = jingrow.local.request.headers
|
||
body = jingrow.request.get_data()
|
||
|
||
# 初始化微信支付API
|
||
wechat_pay = WeChatPayAPI()
|
||
|
||
# 使用SDK的方法解密回调数据
|
||
try:
|
||
# 调用SDK提供的decrypt_callback方法解密数据
|
||
decrypted_data = wechat_pay.wxpay.decrypt_callback(headers, body)
|
||
|
||
# 如果返回值是字符串则解析为JSON
|
||
if isinstance(decrypted_data, str):
|
||
decrypted_data = json.loads(decrypted_data)
|
||
|
||
# 获取关键字段
|
||
order_id = decrypted_data.get("out_trade_no")
|
||
trade_no = decrypted_data.get("transaction_id")
|
||
trade_state = decrypted_data.get("trade_state")
|
||
total_amount = decrypted_data.get("amount", {}).get("total", 0) / 100 # 转换为元
|
||
team_name = decrypted_data.get("attach", "")
|
||
|
||
# 确认交易状态为成功
|
||
if trade_state != "SUCCESS":
|
||
jingrow.log_error(f"微信支付交易状态不成功: {trade_state}", "微信支付通知")
|
||
return "SUCCESS" # 返回成功以避免微信重复通知
|
||
|
||
# 查询支付记录
|
||
payment_record = jingrow.db.get_value(
|
||
"Order",
|
||
{"order_id": order_id},
|
||
["name", "total_amount", "status", "team"],
|
||
as_dict=True
|
||
)
|
||
|
||
if not payment_record:
|
||
jingrow.log_error(f"未找到支付记录: {order_id}", "微信支付错误")
|
||
return "SUCCESS"
|
||
|
||
# 检查记录状态,避免重复处理
|
||
if payment_record.status == "交易成功":
|
||
jingrow.log_error(f"订单已处理: {order_id}", "微信支付通知")
|
||
return "SUCCESS"
|
||
|
||
# 更新订单记录状态
|
||
jingrow.db.set_value(
|
||
"Order",
|
||
payment_record.name,
|
||
{
|
||
"status": "已支付",
|
||
"trade_no": trade_no,
|
||
"payment_method": "微信支付"
|
||
}
|
||
)
|
||
|
||
# 执行支付完成后的业务逻辑
|
||
handle_order_payment_complete(order_id)
|
||
|
||
# 立即提交数据库事务
|
||
jingrow.db.commit()
|
||
|
||
return "SUCCESS"
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("微信支付解密错误", f"处理微信支付通知数据失败: {str(e)}\n请求头: {headers}\n请求体: {body}")
|
||
return "SUCCESS" # 返回成功避免微信重复发送通知
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("微信支付错误", f"处理微信支付通知失败: {str(e)}")
|
||
return "SUCCESS" # 返回成功避免微信重复发送通知
|
||
|
||
|
||
def handle_order_payment_complete(order_id):
|
||
"""处理订单支付完成后的业务逻辑"""
|
||
try:
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
|
||
# 根据订单类型执行不同的业务逻辑
|
||
if order.order_type == "余额充值":
|
||
process_balance_recharge(order)
|
||
elif order.order_type == "网站续费":
|
||
process_site_renew(order_id)
|
||
elif order.order_type == "服务器续费":
|
||
# 异步续费服务器
|
||
jingrow.enqueue('jcloud.api.aliyun_server_light.renew_server', order_name=order.name)
|
||
elif order.order_type == "服务器升级":
|
||
# 异步升级服务器
|
||
jingrow.enqueue('jcloud.api.aliyun_server_light.upgrade_server', order_name=order.name)
|
||
elif order.order_type == "新建服务器":
|
||
# 异步创建服务器
|
||
jingrow.enqueue('jcloud.api.aliyun_server_light.create_aliyun_server', order_name=order.name)
|
||
elif order.order_type == "域名注册":
|
||
# 异步注册域名
|
||
jingrow.enqueue('jcloud.api.domain_west.register_domain_from_order', order_name=order.name)
|
||
elif order.order_type == "域名续费":
|
||
# 异步续费域名
|
||
jingrow.enqueue('jcloud.api.domain_west.renew_domain_from_order', order_name=order.name)
|
||
|
||
return True
|
||
except Exception as e:
|
||
jingrow.log_error("订单处理错误", f"处理订单 {order_id} 支付完成事件失败: {str(e)}")
|
||
return False
|
||
|
||
def process_balance_recharge(order):
|
||
"""处理余额充值业务逻辑"""
|
||
try:
|
||
balance_transaction = jingrow.get_pg({
|
||
"pagetype": "Balance Transaction",
|
||
"team": order.team,
|
||
"type": "Adjustment",
|
||
"source": "Prepaid Credits",
|
||
"amount": order.total_amount,
|
||
"description": f"{order.payment_method}充值 (订单: {order.order_id})",
|
||
})
|
||
|
||
balance_transaction.flags.ignore_permissions = True
|
||
balance_transaction.insert()
|
||
balance_transaction.submit()
|
||
|
||
# 更新订单状态为交易成功
|
||
jingrow.db.set_value("Order", order.name, "status", "交易成功")
|
||
jingrow.db.commit()
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("余额充值错误", f"余额充值失败: 团队 {order.team}, 金额 {order.total_amount}, 错误: {str(e)}")
|
||
raise
|
||
|
||
def process_site_renew(order_id):
|
||
"""处理网站续费,更新到期时间"""
|
||
# 获取订单文档
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
|
||
# 检查订单状态,避免重复处理
|
||
if order.status == "交易成功":
|
||
jingrow.log_error(
|
||
message=f"订单 {order_id} 已处理完成,跳过重复处理",
|
||
title="站点续费提示"
|
||
)
|
||
return {
|
||
"name": order.title,
|
||
"url": order.title,
|
||
"new_end_date": jingrow.db.get_value("Site", order.title, "site_end_date"),
|
||
"already_processed": True
|
||
}
|
||
|
||
# 从订单中提取信息
|
||
site_name = order.title # 网站URL保存在订单的title字段中
|
||
renewal_months = int(order.description) # 续费月数保存在订单的description字段中
|
||
|
||
# 获取站点文档
|
||
site = jingrow.get_pg("Site", site_name)
|
||
|
||
# 计算新的到期日期
|
||
current_end_date = getdate(site.site_end_date or jingrow.utils.today())
|
||
if current_end_date < getdate(jingrow.utils.today()):
|
||
current_end_date = getdate(jingrow.utils.today())
|
||
|
||
new_end_date = add_months(current_end_date, renewal_months)
|
||
|
||
# 更新站点到期日期
|
||
site.site_end_date = new_end_date
|
||
site.save(ignore_permissions=True)
|
||
|
||
# 续费后自动激活站点(如有需要)
|
||
if site.status in ["Inactive", "Suspended"]:
|
||
try:
|
||
site.activate()
|
||
except Exception as e:
|
||
jingrow.log_error("站点自动激活失败", f"站点 {site_name} 续费后自动激活失败: {str(e)}")
|
||
|
||
# 更新订单状态为交易成功,防止重复处理
|
||
jingrow.db.set_value("Order", order.name, "status", "交易成功")
|
||
|
||
return {
|
||
"name": site.name,
|
||
"url": site_name,
|
||
"new_end_date": new_end_date
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def check_payment_status(order_id, payment_type):
|
||
"""检查支付状态"""
|
||
if payment_type == "alipay":
|
||
pagetype = "Order"
|
||
elif payment_type == "wechatpay":
|
||
pagetype = "Order"
|
||
else:
|
||
jingrow.throw("不支持的支付类型")
|
||
|
||
team = get_current_team()
|
||
payment_record = jingrow.db.get_value(
|
||
pagetype,
|
||
{"order_id": order_id, "team": team},
|
||
["status", "total_amount"],
|
||
as_dict=True
|
||
)
|
||
|
||
if not payment_record:
|
||
return {"status": "not_found"}
|
||
|
||
return payment_record
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_alipay_order_for_recharge(amount):
|
||
"""创建支付宝订单用于购买预付费信用额度"""
|
||
team = get_current_team(True)
|
||
|
||
# 金额取整到两位小数
|
||
total_amount = round(float(amount), 2)
|
||
|
||
# 生成唯一订单号
|
||
order_id = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] + ''.join(random.choices('0123456789', k=6))
|
||
|
||
# 创建订单记录
|
||
payment_record = jingrow.get_pg({
|
||
"pagetype": "Order",
|
||
"order_id": order_id,
|
||
"order_type": "余额充值",
|
||
"team": team.name,
|
||
"total_amount": float(total_amount),
|
||
"status": "待支付",
|
||
"payment_method": "支付宝"
|
||
})
|
||
|
||
payment_record.insert(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
# 直接使用AlipayAPI类生成支付链接
|
||
api = AlipayAPI()
|
||
|
||
try:
|
||
# 生成支付链接,使用API类中已配置的默认URL
|
||
payment_url = api.generate_payment_url(
|
||
order_id=order_id,
|
||
amount=amount,
|
||
subject="Jingrow 余额充值",
|
||
team_name=team.name
|
||
)
|
||
|
||
return {
|
||
"payment_url": payment_url,
|
||
"order_id": order_id,
|
||
"payment_record": payment_record.name
|
||
}
|
||
except Exception as e:
|
||
jingrow.log_error("Order", f"创建支付宝订单失败: {str(e)}")
|
||
jingrow.throw(f"创建支付宝订单失败: {str(e)}")
|
||
|
||
|
||
def generate_qr_code(payment_url):
|
||
"""生成二维码图片的Base64字符串"""
|
||
qr = segno.make(payment_url)
|
||
buffer = io.BytesIO()
|
||
qr.save(buffer, kind='png', scale=6)
|
||
img_str = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||
return f"data:image/png;base64,{img_str}"
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_wechatpay_order_for_recharge(amount):
|
||
"""创建微信支付订单用于购买预付费信用额度"""
|
||
team = get_current_team(True)
|
||
total_amount = round(float(amount), 2)
|
||
|
||
# 生成唯一订单号
|
||
order_id = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] + ''.join(random.choices('0123456789', k=6))
|
||
|
||
# 创建订单记录
|
||
payment_record = jingrow.get_pg({
|
||
"pagetype": "Order",
|
||
"order_id": order_id,
|
||
"order_type": "余额充值",
|
||
"team": team.name,
|
||
"total_amount": float(total_amount),
|
||
"status": "待支付",
|
||
"payment_method": "微信支付"
|
||
})
|
||
|
||
payment_record.insert(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
# 使用WeChatPayAPI生成支付链接
|
||
wechat_pay = WeChatPayAPI()
|
||
|
||
try:
|
||
qr_code_url = wechat_pay.generate_payment_url(
|
||
order_id=order_id,
|
||
amount=amount,
|
||
subject="Jingrow 余额充值",
|
||
team_name=team.name
|
||
)
|
||
|
||
# 检查URL是否为空
|
||
if not qr_code_url:
|
||
jingrow.log_error("微信支付错误", "微信支付URL生成为空")
|
||
|
||
# 使用提供的函数生成二维码图片
|
||
qr_code_image = generate_qr_code(qr_code_url)
|
||
result = {
|
||
"qr_code_url": qr_code_url,
|
||
"qr_code_image": qr_code_image,
|
||
"order_id": order_id,
|
||
"payment_record": payment_record.name
|
||
}
|
||
return result
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
|
||
jingrow.throw(f"创建微信支付订单失败")
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_order(**kwargs):
|
||
"""创建站点订单"""
|
||
try:
|
||
# 从kwargs中获取参数,更灵活,且使用正确的字段名
|
||
title = kwargs.get('title')
|
||
description = kwargs.get('description')
|
||
total_amount = kwargs.get('total_amount')
|
||
order_type = kwargs.get('order_type')
|
||
|
||
# 参数验证
|
||
if not title or not description or not total_amount:
|
||
jingrow.throw("必须提供标题、描述和金额")
|
||
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 生成唯一订单号
|
||
order_id = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] + ''.join(random.choices('0123456789', k=6))
|
||
|
||
# 创建订单记录
|
||
order = jingrow.get_pg({
|
||
"pagetype": "Order",
|
||
"order_id": order_id,
|
||
"order_type": order_type,
|
||
"title": title,
|
||
"description": description,
|
||
"team": team.name,
|
||
"total_amount": float(total_amount),
|
||
"status": "待支付",
|
||
})
|
||
|
||
order.insert(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
return {
|
||
"success": True,
|
||
"order": order.as_dict()
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("订单错误", f"创建站点订单失败: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": f"创建订单失败: {str(e)}"
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def create_renewal_order(site, renewal_months=1):
|
||
"""创建网站续费订单"""
|
||
try:
|
||
# 验证输入
|
||
site_pg = jingrow.get_pg("Site", site)
|
||
site_url = f"{site_pg.subdomain}.{site_pg.domain}"
|
||
team = site_pg.team
|
||
|
||
# 验证当前用户权限
|
||
current_team = get_current_team(True)
|
||
if current_team.name != team:
|
||
jingrow.throw("您没有权限为此站点创建续费订单")
|
||
|
||
# 获取当前计划 - 使用正确的字段名 plan 而非 current_plan
|
||
current_plan = jingrow.get_pg("Site Plan", site_pg.plan)
|
||
|
||
# 计算续费金额
|
||
renewal_months = int(renewal_months)
|
||
team_currency = jingrow.db.get_value("Team", team, "currency")
|
||
|
||
if renewal_months == 12:
|
||
# 年付9折
|
||
amount = round(current_plan.price_cny * 12 * 0.9) if team_currency == "CNY" else round(current_plan.price_usd * 12 * 0.9)
|
||
else:
|
||
amount = current_plan.price_cny * renewal_months if team_currency == "CNY" else current_plan.price_usd * renewal_months
|
||
|
||
# 生成唯一订单号
|
||
order_id = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] + ''.join(random.choices('0123456789', k=6))
|
||
|
||
# 创建订单记录
|
||
order = jingrow.get_pg({
|
||
"pagetype": "Order",
|
||
"order_id": order_id,
|
||
"order_type": "网站续费",
|
||
"team": team,
|
||
"status": "待支付",
|
||
"total_amount": amount,
|
||
"title": site_url,
|
||
"description": str(renewal_months) # 简单存储续费月数
|
||
})
|
||
|
||
order.insert(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
return {
|
||
"success": True,
|
||
"order": order.as_dict()
|
||
}
|
||
except Exception as e:
|
||
jingrow.log_error("续费订单错误", f"创建续费订单失败: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": f"创建续费订单失败: {str(e)}"
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def process_balance_payment_for_order(order_id):
|
||
"""使用账户余额支付订单"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您没有权限支付此订单")
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
return {
|
||
"success": False,
|
||
"message": "该订单已支付或已取消"
|
||
}
|
||
|
||
# 使用 Team 类的 get_balance 方法获取余额
|
||
balance = team.get_balance()
|
||
|
||
# 检查余额是否足够
|
||
if balance < order.total_amount:
|
||
return {
|
||
"success": False,
|
||
"message": "余额不足"
|
||
}
|
||
|
||
# 创建余额交易记录(扣款)
|
||
balance_transaction = jingrow.get_pg({
|
||
"pagetype": "Balance Transaction",
|
||
"team": team.name,
|
||
"type": "Adjustment",
|
||
"source": "Prepaid Credits",
|
||
"amount": -1 * float(order.total_amount), # 使用负数表示扣减
|
||
"description": f"{order.order_type}-{order.title}",
|
||
"paid_via_local_pg": 1
|
||
})
|
||
balance_transaction.flags.ignore_permissions = True
|
||
balance_transaction.insert()
|
||
balance_transaction.submit()
|
||
|
||
# 更新订单状态
|
||
order.status = "已支付"
|
||
order.payment_method = "余额支付"
|
||
order.save(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
return {
|
||
"status": "Success",
|
||
"message": "支付成功",
|
||
"order": order.as_dict()
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("支付错误", f"余额支付失败: {str(e)}")
|
||
return {
|
||
"status": "Error",
|
||
"message": f"余额支付失败: {str(e)}"
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def process_balance_payment_for_renew_order(order_id):
|
||
"""使用账户余额支付网站续费订单"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(_(f"找不到订单: {order_id}"))
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw(_("您没有权限支付此订单"))
|
||
|
||
# 验证订单类型
|
||
if order.order_type != "网站续费":
|
||
jingrow.throw(_("此订单不是网站续费订单"))
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
return {
|
||
"success": False,
|
||
"message": _("该订单已支付或已取消")
|
||
}
|
||
|
||
# 使用 Team 类的 get_balance 方法获取余额
|
||
balance = team.get_balance()
|
||
|
||
# 检查余额是否足够
|
||
if balance < order.total_amount:
|
||
return {
|
||
"success": False,
|
||
"message": _("余额不足")
|
||
}
|
||
|
||
# 开始数据库事务
|
||
jingrow.db.begin()
|
||
|
||
try:
|
||
# 创建余额交易记录(扣款)
|
||
balance_transaction = jingrow.get_pg({
|
||
"pagetype": "Balance Transaction",
|
||
"team": team.name,
|
||
"type": "Adjustment",
|
||
"source": "Prepaid Credits",
|
||
"amount": -1 * float(order.total_amount), # 使用负数表示扣减
|
||
"description": f"网站续费: {order.title}",
|
||
"paid_via_local_pg": 1
|
||
})
|
||
balance_transaction.flags.ignore_permissions = True
|
||
balance_transaction.insert()
|
||
balance_transaction.submit()
|
||
|
||
# 更新订单状态
|
||
order.status = "已支付"
|
||
order.payment_method = "余额支付"
|
||
order.save(ignore_permissions=True)
|
||
|
||
# 提取网站URL和续费周期
|
||
site_name = order.title # 网站URL就是name字段
|
||
renewal_months = int(order.description)
|
||
|
||
# 调用网站续期处理函数
|
||
renew_result = process_site_renew(order_id)
|
||
|
||
# 提交数据库事务
|
||
jingrow.db.commit()
|
||
|
||
return {
|
||
"success": True,
|
||
"status": "Success",
|
||
"message": _("支付成功,网站已续费"),
|
||
"order": order.as_dict(),
|
||
"site": renew_result
|
||
}
|
||
|
||
except Exception as inner_error:
|
||
# 回滚事务
|
||
jingrow.db.rollback()
|
||
raise inner_error
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("续费支付错误", f"余额支付续费失败: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"status": "Error",
|
||
"message": _(f"余额支付失败: {str(e)}")
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def process_balance_payment_for_server_order(order_id):
|
||
"""使用账户余额支付订单"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您没有权限支付此订单")
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
return {
|
||
"success": False,
|
||
"message": "该订单已支付或已取消"
|
||
}
|
||
|
||
# 使用 Team 类的 get_balance 方法获取余额
|
||
balance = team.get_balance()
|
||
|
||
# 检查余额是否足够
|
||
if balance < order.total_amount:
|
||
return {
|
||
"success": False,
|
||
"message": "余额不足"
|
||
}
|
||
|
||
# 创建余额交易记录(扣款)
|
||
balance_transaction = jingrow.get_pg({
|
||
"pagetype": "Balance Transaction",
|
||
"team": team.name,
|
||
"type": "Adjustment",
|
||
"source": "Prepaid Credits",
|
||
"amount": -1 * float(order.total_amount), # 使用负数表示扣减
|
||
"description": f"{order.order_type}-{order.title}",
|
||
"paid_via_local_pg": 1
|
||
})
|
||
balance_transaction.flags.ignore_permissions = True
|
||
balance_transaction.insert()
|
||
balance_transaction.submit()
|
||
|
||
# 更新订单状态
|
||
order.status = "已支付"
|
||
order.payment_method = "余额支付"
|
||
order.save(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
# 支付成功后,调用订单完成处理函数
|
||
handle_order_payment_complete(order_id)
|
||
|
||
return {
|
||
"status": "Success",
|
||
"message": "支付成功",
|
||
"order": order.as_dict()
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("支付错误", f"余额支付失败: {str(e)}")
|
||
return {
|
||
"status": "Error",
|
||
"message": f"余额支付失败: {str(e)}"
|
||
}
|
||
|
||
|
||
@jingrow.whitelist()
|
||
def process_alipay_order(order_id):
|
||
"""创建支付宝订单支付链接"""
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您没有权限支付此订单")
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
jingrow.throw("该订单已支付或已取消")
|
||
|
||
# 金额取整到两位小数
|
||
amount = round(float(order.total_amount), 2)
|
||
|
||
# 直接使用AlipayAPI类生成支付链接
|
||
api = AlipayAPI()
|
||
|
||
try:
|
||
# 生成支付链接
|
||
payment_url = api.generate_payment_url(
|
||
order_id=order_id,
|
||
amount=amount,
|
||
subject=order.title or "Jingrow 站点订单",
|
||
team_name=team.name
|
||
)
|
||
|
||
# 更新订单支付方式
|
||
order.payment_method = "支付宝"
|
||
order.save(ignore_permissions=True)
|
||
|
||
return {
|
||
"payment_url": payment_url,
|
||
"order_id": order_id,
|
||
"success": True
|
||
}
|
||
except Exception as e:
|
||
jingrow.log_error("Order", f"创建支付宝订单失败: {str(e)}")
|
||
jingrow.throw(f"创建支付宝订单失败: {str(e)}")
|
||
|
||
@jingrow.whitelist()
|
||
def process_wechatpay_order(order_id):
|
||
"""创建微信支付订单"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您没有权限支付此订单")
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
jingrow.throw("该订单已支付或已取消")
|
||
|
||
# 获取金额
|
||
amount = order.total_amount
|
||
|
||
# 创建微信支付客户端
|
||
wechat_pay = WeChatPayAPI()
|
||
|
||
try:
|
||
# 生成支付URL
|
||
qr_code_url = wechat_pay.generate_payment_url(
|
||
order_id=order_id,
|
||
amount=amount,
|
||
subject=order.title or "Jingrow 站点订单",
|
||
team_name=team.name
|
||
)
|
||
|
||
# 检查URL是否为空
|
||
if not qr_code_url:
|
||
jingrow.log_error("微信支付错误", "微信支付URL生成为空")
|
||
jingrow.throw("生成支付URL失败")
|
||
|
||
# 生成二维码图片
|
||
qr_code_image = generate_qr_code(qr_code_url)
|
||
|
||
# 更新订单支付方式
|
||
order.payment_method = "微信支付"
|
||
order.save(ignore_permissions=True)
|
||
|
||
return {
|
||
"payment_url": qr_code_url,
|
||
"qr_code_image": qr_code_image,
|
||
"order_id": order_id,
|
||
"success": True
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
|
||
jingrow.throw(f"创建微信支付订单失败")
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
|
||
jingrow.throw(f"创建微信支付订单失败")
|
||
|
||
@jingrow.whitelist()
|
||
def check_order_payment_status(order_id):
|
||
"""检查订单支付状态"""
|
||
try:
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
return {
|
||
"success": True,
|
||
"status": order.status,
|
||
"order": order.as_dict()
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("订单错误", f"检查订单状态失败: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": f"检查订单状态失败: {str(e)}"
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def get_orders(page=1, page_size=20, search=None):
|
||
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 构建过滤条件
|
||
filters = {"team": team.name}
|
||
|
||
# 添加搜索条件
|
||
if search:
|
||
filters = [
|
||
["Order", "team", "=", team.name],
|
||
[
|
||
"Order",
|
||
"name|order_id|title|description",
|
||
"like",
|
||
f"%{search}%"
|
||
]
|
||
]
|
||
|
||
# 计算分页参数
|
||
page = int(page)
|
||
page_size = int(page_size)
|
||
start = (page - 1) * page_size
|
||
|
||
# 获取订单总数
|
||
total_count = jingrow.db.count("Order", filters=filters)
|
||
|
||
# 获取分页后的订单列表 - 根据需求修改字段列表
|
||
orders = jingrow.get_all(
|
||
"Order",
|
||
filters=filters,
|
||
fields=[
|
||
"title",
|
||
"order_id",
|
||
"trade_no",
|
||
"order_type",
|
||
"payment_method",
|
||
"description",
|
||
"total_amount as total",
|
||
"status",
|
||
"creation"
|
||
],
|
||
order_by="creation desc",
|
||
start=start,
|
||
limit=page_size
|
||
)
|
||
|
||
# 记录日志以便调试
|
||
jingrow.logger().info(f"获取订单成功: 团队={team.name}, 总数={total_count}")
|
||
|
||
return {
|
||
"orders": orders,
|
||
"total": total_count
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("订单列表错误", f"获取订单列表失败: {str(e)}")
|
||
return {
|
||
"orders": [],
|
||
"total": 0,
|
||
"error": str(e)
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def get_order_details(name):
|
||
"""
|
||
获取订单详情
|
||
|
||
参数:
|
||
name (str): 订单名称
|
||
|
||
返回:
|
||
dict: 包含订单详情
|
||
"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单详情
|
||
order = jingrow.get_pg("Order", name)
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您无权查看此订单")
|
||
|
||
# 构建返回数据
|
||
order_dict = order.as_dict()
|
||
|
||
return {
|
||
"order": order_dict
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("订单详情错误", f"获取订单详情失败: {str(e)}")
|
||
return {
|
||
"error": str(e)
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def get_balance_transactions(page=1, page_size=20, search=None):
|
||
"""
|
||
获取余额变动记录列表
|
||
|
||
参数:
|
||
page (int): 页码,默认为1
|
||
page_size (int): 每页记录数,默认为20
|
||
search (str): 搜索关键词,可选
|
||
|
||
返回:
|
||
dict: 包含transactions列表和total总数
|
||
"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 构建基础过滤条件
|
||
filters = {"team": team.name, "pagestatus": 1} # 已提交的文档
|
||
|
||
# 添加搜索条件(如果提供)
|
||
if search:
|
||
filters = [
|
||
["Balance Transaction", "team", "=", team.name],
|
||
["Balance Transaction", "pagestatus", "=", 1],
|
||
[
|
||
"Balance Transaction",
|
||
"description|source|type|invoice",
|
||
"like",
|
||
f"%{search}%"
|
||
]
|
||
]
|
||
|
||
# 计算分页参数
|
||
page = int(page)
|
||
page_size = int(page_size)
|
||
start = (page - 1) * page_size
|
||
|
||
# 获取总记录数
|
||
total_count = jingrow.db.count("Balance Transaction", filters=filters)
|
||
|
||
# 获取分页数据
|
||
transactions = jingrow.get_all(
|
||
"Balance Transaction",
|
||
filters=filters,
|
||
fields=[
|
||
"name",
|
||
"creation",
|
||
"description",
|
||
"amount",
|
||
"ending_balance",
|
||
"type",
|
||
"source",
|
||
"invoice"
|
||
],
|
||
order_by="creation desc",
|
||
start=start,
|
||
limit=page_size
|
||
)
|
||
|
||
return {
|
||
"transactions": transactions,
|
||
"total": total_count
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("余额记录错误", f"获取余额记录失败: {str(e)}")
|
||
return {
|
||
"transactions": [],
|
||
"total": 0,
|
||
"error": str(e)
|
||
}
|
||
|
||
@jingrow.whitelist()
|
||
def process_balance_payment_for_domain_order(order_id):
|
||
"""处理域名订单的余额支付"""
|
||
try:
|
||
# 获取当前用户团队
|
||
team = get_current_team(True)
|
||
|
||
# 获取订单信息
|
||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||
if not order:
|
||
jingrow.throw(f"找不到订单: {order_id}")
|
||
|
||
# 验证订单是否属于当前团队
|
||
if order.team != team.name:
|
||
jingrow.throw("您没有权限支付此订单")
|
||
|
||
# 检查订单状态
|
||
if order.status != "待支付":
|
||
return {
|
||
"success": False,
|
||
"message": "该订单已支付或已取消"
|
||
}
|
||
|
||
# 使用 Team 类的 get_balance 方法获取余额
|
||
balance = team.get_balance()
|
||
|
||
# 检查余额是否足够
|
||
if balance < order.total_amount:
|
||
return {
|
||
"success": False,
|
||
"message": "余额不足"
|
||
}
|
||
|
||
# 创建余额交易记录(扣款)
|
||
balance_transaction = jingrow.get_pg({
|
||
"pagetype": "Balance Transaction",
|
||
"team": team.name,
|
||
"type": "Adjustment",
|
||
"source": "Prepaid Credits",
|
||
"amount": -1 * float(order.total_amount), # 使用负数表示扣减
|
||
"description": f"{order.order_type}-{order.title}",
|
||
"paid_via_local_pg": 1
|
||
})
|
||
balance_transaction.flags.ignore_permissions = True
|
||
balance_transaction.insert()
|
||
balance_transaction.submit()
|
||
|
||
# 更新订单状态
|
||
order.status = "已支付"
|
||
order.payment_method = "余额支付"
|
||
order.save(ignore_permissions=True)
|
||
jingrow.db.commit()
|
||
|
||
# 支付成功,订单状态已更新
|
||
|
||
# 调用统一的订单支付完成处理函数
|
||
handle_order_payment_complete(order_id)
|
||
|
||
return {
|
||
"status": "Success",
|
||
"message": "支付成功",
|
||
"order": order.as_dict()
|
||
}
|
||
|
||
except Exception as e:
|
||
jingrow.log_error("支付错误", f"域名订单余额支付失败: {str(e)}")
|
||
return {
|
||
"status": "Error",
|
||
"message": f"余额支付失败: {str(e)}"
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|