Beispiel #1
0
class FundWeekly(Article):
    kind = FUNDWEEKLY.KIND
    kind_cn = u'基金组合周报'
    type = FUNDWEEKLY.TYPE

    title = PropsItem('title', '')
    description = PropsItem('description', '')
    content = PropsItem('content', '')
    author = PropsItem('author', '')

    _const = FUNDWEEKLY

    def __repr__(self):
        return '<FundWeekly id=%s, type=%s, status=%s>' % (self.id, self.type,
                                                           self.status)

    @permalink('article.article_detail')
    def url(self):
        return {'article_type': 'fund/weekly', 'id': self.id}

    def mark_as_read(self, user_id):
        mc.set(FUNDWEEKLY_READ_CACHE_KEY % (self.id, user_id), True)

    def has_read(self, user_id):
        return mc.get(FUNDWEEKLY_READ_CACHE_KEY % (self.id, user_id))
Beispiel #2
0
class ViewPoint(Article):
    kind = VIEWPOINT.KIND
    kind_cn = u'理财师观点'
    type = VIEWPOINT.TYPE

    title = PropsItem('title', '')
    content = PropsItem('content', '')
    author = PropsItem('author', '')

    _const = VIEWPOINT

    def __repr__(self):
        return '<ViewPoint id=%s, type=%s, status=%s>' % (self.id, self.type,
                                                          self.status)

    @permalink('article.article_detail')
    def url(self):
        return {'article_type': 'viewpoints', 'id': self.id}

    def make_feed_entry(self):
        return FeedEntry(self.title,
                         render(self.content),
                         content_type='html',
                         author=self.author or u'好规划网理财师',
                         url=self.url,
                         updated=self.update_time,
                         published=self.publish_time)
Beispiel #3
0
class Insure(ProductBase):
    '''
    保险
    '''

    duration = PropsItem('duration', '')  # 保险期间
    pay_duration = PropsItem('pay_duration', '')  # 缴费期限
    insure_duty = PropsItem('insure_duty', '')  # 保险责任
    throng = PropsItem('throng', '')  # 适合人群
    prospect = PropsItem('prospect', '')  # 保费预估

    kind = 'insure'
    _table = 'product_insure'

    def __repr__(self):
        return '<Product Insure id=%s, type=%s, status=%s>' % (
            self.id, self.type, self.status
        )

    @classmethod
    def gets_by_type(cls, type):
        funds = super(Insure, cls).get_all(limit=1000)
        if not funds:
            return []
        if type and str(type) not in INSURE_TYPE.values():
            return []
        return filter(lambda x: x.type == str(type), funds)

    @property
    def type_name(self):
        for name, value in INSURE_TYPE.items():
            if self.type == value:
                return INSURE_NAME.get(name)
        return '未命名'
Beispiel #4
0
class Profile(PropsMixin):
    table_name = 'insurance_profile'

    user_will = PropsItem('user_will', default=0, secret=True)
    baby_birthday = PropsItem('baby_birthday',
                              default=datetime.now(),
                              secret=True)
    baby_gender = PropsItem('baby_gender', default='', secret=True)
    child_medicare = PropsItem('child_medicare', default='')
    childins_supplement = PropsItem('childins_supplement', default='')
    child_genetic = PropsItem('child_genetic', default='')
    child_edu = PropsItem('child_edu', default='')
    project = PropsItem('project', default='')

    is_six = PropsItem('is_six', default=False)

    result_data = PropsItem('result_data', default={}, secret=True)

    def __init__(self, account_id, create_time):
        self.user_id = account_id
        self.create_time = create_time

    def get_db(self):
        return 'insurance_profile'

    def get_uuid(self):
        return 'insurance_profile:insurance:%s' % self.user_id

    @classmethod
    def add(cls, account_id):
        if not Account.get(account_id):
            raise InsuranceNotFoundError(account_id, Account)

        existent = cls.get(account_id)
        if existent:
            return existent

        sql = ('insert into {.table_name} (account_id, create_time) '
               'values ( %s, %s)').format(cls)

        params = (account_id, datetime.now())
        db.execute(sql, params)
        db.commit()
        cls.clear_cache(account_id)
        return cls.get(account_id)

    @classmethod
    @cache(INSURE_PROFILE_CACHE_KEY)
    def get(cls, account_id):
        sql = ('select account_id, create_time '
               'from {.table_name} where account_id= %s').format(cls)
        param = account_id
        rs = db.execute(sql, param)
        if rs:
            return cls(*rs[0])

    @classmethod
    def clear_cache(cls, account_id):
        mc.delete(INSURE_PROFILE_CACHE_KEY.format(account_id=account_id))
Beispiel #5
0
class Feedback(PropsMixin):

    def __init__(self, id, contact, create_time, update_time):
        self.id = str(id)
        self.contact = contact
        self.create_time = create_time
        self.update_time = update_time

    def get_db(self):
        return 'feedback'

    def get_uuid(self):
        return 'feedback:%s' % self.id

    content = PropsItem('content', '')
    answer = PropsItem('answer', '')
    admin = PropsItem('admin', '')

    @classmethod
    def add(cls, contact, content):
        try:
            id = db.execute('insert into feedback '
                            '(contact, create_time) '
                            'values(%s, %s)',
                            (contact, datetime.now()))
            if id:
                db.commit()
                c = cls.get(id)
                c.content = content
                return c
            else:
                db.rollback()
        except IntegrityError:
            db.rollback()
            warn('insert feedback failed')

    @classmethod
    @cache(FEEDBACK_CACHE_KEY % '{id}')
    def get(cls, id):
        rs = db.execute('select id, contact, create_time, '
                        'update_time from feedback '
                        'where id=%s', (id,))
        return cls(*rs[0]) if rs else None

    @classmethod
    @pcache(ALL_FEEDBACK_CACHE_KEY)
    def _get_all_ids(cls, start=0, limit=20):
        sql = 'select id from feedback order by update_time desc limit %s,%s'
        rs = db.execute(sql, (start, limit))
        ids = [str(id) for (id,) in rs]
        return ids

    @classmethod
    def get_all(cls, start=0, limit=20):
        ids = cls._get_all_ids(start=start, limit=limit)
        return [cls.get(id) for id in ids]
Beispiel #6
0
class PersonelGroup(PropsMixin):
    storage = WeakValueDictionary()

    # the users dict stored
    users = PropsItem('users', default={}, secret=True)
    update_time = PropsItem('update_time', None, date_type)

    def __init__(self, name):
        self.name = name
        if name in self.storage:
            raise ValueError('name %r has been used' % name)
        self.storage[name] = self

    @classmethod
    def get(cls, name):
        return cls.storage.get(name)

    def get_uuid(self):
        return 'group:{name}'.format(name=self.name)

    def get_db(self):
        return 'personel'

    def add_personel(self, user_id, user_name):
        group = self.users
        account = Account.get(user_id)
        if not account:
            raise ValueError('%s is not existed' % user_id)
        if user_id in group:
            raise KeyError('key %s conflicted' % user_id)
        group.update({user_id: user_name})
        self.update_props_items({
            'users': group,
            'update_time': str(datetime.now())
        })

    def del_personel(self, user_id):
        group = self.users
        group.pop(user_id, None)
        self.update_props_items({
            'users': group,
            'update_time': str(datetime.now())
        })

    def update_personel(self, user_id, user_name):
        group = self.users
        if user_id in group:
            group[user_id] = user_name
            self.update_props_items({
                'users': group,
                'update_time': str(datetime.now())
            })
Beispiel #7
0
class Question(Article):
    kind = QUESTION.KIND
    type = QUESTION.TYPE

    ask = PropsItem('ask', '')
    answer = PropsItem('answer', '')

    _const = QUESTION

    def __repr__(self):
        return '<Question id=%s, type=%s, status=%s>' % (self.id, self.type,
                                                         self.status)

    @permalink('article.article_detail')
    def url(self):
        return {'article_type': 'consultations', 'id': self.id}
Beispiel #8
0
class Activity61(PropsMixin):
    table_name = 'insurance_61'

    phone_number = PropsItem('phone_number', default='')
    recommend = PropsItem('recommend', default=[])

    def __init__(self, account_id):
        self.user_id = account_id

    def get_db(self):
        return 'insurance_61'

    def get_uuid(self):
        return 'insurance_61:insurance:%s' % self.user_id

    @classmethod
    def add(cls, account_id):
        if not Account.get(account_id):
            raise InsuranceNotFoundError(account_id, Account)

        existent = cls.get(account_id)
        if existent:
            return existent

        sql = ('insert into {.table_name} (account_id, create_time) '
               'values (%s, %s)').format(cls)

        params = (account_id, datetime.now())
        db.execute(sql, params)
        db.commit()
        cls.clear_cache(account_id)
        return cls.get(account_id)

    @classmethod
    @cache(INSURE_61_CACHE_KEY)
    def get(cls, account_id):
        sql = ('select account_id from {.table_name} where account_id= %s'
               ).format(cls)
        param = account_id
        rs = db.execute(sql, param)
        if rs:
            return cls(*rs[0])

    @classmethod
    def clear_cache(cls, account_id):
        mc.delete(INSURE_61_CACHE_KEY.format(account_id=account_id))
Beispiel #9
0
class ZhiwangLadderDuedayProduct(ZhiwangProduct):

    annual_rate_layers = PropsItem('annual_rate_layers', [], list)
    last_due_date = PropsItem('last_due_date', None, date_type)

    @property
    def first_due_date(self):
        return self.due_date

    @property
    def final_due_date(self):
        return self.last_due_date or self.due_date

    @property
    def local_name(self):
        return u'%s~%s不定期产品' % (self.profit_period['min'].display_text,
                                self.profit_period['max'].display_text)

    @cached_property
    def profit_period(self):
        min_value = (self.first_due_date - self.start_date).days
        max_value = (self.final_due_date - self.start_date).days
        return {
            'min': ProfitPeriod(min_value, 'day'),
            'max': ProfitPeriod(max_value, 'day')
        }

    @cached_property
    def profit_annual_rate(self):
        rates = [layer['annual_rate'] for layer in self.annual_rate_layers]
        return {'min': Decimal(min(rates)), 'max': Decimal(max(rates))}

    def get_annual_rate_by_date(self, day):
        assert isinstance(day, datetime.date)
        return Decimal(
            self.get_annual_rate_by_days((day - self.start_date).days))

    def get_annual_rate_by_days(self, days):
        matched_layers = [
            layer['annual_rate'] for layer in self.annual_rate_layers
            if days >= layer['min_days']
        ]
        return matched_layers[-1] if matched_layers else 0
Beispiel #10
0
class NotSecretTest(PropsMixin):
    def __init__(self, id=None):
        self.id = id or 10000

    def get_db(self):
        return 'test_db'

    def get_uuid(self):
        return self.id

    data = PropsItem('data', 'nothing')
Beispiel #11
0
class BackwardProfile(PropsMixin):
    """Do not use this outside."""

    person_name = PropsItem('person_name', default='', secret=True)
    person_ricn = PropsItem('person_ricn', default='', secret=True)

    def get_uuid(self):
        return 'user:profile:{account_id}'.format(account_id=self.account_id)

    def get_db(self):
        return 'hoard'

    def __init__(self, account_id):
        self.account_id = account_id

    def remove(self):
        self.update_props_items({
            'person_name': '',
            'person_ricn': '',
        })
Beispiel #12
0
class Program(PropsMixin):

    quota = PropsItem('quota', default={})

    def __init__(self):
        pass

    def get_db(self):
        return 'insurance_program'

    def get_uuid(self):
        return 'insurance_program:quota'
Beispiel #13
0
class P2P(ProductBase):
    '''
    P2P
    '''

    kind = 'p2p'
    _table = 'product_p2p'

    year_rate = PropsItem('year_rate', 0)  # 预期年化收益率
    pay_return_type = PropsItem('pay_return_type', '')  # 返还方式
    deadline = PropsItem('deadline', '')  # 投资期限
    min_money = PropsItem('min_money', '')  # 购买起点
    protect = PropsItem('protect', '')  # 保障

    def __repr__(self):
        return '<Product P2P id=%s, type=%s, status=%s>' % (self.id, self.type,
                                                            self.status)

    @property
    def type_name(self):
        return 'P2P'
Beispiel #14
0
class InsProperty(PropsMixin):
    feerate = PropsItem('feerate', {})
    rec_reason = PropsItem('rec_reason', '')
    buy_url = PropsItem('buy_url')
    ins_title = PropsItem('ins_title')
    ins_sub_title = PropsItem('ins_sub_title', 'ins_sub_title')

    def __init__(self, insurance_id, kind):
        self.id = insurance_id
        self.kind = kind
        self._age = None

    def get_db(self):
        return 'insure_fee_rate'

    def get_uuid(self):
        return 'insure_fee_rate:insure:%s' % self.id

    def get_ins_sub_title(self, package_id, insurance_id):
        return self.ins_sub_title

    def add_props(self, feerate, rec_reason, buy_url, ins_title,
                  ins_sub_title):
        self.feerate = feerate
        self.rec_reason = rec_reason
        self.buy_url = buy_url
        self.ins_title = ins_title
        self.ins_sub_title = ins_sub_title

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, ageobj):
        self._age = ageobj
Beispiel #15
0
class Fund(ProductBase):
    '''
    基金
    '''

    kind = 'fund'
    _table = 'product_fund'

    code = PropsItem('code', '')  # 基金代码
    found_date = PropsItem('found_date', '')  # 成立日期
    index = PropsItem('index', '')  # 跟踪指数
    risk = PropsItem('risk', '')  # 风险

    manager = PropsItem('manager', '')  # 基金经理
    year_rate = PropsItem('year_rate', '')  # 近一年涨幅

    nickname = PropsItem('nickname', '')  # 别名

    def __repr__(self):
        return '<Product fund id=%s, type=%s, status=%s>' % (
            self.id, self.type, self.status)

    @classmethod
    def gets_by_type(cls, type):
        funds = super(Fund, cls).get_all(limit=1000)
        if not funds:
            return []
        if type and str(type) not in FUND_TYPE.values():
            return []
        return filter(lambda x: x.type == str(type), funds)

    @property
    def type_name(self):
        for name, value in FUND_TYPE.items():
            if self.type == value:
                return FUND_NAME.get(name)
        return '未命名'

    @classmethod
    def gets_by_risk(cls, risk):
        if risk not in ('高', '中', '低'):
            return []
        funds = super(Fund, cls).get_all(limit=1000)
        if not funds:
            return []
        return filter(lambda x: x.risk == risk, funds)
