362 lines
12 KiB
Python
362 lines
12 KiB
Python
from datetime import datetime, timedelta
|
|
from appPublic.qr import gen_qr_withlogo
|
|
from appPublic.uniqueID import getID
|
|
from appPublic.jsonConfig import getConfig
|
|
from appPublic.dictObject import DictObject
|
|
from sqlor.dbpools import DBPools
|
|
from ahserver.serverenv import ServerEnv
|
|
from ahserver.filestorage import FileStorage
|
|
|
|
|
|
async def discount_qrcode(request, params_kw):
|
|
"""
|
|
Generate a promotional QR code for discount
|
|
discount less then 1 and greate then 0
|
|
valid_term is digit + one of ["D", "M", "Y"]
|
|
expired_date is a date after it the promote qrcode invalidable
|
|
"""
|
|
discount = params_kw.discount
|
|
valid_term = params_kw.valid_term
|
|
expired_date = params_kw.expired_date
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
resellerid = await env.get_userorgid()
|
|
qr_id = getID()
|
|
url = env.entire_url('./promote') + f'?id={qr_id}'
|
|
fs = FileStorage()
|
|
p = fs._name2path(f'{getID()}.png')
|
|
gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc')
|
|
webp = fs.webpath(p)
|
|
async with db.sqlorContext(dbname) as sor:
|
|
biz_date = await env.get_business_date(sor)
|
|
if biz_date >= expired_date:
|
|
raise Exception('Promote QRCODE is out of time')
|
|
|
|
ret = {
|
|
'id': qr_id,
|
|
'resellerid': resellerid,
|
|
'discount': discount,
|
|
'valid_term': valid_term,
|
|
'expired_date': expired_date,
|
|
'qr_webpath': webp
|
|
}
|
|
await sor.C('discount_qr', ret.copy())
|
|
return DictObject(**ret)
|
|
return None
|
|
|
|
|
|
async def set_promote_discount(request, params_kw):
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
id = params_kw.id
|
|
customerid = await env.get_userorgid()
|
|
async with db.sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_qr', {'id': id})
|
|
if not recs:
|
|
raise Exception(f'promote id({id}) not exists')
|
|
biz_date = await env.get_business_date(sor)
|
|
if recs[0].expired_date <= biz_date:
|
|
raise Exception('Promote QRCODE is out of time')
|
|
cnt = int(recs[0].valid_term[:-1])
|
|
unit = recs[0].valid_term[-1]
|
|
enabled_date = biz_date
|
|
expired_date = ''
|
|
if unit == 'D':
|
|
expired_date = env.strdate_add(enabled_date, days=cnt)
|
|
elif unit == 'M':
|
|
expired_date = env.strdate_add(enabled_date, months=cnt)
|
|
elif unit == 'Y':
|
|
expired_date = env.strdate_add(enabled_date, years=cnt)
|
|
else:
|
|
raise Exception(f'Invalid valid_term({recs[0].valid_term})')
|
|
|
|
# Disable old active discount for this customer
|
|
await disable_old_discount(sor, recs[0].resellerid, customerid, biz_date)
|
|
|
|
# Create new discount record (no longer stores discount value directly)
|
|
discountid = getID()
|
|
ret = {
|
|
'id': discountid,
|
|
'name': f'促销折扣-{biz_date}',
|
|
'resellerid': recs[0].resellerid,
|
|
'customerid': customerid,
|
|
'enabled_date': enabled_date,
|
|
'expired_date': expired_date
|
|
}
|
|
await sor.C('discount', ret.copy())
|
|
|
|
# Create discount_detail record with the discount value from QR code
|
|
# prodtypeid=None, productid=None means applies to all products
|
|
detail_ret = {
|
|
'id': getID(),
|
|
'discountid': discountid,
|
|
'resellerid': recs[0].resellerid,
|
|
'prodtypeid': None,
|
|
'productid': None,
|
|
'discount': recs[0].discount,
|
|
}
|
|
await sor.C('discount_detail', detail_ret.copy())
|
|
|
|
return recs[0].discount
|
|
return None
|
|
|
|
|
|
async def disable_old_discount(sor, resellerid, customerid, biz_date):
|
|
"""Disable any active discount record for the given reseller+customer pair."""
|
|
# Use sor.R with sort instead of raw SQL + FOR UPDATE (DB-agnostic)
|
|
recs = await sor.R('discount', {
|
|
'resellerid': resellerid,
|
|
'customerid': customerid,
|
|
'sort': 'enabled_date desc'
|
|
})
|
|
if not recs:
|
|
return
|
|
# Find the active one in Python (DB-agnostic date comparison)
|
|
for rec in recs:
|
|
if rec.get('enabled_date', '') <= biz_date and rec.get('expired_date', '') > biz_date:
|
|
await sor.U('discount', {'id': rec['id'], 'expired_date': biz_date})
|
|
return
|
|
|
|
|
|
async def _discount_detail_exists(sor):
|
|
"""Check if discount_detail table exists."""
|
|
try:
|
|
await sor.sqlExe("SELECT 1 FROM discount_detail LIMIT 0", {})
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def sor_get_star_discount(sor, resellerid, biz_date):
|
|
"""Get default discount for a reseller (no specific customer)."""
|
|
sql = """select d.id, d.discount from discount d
|
|
where d.resellerid = ${resellerid}$
|
|
and d.customerid is NULL
|
|
and d.enabled_date <= ${biz_date}$
|
|
and d.expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return 1
|
|
|
|
discountid = recs[0].id
|
|
# If discount_detail table exists, look up default detail
|
|
if await _discount_detail_exists(sor):
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs2 = await sor.sqlExe(sql2, {'discountid': discountid})
|
|
if not recs2:
|
|
return 1
|
|
return recs2[0].discount
|
|
# Fallback: read discount value directly from discount table
|
|
return recs[0].discount if recs[0].discount is not None else 1
|
|
|
|
|
|
async def sor_get_customer_discount(sor, resellerid, customerid):
|
|
"""Get discount record for a customer (legacy, returns record not value).
|
|
Use sor_get_product_discount for product-specific discount."""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
sql = """select * from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid = ${customerid}$
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"customerid": customerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return await sor_get_star_discount(sor, resellerid, biz_date)
|
|
|
|
# Return default discount for this customer (prodtypeid=None, productid=None)
|
|
discountid = recs[0].id
|
|
if await _discount_detail_exists(sor):
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs2 = await sor.sqlExe(sql2, {'discountid': discountid})
|
|
if not recs2:
|
|
return 1
|
|
return recs2[0].discount
|
|
# Fallback: read discount value directly from discount table
|
|
return recs[0].discount if recs[0].discount is not None else 1
|
|
|
|
|
|
async def sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid):
|
|
"""
|
|
Get product-specific discount.
|
|
Lookup priority:
|
|
1. Exact match: discountid -> (prodtypeid, productid)
|
|
2. Type-level match: discountid -> (prodtypeid, NULL)
|
|
3. Default match: discountid -> (NULL, NULL)
|
|
|
|
Returns discount value (float), default 1.0 (no discount).
|
|
"""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
|
|
# Step 1: Find active discount record for reseller+customer
|
|
sql = """select id from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid = ${customerid}$
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"customerid": customerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
# Try reseller-level default (customerid is NULL)
|
|
sql = """select id from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid is NULL
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return 1.0
|
|
|
|
discountid = recs[0].id
|
|
|
|
# If discount_detail table doesn't exist, fall back to discount.discount
|
|
if not await _discount_detail_exists(sor):
|
|
return recs[0].discount if recs[0].discount is not None else 1.0
|
|
|
|
# Step 2: Try exact product match
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid = ${prodtypeid}$
|
|
and productid = ${productid}$"""
|
|
recs2 = await sor.sqlExe(sql2, {
|
|
'discountid': discountid,
|
|
'prodtypeid': prodtypeid,
|
|
'productid': productid
|
|
})
|
|
if recs2:
|
|
return recs2[0].discount
|
|
|
|
# Step 3: Try product type level match (productid is NULL)
|
|
sql3 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid = ${prodtypeid}$
|
|
and productid is NULL"""
|
|
recs3 = await sor.sqlExe(sql3, {
|
|
'discountid': discountid,
|
|
'prodtypeid': prodtypeid,
|
|
})
|
|
if recs3:
|
|
return recs3[0].discount
|
|
|
|
# Step 4: Try default match (both NULL)
|
|
sql4 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs4 = await sor.sqlExe(sql4, {'discountid': discountid})
|
|
if recs4:
|
|
return recs4[0].discount
|
|
|
|
return 1.0
|
|
|
|
|
|
async def get_customer_discount(resellerid, customerid):
|
|
"""Legacy: get default discount for a customer (all products)."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_customer_discount(sor, resellerid, customerid)
|
|
return 1
|
|
|
|
|
|
async def get_product_discount(resellerid, customerid, prodtypeid, productid):
|
|
"""
|
|
Get product-specific discount.
|
|
Parameters:
|
|
resellerid: merchant ID
|
|
customerid: customer ID
|
|
prodtypeid: product type ID
|
|
productid: product ID
|
|
Returns: discount value (float), 1.0 means no discount.
|
|
"""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid)
|
|
return 1.0
|
|
|
|
|
|
async def get_discount_details(sor, discountid):
|
|
"""Get all product detail records for a given discount."""
|
|
sql = """select * from discount_detail
|
|
where discountid = ${discountid}$
|
|
order by prodtypeid, productid"""
|
|
recs = await sor.sqlExe(sql, {'discountid': discountid})
|
|
return recs
|
|
|
|
|
|
async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount):
|
|
"""Add a product-specific discount detail."""
|
|
if discount <= 0 or discount >= 1:
|
|
raise Exception(f'discount({discount}) invalid, must be between 0 and 1')
|
|
|
|
ret = {
|
|
'id': getID(),
|
|
'discountid': discountid,
|
|
'resellerid': resellerid,
|
|
'prodtypeid': prodtypeid if prodtypeid else None,
|
|
'productid': productid if productid else None,
|
|
'discount': discount,
|
|
}
|
|
await sor.C('discount_detail', ret.copy())
|
|
return ret
|
|
|
|
|
|
async def update_discount_detail(sor, detail_id, discount):
|
|
"""Update a discount detail record."""
|
|
if discount <= 0 or discount >= 1:
|
|
raise Exception(f'discount({discount}) invalid, must be between 0 and 1')
|
|
|
|
await sor.U('discount_detail', {'id': detail_id, 'discount': discount})
|
|
|
|
|
|
async def delete_discount_detail(sor, detail_id):
|
|
"""Delete a discount detail record."""
|
|
await sor.D('discount_detail', {'id': detail_id})
|
|
|
|
|
|
def load_discount():
|
|
env = ServerEnv()
|
|
env.get_customer_discount = get_customer_discount
|
|
env.sor_get_customer_discount = sor_get_customer_discount
|
|
env.get_product_discount = get_product_discount
|
|
env.sor_get_product_discount = sor_get_product_discount
|
|
env.discount_qrcode = discount_qrcode
|
|
env.set_promote_discount = set_promote_discount
|
|
env.get_discount_details = get_discount_details
|
|
env.add_discount_detail = add_discount_detail
|
|
env.update_discount_detail = update_discount_detail
|
|
env.delete_discount_detail = delete_discount_detail
|