Example #1
0
 def assert_round_down(amount, expected):
     self.assertEqual(round_down_cent(Decimal(amount)),
                      Decimal(expected))
Example #2
0
 def assert_round_down(amount, expected):
     self.assertEqual(
         round_down_cent(Decimal(amount)), 
         Decimal(expected)
     )
Example #3
0
    def cancel(self, guid, prorated_refund=False, refund_amount=None):
        """Cancel a subscription

        :param guid: the guid of subscription to cancel
        :param prorated_refund: Should we generate a prorated refund 
            transaction according to remaining time of subscription period?
        :param refund_amount: if refund_amount is given, it will be used 
            to refund customer, you cannot set prorated_refund with 
            refund_amount
        :return: if prorated_refund is True, and the subscription is 
            refundable, the refund transaction guid will be returned
        """
        if prorated_refund and refund_amount is not None:
            raise ValueError('You cannot set refund_amount when '
                             'prorated_refund is True')

        tx_model = TransactionModel(self.session)

        subscription = self.get(guid, raise_error=True)
        if subscription.canceled:
            raise SubscriptionCanceledError('Subscription {} is already '
                                            'canceled'.format(guid))
        now = tables.now_func()
        subscription.canceled = True
        subscription.canceled_at = now
        tx_guid = None

        # should we do refund
        do_refund = False
        # we want to do a prorated refund here, however, if there is no any 
        # issued transaction, then no need to do a refund, just skip
        if (
            (prorated_refund or refund_amount is not None) and 
            subscription.period
        ):
            previous_transaction = (
                self.session.query(tables.Transaction)
                .filter_by(subscription_guid=subscription.guid)
                .order_by(tables.Transaction.scheduled_at.desc())
                .first()
            )
            # it is possible the previous transaction is failed or retrying,
            # so that we should only refund finished transaction
            if previous_transaction.status == TransactionModel.STATUS_DONE:
                do_refund = True

        if do_refund:
            if prorated_refund:
                previous_datetime = previous_transaction.scheduled_at
                # the total time delta in the period
                total_delta = (
                    subscription.next_transaction_at - previous_datetime
                )
                total_seconds = decimal.Decimal(total_delta.total_seconds())
                # the passed time so far since last transaction
                elapsed_delta = now - previous_datetime
                elapsed_seconds = decimal.Decimal(elapsed_delta.total_seconds())

                # TODO: what about calculate in different granularity here?
                #       such as day or hour granularity?
                rate = 1 - (elapsed_seconds / total_seconds)
                amount = previous_transaction.amount * rate
                amount = round_down_cent(amount)
            else:
                amount = round_down_cent(decimal.Decimal(refund_amount))
                if amount > previous_transaction.amount:
                    raise ValueError('refund_amount cannot be grather than '
                                     'subscription amount {}'
                                     .format(previous_transaction.amount))

            # make sure we will not refund zero dollar
            # TODO: or... should we?
            if amount:
                tx_guid = tx_model.create(
                    subscription_guid=subscription.guid, 
                    amount=amount, 
                    transaction_type=tx_model.TYPE_REFUND, 
                    scheduled_at=subscription.next_transaction_at, 
                    refund_to_guid=previous_transaction.guid, 
                )

        # cancel not done transactions (exclude refund transaction)
        Transaction = tables.Transaction
        not_done_transactions = (
            self.session.query(Transaction)
            .filter_by(subscription_guid=guid)
            .filter(Transaction.transaction_type != TransactionModel.TYPE_REFUND)
            .filter(Transaction.status.in_([
                tx_model.STATUS_INIT,
                tx_model.STATUS_RETRYING,
            ]))
        )
        not_done_transactions.update(dict(
            status=tx_model.STATUS_CANCELED,
            updated_at=now, 
        ), synchronize_session='fetch')

        self.session.add(subscription)
        self.session.flush()
        return tx_guid