Beispiel #16
0
class BankCardManager(PropsMixin):
    """The bank card manager which should be bound with user profiles."""

    last_used_bankcard_id = PropsItem('last_used_bankcard_id', default=0)

    def get_uuid(self):
        return 'user:{.user_id}:bankcards'.format(self)

    def get_db(self):
        return 'hoard'

    def __init__(self, user_id):
        self.user_id = user_id

    def get_all(self, partner=None):
        """Gets all bank cards of current user.

        :returns: the list of :class:
        """
        cards = [c for c in BankCard.get_by_user(self.user_id) if c.is_active]
        cards = sorted(cards, key=attrgetter('is_default'), reverse=True)
        if cards and not cards[0].is_default:
            cards[0].is_default = True
        if partner:
            cards = [c for c in cards if partner in c.bank.available_in]
        return cards

    def get_latest(self):
        bankcards = sorted(
            self.get_all(), key=attrgetter('creation_time'), reverse=True)
        return first(bankcards, default=None)

    def get_last_used(self):
        bankcard = BankCard.get(self.last_used_bankcard_id)
        if bankcard and bankcard.is_active:
            return bankcard
        return self.get_latest()

    def get_default(self):
        bankcards = self.get_all()
        card = first(bankcards, default=None)
        if not card.is_default:
            card.is_default = True
        return card

    def set_default(self, bankcard):
        if not isinstance(bankcard, BankCard):
            raise TypeError
        if bankcard.is_default:
            return True
        bankcards = self.get_all()
        if bankcard not in bankcards:
            raise BankCardNotActive(bankcard.id_)
        default_card = self.get_default()
        default_card.is_default = False
        bankcard.is_default = True
        return True

    def add(self, **kwargs):
        """Adds a new bank card for current user.

        :params kwargs: the same as :meth:`BankCard.add` except ``user_id``.
        :returns: the created bank card.
        """
        # 静默删除已存在但未使用过的同一银行的卡
        for bankcard in self.get_multi_by_bank(kwargs['bank_id']):
            self.remove(bankcard.card_number, silent=True)
        # 静默删除同一个卡号但未使用过的卡
        bankcard = self.get_by_card_number(kwargs['card_number'])
        if bankcard:
            self.remove(bankcard.card_number, silent=True)

        return BankCard.add(user_id=self.user_id, **kwargs)

    def create_or_update(self, card_number, **kwargs):
        # TODO (tonyseek) 不应该让所有字段都可以更新
        cards = self.get_all()
        is_default = False if cards else True
        kwargs.update({'is_default': is_default})
        card = self.get_by_card_number(card_number)
        if card:
            card.update(**kwargs)
            self.last_used_bankcard_id = card.id_
            return card
        else:
            return self.add(card_number=card_number, **kwargs)

    def get_by_card_number(self, card_number):
        digest = calculate_checksum(card_number)
        return first(
            (c for c in self.get_all() if c.card_number_sha1 == digest), None)

    def get(self, id_):
        return first((c for c in self.get_all() if c.id_ == str(id_)), None)

    def get_multi_by_bank(self, bank_id):
        return [c for c in self.get_all() if c.bank_id == str(bank_id)]

    def remove(self, card_number, silent=False):
        try:
            BankCard.delete_by_card_number(card_number, user_id=self.user_id)
        except CardDeletingError as deleting_error:
            if silent:
                rsyslog.send('%s\t%s' % (card_number, deleting_error),
                             'bankcard_removing_denied')
            else:
                raise
        else:
            rsyslog.send(card_number, 'bankcare_removed')

    def restore(self, id_):
        return BankCard.restore(id_, self.user_id)
Beispiel #17
0
class Product(PropsMixin):
    """产品"""

    table_name = 'hoarder_product'
    cache_key = 'hoarder:product:v1:{product_id}'
    cache_by_vendor = 'hoarder:products:v1:{vendor_id}'

    class Status(Enum):
        """产品状态"""
        # 在售
        on_sell = 'S'
        # 废弃
        deprecated = 'D'

    class Type(Enum):
        """产品期限分类"""
        # 固定期限类
        classic = 'C'
        # 不定期限类
        dynamic = 'D'
        # 不限期限类
        unlimited = 'U'

    class RedeemType(Enum):
        """赎回类型"""
        # 到期自动赎回
        auto = '2'
        # 用户申请赎回
        user = '******'

    class Kind(Enum):
        """父子产品分类"""
        # 父产品
        father = 'F'
        # 子产品
        child = 'C'

    # 产品名称
    name = PropsItem('name', u'', unicode_type)
    # 产品可用配额
    quota = PropsItem('quota', 0, Decimal)
    # 产品总配额
    total_quota = PropsItem('total_quota', 0, Decimal)
    # 产品当日配额
    today_quota = PropsItem('today_quota', 0, Decimal)
    # 产品累计交易额
    total_amount = PropsItem('total_amount', 0, Decimal)
    # 客户最大可购买 金额(客户对产品 购买的最大累计 金额)
    total_buy_amount = PropsItem('total_buy_amount', 0, Decimal)
    # 最小赎回金额(针对允许用户主动 发起赎回的产品, 例如活期类日日盈产品等)
    min_redeem_amount = PropsItem('min_redeem_amount', 0, Decimal)
    # 最大赎回金额
    max_redeem_amount = PropsItem('max_redeem_amount', 0, Decimal)
    # 每日赎回限额
    day_redeem_amount = PropsItem('day_redeem_amount', 0, Decimal)
    # 加息年化利率
    interest_rate_hike = PropsItem('interest_rate_hike', 0, Decimal)
    # 备注信息
    description = PropsItem('description', u'', unicode_type)
    # 上架下架控制
    is_taken_down = PropsItem('is_taken_down', True, bool)
    # 预售控制
    is_pre_sale = PropsItem('is_pre_sale', False, bool)
    # 预售时间
    pre_hour = PropsItem('pre_hour')

    expire_period_unit = PropsItem('expire_period_unit', PeriodUnit.day.value,
                                   int)
    expire_period = PropsItem('expire_period', 0, int)

    is_accepting_bonus = False

    def __init__(self, product_id, remote_id, status, product_type, kind,
                 min_amount, max_amount, rate_type, rate, effect_day_condition,
                 effect_day, effect_day_unit, redeem_type, start_sell_date,
                 end_sell_date, update_time, creation_time, vendor_id):
        self.id_ = product_id
        self.remote_id = remote_id
        self._status = status
        self._type = product_type
        self._kind = kind
        self.min_amount = min_amount
        self.max_amount = max_amount
        self.rate_type = rate_type
        self.rate = rate
        self.effect_day_condition = effect_day_condition
        self.effect_day = effect_day
        self.effect_day_unit = effect_day_unit
        # 赎回类型
        self.redeem_type = redeem_type
        self.start_sell_date = start_sell_date
        self.end_sell_date = end_sell_date
        self.update_time = update_time
        self.creation_time = creation_time
        self.vendor_id = vendor_id

    def get_db(self):
        return 'hoarder'

    def get_uuid(self):
        return 'product:{product_id}'.format(product_id=self.id_)

    @property
    def status(self):
        return self.Status(self._status)

    @property
    def ptype(self):
        return self.Type(self._type)

    @property
    def kind(self):
        return self.Kind(self._kind)

    @property
    def product_type(self):
        return self.ptype

    @property
    def can_redeem(self):
        """是否可手动赎回"""
        t = self.RedeemType(self.redeem_type)
        if t is self.RedeemType.auto:
            return False
        # TODO:增加其他不可赎回限制条件

        return True

    @property
    def value_date(self):
        """预期起息日"""
        start_time = datetime.now() + timedelta(days=1)
        return get_effect_date(start_time)

    @property
    def check_benefit_date(self):
        if self.ptype is self.Type.unlimited:
            start_time = datetime(self.value_date.year, self.value_date.month,
                                  self.value_date.day)
            return get_effect_date(start_time + timedelta(days=1))
        return self.value_date

    @property
    def expect_due_date(self):
        """预期到期日"""
        if self.ptype is not self.Type.unlimited:
            start_time = datetime(self.value_date.year, self.value_date.month,
                                  self.value_date.day)
            return get_effect_date(start_time +
                                   timedelta(days=self.frozen_days))

    @property
    def is_sold_out(self):
        from .order import HoarderOrder
        if self.kind is self.Kind.father:
            local_daily_sold_amount = HoarderOrder.get_product_daily_sold_amount_by_now(
                self.id_)
            if local_daily_sold_amount >= self.quota - get_savings_new_comer_product_threshold(
            ):
                return True
        if self.quota < self.min_amount:
            return True
        return False

    @property
    def is_in_sale_period(self):
        return self.start_sell_date <= date.today() < self.end_sell_date

    @property
    def is_on_sale(self):
        if self.is_taken_down or self.is_sold_out:
            return False
        if self.status is self.Status.deprecated:
            return False
        return True

    @classmethod
    @cache(cache_key)
    def get(cls, product_id):
        sql = (
            'select id, remote_id, status, type, kind, min_amount, max_amount, rate_type, rate,'
            ' effect_day_condition, effect_day, effect_day_unit, redeem_type,'
            ' start_sell_date, end_sell_date, update_time, creation_time, vendor_id'
            ' from {.table_name} where id=%s').format(cls)
        params = (product_id, )
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    def add_or_update(cls,
                      vendor_id,
                      remote_id,
                      name,
                      quota,
                      total_quota,
                      today_quota,
                      total_amount,
                      total_buy_amount,
                      min_redeem_amount,
                      max_redeem_amount,
                      day_redeem_amount,
                      interest_rate_hike,
                      description,
                      product_type,
                      min_amount,
                      max_amount,
                      rate_type,
                      rate,
                      effect_day_condition,
                      effect_day,
                      effect_day_unit,
                      redeem_type,
                      start_sell_date,
                      end_sell_date,
                      expire_period=1,
                      expire_period_unit=PeriodUnit.day.value,
                      is_child_product=False):
        assert isinstance(product_type, cls.Type)
        sql = (
            'insert into {.table_name} (remote_id, status, type, kind, min_amount, max_amount,'
            'rate_type, rate, effect_day_condition, effect_day, effect_day_unit,'
            'redeem_type, start_sell_date, end_sell_date, update_time,'
            'creation_time, vendor_id) '
            'values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) '
            'on duplicate key update rate=values(rate), update_time=values(update_time)'
        ).format(cls)
        kind = cls.Kind.child.value if is_child_product else cls.Kind.father.value
        params = (remote_id, cls.Status.on_sell.value, product_type.value,
                  kind, min_amount, max_amount, rate_type, rate,
                  effect_day_condition, effect_day, effect_day_unit,
                  redeem_type, start_sell_date, end_sell_date, datetime.now(),
                  datetime.now(), vendor_id)

        rs = db.execute(sql, params)
        db.commit()

        if rs:
            cls.clear_cache(rs)

        p = cls.get_by_remote_id(vendor_id, remote_id)
        original_quota = p.quota
        if p:
            p.name = name
            p.quota = quota
            p.total_quota = total_quota
            p.today_quota = today_quota
            p.total_amount = total_amount
            p.total_buy_amount = total_buy_amount
            p.min_redeem_amount = min_redeem_amount
            p.max_redeem_amount = max_redeem_amount
            p.day_redeem_amount = day_redeem_amount
            p.interest_rate_hike = interest_rate_hike
            p.description = description
            p.expire_period = expire_period
            p.expire_period_unit = expire_period_unit
            # 当销售周期内产品额度调整时,发出BC通知

            if bearychat.configured:
                quota_txt = format_number(quota, locale='en_US')
                original_quota_txt = format_number(original_quota,
                                                   locale='en_US')
                if quota < p.min_amount:
                    txt = u'最近一笔:**¥{}**,当前额度:**¥{}**'.format(
                        original_quota_txt, quota_txt)
                    attachment = bearychat.attachment(title=None,
                                                      text=txt,
                                                      color='#ffa500',
                                                      images=[])
                    bearychat.say(
                        u'产品 **{}** **售罄** 啦,正在尝试释放未支付订单,请周知。'.format(name),
                        attachments=[attachment])
                if quota > int(original_quota) + int(p.min_amount):
                    txt = u'更新前额度:**¥{}**, 当前额度:**¥{}**'.format(
                        original_quota_txt, quota_txt)
                    attachment = bearychat.attachment(title=None,
                                                      text=txt,
                                                      color='#a5ff00',
                                                      images=[])
                    bearychat.say(
                        u'产品 **{}** **额度** 增加啦 :clap:,请周知。'.format(name),
                        attachments=[attachment])
        return p

    @classmethod
    @cache(cache_by_vendor)
    def get_product_ids_by_vendor_id(cls, vendor_id):
        sql = 'select id from {.table_name} where vendor_id=%s'.format(cls)
        params = (vendor_id, )
        rs = db.execute(sql, params)
        return [p[0] for p in rs if rs]

    @classmethod
    def get_products_by_vendor_id(cls, vendor_id):
        return [
            cls.get(id_) for id_ in cls.get_product_ids_by_vendor_id(vendor_id)
        ]

    @classmethod
    def get_by_remote_id(cls, vendor_id, remote_id):
        sql = 'select id from {.table_name} where vendor_id=%s and remote_id=%s'.format(
            cls)
        params = (
            vendor_id,
            remote_id,
        )
        rs = db.execute(sql, params)
        if rs:
            return cls.get(rs[0][0]) if rs else None

    def go_on_sale(self):
        """上架"""
        if not self.is_taken_down:
            raise ProductDuplicateSaleModeError()
        if self.status is self.Status.deprecated:
            raise ProductDeprecatedError()
        if self.quota < self.min_amount:
            raise ProductLowQuotaError()
        self.is_taken_down = False

    def go_off_sale(self):
        """下架"""
        self.is_taken_down = True

    @classmethod
    def get_products_on_sale(cls):
        vendors = Vendor.get_all()
        for vendor in vendors:
            for p in cls.get_products_by_vendor_id(vendor.id_):
                if p.is_on_sale:
                    yield p

    @property
    def vendor(self):
        return Vendor.get(self.vendor_id)

    @classmethod
    def clear_cache(cls, product_id):
        mc.delete(cls.cache_key.format(product_id=product_id))
        p = cls.get(product_id)
        if p:
            mc.delete(cls.cache_by_vendor.format(vendor_id=p.vendor_id))

    @property
    def frozen_days(self):
        if PeriodUnit(self.expire_period_unit) is PeriodUnit.day:
            return self.expire_period
        if PeriodUnit(self.expire_period_unit) is PeriodUnit.month:
            return self.expire_period * 30

    @property
    def profit_period(self):
        from core.models.hoard.common import ProfitPeriod
        value = ProfitPeriod(self.frozen_days, 'day')
        return {'min': value, 'max': value}
