# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging from collections import namedtuple, defaultdict from typing import Optional, TYPE_CHECKING from apilib.monetary import RMB, Percent from apilib.utils import flatten from apps.web.agent.models import Agent from apps.web.constant import PARTITION_ROLE, RechargeRecordVia from apps.web.core import PayAppType from apps.web.core.payment import PaymentGateway from apps.web.dealer.define import DealerConst, DEALER_INCOME_TYPE from apps.web.dealer.models import Dealer from apps.web.dealer.proxy import record_income_proxy from apps.web.device.models import GroupDict from apps.web.report.utils import CentralDataProcessor if TYPE_CHECKING: from apps.web.user.models import RechargeRecord from apps.web.dealer.proxy import DealerGroupStats from apps.web.dealer.models import DealerDict logger = logging.getLogger(__name__) class Ledger(object): """ 对用户扫码使用或者充值金额进行分账 """ def __init__(self, source, record, notify=True, ledgerTime=None): # type: (str, RechargeRecord, bool, Optional[datetime])->None self.journal_logger = logging.getLogger('ledger') self.source = source # 指的是用户的充值目的 self.record = record self.money = record.money self.group = record.group self.dealer = self.record.owner # type: Dealer self.agent = Agent.objects(id=self.dealer.agentId).get() # type: Agent self.notify = notify if ledgerTime: self.ledgerTime = ledgerTime elif self.record.finishedTime: self.ledgerTime = self.record.finishedTime else: self.ledgerTime = datetime.datetime.now() def __repr__(self): return '<%s source=%s record=%r>' % (self.__class__.__name__, self.source, self.record) def execute(self, journal=False, stats=False, check=False): # type: (bool, bool, bool)->None # 订单的分账永远只有经销商一个人 partition_map = self.record.partition_map if journal: self.journal_logger.info( 'ledger report: record = {}; source = {}; group = {}; ' 'money = {}; partition = {}; isLedgered = {}; time = {}'.format( str(self.record.id), self.source, self.record.group.groupId, self.money, partition_map, self.record.is_ledgered, self.ledgerTime ) ) if not self.record.is_success: logger.debug('{} is not success.'.format(repr(self.record))) return if self.record.is_ledgered: logger.debug('{} has been leger.'.format(repr(self.record))) return if self.record.via == RechargeRecordVia.RefundCash: if self.record.money > RMB(0): logger.debug('ledger money is more zero in refundCash. record = {}'.format(repr(self.record))) return else: if self.record.money < RMB(0): logger.debug('ledger money is less zero. record = {}'.format(repr(self.record))) return if not self.record.ledger_enable: logger.debug('status of {} is not ledger enable.'.format(repr(self.record))) return proxy = record_income_proxy(self.source, self.record, dateTime=self.ledgerTime) if stats: if not self.record.logicalCode: allowed = {'dealer': True, 'group': True, 'device': False} CentralDataProcessor(record=proxy, check=check, allowed=allowed).process() else: CentralDataProcessor(record=proxy, check=check).process() self.record_balance() self.record.set_ledgered() def __need_ledger(self, entity, share_money): if share_money == RMB(0): logger.debug('share money for {} is zero.'.format(str(entity))) return False if self.record.via == RechargeRecordVia.RefundCash: if share_money > RMB(0): logger.debug('refund share money for {} is bigger than zero({}).'.format(str(entity), str(share_money))) return False else: if share_money < RMB(0): logger.debug('refund share money for {} is less than zero({}).'.format(str(entity), str(share_money))) return False return True def record_balance(self): self.__record_dealer_balance() def __record_dealer_balance(self): from taskmanager.mediator import task_caller source_key = self.record.withdrawSourceKey source = DealerConst.MAP_USER_SOURCE_TO_DEALER_SOURCE[self.source] infoDict = self.record.to_json_dict() owner = self.record.owner share_money = self.record.money logger.info('[recordDealerIncome] dealer(id={}), amount={}, source={}'.format(owner.id, share_money, source)) if not self.__need_ledger('owner'.format(str(owner.id)), share_money): return logger.info('record owner(id=%s) balance, amount=%s' % (str(owner.id), share_money)) owner.record_income(source, source_key, share_money) infoDict.update({'ownerId': owner.id}) if self.notify and owner.newUserPaymentOrderPushSwitch: task_caller('report_new_payment_to_dealer_via_wechat', record = infoDict) ShareItem = namedtuple("ShareItem", ["role", "id", "share", "money"]) class IncomeStrategy(object): """ 用户每支付一次 都需要进行一笔分账 分账的基本顺序如下 首先 是 运营的平台部分收取 收取方式分为 固定或者百分比 对象是用户支付的总金额 然后 是 代理商的部分收取 收取方式为 固定或者百分比 对象是用户支付的总金额 这部分收取完之后 才会是经销商实际应该收的总金额 然后是经销商和合伙人进行分 先按百分比分合伙人的 最后的就是经销商实际应得的 """ def __init__(self, dealer, group, record, payment_gateway=None): assert record.via != RechargeRecordVia.Mix, u'混合订单不需要计算分账MAP' self._dealer = dealer # type: Dealer self._group = group # type: GroupDict self._record = record # type: RechargeRecord self._payment_gateway = payment_gateway # type: PaymentGateway def calc_account_split_map(self): # type:() -> dict raise NotImplementedError(u"尚未实现") @property def agent_profit_share(self): """ 代理商的分成比例分为两种 由订单本身的属性决定 一种是 普通的经营分成比例 一种是 经销商商户收款模式下的经营分成比例 """ if 'isBt' in self._record.attachParas and self._record.attachParas['isBt']: return self._dealer.agentProfitShare if not self._payment_gateway: self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway return self._dealer.agentProfitShare @property def manager_profit_share(self): """ 厂商的分成比例 目前仅允许资金池的分账 如果厂商配置了分账 直接抛出异常即可 """ if not self._payment_gateway: self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway return self._dealer.my_agent.managerProfitShare @staticmethod def check_partition_map(partitionMap): partitions = list(flatten(partitionMap.values())) sumShares = Percent(0) for _item in partitions: # 验算范围 if Percent(_item['share']) < Percent(0): raise ValueError('partition share must gte 0!') sumShares += Percent(_item["share"]) # 验算累加和 if sumShares != Percent(100): raise ValueError('sum of shares has to be 100') return partitionMap class AgentLedgerFirst(IncomeStrategy): """ 层级结构的分成比例 依次是 厂商 --- 代理商 --- 经销商(合伙人) 每层分账的比例 为上一层分成比例的剩下值 举例 厂商设置自己 收益10% 代理商设置自己收益10% 则厂商的收益为 10% 代理商收益为 (1- 10%) * 10% = 9% 经销商的收益为 1- 10% - 9% = 81% """ def calc_account_split_map(self): # type:() -> dict # 保持和之前的一致 优先提取下参数 dealerId = str(self._dealer.id) agentId = self._dealer.agentId group = self._group # 厂商的分成比例构建 实际使用primaryAgent代替 primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId) managerProfitShare = self.manager_profit_share managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount} # 代理商的分成比例构建 discount_multiplier = Percent(100) - managerProfitShare agentProfitShare = self.agent_profit_share * discount_multiplier agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount} # 合伙人分成比例的构建 discount_multiplier = discount_multiplier - agentProfitShare partners = group['partnerDict'].values() partnerPartition = [ { 'role': PARTITION_ROLE.PARTNER, 'id': partner['id'], 'share': (discount_multiplier * Percent(partner['percent'])).mongo_amount } for partner in partners ] partnerShares = sum((Percent(_['share']) for _ in partnerPartition), Percent(0)) dealerShare = discount_multiplier - partnerShares ownerShare = { 'role': PARTITION_ROLE.OWNER, 'id': dealerId, 'share': dealerShare.mongo_amount } partitionMap = { PARTITION_ROLE.AGENT: [managerShare, agentShare], PARTITION_ROLE.PARTNER: partnerPartition, PARTITION_ROLE.OWNER: [ownerShare] } return self.check_partition_map(partitionMap) class PartnerLedgerFirst(IncomeStrategy): """ 平级结构的分成 所有角色共分比例 100% 举例 厂商设置自己 收益10% 代理商设置自己收益10% 则厂商的收益为 10% 代理商收益为 10 经销商的收益为 1- 10% - 10% = 80% """ def calc_account_split_map(self): # type:() -> dict dealerId = str(self._dealer.id) group = self._group # 计算合伙人所有产生的收益 partners = group['partnerDict'].values() partnerPartition = [ { 'role': PARTITION_ROLE.PARTNER, 'id': partner['id'], 'share': Percent(partner['percent']).mongo_amount } for partner in partners ] partnerProfitShare = sum((Percent(_['share']) for _ in partnerPartition), Percent(0)) # 计算代理商产生的收益比例 agentId = self._dealer.agentId agentProfitShare = self.agent_profit_share agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount} # 计算厂商所产生的收益 primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId) managerProfitShare = self.manager_profit_share managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount} # 最后经销商就是剩下的比例 dealerProfitShare = Percent(100) - managerProfitShare - agentProfitShare - partnerProfitShare ownerShare = { 'role': PARTITION_ROLE.OWNER, 'id': str(dealerId), 'share': dealerProfitShare.mongo_amount } partitionMap = { PARTITION_ROLE.AGENT: [managerShare, agentShare], PARTITION_ROLE.PARTNER: partnerPartition, PARTITION_ROLE.OWNER: [ownerShare] } return self.check_partition_map(partitionMap) class LedgerConsumeOrder(object): """ 对每日的消费订单统计进行分润 """ _source = "ledger_consume" def __init__(self, statsRecord, ledgerTime=None): # type: (DealerGroupStats, str) -> None self._record = statsRecord self._time = ledgerTime self._owner = self._record.dealer # type: Dealer self._source_key = self._record.withdrawSourceKey def execute(self): logger.info("[LedgerConsumeOrder] stats = {}".format(self._record)) if self._record.is_ledgered: logger.warning("[LedgerConsumeOrder] stats <{}> has been ledgered!".format(self._record)) return # if not self._record.ledger_enable: # logger.warning("[LedgerConsumeOrder] stats <{}> not allow ledger!".format(self._record)) # return # TODO 每日统计等等统计暂时不处理 # 创建分账以及收益信息 partition = self._get_partition_map() self._record.set_partition(partition) if self._owner.sub_balance(DEALER_INCOME_TYPE.DEVICE_INCOME) < self._record.amount: self._record.set_description(u"资金账户余额不足,分账失败") return # 记录消费收益之前 首先从经销商的资金池里面扣除 然后再对消费进行分钱 收益前先冻结 经销商的资金池 完成之后再清除 self._owner.freeze_ledger_balance(self._record.amount, self._source_key, str(self._record.id)) self._record_balance(partition) self._owner.clear_ledger_balance(str(self._record.id)) # 设置分账标志 t = self._time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self._record.set_ledgered(t) def get_partition_map(self): return self._get_partition_map() def _get_elec_payer(self, partners): # type:(list) -> dict payer = {} for _partner in partners: if _partner["payElecFee"]: if payer: raise ValueError("payer count gte 1! record = {}".format(self._record)) payer = _partner return payer def _get_partition_map(self): # type:() -> list group = self._record.group # type: GroupDict partners = group.partners elecFee = group.elecFee * self._record.elecCount # type: RMB if elecFee < RMB(0): raise ValueError("elec lt 0, record = {}".format(self._record)) if self._record.amount < RMB(0): raise ValueError("totalAmount lt 0, record = {}".format(self._record)) # 找出电费承担方 如果电费承担不存在 则集体承担 # 不对totalAmount做非0校验 这个数值确实可能小于0 比如全部使用的是赠送金额的钱 但是存在电费 elecPayer = self._get_elec_payer(partners) totalAmount = self._record.amount - elecFee if elecPayer else self._record.amount totalMoney, totalP, partition = RMB(0), Percent(0), list() for _partner in partners: _m = totalAmount * Percent(_partner["percent"]).as_ratio _s = Percent(_partner["percent"]) _p = { "role": PARTITION_ROLE.PARTNER, "id": _partner["id"], "share": _s.mongo_amount, "money": _m.mongo_amount } if elecPayer and _p["id"] == elecPayer["id"]: _p.update({ "elecFee": elecFee.mongo_amount }) totalMoney += _m totalP += _s partition.append(_p) if abs(totalMoney) > abs(totalAmount): raise ValueError("total share money <{}> gt total amount <{}>, record = {}".format(totalMoney, totalAmount, self._record)) if totalP > Percent(100): raise ValueError(u"total share percent gt 100, record = {}".format(self._record)) partition.append({ "role": PARTITION_ROLE.OWNER, "id": str(self._record.dealerId), "share": (Percent(100) - totalP).mongo_amount, "money": (totalAmount - totalMoney).mongo_amount }) # TODO 最后再校验一次是否有必要 logger.info('[_get_partition_map] record = {}, get partition = {}'.format(self._record, partition)) return partition def _record_balance(self, partition): """ 根据收益的划分 对经销商 以及合伙人 收益进行增加 """ for _owner in partition: _share_money = RMB(_owner['money']) + RMB(_owner.get("elecFee", 0)) _dealer = Dealer.objects.get(id=_owner["id"]) self._record_dealer_balance(_dealer, _share_money) def _record_dealer_balance(self, dealer, money): logger.info("[_record_dealer_balance] dealer = {}, money ={}, record = {}".format( dealer, money, self._record )) if money == RMB(0): return dealer.record_income( source=self._source, source_key=self._source_key, money=money )