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