Beispiel #18
0
class RedeemCode(PropsMixin):
    """兑换码为8位随机码或者定制码, 用来换取对应奖品

    兑换码有两种,一种是8位随机码,由数字和字母随机生成,另外一种为定制码(可能包含汉字),
    用户可根据兑换码在规定时间内,根据活动规则,领取兑换码内包含的奖品(奖品因活动不同而不同),
    使用兑换码时不区分字母的大小写

    :param code: 兑换码
    :param activity_id: 兑换码所属活动的id
    :param max_usage_limit_per_code: 每个兑换码允许使用的最大次数
    """

    table_name = 'redeem_code'
    cache_key = 'redeem_code:{id_}'
    cache_ids_by_activity_id_key = 'redeem_code:activity_id:{activity_id}'

    #: 兑换码用途的详细描述
    description = PropsItem('description', '')

    class Status(Enum):
        """兑换码的使用状态

        兑换码的状态分为有效和无效,有效的兑换码才能兑换礼包
        """
        #: 有效
        validated = 'V'

        #: 无效
        invalidated = 'I'

    class Source(Enum):
        """兑换码生成方式

        兑换码可由后台管理员根据需求生成或者由系统调用相应方法生成
        """

        #: 系统
        robot = 'R'

        #: 管理员
        manager = 'M'

    class Kind(Enum):
        """兑换码兑换策略

        兑换码进行分发礼包的策略,根据不同的策略完成分发
        """

        # 正常
        normal_package = 1

        # 特殊
        special_package = 2

    def __init__(self, id_, code, source, kind, status, activity_id,
                 max_usage_limit_per_code, creation_time, effective_time,
                 expire_time):
        self.id_ = str(id_)
        self.code = code
        self._source = source
        self._kind = kind
        self._status = status
        self.activity_id = str(activity_id)
        self.max_usage_limit_per_code = max_usage_limit_per_code
        self.creation_time = creation_time
        self.effective_time = effective_time
        self.expire_time = expire_time

    def get_db(self):
        return 'redeem_code'

    def get_uuid(self):
        return 'redeem_code:{id_}'.format(id_=self.id_)

    @cached_property
    def kind(self):
        return self.Kind(self._kind)

    @cached_property
    def activity(self):
        return RedeemCodeActivity.get(self.activity_id)

    @property
    def is_effective(self):
        return self.effective_time <= datetime.now()

    @property
    def is_expired(self):
        return self.expire_time < datetime.now()

    @property
    def status(self):
        return self.Status(self._status)

    @status.setter
    def status(self, new_status):
        assert isinstance(new_status, self.Status)
        self._status = new_status.value

    @cached_property
    def source(self):
        return self.Source(self._source)

    @classmethod
    def create(cls,
               activity_id,
               kind,
               description,
               max_usage_limit_per_code,
               customized_code,
               effective_time,
               expire_time,
               _commit=True):
        assert isinstance(effective_time, date)
        assert isinstance(expire_time, date)
        assert max_usage_limit_per_code >= 1

        if customized_code:
            code = (customized_code.encode('utf-8') if
                    is_include_chinese(customized_code) else customized_code)
        else:
            code = ''.join(random.sample(REDEEM_CODE_ALPHABET, 8))
        sql = ('insert into {.table_name} (code, activity_id, source,'
               ' max_usage_limit_per_code, kind, status, creation_time,'
               ' effective_time, expire_time)'
               ' values (%s, %s, %s, %s, %s, %s, %s, %s, %s)').format(cls)
        params = (code, activity_id, cls.Source.manager.value,
                  max_usage_limit_per_code, kind, cls.Status.validated.value,
                  datetime.now(), effective_time, expire_time)
        try:
            id_ = db.execute(sql, params)
        except MySQLdb.IntegrityError:
            raise RedeemCodeExistedError()
        if _commit:
            db.commit()

        #: 清除缓存
        cls.clear_cache(id_)
        cls.clear_cache_ids_by_activity_id(activity_id)

        instance = cls.get(id_)
        instance.description = unicode(description)
        return instance

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = (
            'select id, code, source, kind, status, activity_id, max_usage_limit_per_code,'
            ' creation_time, effective_time, expire_time'
            ' from {.table_name} where id=%s').format(cls)
        params = (id_, )
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    def get_id_by_code(cls, code):
        sql = 'select id from {.table_name} where code=%s'.format(cls)
        params = (code, )
        rs = db.execute(sql, params)
        if rs:
            return rs[0][0]

    @classmethod
    def get_by_code(cls, code):
        id_ = cls.get_id_by_code(code)
        return cls.get(id_)

    @classmethod
    @cache(cache_ids_by_activity_id_key)
    def get_ids_by_activity_id(cls, activity_id):
        sql = 'select id from {.table_name} where activity_id=%s'.format(cls)
        params = (activity_id, )
        rs = db.execute(sql, params)
        if rs:
            return [r[0] for r in rs]

    @classmethod
    def get_by_activity_id(cls, activity_id):
        ids = cls.get_ids_by_activity_id(activity_id)
        return cls.get_multi_by_ids(ids)

    @classmethod
    def get_multi_by_ids(cls, ids):
        return [cls.get(str(id_)) for id_ in ids]

    def invalidate(self):
        sql = 'update {.table_name} set status = %s where id=%s'.format(self)
        params = (self.Status.invalidated.value, self.id_)
        db.execute(sql, params)
        db.commit()

        #: 清除缓存并更新兑换码可用状态
        self.clear_cache(self.id_)
        self.clear_cache_ids_by_activity_id(self.activity_id)
        self._clear_cached_properties()
        self.status = self.Status.invalidated

    @classmethod
    def create_multi_codes(cls, activity_id, kind, description,
                           max_usage_limit_per_code, redeem_code_count,
                           effective_time, expire_time):
        customized_code = None
        try:
            for _ in xrange(int(redeem_code_count)):
                cls.create(activity_id,
                           kind,
                           description,
                           max_usage_limit_per_code,
                           customized_code,
                           effective_time,
                           expire_time,
                           _commit=False)
        except:
            db.rollback()
            raise
        else:
            db.commit()

    def _redeem_normal_package(self, user, redeem_code_usage):
        distribute_welfare_gift(user,
                                self.activity.reward_welfare_package_kind,
                                redeem_code_usage)

    kind_to_strategy = {Kind.normal_package: _redeem_normal_package}

    def _apply_strategy(self, user, redeem_code_usage):
        strategy = self.kind_to_strategy[self.kind]
        return strategy(self, user, redeem_code_usage)

    def redeem(self, user):
        self.check_for_available(user)
        redeem_code_usage = RedeemCodeUsage.add(self, user)
        try:
            self._apply_strategy(user, redeem_code_usage)
        except (RedeemCodeIneffectiveError, RedeemCodeExpiredError,
                RedemptionBeyondLimitPerUserError):
            redeem_code_usage.delete_by_id(redeem_code_usage.id_)
            raise

    def check_for_available(self, user):
        if not self.is_effective:
            raise RedeemCodeIneffectiveError()
        if self.is_expired:
            raise RedeemCodeExpiredError()

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(id_=id_))

    @classmethod
    def clear_cache_ids_by_activity_id(cls, activity_id):
        mc.delete(
            cls.cache_ids_by_activity_id_key.format(activity_id=activity_id))

    def _clear_cached_properties(self):
        self.__dict__.pop('source', None)
        self.__dict__.pop('kind', None)
        self.__dict__.pop('activity', None)
Beispiel #19
0
class Mail(PropsMixin):
    table_name = 'email'

    def __init__(self, id_, sender, receiver, kind_id, creation_time):
        self.id_ = str(id_)
        self.sender = sender
        self.receiver = receiver
        self.kind_id = kind_id
        self.creation_time = creation_time

    mail_args = PropsItem('mail_args', '')
    sender_name = PropsItem('sender_name', '')

    def __repr__(self):
        return '<Mail id=%s>' % (self.id_)

    def get_uuid(self):
        return 'email:%s' % (self.id_)

    def get_db(self):
        return 'email'

    @cached_property
    def kind(self):
        return MailKind.get(self.kind_id)

    @cached_property
    def mail_body(self):
        return render_template(self.kind.template, **self.mail_args)

    @classmethod
    def _create(cls, sender, receiver, mail_kind, sender_name, commit_=True, **mail_args):
        if validate_email(receiver) != errors.err_ok:
            raise ValueError('email format is not valid!')

        sql = ('insert into {.table_name}'
               ' (sender, receiver, kind_id, creation_time)'
               ' values (%s, %s, %s, %s)').format(cls)
        params = (sender, receiver, mail_kind.id_, datetime.now())

        id_ = db.execute(sql, params)
        if commit_:
            db.commit()

        instance = cls.get(id_)
        instance.update_props_items({
            'mail_args': mail_args,
            'sender_name': sender_name,
        })
        return instance

    @classmethod
    def create(cls, receivers, mail_kind, sender='', sender_name='',
               callback=None, **mail_args):
        assert isinstance(mail_kind, MailKind)

        if isinstance(receivers, basestring):
            return cls._create(sender, receivers, mail_kind, sender_name, **mail_args)
        elif isinstance(receivers, list):
            multi_mails = []
            try:
                for receiver in receivers:
                    multi_mails.append(cls._create(
                        sender, receiver, mail_kind, sender_name, commit_=False, **mail_args))
            except:
                db.rollback()
                raise
            else:
                db.commit()
            return multi_mails
        else:
            raise TypeError('receivers need to be string or list!')

    def send(self):
        if not (current_app and current_app.debug):
            email_sender.produce(self.id_)

    @classmethod
    def send_multi(cls, mails):
        for mail in mails:
            mail.send()

    @classmethod
    def get(cls, id_):
        sql = ('select id, sender, receiver, kind_id, creation_time'
               ' from {.table_name} where id=%s').format(cls)
        params = (id_,)
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    def gets(cls, ids):
        return [cls.get(id) for id in ids]

    def delete(self):
        sql = 'delete from {.table_name} where id=%s'.format(self)
        params = (self.id_,)
        db.execute(sql, params)
        db.commit()
        self.clean_props_item()
