def assert_round_down(amount, expected): self.assertEqual(round_down_cent(Decimal(amount)), Decimal(expected))
def assert_round_down(amount, expected): self.assertEqual( round_down_cent(Decimal(amount)), Decimal(expected) )
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