Beispiel #20
0
class HoardOrder(PropsMixin):
    """The order entity created by users."""

    provider = yirendai

    table_name = 'hoard_order'
    cache_key = 'hoard:order:{id_}:v1'
    fin_order_cache_key = 'hoard:fin:order:{fin_order_id}'
    orders_by_user_cache_key = 'hoard:orders:{user_id}:2'
    cache_key_for_total_orders = 'hoard:orders:total:{user_id}:2'

    stashed_order_id = PropsItem('stashed_order_id')

    def get_uuid(self):
        return 'order:{.id_}'.format(self)

    def get_db(self):
        return 'hoard'

    def __init__(self, id_, service_id, user_id, creation_time, fin_order_id,
                 order_amount, order_id, bankcard_id, status):
        self.id_ = str(id_)
        self.service_id = str(service_id)
        self.user_id = str(user_id)
        self.creation_time = creation_time
        self.fin_order_id = str(fin_order_id) if fin_order_id else None
        self.order_amount = order_amount
        self.order_id = str(order_id) if order_id else None
        self.bankcard_id = str(bankcard_id) if bankcard_id else None
        self.status = OrderStatus(status)

    def __str__(self):
        return '<HoardOrder %s>' % self.id_

    def is_owner(self, user):
        """Checks the specific user is order owner or not."""
        return user and user.id == self.user_id

    @cached_property
    def user(self):
        return Account.get(self.user_id)

    @cached_property
    def profit_period(self):
        return self.service.profit_period['min']  # XXX: For API

    @cached_property
    def annual_rate(self):
        return self.service.profit_annual_rate['min']

    def bind_bankcard(self, bankcard):
        """Binds a bank card."""
        if not bankcard or bankcard.user_id != self.user_id:
            raise ValueError('invalid bankcard %r' % bankcard)
        sql = ('update {.table_name} set bankcard_id = %s '
               'where id = %s').format(self)
        params = (bankcard.id_, self.id_)
        self._commit_and_refresh(sql, params)

    def restore_bankcard(self, force=False):
        if not self.bankcard_id:
            raise ValueError('missing bankcard_id')
        try:
            return BankCard.restore(self.bankcard_id, self.user_id)
        except BankCardChanged as e:
            if force:
                new_bankcard_id = e.args[0]
                self.migrate_bankcard(self.bankcard_id, new_bankcard_id)
                self.bankcard_id = new_bankcard_id
                try:
                    del self.bankcard
                except AttributeError:
                    pass
                return self.bankcard
            else:
                raise

    @classmethod
    def migrate_bankcard(cls, old_bankcard_id, new_bankcard_id):
        order_ids = cls.get_id_list_by_bankcard_id(old_bankcard_id)
        sql = ('update {.table_name} set bankcard_id = %s '
               'where id = %s').format(cls)
        for order_id in order_ids:
            db.execute(sql, (new_bankcard_id, order_id))
        db.commit()
        for order_id in order_ids:
            order = cls.get(order_id)
            order.clear_cache()
        rsyslog.send('%s to %s\t%r' %
                     (old_bankcard_id, new_bankcard_id, order_ids),
                     tag='hoard_migrate_bankcard')

    def track_for_payment(self):
        if self.status is OrderStatus.unpaid:
            mq_payment_tracking.produce(self.id_)

    def mark_as_paid(self, order_id):
        """Marks this order as paid.

        :param order_id: the ``orderNo`` from Yixin API.
        """
        if self.is_success:
            raise DuplicatePaymentError('order has been paid', self.id_)

        sql = ('update {.table_name} set order_id = %s, status = %s'
               'where id = %s').format(self)
        params = (order_id, OrderStatus.paid.value, self.id_)
        self._commit_and_refresh(sql, params)

        # trigger event
        yrd_order_paid.send(self)

        # request to confirm
        mq_confirming.produce(self.id_)

    def mark_as_confirmed(self):
        if self.status is OrderStatus.unpaid:
            raise ValueError('order has not been paid', self.id_)
        if self.status is OrderStatus.confirmed:
            return

        sql = ('update {.table_name} set status = %s'
               'where id = %s').format(self)
        params = (OrderStatus.confirmed.value, self.id_)
        self._commit_and_refresh(sql, params)

        # trigger event
        yrd_order_confirmed.send(self)

        # request to register withdrawing
        mq_withdrawing.produce(self.id_)

    def mark_as_exited(self):
        if self.status is OrderStatus.exited:
            return

        sql = ('update hoard_order set status = %s '
               'where id = %s').format(self)
        params = (OrderStatus.exited.value, self.id_)
        self._commit_and_refresh(sql, params)

        # remove event push code for batch updating
        # trigger event
        # yrd_order_exited.send(self)

        # request exit notification sms when order exit is caught in time
        # expect_exit_date = (
        #    self.creation_time.date() + relativedelta(days=self.profit_period.value))
        # if (datetime.date.today() - expect_exit_date).days < 6:
        #    mq_sms_sender.produce(self.id_)

    def track_for_exited(self):
        if self.status is OrderStatus.exited:
            return

        # request to check status
        mq_exiting_checker.produce(self.id_)

    def mark_as_failure(self):
        if self.status is OrderStatus.confirmed or OrderStatus.exited:
            raise DuplicatePaymentError('order has been confirmed', self.id_)

        sql = ('update {.table_name} set status = %s'
               'where id = %s').format(self)
        params = (OrderStatus.failure.value, self.id_)
        self._commit_and_refresh(sql, params)

        # trigger event
        yrd_order_failure.send(self)

    @classmethod
    def get_orders_by_period(cls, date_from, date_to, closure):
        sql = ('select id from hoard_order where status=%s '
               'and date(creation_time) between %s and %s').format(cls)
        params = (OrderStatus.confirmed.value, date_from, date_to)
        rs = db.execute(sql, params)
        orders = (cls.get(r[0]) for r in rs)
        return [
            order for order in orders
            if int(order.service.frozen_time) == closure
        ]

    def _commit_and_refresh(self, sql, params):
        # 执行SQL并提交
        db.execute(sql, params)
        db.commit()

        # 清除数据库缓存
        self.clear_cache()

        # 刷新实例内属性值
        new_state = vars(self.get(self.id_))
        vars(self).update(new_state)

        # 清除实例内缓存
        self.__dict__.pop('bankcard', None)

    @property
    def is_success(self):
        """``True`` if this order has been paid."""
        return self.status in [
            OrderStatus.confirmed, OrderStatus.paid, OrderStatus.exited
        ]

    @property
    def is_failure(self):
        return self.status == OrderStatus.failure

    @cached_property
    def service(self):
        return YixinService.get(self.service_id)

    @cached_property
    def due_date(self):
        order_info, _ = self._fetch_order_info()
        return order_info['frozenDatetime']

    @classmethod
    def check_before_adding(cls, service_id, user_id, order_amount):
        yixin_service = YixinService.get(service_id)
        yixin_account = YixinAccount.get_by_local(user_id)
        user = Account.get(user_id)

        # checks the related entities
        if not yixin_service:
            raise NotFoundError(service_id, YixinService)
        if not user:
            raise NotFoundError(user_id, Account)
        if not yixin_account:
            raise UnboundAccountError(user_id)

        # checks the identity
        if not has_real_identity(user):
            raise InvalidIdentityError

        # checks available
        if yixin_service.sell_out:
            raise SellOutError(yixin_service.uuid)
        if yixin_service.take_down:
            raise TakeDownError(yixin_service.uuid)

        # checks the amount type
        if not isinstance(order_amount, decimal.Decimal):
            raise TypeError('order_amount must be decimal')

        # checks the amount range
        amount_range = (yixin_service.invest_min_amount,
                        yixin_service.invest_max_amount)
        if (order_amount.is_nan() or order_amount < 0
                or order_amount < yixin_service.invest_min_amount
                or order_amount > yixin_service.invest_max_amount):
            raise OutOfRangeError(order_amount, amount_range)

    @classmethod
    def add(cls,
            service_id,
            user_id,
            order_amount,
            fin_order_id,
            creation_time=None):
        """Creates a unpaid order.

        :param service_id: the UUID of chosen P2P service.
        :param user_id: the user id of order creator.
        :param order_amount: the payment amount for this order.
        :param fin_order_id: the UUID of remote order.
        :returns: the created order.
        """
        cls.check_before_adding(service_id, user_id, order_amount)

        creation_time = creation_time or datetime.datetime.now()
        sql = ('insert into {.table_name} (service_id, user_id, order_amount,'
               ' fin_order_id, creation_time, status) '
               'values (%s, %s, %s, %s, %s, %s)').format(cls)
        params = (service_id, user_id, order_amount, fin_order_id,
                  creation_time, OrderStatus.unpaid.value)

        id_ = db.execute(sql, params)
        db.commit()

        order = cls.get(id_)
        order.clear_cache()
        return order

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = ('select id, service_id, user_id, creation_time, fin_order_id,'
               ' order_amount, order_id, bankcard_id, status '
               'from {.table_name} where id = %s').format(cls)
        params = (id_, )

        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    def get_by_order_no(cls, order_id):
        sql = ('select id from {.table_name} where order_id = %s').format(cls)
        params = (order_id, )

        rs = db.execute(sql, params)
        if rs:
            return cls.get(rs[0][0])

    @classmethod
    @cache(fin_order_cache_key)
    def get_id_by_fin_order_id(cls, fin_order_id):
        sql = ('select id from {.table_name} '
               'where fin_order_id = %s').format(cls)
        params = (fin_order_id, )

        rs = db.execute(sql, params)
        if rs:
            return rs[0][0]

    @classmethod
    def get_by_fin_order_id(cls, fin_order_id):
        id = cls.get_id_by_fin_order_id(fin_order_id)
        if id:
            return cls.get(id)

    @classmethod
    @cache(orders_by_user_cache_key)
    def get_id_list_by_user_id(cls, user_id):
        sql = ('select id from {.table_name} where user_id = %s '
               'order by creation_time desc').format(cls)
        params = (user_id, )

        rs = db.execute(sql, params)
        if rs:
            return [r[0] for r in rs]

    @classmethod
    def gets_by_user_id(cls, user_id):
        """get all paid orders by user"""
        id_list = cls.get_id_list_by_user_id(user_id)
        orders = [cls.get(id_) for id_ in id_list or []]
        return [o for o in orders if o.is_success]

    @classmethod
    def gets_by_user_in_period(cls, user_id, start, end):
        """get user successed orders in period"""
        id_list = cls.get_id_list_by_user_id(user_id)
        if id_list:
            orders = [cls.get(id_) for id_ in id_list]
            successful_orders = [
                order for order in orders
                if order.status is OrderStatus.confirmed or OrderStatus.exited
            ]
            return [
                o for o in successful_orders if start <= o.creation_time < end
            ]

    @classmethod
    def gets_by_date(cls, date):
        sql = ('select id from {.table_name} '
               'where DATE(creation_time) = %s ').format(cls)
        params = (date, )

        rs = db.execute(sql, params)
        if rs:
            return [cls.get(r[0]) for r in rs]

    @classmethod
    def get_id_list_by_bankcard_id(cls, bankcard_id):
        sql = 'select id from {.table_name} where bankcard_id = %s'.format(cls)
        params = (bankcard_id, )

        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    def get_multi_by_bankcard(cls, bankcard_id):
        id_list = cls.get_id_list_by_bankcard_id(bankcard_id)
        orders = [cls.get(id_) for id_ in id_list or []]
        return [o for o in orders if o.is_success]

    def clear_cache(self):
        mc.delete(self.cache_key.format(id_=self.id_))
        mc.delete(self.cache_key_for_total_orders.format(user_id=self.user_id))
        mc.delete(self.orders_by_user_cache_key.format(user_id=self.user_id))
        mc.delete(
            self.fin_order_cache_key.format(fin_order_id=self.fin_order_id))

    @classmethod
    @cache(cache_key_for_total_orders)
    def get_total_orders(cls, user_id):
        sql = ('select count(id) from {.table_name} where user_id=%s '
               'and (status=%s or status=%s or status=%s)').format(cls)
        params = (user_id, OrderStatus.paid.value, OrderStatus.confirmed.value,
                  OrderStatus.exited.value)
        rs = db.execute(sql, params)
        return rs[0][0]

    @cached_property
    def bankcard(self):
        if not self.bankcard_id:
            return
        return BankCard.get(self.bankcard_id)

    @classmethod
    def gets_by_month(cls, date):
        # start, end should be 'yyyy-mm'
        year, month = date.split('-')

        sql = ('select id from {.table_name} where year(creation_time) = %s '
               'and month(creation_time) = %s').format(cls)
        params = (year, month)

        rs = db.execute(sql, params)
        if rs:
            return [cls.get(r[0]) for r in rs]

    def register_for_withdrawing(self, client, token):
        return client.query.set_finance_exit_bank_info(
            token, self.fin_order_id, self.bankcard.bank.yxlib_id,
            self.bankcard.city_id, self.bankcard.province_id,
            self.bankcard.card_number, self.bankcard.local_bank_name)

    @cached_property
    def expected_profit(self):
        """预期的到期总收益."""
        order_info, order_status = self._fetch_order_info()
        if order_status == u'已转出':
            return decimal.Decimal(order_info['incomeAmount'])
        monthly_ratio = self.service.expected_income / 100 / 12
        result = monthly_ratio * self.order_amount * self.service.frozen_time
        return round_half_up(result, 2)

    def fetch_status(self, orders=None):
        """该订单在宜人贷的状态."""
        order_info, order_status = self._fetch_order_info(orders)
        if order_info and order_status:
            return order_status
        return getattr(self, '_order_status', None)  # for testing

    def fetch_daily_profit(self, orders=None):
        """该订单平均到每日的收益."""
        amount = decimal.Decimal(self.order_amount)
        order_info, order_status = self._fetch_order_info(orders)
        if not order_info or not order_status:
            return decimal.Decimal(0)
        if order_status not in [u'攒钱中', u'转出中']:
            return decimal.Decimal(0)
        return (amount * decimal.Decimal(order_info['expectedIncome']) / 100 /
                365)

    def fetch_profit_until(self, date, orders=None):
        """该订单到某日为止的累计收益."""
        order_info, order_status = self._fetch_order_info(orders)

        # 已转出则返回已结算的收益
        if order_status == u'已转出':
            return decimal.Decimal(order_info['incomeAmount'])

        # 未转出则使用预期每日收益按日累加
        invest_date = arrow.get(order_info['investDate']).date()
        days = (date - invest_date).days
        if days <= 0:
            return decimal.Decimal(0)
        return days * self.fetch_daily_profit(orders)

    def _fetch_orders(self):
        from .profile import HoardProfile
        profile = HoardProfile.add(self.user_id)
        return profile.orders()

    def _fetch_order_info(self, orders=None):
        orders = orders or self._fetch_orders()
        for order, order_info, order_status in orders:
            if order.id_ == self.id_:
                return order_info, order_status
        return None, None
Beispiel #21
0
class XMLoansDigest(PropsMixin):
    table_name = 'hoard_xm_loans_digest'
    cache_key = 'hoard:xm:loans_digest:id:{id_}:v1'
    cache_key_by_asset_id = 'hoard:xm:loans_digest:asset_id:{asset_id}:v1'

    # 出借咨询与服务协议
    contract_no = PropsItem('contract_no', '', unicode_type)

    # 资金出借/回收方式
    reinvest = PropsItem('reinvest', '', unicode_type)

    # 实际出借金额
    principle_amount = PropsItem('principle_amount', 0, Decimal)

    # 协议与债权占比(持有比例)
    receipt_hold_scale = PropsItem('receipt_hold_scale', 0, Decimal)

    # 初始出借日期
    invest_start_date = PropsItem('invest_start_date', '', date_type)

    def __init__(self, id_, asset_id, creation_time):
        self.id_ = id_
        self.asset_id = asset_id
        self.creation_time = creation_time

    def get_db(self):
        return 'hoard'

    def get_uuid(self):
        return 'xm:loans_digest:{0}'.format(self.id_)

    @property
    def loans(self):
        return XMLoan.get_multi_by_loans_digest_id(self.id_)

    @classmethod
    def create(cls, asset, loans_digest_info):
        assert isinstance(asset, XMAsset)
        assert isinstance(loans_digest_info, CreditorRights)

        sql = 'insert into {.table_name} (asset_id, creation_time) values (%s, %s)'.format(
            cls)
        params = (asset.id_, datetime.datetime.now())

        id_ = db.execute(sql, params)
        db.commit()
        instance = cls.get(id_)

        instance.update_props_items({
            'contract_no':
            loans_digest_info['loan_receipt_no'],
            'reinvest':
            loans_digest_info['invest_lending_type'],
            'principle_amount':
            str(loans_digest_info['loan_receipt_amt']),
            'receipt_hold_scale':
            loans_digest_info['receipt_hold_scale'],
            'invest_start_date':
            loans_digest_info.start_date.isoformat()
        })

        # 创建借贷人记录
        loans = loans_digest_info
        try:
            XMLoan.create(instance, loans, _commit=False)
        except:
            db.rollback()
            raise
        else:
            db.commit()

        cls.clear_cache(id_)
        cls.clear_cache_by_asset_id(asset.id_)
        return instance

    @classmethod
    def update(cls, loans_digest, loans_digest_info):
        assert isinstance(loans_digest_info, CreditorRights)
        assert isinstance(loans_digest, XMLoansDigest)

        loans = loans_digest_info.loans
        if len(loans) > len(loans_digest.loans):
            loan_receipt_no_list = [
                loan.loan_receipt_no for loan in loans_digest.loans
            ]
            try:
                for loan in loans:
                    if loan.loan_receipt_no not in loan_receipt_no_list:
                        XMLoan.create(loans_digest, loan, _commit=False)
            except:
                db.rollback()
                raise
            else:
                db.commit()
        return loans_digest

    @classmethod
    def create_or_update(cls, asset, loans_digest_info):
        loans_digest = cls.get_by_asset_id(asset.id_)
        if loans_digest:
            return cls.update(loans_digest, loans_digest_info)
        else:
            return cls.create(asset, loans_digest_info)

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = 'select id, asset_id, creation_time from {.table_name} where id=%s'.format(
            cls)
        params = (id_, )
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    @cache(cache_key_by_asset_id)
    def get_id_by_asset_id(cls, asset_id):
        sql = 'select id from {.table_name} where asset_id=%s'.format(cls)
        params = (asset_id, )
        rs = db.execute(sql, params)
        if rs:
            return str(rs[0][0])

    @classmethod
    def get_by_asset_id(cls, asset_id):
        return cls.get(cls.get_id_by_asset_id(asset_id))

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(id_=id_))

    @classmethod
    def clear_cache_by_asset_id(cls, asset_id):
        mc.delete(cls.cache_key_by_asset_id.format(asset_id=asset_id))
Beispiel #22
0
class Package(PropsMixin):
    table_name = 'insurance_package'
    insurance_ability = PropsItem('insurance_ability', '')
    addition_ability = PropsItem('addition_ability', '')
    name = PropsItem('name', u'基础套餐')
    title = PropsItem('title')
    sub_title = PropsItem('sub_title')
    quota = PropsItem('quota')
    quota_b = PropsItem('quota_b', 'quota_b commedy')
    radar = PropsItem('radar')

    def __init__(self, package_id,
                 insurance_id,
                 rec_rank_in_package,
                 package_rec_rank):
        self.id = package_id
        self.insurance_id = insurance_id
        self.rec_rank_in_package = rec_rank_in_package
        self.package_rec_rank = package_rec_rank

    def get_db(self):
        return 'package_insurance'

    def get_uuid(self):
        return 'package_insurance:ability:%s' % self.id

    @classmethod
    def add(cls, package_id, pkg_name, insurance_id, insurance_name, status,
            rec_rank_in_package, package_rec_rank):
        sql = ('insert into {.table_name} '
               '(package_id, pkg_name, insurance_id, insurance_name, status, '
               'rec_rank_in_package, package_rec_rank, create_time, update_time) '
               'values ( %s, %s, %s, %s, %s, %s, %s ,%s, %s)').format(cls)

        params = (package_id, pkg_name, insurance_id, insurance_name, status,
                  rec_rank_in_package, package_rec_rank,
                  datetime.now(), datetime.now())
        db.execute(sql, params)
        db.commit()

        cls.clear_cache(package_id)
        cls.get(package_id)

    @classmethod
    @cache(PACKAGE_CACHE_KEY)
    def get(cls, package_id):
        sql = ('select package_id, insurance_id,rec_rank_in_package, '
               'package_rec_rank from {.table_name} where package_id = %s').format(cls)
        param = package_id
        rs = db.execute(sql, param)
        return [cls(*item)
                for item in rs if item is not None]

    @classmethod
    def clear_cache(cls, package_id):
        mc.delete(PACKAGE_CACHE_KEY.format(package_id=package_id))

    @classmethod
    def get_by_insurance_id(cls, insurance_id):
        sql = ('select package_id, insurance_id from {.table_name} '
               'where insurance_id = %s').format(cls)
        param = insurance_id
        rs = db.execute(sql, param)
        pkg_ids = [str(pkg_id) for pkg_id in rs]
        return [cls.get(pkg_id) for pkg_id in pkg_ids]

    # get package by package_id and insurance_id
    @classmethod
    def get_by_pkg_id_insurance_id(cls, pkg_id, insurance_id):
        return cls(pkg_id, insurance_id)

    def cat_ability(self, ageobj):
        if (ageobj.birth[0] == 0 and ageobj.birth[1] < 60 and
                self.id in [PACKAGE_UPGRADE_1, PACKAGE_UPGRADE_2]):
            return ''.join([self.insurance_ability, self.addition_ability])
        return self.insurance_ability
Beispiel #23
0
class Article(PropsMixin):
    # need to add
    # kind is uuid
    # type is article type

    _const = {}
    kind = 'article'
    type = 'article'

    # admin record
    _add_admin = PropsItem('add_admin', '')
    _publish_admin = PropsItem('publish_admin', '')
    _delete_admin = PropsItem('delete_admin', '')

    def __init__(self, id, category, create_time, update_time, publish_time,
                 status):
        self.id = str(id)
        self.category = str(category)
        self.create_time = create_time
        self.update_time = update_time
        self.publish_time = publish_time
        self.status = int(status)

    def __repr__(self):
        return '<Article id=%s, type=%s, status=%s>' % (self.id, self.type,
                                                        self.status)

    def get_uuid(self):
        return '%s:content:%s' % (self.kind, self.id)

    def get_db(self):
        # All the articles save in one CouchDB
        return 'article'

    @classmethod
    @cache(ARTICLE_CACHE_KEY % '{doc_id}')
    def get(cls, doc_id):
        from .viewpoint import ViewPoint
        from .question import Question
        from .fundweekly import FundWeekly
        doc_id = str(doc_id)
        rs = db.execute(
            'select id, type, category, create_time, update_time, '
            'publish_time, '
            'status from article where id=%s', (doc_id, ))
        if rs:
            (id, type, category, create_time, update_time, publish_time,
             status) = rs[0]
            if type == VIEWPOINT.TYPE:
                return ViewPoint(id, category, create_time, update_time,
                                 publish_time, status)
            elif type == QUESTION.TYPE:
                return Question(id, category, create_time, update_time,
                                publish_time, status)
            elif type == FUNDWEEKLY.TYPE:
                return FundWeekly(id, category, create_time, update_time,
                                  publish_time, status)

    @classmethod
    def gets(cls, doc_ids):
        return [cls.get(id) for id in doc_ids]

    @classmethod
    @pcache(ARTICLE_CATE_CACHE_KEY % ('{type}', '{category}', '{status}'),
            count=20)
    def _get_article_ids_by_type_and_category(cls,
                                              type,
                                              category,
                                              status=STATUS.PUBLISHED,
                                              start=0,
                                              limit=20):
        rs = db.execute(
            'select id '
            'from article '
            'where type=%s and category=%s and status=%s '
            'order by publish_time desc limit %s, %s',
            (type, category, status, start, limit))
        return [str(id) for (id, ) in rs]

    @classmethod
    def get_articles_by_type_and_category(cls,
                                          type,
                                          category,
                                          status=STATUS.PUBLISHED,
                                          start=0,
                                          limit=20):
        ids = cls._get_article_ids_by_type_and_category(
            type, category, status, start, limit)
        return cls.gets(ids)

    @classmethod
    def get_articles_by_category(cls,
                                 category,
                                 status=STATUS.PUBLISHED,
                                 start=0,
                                 limit=20):
        return cls.get_articles_by_type_and_category(cls.type, category,
                                                     status, start, limit)

    @classmethod
    @pcache(ARTICLE_ALL_CACHE_KEY % ('{type}', '{status}'))
    def _get_all_ids(cls, type, status=STATUS.PUBLISHED, start=0, limit=20):
        rs = db.execute(
            'select id from article '
            'where status=%s and type=%s '
            'order by publish_time desc limit %s,%s ',
            (status, type, start, limit))
        ids = [str(id) for (id, ) in rs]
        return ids

    @classmethod
    def get_all(cls, type=None, status=STATUS.PUBLISHED, start=0, limit=20):
        if type is None:
            type = cls.type
        ids = cls._get_all_ids(type=type,
                               status=status,
                               start=start,
                               limit=limit)
        return cls.gets(ids)

    @classmethod
    @cache(ARTICLE_COUNT_CACHE_KEY % ('{type}', '{status}'))
    def get_count(cls, type=None, status=STATUS.PUBLISHED):
        if type is None:
            type = cls.type
        return db.execute(
            'select count(id) from article '
            'where status=%s and type=%s', (status, type))[0][0]

    @classmethod
    def get_count_by_category(cls, category):
        ids = cls._get_article_ids_by_type_and_category(cls.kind, category)
        return len(ids)

    @classmethod
    def add(cls, type=None, category=0, create_time=None, admin_id=None):
        type = type or cls.type
        create_time = create_time or datetime.now()
        try:
            id = db.execute(
                'insert into article '
                '(type, category, create_time) '
                'values (%s, %s, %s)', (type, category, create_time))
            if id:
                db.commit()
                article = cls.get(id)
                article.clear_cache()
                article._add_admin = admin_id
                return article
            else:
                db.rollback()
        except IntegrityError:
            db.rollback()
            warn('insert article failed')

    def is_deleted(self):
        return self.status == STATUS.DELETED

    def is_published(self):
        return self.status == STATUS.PUBLISHED

    @property
    def category_name(self):
        _cate_type = self._const.CATEGORY
        if _cate_type:
            return _cate_type.get(self.category)

    def _upadte_status(self, status):
        db.execute('update article set status=%s where id=%s',
                   (status, self.id))
        db.commit()
        self.clear_cache()

    def delete(self):
        self._upadte_status(STATUS.DELETED)

    def publish(self, publish_time=None):
        publish_time = publish_time or datetime.now()
        db.execute('update article set publish_time=%s where id=%s',
                   (publish_time, self.id))
        self._upadte_status(STATUS.PUBLISHED)
        self.clear_cache()

    def hide(self):
        self._upadte_status(STATUS.NONE)

    def clear_cache(self):
        mc.delete(ARTICLE_CACHE_KEY % (self.id))
        for name, status in STATUS.items():
            mc.delete(ARTICLE_ALL_CACHE_KEY % (self.type, status))
            mc.delete(ARTICLE_ALL_TYPE_CACHE_KEY % (status))
            mc.delete(ARTICLE_COUNT_CACHE_KEY % (self.type, status))
            mc.delete(ARTICLE_CATE_CACHE_KEY %
                      (self.type, self.category, status))
Beispiel #24
0
class ShortMessage(PropsMixin):
    """短消息"""

    # default tag
    tag = u'好规划'

    # storage of sms sending info
    receiver_mobile = PropsItem('receiver_mobile', '')
    sms_kind_id = PropsItem('sms_kind_id', '')
    sms_args = PropsItem('sms_args', {})
    is_sent = PropsItem('is_sent', False)

    def __init__(self, uuid):
        self.uuid = UUID(uuid)

    def get_db(self):
        return 'sms'

    def get_uuid(self):
        return 'item:{uuid}'.format(uuid=self.uuid.hex)

    @property
    def kind(self):
        return ShortMessageKind.get(self.sms_kind_id)

    @classmethod
    def create(cls, mobile, sms_kind, user_id=None, **sms_args):
        """为已注册用户发送短信"""
        assert isinstance(sms_kind, ShortMessageKind)

        if validate_phone(mobile) != errors.err_ok:
            raise ValueError(u'invalid mobile %s' % mobile)

        if sms_kind.need_verify:
            if not (user_id and Account.get(user_id)):
                raise ValueError(u'unable to verify user %s' % user_id)
            v = Verify.add(user_id, sms_kind.verify_type,
                           sms_kind.verify_delta)
            sms_args.update(verify_code=v.code)

        sms = cls(uuid4().hex)
        # simply check formatting
        sms_kind.content.format(**sms_args)
        sms.update_props_items({
            u'receiver_mobile': mobile,
            u'sms_kind_id': sms_kind.id_,
            u'sms_args': sms_args
        })
        return sms

    @classmethod
    def get(cls, uuid):
        return cls(uuid) if uuid else None

    def send_async(self):
        """将短信发送加入队列进行异步发送"""
        if not self.is_sent:
            mq_sms_sender.produce(self.uuid.hex)

    def send(self, provider=None):
        """即时发送短信"""
        if self.is_sent:
            return True

        for k, v in self.sms_args.items():
            self.sms_args[k] = v.decode('utf-8')
        text = self.kind.content.format(**self.sms_args)

        # 如果不强制使用其他服务商,则经由短信类型偏好服务商发送
        channel = provider or self.kind.prefer_provider
        result = SMSClient.send(self.receiver_mobile, text, self.tag, channel)
        if result:
            rsyslog.send(u'\t'.join([
                self.uuid.hex, self.receiver_mobile,
                str(self.sms_kind_id), text
            ]),
                         tag=u'sms_history')
            self.is_sent = True
        return result
Beispiel #25
0
class Notification(EntityModel, PropsMixin, PushSupport):
    """消息通知"""

    table_name = 'notification'
    cache_key = 'notification:{id_}:v1'
    cache_key_by_user_id = 'notification:user:{user_id}:v1'
    cache_key_by_user_unread = 'notification:user:{user_id}:unread:v1'
    cache_key_by_user_and_kind = 'notification:user:{user_id}:kind:{kind_id}:v1'

    #: the properties to render(id of linked entity is recommended)
    properties = PropsItem('properties', {})

    def __init__(self, id_, user_id, kind_id, is_read, creation_time, read_time):
        self.id_ = str(id_)
        self.user_id = str(user_id)
        self.kind_id = str(kind_id)
        self.is_read = bool(is_read)
        self.creation_time = creation_time
        self.read_time = read_time

    def get_uuid(self):
        return 'item:{id_}'.format(id_=self.id_)

    def get_db(self):
        return 'notification'

    @property
    def status(self):
        return self.Status(self._status)

    @cached_property
    def user(self):
        return Account.get(self.user_id)

    @cached_property
    def kind(self):
        return NotificationKind.get(self.kind_id)

    @cached_property
    def template(self):
        return render_template(
            self.kind.common_template_location, palette=self, link=self.kind.web_target_link)

    @cached_property
    def title(self):
        # 暂要求单播通知不使用消息类型中的默认标题
        return render_template_def(
            self.kind.common_template_location, 'notification_title', palette=self).strip()

    @cached_property
    def timestamp(self):
        return render_template_def(
            self.kind.common_template_location, 'notification_timestamp', palette=self).strip()

    @cached_property
    def content(self):
        # 暂要求单播通知不使用消息类型中的默认内容
        return render_template_def(
            self.kind.common_template_location, 'notification_content', palette=self).strip()

    def mark_as_read(self):
        """标记消息为已读"""
        if self.is_read:
            return

        read_time = datetime.now()
        sql = 'update {.table_name} set is_read=%s, read_time=%s where id=%s'.format(self)
        params = (True, read_time, self.id_)
        db.execute(sql, params)
        db.commit()

        # 刷新实例属性并清除缓存
        self.is_read = True
        self.read_time = read_time
        self.clear_cache(self.id_)
        self.clear_cache_by_user(self.user_id)
        self.clear_cache_by_user_and_kind(self.user_id, self.kind_id)

    @property
    def allow_push(self):
        return self.kind.allow_push

    @property
    def is_unicast_push_only(self):
        return self.kind.is_unicast_push_only

    @property
    def push_platforms(self):
        """实际推送平台将由具体类型优先定夺,以类型定义为fallback默认值"""
        from core.models.welfare import Package

        if self.allow_push:
            if self.kind is welfare_gift_notification:
                # 礼包类型单播推送
                package = Package.get(self.properties.get('welfare_package_id'))
                return package.kind.push_platforms
            return self.kind.push_platforms

    def make_push_pack(self, audience, platform):
        """创建单播通知(面向设备)的推送"""
        from core.models.welfare import Package
        from core.models.pusher.element import Pack, Notice, SingleDeviceAudience

        assert isinstance(audience, SingleDeviceAudience)
        assert isinstance(platform, Platform)

        if self.allow_push:
            if self.kind is welfare_gift_notification:
                # 礼包类型单播推送
                package = Package.get(self.properties.get('welfare_package_id'))
                notice = Notice(
                    package.kind.description or self.content, title=self.title)
            else:
                # 其他类型单播推送
                notice = Notice(self.content, title=self.title)

            return Pack(
                audience, notice, [platform],
                target_link=self.kind.app_target_link,
                needs_following_up=True)

    @classmethod
    def create(cls, user_id, kind, properties=None):
        assert isinstance(kind, NotificationKind)
        assert properties is None or isinstance(properties, dict)

        # 校验参数
        user = Account.get(user_id)
        if not user:
            raise ValueError('invalid user id')

        if kind.is_once_only:
            id_list = cls.get_id_list_by_user_and_kind(user.id_, kind.id_)
            if id_list:
                return cls.get(id_list[0])

        sql = ('insert into {.table_name} (user_id, kind_id, is_read, '
               'creation_time) values (%s, %s, %s, %s)').format(cls)
        params = (user_id, kind.id_, False, datetime.now())
        id_ = db.execute(sql, params)
        db.commit()

        instance = cls.get(id_)
        instance.properties = properties or {}

        # 单播消息则提交并加入推送队列
        cls.clear_cache_by_user(user.id_)
        cls.clear_cache_by_user_and_kind(user.id_, kind.id_)

        # 由推送控制中心来记录和完成推送
        if kind.allow_push:
            mq_notification_push.produce(str(id_))
        return instance

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = ('select id, user_id, kind_id, is_read, creation_time, '
               'read_time from {.table_name} where id=%s').format(cls)
        params = (id_,)
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    @cache(cache_key_by_user_id)
    def get_id_list_by_user_id(cls, user_id):
        sql = ('select id from {.table_name} where user_id=%s '
               'order by creation_time desc').format(cls)
        params = (user_id,)
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    @cache(cache_key_by_user_unread)
    def get_unread_id_list_by_user_id(cls, user_id):
        sql = ('select id from {.table_name} where user_id=%s '
               'and is_read=%s order by creation_time desc').format(cls)
        params = (user_id, False)
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    @cache(cache_key_by_user_and_kind)
    def get_id_list_by_user_and_kind(cls, user_id, kind_id):
        sql = ('select id from {.table_name} where user_id=%s '
               'and kind_id=%s order by creation_time desc').format(cls)
        params = (user_id, kind_id)
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    def get_multi_by_user(cls, user_id):
        id_list = cls.get_id_list_by_user_id(user_id)
        return cls.get_multi(id_list)

    @classmethod
    def get_multi_unreads_by_user(cls, user_id):
        id_list = cls.get_unread_id_list_by_user_id(user_id)
        return cls.get_multi(id_list)

    @classmethod
    def get_multi_by_user_and_kind(cls, user_id, kind_id):
        id_list = cls.get_id_list_by_user_and_kind(user_id, kind_id)
        return cls.get_multi(id_list)

    @classmethod
    def get_multi(cls, id_list):
        return [cls.get(id_) for id_ in id_list]

    @classmethod
    def get_merged_popout_template(cls, notifications):
        kinds = list(set([n.kind for n in notifications]))
        if len(kinds) > 1:
            raise ValueError('template merge of different notifications is not supported')
        return render_template(kinds[0].popout_template_location, palettes=notifications)

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(**locals()))

    @classmethod
    def clear_cache_by_user(cls, user_id):
        mc.delete(cls.cache_key_by_user_id.format(**locals()))
        mc.delete(cls.cache_key_by_user_unread.format(**locals()))

    @classmethod
    def clear_cache_by_user_and_kind(cls, user_id, kind_id):
        mc.delete(cls.cache_key_by_user_and_kind.format(**locals()))
Beispiel #26
0
class Asset(PropsMixin):
    """资产"""

    table_name = 'hoarder_asset'

    class Status(Enum):
        """本地资产状态"""
        unpaid = 'U'
        earning = 'E'
        withdrawing = 'W'
        redeemed = 'R'
        cancel = 'C'

    cache_key = 'hoarder:asset:{asset_id}:v1'
    user_cache_key = 'hoarder:assets:user:{user_id}:{product_id}:v1'
    product_cache_key = 'hoarder:assets:all_ids:{product_id}:v1'
    order_code_cache_key = 'hoarder:asset:order:{order_code}:{product_id}:v1'
    user_id_cache_key = 'hoarder:assets:user_id:{user_id}:v1'

    # 实时年化收益率(日日盈类产品每日更新)
    actual_annual_rate = PropsItem('actual_annual_rate', 0.00, Decimal)
    # 累计收益
    hold_profit = PropsItem('hold_profit', 0.00, Decimal)
    # 持有资产
    hold_amount = PropsItem('hold_amount', 0.00, Decimal)
    # 未到账资产
    uncollected_amount = PropsItem('uncollected_amount', 0.00, Decimal)
    # 昨日收益
    yesterday_profit = PropsItem('yesterday_profit', 0, Decimal)
    # 剩余免费赎回次数
    residual_redemption_times = PropsItem('residual_redemption_times', 0, int)

    def __init__(self, id_, asset_no, order_code, bankcard_id, bank_account,
                 product_id, user_id, status, remote_status, annual_rate,
                 create_amount, current_amount, fixed_service_fee,
                 service_fee_rate, base_interest, expect_interest,
                 current_interest, interest_start_date, interest_end_date,
                 expect_payback_date, buy_time, creation_time, update_time):
        self.id_ = id_
        self.asset_no = asset_no
        self.order_code = order_code
        self.bankcard_id = bankcard_id
        self.bank_account = bank_account
        self.product_id = product_id
        self.user_id = user_id
        self._status = status
        self.remote_status = remote_status
        self.annual_rate = annual_rate
        self.create_amount = create_amount
        self.current_amount = current_amount
        self.fixed_service_fee = fixed_service_fee
        self.service_fee_rate = service_fee_rate
        self.base_interest = base_interest
        self.expect_interest = expect_interest
        self.current_interest = current_interest
        self.interest_start_date = interest_start_date
        self.interest_end_date = interest_end_date
        self.expect_payback_date = expect_payback_date
        self.buy_time = buy_time
        self.creation_time = creation_time
        self.update_time = update_time

    def __str__(self):
        return '<Asset %s>' % self.id_

    def get_db(self):
        return 'hoarder'

    def get_uuid(self):
        return 'hoarder:asset:{.id_}'.format(self)

    def is_owner(self, user):
        return user and user.id_ == self.user_id

    @cached_property
    def product(self):
        return Product.get(self.product_id)

    @cached_property
    def user(self):
        return Account.get(self.user_id)

    @property
    def bankcard(self):
        if not self.bankcard_id:
            return
        return BankCard.get(self.bankcard_id)

    @property
    def status(self):
        return self.Status(self._status)

    @status.setter
    def status(self, item):
        sql = 'update {.table_name} set status=%s where id=%s;'.format(self)
        params = (
            item.value,
            self.id_,
        )
        db.execute(sql, params)
        db.commit()
        self.clear_cache()
        self._status = item.value

    @property
    def display_status(self):
        return {
            'U': u'处理中',
            'E': u'攒钱中',
            'W': u'转出中',
            'R': u'已转出',
            'C': u'已取消',
        }.get(self.status.value, u'未知')

    @property
    def frozen_days(self):
        return (self.interest_end_date - self.interest_start_date).days

    @property
    def daily_profit(self):
        """资产平均到每日的收益."""
        # 当资产到期时,每日收益变为0
        if self.status is not self.Status.earning:
            return Decimal(0)

        # 当超出攒钱封闭期时,每日收益变为0
        if (self.interest_start_date.date() <= datetime.date.today() <
                self.interest_end_date.date()):
            return self.expect_interest / self.frozen_days
        return Decimal(0)

    def update_service_fee(self, fixed_service_fee, service_fee_rate):
        need_update = False
        if self.fixed_service_fee != fixed_service_fee:
            self.fixed_service_fee = fixed_service_fee
            need_update = True
        if self.service_fee_rate != service_fee_rate:
            self.service_fee_rate = service_fee_rate
            need_update = True
        if not need_update:
            return
        sql = (
            'update {.table_name} set fixed_service_fee=%s, service_fee_rate=%s '
            'where id=%s').format(self)
        params = (
            self.fixed_service_fee,
            self.service_fee_rate,
            self.id_,
        )
        db.execute(sql, params)
        db.commit()
        self.clear_cache()

    def fetch_profit_until(self, date):
        """该资产到某日为止的累计收益."""
        # 已转出则返回已结算的收益
        if self.status is self.Status.redeemed:
            return self.current_interest

        # 未转出则使用预期每日收益按日累加
        if date >= self.interest_end_date.date():
            return self.expect_interest
        elif date < self.interest_start_date.date():
            return Decimal(0)
        else:
            return (date -
                    self.interest_start_date.date()).days * self.daily_profit

    @classmethod
    def add(cls,
            asset_no,
            order_code,
            bankcard_id,
            bank_account,
            product_id,
            user_id,
            status,
            remote_status,
            fixed_service_fee,
            service_fee_rate,
            annual_rate,
            create_amount,
            current_amount,
            base_interest,
            expect_interest,
            current_interest,
            interest_start_date,
            interest_end_date,
            expect_payback_date,
            buy_time,
            creation_time=None):

        sql = (
            'insert into {.table_name}(asset_no, order_code, bankcard_id, bank_account, '
            'product_id, user_id, status, remote_status, fixed_service_fee, service_fee_rate, '
            'annual_rate, create_amount, '
            'current_amount, base_interest, expect_interest, current_interest, '
            'interest_start_date, interest_end_date, expect_payback_date, buy_time, '
            'creation_time) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '
            '%s, %s, %s, %s, %s, %s, %s, %s, %s)').format(cls)
        params = (asset_no, order_code, bankcard_id, bank_account, product_id,
                  user_id, status.value, remote_status, fixed_service_fee,
                  service_fee_rate, annual_rate, create_amount, current_amount,
                  base_interest, expect_interest, current_interest,
                  interest_start_date, interest_end_date, expect_payback_date,
                  buy_time, creation_time or datetime.datetime.now())
        id_ = db.execute(sql, params)
        db.commit()

        instance = cls.get(id_)
        instance.clear_cache()
        return instance

    def update_bankcard(self, new_card):
        """更新资产回款卡(当且仅当用户挂失银行卡情况被确认并修改后调用)"""
        assert isinstance(new_card, BankCard)

        if new_card.user_id != self.user_id or new_card.status is not BankCard.Status.active:
            raise InvalidRedeemBankCardError()

        sql = 'update {.table_name} set bankcard_id=%s where id=%s'.format(
            self)
        params = (new_card.id_, self.id_)
        self._commit_and_refresh(sql, params)

    @classmethod
    @cache(cache_key)
    def get(cls, asset_id):
        sql = (
            'select id, asset_no, order_code, bankcard_id, bank_account, product_id, '
            'user_id, status, remote_status, annual_rate, create_amount, current_amount, '
            'fixed_service_fee, service_fee_rate, base_interest, expect_interest, '
            'current_interest, interest_start_date, interest_end_date, '
            'expect_payback_date, buy_time, creation_time, update_time from {.table_name} '
            'where id = %s').format(cls)
        params = (asset_id, )
        rs = db.execute(sql, params)
        return cls(*rs[0]) if rs else None

    @classmethod
    def get_by_asset_no_with_product_id(cls, asset_no, product_id):
        sql = ('select id from {.table_name} where '
               'asset_no=%s, product_id=%s').format(cls)
        params = (
            asset_no,
            product_id,
        )
        rs = db.execute(sql, params)
        return cls.get(rs[0][0]) if rs else None

    @classmethod
    @cache(order_code_cache_key)
    def get_by_order_code_with_product_id(cls, order_code, product_id):
        sql = ('select id from {.table_name} where '
               'order_code=%s, product_id=%s').format(cls)
        params = (
            order_code,
            product_id,
        )
        rs = db.execute(sql, params)
        return cls.get(rs[0][0]) if rs else None

    @classmethod
    @cache(user_cache_key)
    def get_id_list_by_user_id_with_product_id(cls, user_id, product_id):
        sql = (
            'select id from {.table_name} where user_id = %s and product_id=%s '
            'order by creation_time desc').format(cls)
        params = (
            user_id,
            product_id,
        )
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    def gets_by_user_id_with_product_id(cls, user_id, product_id):
        ids = cls.get_id_list_by_user_id_with_product_id(user_id, product_id)
        return [cls.get(id_) for id_ in ids]

    def _commit_and_refresh(self, sql, params):
        db.execute(sql, params)
        db.commit()
        self.clear_cache()

        new_state = vars(self.get(self.id_))
        vars(self).update(new_state)

    def clear_cache(self):
        mc.delete(self.cache_key.format(asset_id=self.id_))
        mc.delete(self.user_id_cache_key.format(user_id=self.user_id))
        mc.delete(
            self.user_cache_key.format(user_id=self.user_id,
                                       product_id=self.product_id))
        mc.delete(
            self.order_code_cache_key.format(order_code=self.order_code,
                                             product_id=self.product_id))
        mc.delete(self.product_cache_key.format(product_id=self.product_id))

    @classmethod
    @cache(user_id_cache_key)
    def get_id_list_by_user_id(cls, user_id):
        sql = ('select id from {.table_name} where user_id = %s '
               'order by creation_time desc').format(cls)
        params = (user_id, )
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    @cache(product_cache_key)
    def get_ids_by_product_id(cls, product_id):
        sql = ('select id from {.table_name} where product_id = %s '
               'order by creation_time desc').format(cls)

        params = (product_id, )
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    def gets_by_user_id(cls, user_id):
        ids = cls.get_id_list_by_user_id(user_id)
        return [cls.get(id_) for id_ in ids]

    @property
    def total_amount(self):
        return self.hold_amount + self.uncollected_amount

    @property
    def redeemed_amount_today(self):
        return HoarderOrder.get_redeemed_amount_by_user_today(self.user_id)

    @property
    def remaining_amount_today(self):
        if not self.product.can_redeem:
            return 0
        return min(self.hold_amount,
                   self.product.day_redeem_amount - self.redeemed_amount_today,
                   self.product.max_redeem_amount - self.redeemed_amount_today)
Beispiel #27
0
class HoarderOrder(PropsMixin):

    table_name = 'hoarder_order'
    cache_key = 'hoarder:order:{id_}'
    cache_ids_by_user_key = 'hoarder:order:user:{user_id}:ids'
    raw_product_sold_amount_cache_key = 'hoarder:order:sold_amount:raw_product:{product_id}:'
    product_daily_sold_amount_cache_key = 'hoarder:order:daily_sold_amount:product:{product_id}:'
    order_amount_cache_key_by_user = '******'

    #: 资产交割后支付金额
    repay_amount = PropsItem('repay_amount', default='')
    #: 加急手续费
    exp_sell_fee = PropsItem('exp_sell_fee', default='')
    #: 固定手续费
    fixed_service_fee = PropsItem('fixed_service_fee', default='')
    #: 赎回手续费
    service_fee = PropsItem('service_fee', default='')

    class Direction(Enum):
        #: 存入
        save = 'S'
        #: 赎回
        redeem = 'R'

    class Status(Enum):
        #: 本地购买初始状态
        unpaid = 'U'
        #: 本地支付前状态
        committed = 'C'
        #: 远端暂时搁置状态
        shelved = 'V'
        #: 远端正在支付状态
        paying = 'P'
        #: 远端已成功状态
        success = 'S'
        #: 远端已失败状态
        failure = 'F'

        #: 远端申请赎回状态
        applyed = 'A'
        #: 远端正在赎回状态
        redeeming = 'R'
        #: 远端待回款状态
        waiting_back = 'W'
        #: 远端回款中状态
        backing = 'B'
        #: 远端回款结束
        backed = 'D'

    #: 状态显示文案
    Status.unpaid.display_text = u'未支付'
    Status.committed.display_text = u'未支付'
    Status.shelved.display_text = u'处理中'
    Status.paying.display_text = u'处理中'
    Status.applyed.display_text = u'转出中'
    Status.redeeming.display_text = u'转出中'
    Status.waiting_back.display_text = u'待回款'
    Status.backing.display_text = u'回款中'
    Status.backed.display_text = u'已转出'
    Status.success.display_text = u'已存入'
    Status.failure.display_text = u'订单失败'

    # 状态迁移顺序
    Status.unpaid.sequence = 0
    Status.committed.sequence = 1
    Status.shelved.sequence = 2
    Status.paying.sequence = 3
    Status.applyed.sequence = 3
    Status.redeeming.sequence = 4
    Status.waiting_back.sequence = 5
    Status.backing.sequence = 6
    Status.backed.sequence = 7
    Status.success.sequence = 7
    Status.failure.sequence = 7

    #: 远端与本地状态映射
    MUTUAL_STATUS_MAP = {
        OrderStatus.waiting: Status.paying,
        OrderStatus.paying: Status.shelved,
        OrderStatus.payed: Status.success,
        OrderStatus.applying: Status.success,
        OrderStatus.applyed: Status.success,
        OrderStatus.finished: Status.success,
        OrderStatus.auto_cancel: Status.failure,
        OrderStatus.user_cancel: Status.failure,
    }
    MUTUAL_REDEEM_MAP = {
        RedeemStatus.applyed: Status.applyed,
        RedeemStatus.redeeming: Status.redeeming,
        RedeemStatus.waiting_back: Status.waiting_back,
        RedeemStatus.backing: Status.backing,
        RedeemStatus.backed: Status.backed,
    }

    #: 状态和颜色映射
    ORDER_STATUS_COLOR_MAP = {
        u'处理中': '#9B9B9B',
        u'已存入': '#6192B3',
        u'转出中': '#F5A623',
        u'回款中': '#F5A623',
        u'错误': '#D42C41',
        u'已转出': '#6C9F31',
        u'未知状态': '#9B9B9B',
    }

    def __init__(self, id_, user_id, product_id, bankcard_id, amount,
                 pay_amount, expect_interest, order_code, pay_code, direction,
                 status, remote_status, start_time, due_time, update_time,
                 creation_time):
        self.id_ = str(id_)
        self.user_id = str(user_id)
        self.product_id = str(product_id)
        self.bankcard_id = str(bankcard_id)
        self.amount = amount
        self.pay_amount = pay_amount
        self.expect_interest = expect_interest
        self.order_code = order_code if order_code else None
        self.pay_code = pay_code if pay_code else None
        self._direction = direction
        self._status = status
        self.remote_status = remote_status
        self.start_time = start_time
        self.due_time = due_time
        self.update_time = update_time
        self.creation_time = creation_time

    def __str__(self):
        return '<HoarderOrder {.id_}>'.format(self)

    def get_db(self):
        return 'hoarder'

    def get_uuid(self):
        return 'hoarder:order:{.id_}'.format(self)

    @cached_property
    def owner(self):
        return Account.get(self.user_id)

    @cached_property
    def direction(self):
        return self.Direction(self._direction)

    @cached_property
    def product(self):
        return HoarderProduct.get(self.product_id)

    @cached_property
    def asset(self):
        from .asset import Asset
        return Asset.get_by_order_code(self.order_code)

    @cached_property
    def profit_period(self):
        return ProfitPeriod((self.due_date - self.start_date).days, 'day')

    @property
    def profit_hikes(self):
        return []

    @property
    def coupon(self):
        if self.coupon_record:
            return self.coupon_record.coupon

    @property
    def coupon_record(self):
        from core.models.welfare import CouponUsageRecord
        return CouponUsageRecord.get_by_partner_order(self.product.vendor_id,
                                                      self.id_)

    @property
    def woods_burning(self):
        from core.models.welfare import FirewoodBurning
        return FirewoodBurning.get_by_provider_order(self.product.vendor_id,
                                                     self.id_)

    @property
    def display_status(self):
        return self.status.display_text

    @property
    def computed_expect_interest(self):
        return (self.actual_annual_rate * self.amount *
                self.profit_period.value / 100 / 365)

    @property
    def original_annual_rate(self):
        return self.product.annual_rate

    @property
    def actual_annual_rate(self):
        return self.original_annual_rate

    @property
    def bankcard(self):
        return BankCard.get(self.bankcard_id)

    @property
    def status(self):
        return self.Status(self._status)

    @property
    def status_color(self):
        return self.ORDER_STATUS_COLOR_MAP.get(self.display_status)

    @status.setter
    def status(self, new_status):
        self.check_before_setting_status(new_status)

        sql = 'update {.table_name} set status=%s, update_time=%s where id=%s;'.format(
            self)
        params = (new_status.value, datetime.now(), self.id_)
        db.execute(sql, params)

        db.commit()

        self.clear_cache(self.id_)
        self.clear_cache_by_user(self.user_id)

        self.act_after_setting_status(new_status)

    def update_by_remote_status(self, remote_status):
        """ remote_status 为OrderStatus 或 RedeemStatus"""
        local_status = self.get_local_status_by_remote_status(remote_status)
        if not local_status:
            raise UnsupportedStatusError(type(remote_status))
        self.check_before_setting_status(local_status)
        sql = (
            'update {.table_name} set status=%s, remote_status=%s, update_time=%s'
            ' where id=%s;').format(self)
        params = (local_status.value, remote_status.value, datetime.now(),
                  self.id_)
        db.execute(sql, params)

        db.commit()

        self.clear_cache(self.id_)
        self.clear_cache_by_user(self.user_id)

        self.act_after_setting_status(local_status)

    def get_local_status_by_remote_status(self, remote_status):
        """ remote_status 为OrderStatus 或 RedeemStatus"""
        if isinstance(remote_status, OrderStatus):
            return self.MUTUAL_STATUS_MAP.get(remote_status)
        elif isinstance(remote_status, RedeemStatus):
            return self.MUTUAL_REDEEM_MAP.get(remote_status)

    def check_before_setting_status(self, new_status):
        if not isinstance(new_status, self.Status):
            raise ValueError(u'错误的状态类型:%r' % new_status)

        # 不可反向跳转状态
        if new_status.sequence < self.status.sequence:
            raise SequenceError()

    def act_after_setting_status(self, new_status):

        old_status = self._status
        # 本身已是成功状态则直接舍弃。
        if self.Status(old_status) is self.Status.success:
            return
        self._status = new_status.value
        rsyslog.send('order %s status changes from %s to %s' %
                     (self.id_, old_status, self._status),
                     tag='hoarder_order_status_change')

        if new_status in [self.Status.shelved, self.Status.paying]:
            # 当支付未完成时将订单放入状态跟踪MQ
            self.track_for_payment()
            return

        if new_status is self.Status.success:
            # 当支付成功时将订单放入获取资产MQ并发送成功广播信号
            rsyslog.send('fetch asset for order %s' % self.id_,
                         tag='hoarder_fetch_asset')
            hoarder_asset_fetching.produce(self.id_)
            hoarder_order_succeeded.send(self)
            return

        if new_status is self.Status.failure:
            # 当支付失败时发送失败广播信号
            hoarder_order_failed.send(self)

    @classmethod
    def check_before_adding(cls, vendor, user_id, bankcard_id, product_id,
                            amount):
        if not vendor:
            raise NotFoundEntityError(vendor.id_, Account)

        product = HoarderProduct.get(product_id)
        local_account = Account.get(user_id)
        bankcard = BankCard.get(bankcard_id)
        hoarder_account = HoarderAccount.get(vendor.id_, user_id)

        if not local_account:
            raise NotFoundEntityError(user_id, Account)
        if not bankcard:
            raise NotFoundEntityError(bankcard_id, BankCard)
        if not hoarder_account:
            raise UnboundAccountError(user_id)

        if not has_real_identity(local_account):
            raise InvalidIdentityError()

        # 产品是否处于可售状态
        if product.is_sold_out:
            raise SoldOutError(product_id)
        # 产品是否处于正常销售状态
        if product.is_taken_down:
            raise SuspendedError(product_id)
        # 产品是否处于在售状态
        if not product.is_on_sale:
            raise OffShelfError(product_id)

        # checks the product amount limit
        if not isinstance(amount, Decimal):
            raise TypeError('order amount must be decimal')

        amount_range = (product.min_amount, product.max_amount)
        amount_check = [
            amount.is_nan(), amount < 0, amount < amount_range[0],
            amount > amount_range[1]
        ]
        if any(amount_check):
            raise OutOfRangeError(amount, amount_range)

    @classmethod
    def add(cls,
            user_id,
            product_id,
            bankcard_id,
            amount,
            order_code,
            direction,
            status,
            remote_status,
            expect_interest=None,
            pay_amount=None,
            pay_code=None,
            repay_amount=None,
            redeem_pay_amount=None,
            exp_sell_fee=None,
            fixed_service_fee=None,
            service_fee=None,
            start_time=None,
            due_time=None):

        assert isinstance(status, cls.Status)

        sql = (
            'insert into {.table_name} (user_id, product_id, bankcard_id, amount, pay_amount,'
            'expect_interest, order_code, pay_code, direction, status, remote_status, start_time,'
            ' due_time, update_time, creation_time) '
            'values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)'
        ).format(cls)
        params = (user_id, product_id, bankcard_id, amount, pay_amount,
                  expect_interest, order_code, pay_code, direction.value,
                  status.value, remote_status.value, start_time, due_time,
                  datetime.now(), datetime.now())

        id_ = db.execute(sql, params)
        db.commit()

        cls.clear_cache(id_)
        cls.clear_cache_by_user(user_id)

        instance = cls.get(id_)
        instance.update_props_items({
            'repay_amount': repay_amount,
            'exp_sell_fee': exp_sell_fee,
            'service_fee': service_fee,
            'fixed_service_fee': fixed_service_fee
        })
        return instance

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = (
            'select id, user_id, product_id, bankcard_id, amount, pay_amount, expect_interest, '
            'order_code, pay_code, direction, status, remote_status, start_time, due_time, '
            'update_time, creation_time '
            'from {.table_name} where id=%s').format(cls)
        params = (id_, )
        rs = db.execute(sql, params)
        return cls(*rs[0]) if rs else None

    @classmethod
    @cache(cache_ids_by_user_key)
    def get_ids_by_user(cls, user_id):
        sql = ('select id from {.table_name} where user_id=%s'
               ' order by creation_time desc').format(cls)
        params = (user_id, )
        rs = db.execute(sql, params)
        return [str(r[0]) for r in rs]

    @classmethod
    def get_multi_by_user(cls, user_id):
        ids = cls.get_ids_by_user(user_id)
        return cls.get_multi(ids)

    @classmethod
    def gets_by_user_with_page(cls, user_id, offset, count):
        """分页获取订单信息 过滤掉失败和未支付订单"""
        sql = (
            'select id from {.table_name} where user_id=%s and not status=%s and not status=%s'
            'order by creation_time desc limit %s, %s').format(cls)
        params = (
            user_id,
            cls.Status.unpaid.value,
            cls.Status.failure.value,
            offset,
            count,
        )
        rs = db.execute(sql, params)
        return cls.get_multi([str(r[0]) for r in rs]) if rs else []

    @classmethod
    def get_redeemed_amount_by_user_today(cls, user_id):
        sql = (
            'select sum(amount) from {.table_name} where user_id=%s and direction=%s'
            ' and creation_time > %s').format(cls)
        params = (user_id, cls.Direction.redeem.value, datetime.today().date())
        rs = db.execute(sql, params)
        return round_half_up(
            rs[0][0], 2) if rs and rs[0] and rs[0][0] else Decimal('0.00')

    @classmethod
    def get_ids_by_bankcard(cls, bankcard_id):
        sql = 'select id from {.table_name} where bankcard_id=%s'.format(cls)
        params = (bankcard_id, )
        rs = db.execute(sql, params)
        return [str(r[0]) for r in rs]

    @classmethod
    def get_multi_by_bankcard(cls, bankcard_id):
        ids = cls.get_ids_by_bankcard(bankcard_id)
        return cls.get_multi(ids)

    @classmethod
    def is_bankcard_swiped(cls, bankcard):
        assert isinstance(bankcard, BankCard)

        return bool(
            len([
                o for o in cls.get_multi_by_bankcard(bankcard.id_)
                if o.status is cls.Status.success
            ]))

    @classmethod
    @cache(raw_product_sold_amount_cache_key)
    def get_raw_product_sold_amount(cls, raw_product_id):
        sql = ('select sum(amount) from {.table_name} '
               'where product_id=%s and (status=%s or status=%s)').format(cls)
        params = (raw_product_id, cls.Status.paying.value,
                  cls.Status.success.value)
        rs = db.execute(sql, params)
        return rs[0][0] or 0

    @classmethod
    @cache(product_daily_sold_amount_cache_key)
    def get_product_daily_sold_amount_by_now(cls, product_id):
        sql = ('select sum(amount) from {.table_name}'
               ' where product_id=%s and (status=%s or status=%s)'
               ' and creation_time between %s and %s').format(cls)
        end_time = datetime.now()
        start_time = datetime.combine(end_time, time.min)
        params = (product_id, cls.Status.paying.value,
                  cls.Status.success.value, start_time, end_time)
        rs = db.execute(sql, params)
        return rs[0][0] or 0

    @classmethod
    def get_by_order_code(cls, order_code):
        sql = 'select id from {.table_name} where order_code=%s'.format(cls)
        params = (order_code, )
        rs = db.execute(sql, params)
        return cls.get(rs[0][0]) if rs else None

    @classmethod
    @cache(order_amount_cache_key_by_user)
    def get_order_amount_by_user(cls, user_id):
        sql = ('select count(id) from {.table_name} '
               'where user_id=%s and (status=%s or status=%s)').format(cls)
        params = (user_id, cls.Status.paying.value, cls.Status.success.value)
        rs = db.execute(sql, params)
        return rs[0][0]

    @classmethod
    def get_multi(cls, ids):
        return [cls.get(id_) for id_ in ids]

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(**locals()))

    @classmethod
    def clear_cache_by_user(cls, user_id):
        mc.delete(cls.cache_ids_by_user_key.format(**locals()))
        mc.delete(cls.order_amount_cache_key_by_user.format(**locals()))

    @classmethod
    def clear_local_sold_amount_cache(cls, product_id):
        mc.delete(cls.raw_product_sold_amount_cache_key.format(**locals()))

    def track_for_payment(self):
        hoarder_payment_tracking.produce(self.id_)

    def lock_bonus(self):
        """对订单礼券、抵扣金进行加锁冻结(发生在订单提交支付前)"""
        from core.models.welfare import FirewoodWorkflow, FirewoodBurning

        if self.woods_burning:
            flow = FirewoodWorkflow(self.user_id)
            flow.pick(self.woods_burning,
                      tags=[FirewoodBurning.Kind.deduction.name])

        if self.coupon:
            self.coupon.shell_out(self.product, self.amount)

    def confirm_bonus(self):
        """确认礼券、抵扣金被使用(发生在订单已经被告知成功)"""
        from core.models.welfare import FirewoodWorkflow
        if self.status is not self.Status.success:
            raise ValueError('order %s payment has not succeeded' % self.id_)

        if self.woods_burning:
            FirewoodWorkflow(self.user_id).burn(self.woods_burning)

        if self.coupon:
            self.coupon.confirm_consumption()
            self.coupon_record.commit()
            # TODO: 增加折扣判断
            # for hike in self.profit_hikes:
            #     hike.achieve()

    def unlock_bonus(self):
        """释放礼券、抵扣金(发生在订单已经被告知失败)"""
        from core.models.welfare import FirewoodWorkflow
        if self.status not in [
                self.Status.paying, self.Status.committed, self.Status.failure
        ]:
            raise ValueError('order %s payment has not terminated' % self.id_)

        if self.woods_burning:
            FirewoodWorkflow(self.user_id).release(self.woods_burning)

        if self.coupon:
            self.coupon.put_back_wallet()

        for hike in self.profit_hikes:
            hike.renew()
Beispiel #28
0
class ZhiwangWrappedProduct(PropsMixin, ProfitHikeMixin):
    """
    Wrapped product is partly reformed from the raw product provided by zhiwang to fit our use.
    """
    class Type(Enum):
        newcomer = 'GH_NEWCOMER'

    Type.newcomer.label = u'新手专享'

    # store
    table_name = 'hoard_zhiwang_wrapped_product'
    cache_key = 'hoard:zhiwang:wrapped_product:{id_}'
    all_ids_cache_key = 'hoard:zhiwang:wrapped_product:all_ids'
    product_ids_by_raw_id_cache_key = 'hoard:zhiwang:wrapped_product:raw_id:{raw_id}'
    product_by_kind_and_raw_id_cache_key = (
        'hoard:zhiwang:wrapped_product:kind_id:{kind_id}:raw_id:{raw_product_id}'
    )

    # local attrs
    name = PropsItem('name', '', unicode_type)
    allocated_amount = PropsItem('allocated_amount', 0, Decimal)
    min_amount = PropsItem('min_amount', 0, Decimal)
    max_amount = PropsItem('max_amount', 0, Decimal)
    annual_rate = PropsItem('annual_rate', 0, Decimal)
    start_date = PropsItem('start_date', None, date_type)
    due_date = PropsItem('due_date', None, date_type)

    # local config
    is_taken_down = PropsItem('is_taken_down', True)

    # delegation
    sale_mode = DelegatedProperty('sale_mode', to='raw_product')
    product_type = DelegatedProperty('product_type', to='raw_product')

    # 合作方
    provider = zhiwang

    # 接受用户使用礼券抵扣优惠
    is_accepting_bonus = False

    def __init__(self, id_, kind_id, raw_product_id, creation_time):
        self.id_ = str(id_)
        self.kind_id = str(kind_id)
        self.raw_product_id = str(raw_product_id)
        self.creation_time = creation_time

    def get_db(self):
        return 'hoard'

    def get_uuid(self):
        return 'zhiwang:wrapped_product:{id_}'.format(id_=self.id_)

    @cached_property
    def kind(self):
        from .wrapper_kind import WrapperKind
        return WrapperKind.get(self.kind_id)

    @cached_property
    def raw_product(self):
        from .product import ZhiwangProduct
        return ZhiwangProduct.get(self.raw_product_id)

    @property
    def display_privilege(self):
        raise NotImplementedError

    @property
    def wrapped_product_type(self):
        return self.kind.wrapped_product_type

    @property
    def is_either_sold_out(self):
        """是否已售罄"""
        from .product import SaleMode
        from .order import ZhiwangOrder

        # 首先判断基础产品总量是否售罄
        if self.raw_product.is_sold_out:
            return True

        # 当产品销售模式为共享时,取决于父产品的销售情况
        if self.sale_mode is SaleMode.share:
            return self.raw_product.is_either_sold_out
        elif self.sale_mode is SaleMode.mutex:
            local_sold_amount = ZhiwangOrder.get_wrapped_product_sold_amount(
                self.raw_product.product_id, self.id_)
            return local_sold_amount > (self.allocated_amount -
                                        ZW_SAFE_RESERVATION_AMOUNT)
        else:
            raise ValueError('invalid sale mode %s' % self.sale_mode)

    @property
    def in_sale(self):
        """是否在销售期内"""
        return self.raw_product.in_sale

    @property
    def in_stock(self):
        # 是否可售状态决定于以下几种情况
        # 1. 是否在销售期内
        # 2. 是否已售罄
        # 3. 是否因其他原因而被暂时设置为暂停销售

        if self.is_either_sold_out or self.is_taken_down:
            return False
        return self.in_sale

    @classmethod
    def create(cls, raw_product, wrapper_kind):
        # check the limit
        if (min(wrapper_kind.limit) < raw_product.min_amount
                or max(wrapper_kind.limit) > raw_product.max_amount):
            raise InvalidWrapRule(wrapper_kind.limit)

        # check the frozen time
        raw_days_period = [
            raw_product.profit_period['min'].value,
            raw_product.profit_period['max'].value
        ]
        if not min(raw_days_period) <= wrapper_kind.frozen_days.value <= max(
                raw_days_period):
            raise InvalidWrapRule(wrapper_kind.id_)

        instance = cls.get_by_kind_and_raw_product_id(wrapper_kind.id_,
                                                      raw_product.product_id)
        if instance is None:
            sql = ('insert into {.table_name} (kind_id, raw_product_id, '
                   'creation_time) values (%s, %s, %s)').format(cls)
            params = (wrapper_kind.id_, raw_product.product_id, datetime.now())
            id_ = db.execute(sql, params)
            db.commit()
            instance = cls.get(id_)
            instance.deploy(wrapper_kind)

            cls.clear_cache(id_)
            cls.clear_all_ids_cache()
            cls.clear_product_ids_by_raw_id_cache(raw_product.product_id)
            cls.clear_product_by_kind_and_raw_cache(wrapper_kind.id_,
                                                    raw_product.product_id)
        return instance

    def deploy(self):
        raise NotImplementedError

    def is_qualified(self, user_id):
        raise NotImplementedError

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        from .wrapper_kind import WrapperKind
        sql = (
            'select id, kind_id, raw_product_id, creation_time from {.table_name} '
            'where id=%s').format(cls)
        params = (id_, )
        rs = db.execute(sql, params)
        if rs:
            kind = WrapperKind.get(rs[0][1]).wrapped_product_type
            # FIXME: 避免使用subclasses反射
            pcls = next(scls for scls in cls.__subclasses__()
                        if scls.wrapped_product_kind is kind)
            return pcls(*rs[0])

    @classmethod
    def get_all(cls):
        """获取所有可展示子产品"""
        ids = cls.get_all_ids()
        products = (cls.get(id_) for id_ in ids)
        return [p for p in products if p.in_sale]

    @classmethod
    @cache(all_ids_cache_key)
    def get_all_ids(cls):
        sql = ('select id from {.table_name} order by creation_time desc'
               ).format(cls)
        rs = db.execute(sql)
        return [r[0] for r in rs]

    @classmethod
    def get_multi_by_raw(cls, raw_id):
        product_ids = cls.get_ids_by_raw(raw_id)
        return [cls.get(product_id) for product_id in product_ids]

    @classmethod
    @cache(product_ids_by_raw_id_cache_key)
    def get_ids_by_raw(cls, raw_id):
        sql = (
            'select id from {.table_name} where raw_product_id=%s').format(cls)
        params = (raw_id, )
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    @cache(product_by_kind_and_raw_id_cache_key)
    def get_by_kind_and_raw_product_id(cls, kind_id, raw_product_id):
        sql = (
            'select id from {.table_name} where kind_id=%s and raw_product_id=%s'
        ).format(cls)
        params = (kind_id, raw_product_id)
        rs = db.execute(sql, params)
        if rs:
            return cls.get(rs[0][0])

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(**locals()))

    @classmethod
    def clear_all_ids_cache(cls):
        mc.delete(cls.all_ids_cache_key)

    @classmethod
    def clear_product_ids_by_raw_id_cache(cls, raw_id):
        mc.delete(cls.product_ids_by_raw_id_cache_key.format(**locals()))

    @classmethod
    def clear_product_by_kind_and_raw_cache(cls, kind_id, raw_product_id):
        mc.delete(cls.product_by_kind_and_raw_id_cache_key.format(**locals()))
Beispiel #29
0
class Announcement(EntityModel, PropsMixin):
    """The announcement information of whole site."""
    class SubjectType(Enum):
        notice = 'N'

    class Status(Enum):
        present = 'P'
        absent = 'A'

    class ContentType(Enum):
        markdown = 'M'

    ContentType.markdown.to_html = render_markdown

    table_name = 'site_announcement'
    cache_key = 'site:announcement:{id_}'
    cache_by_date_key = 'site:announcement:date:{date}:ids'

    #: The subject of announcement
    subject = PropsItem('subject', default=u'')

    #: The content of announcement
    content = PropsItem('content', default=u'')

    def __init__(self, id_, subject_type_code, content_type_code, status_code,
                 start_time, stop_time, endpoint, creation_time):
        self.id_ = bytes(id_)
        self.subject_type_code = subject_type_code
        self.content_type_code = content_type_code
        self.status_code = status_code

        #: The announcement be visible since this time
        self.start_time = start_time
        #: The announcement be invisible since this time
        self.stop_time = stop_time
        #: The announcement only be visible in special Flask endpoint
        self.endpoint = endpoint
        #: The creation time of announcement
        self.creation_time = creation_time

    @cached_property
    def subject_type(self):
        """The subject type of announcement.

        :rtype: :class:`.Announcement.SubjectType`
        """
        return self.SubjectType(self.subject_type_code)

    @cached_property
    def content_type(self):
        """The content type of announcement.

        :rtype: :class:`.Announcement.ContentType`
        """
        return self.ContentType(self.content_type_code)

    @property
    def content_as_html(self):
        """The announcement content as HTML format."""
        return self.content_type.to_html(self.content).strip()

    @property
    def status(self):
        """The status of announcement.

        :rtype: :class:`.Announcement.Status`
        """
        return self.Status(self.status_code)

    def get_db(self):
        return 'site_announcement'

    def get_uuid(self):
        return self.id_

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = ('select id, subject_type, content_type, status, start_time,'
               ' stop_time, endpoint, creation_time '
               'from {0} where id = %s').format(cls.table_name)
        params = (id_, )
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    def add(cls, subject, subject_type, content, content_type, start_time,
            stop_time, endpoint):
        assert isinstance(subject_type, cls.SubjectType)
        assert isinstance(content_type, cls.ContentType)
        assert start_time < stop_time
        assert datetime.datetime.now() < stop_time

        initial_status = cls.Status.present

        sql = ('insert into {0} (subject_type, content_type, status,'
               ' start_time, stop_time, endpoint, creation_time) '
               'values (%s, %s, %s, %s, %s, %s, %s)').format(cls.table_name)
        params = (subject_type.value, content_type.value, initial_status.value,
                  start_time, stop_time, endpoint, datetime.datetime.now())

        id_ = db.execute(sql, params)
        db.commit()

        cls.clear_cache(id_)
        for date in datetime_range(start_time, stop_time):
            cls.clear_cache_by_date(date)

        instance = cls.get(id_)
        instance.subject = subject
        instance.content = content

        return instance

    @classmethod
    @cache(cache_by_date_key)
    def get_ids_by_date(cls, date):
        assert isinstance(date, datetime.date)
        sql = ('select id from {0} where date(start_time) <= %s and'
               ' date(stop_time) > %s').format(cls.table_name)
        params = (date, date)
        rs = db.execute(sql, params)
        return [r[0] for r in rs]

    @classmethod
    def get_multi_by_date(cls, date, status=Status.present):
        ids = cls.get_ids_by_date(date)
        announcements = (cls.get(id_) for id_ in ids)
        return [a for a in announcements if a.status is status]

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(**locals()))

    @classmethod
    def clear_cache_by_date(cls, date):
        mc.delete(cls.cache_by_date_key.format(**locals()))

    def is_suitable(self, request):
        return glob.fnmatch.fnmatch(request.endpoint, self.endpoint)
Beispiel #30
0
class XMLoan(PropsMixin):
    table_name = 'hoard_xm_loan'
    cache_key = 'hoard:xm:loan:id:{id_}:v1'
    cache_key_by_loans_digest_id = 'hoard:xm:loan:loans_digest_id:{loans_digest_id}'

    # 借款协议编号
    loan_receipt_no = PropsItem('loan_receipt_no', '', unicode_type)

    # 投资编号
    invest_id = PropsItem('invest_id', '', int)

    # 借款人姓名
    debtor_name = PropsItem('debtor_name', '', unicode_type)

    # 借款人身份证号
    debtor_ricn = PropsItem('debtor_ricn', '', unicode_type)

    # 借款人身份
    debtor_type = PropsItem('debtor_type', '', unicode_type)

    # 借款用途
    debt_purpose = PropsItem('debt_purpose', '', unicode_type)

    # 借款金额
    lending_amount = PropsItem('lending_amount', 0, Decimal)

    # 借款人开始还款的日期
    start_date = PropsItem('start_date', '', date_type)

    def __init__(self, id_, loans_digest_id, creation_time):
        self.id_ = id_
        self.loans_digest_id = loans_digest_id
        self.creation_time = creation_time

    def get_db(self):
        return 'hoard'

    def get_uuid(self):
        return 'xm:loan:{0}'.format(self.id_)

    @classmethod
    @cache(cache_key)
    def get(cls, id_):
        sql = ('select id, loans_digest_id, creation_time from {.table_name}'
               ' where id=%s').format(cls)
        params = (id_, )
        rs = db.execute(sql, params)
        if rs:
            return cls(*rs[0])

    @classmethod
    @cache(cache_key_by_loans_digest_id)
    def get_ids_by_loans_digest_id(cls, loans_digest_id):
        sql = 'select id from {.table_name} where loans_digest_id=%s'.format(
            cls)
        params = (loans_digest_id, )
        rs = db.execute(sql, params)
        return [str(r[0]) for r in rs]

    @classmethod
    def get_multi(cls, ids):
        return [cls.get(id_) for id_ in ids]

    @classmethod
    def get_multi_by_loans_digest_id(cls, loans_digest_id):
        return cls.get_multi(cls.get_ids_by_loans_digest_id(loans_digest_id))

    @classmethod
    def create(cls, loans_digest, loans, _commit=True):
        assert isinstance(loans_digest, XMLoansDigest)
        sql = 'insert into {.table_name} (loans_digest_id, creation_time) values(%s, %s)'.format(
            cls)
        params = (loans_digest.id_, datetime.datetime.now())
        id_ = db.execute(sql, params)

        if _commit:
            db.commit()
        instance = cls.get(id_)

        cls.clear_cache(id_)
        cls.clear_cache_by_loans_digest_id(loans_digest.id_)

        instance.update_props_items({
            'loan_receipt_no':
            loans.loan_receipt_no,
            'invest_id':
            loans.order_id,
            'debtor_name':
            loans.bc_name,
            'debtor_ricn':
            loans.debtor_identity_no,
            'debtor_type':
            loans.debtor_type,
            'debt_purpose':
            loans.debt_desc,
            'lending_amount':
            str(loans.loan_receipt_amt),
            'start_date':
            loans.start_date.isoformat()
        })
        return instance

    @classmethod
    def clear_cache(cls, id_):
        mc.delete(cls.cache_key.format(id_=id_))

    @classmethod
    def clear_cache_by_loans_digest_id(cls, loans_digest_id):
        mc.delete(
            cls.cache_key_by_loans_digest_id.format(
                loans_digest_id=loans_digest_id))