コード例 #1
0
ファイル: customer.py プロジェクト: Imaxinacion/billy
    def create(
        self,
        company,
        processor_uri=None,
    ):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company=company,
            processor_uri=processor_uri,
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()

        processor = self.factory.create_processor()
        processor.configure_api_key(customer.company.processor_key)
        # create customer
        if customer.processor_uri is None:
            customer.processor_uri = processor.create_customer(customer)
        # validate the customer processor URI
        else:
            processor.validate_customer(customer.processor_uri)

        self.session.flush()
        return customer
コード例 #2
0
ファイル: invoice.py プロジェクト: winstonhoseadriggins/billy
    def transaction_status_update(self, invoice, transaction, original_status):
        """Called to handle transaction status update

        """
        # we don't have to deal with refund/reversal status change
        if transaction.transaction_type not in [
            TransactionModel.types.DEBIT,
            TransactionModel.types.CREDIT,
        ]:
            return

        def succeeded():
            invoice.status = self.statuses.SETTLED

        def processing():
            invoice.status = self.statuses.PROCESSING

        def failed():
            invoice.status = self.statuses.FAILED

        status_handlers = {
            # succeeded status
            TransactionModel.statuses.SUCCEEDED: succeeded,
            # processing
            TransactionModel.statuses.PENDING: processing,
            # failed
            TransactionModel.statuses.FAILED: failed,
        }
        new_status = transaction.status
        status_handlers[new_status]()

        invoice.updated_at = tables.now_func()
        self.session.flush()
コード例 #3
0
    def create(
        self,
        company,
        processor_uri=None,
    ):
        """Create a customer and return its id

        """
        now = tables.now_func()
        customer = tables.Customer(
            guid='CU' + make_guid(),
            company=company,
            processor_uri=processor_uri,
            created_at=now,
            updated_at=now,
        )
        self.session.add(customer)
        self.session.flush()

        processor = self.factory.create_processor()
        processor.configure_api_key(customer.company.processor_key)
        # create customer
        if customer.processor_uri is None:
            customer.processor_uri = processor.create_customer(customer)
        # validate the customer processor URI
        else:
            processor.validate_customer(customer.processor_uri)

        self.session.flush()
        return customer
コード例 #4
0
ファイル: plan.py プロジェクト: winstonhoseadriggins/billy
    def create(
        self,
        company,
        plan_type,
        amount,
        frequency,
        interval=1,
        external_id=None,
        name=None,
        description=None,
    ):
        """Create a plan and return its ID

        """
        if interval < 1:
            raise ValueError('Interval can only be >= 1')
        now = tables.now_func()
        plan = tables.Plan(
            guid='PL' + make_guid(),
            company=company,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan
コード例 #5
0
ファイル: transaction.py プロジェクト: carriercomm/billy
    def update(self, transaction, **kwargs):
        """Update a transaction

        """
        now = tables.now_func()
        transaction.updated_at = now
        if kwargs:
            raise TypeError("Unknown attributes {} to update".format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #6
0
ファイル: transaction.py プロジェクト: carriercomm/billy
    def add_event(self, transaction, status, processor_id, occurred_at):
        """Add a status updating event of transaction from callback

        """
        now = tables.now_func()
        # the latest event of this transaction
        last_event = transaction.events.first()

        event = tables.TransactionEvent(
            guid="TE" + make_guid(),
            transaction=transaction,
            processor_id=processor_id,
            occurred_at=occurred_at,
            status=status,
            created_at=now,
        )
        self.session.add(event)

        # ensure won't duplicate
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateEventError("Event {} already exists for {}".format(processor_id, transaction.guid))

        # Notice: we only want to update transaction status if this event
        # is the latest one we had seen in Billy system. Why we are doing
        # here is because I think of some scenarios like
        #
        #  1. Balanced cannot reach Billy for a short while, and retry later
        #  2. Attacker want to fool us with old events
        #
        # These will lead the status of invoice to be updated incorrectly.
        # For case 1, events send to Billy like this:
        #
        #     succeeded (failed to send to Billy, retry 1 minute later)
        #     failed
        #     succeeded (retry)
        #
        # See? The final status should be `failed`, but as the `succeeded`
        # was resent later, so it became `succeded` eventually. Similarly,
        # attackers can send us an old `succeeded` event to make the invoice
        # settled.  This is why we need to ensure only the latest event can
        # affect status of invoice.
        if last_event is not None and occurred_at <= last_event.occurred_at:
            return

        old_status = transaction.status
        transaction.updated_at = now
        transaction.status = status
        # update invoice status
        invoice_model = self.factory.create_invoice_model()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice, transaction=transaction, original_status=old_status
        )
        self.session.flush()
コード例 #7
0
    def update(self, transaction, **kwargs):
        """Update a transaction

        """
        now = tables.now_func()
        transaction.updated_at = now
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(
                tuple(kwargs.keys())))
        self.session.flush()
コード例 #8
0
    def cancel(self, subscription):
        """Cancel a subscription

        :param subscription: the subscription to cancel
        """
        if subscription.canceled:
            raise SubscriptionCanceledError(
                'Subscription {} is already canceled'.format(subscription.guid)
            )
        now = tables.now_func()
        subscription.canceled = True
        subscription.canceled_at = now
コード例 #9
0
ファイル: company.py プロジェクト: Imaxinacion/billy
    def update(self, company, **kwargs):
        """Update a company

        """
        now = tables.now_func()
        company.updated_at = now
        for key in ['name', 'processor_key', 'api_key']:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(company, key, value)
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #10
0
ファイル: plan.py プロジェクト: carriercomm/billy
    def update(self, plan, **kwargs):
        """Update a plan

        """
        now = tables.now_func()
        plan.updated_at = now
        for key in ["name", "external_id", "description"]:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(plan, key, value)
        if kwargs:
            raise TypeError("Unknown attributes {} to update".format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #11
0
ファイル: customer.py プロジェクト: Imaxinacion/billy
    def update(self, customer, **kwargs):
        """Update a customer

        """
        now = tables.now_func()
        customer.updated_at = now
        for key in ['processor_uri']:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(customer, key, value)
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #12
0
ファイル: plan.py プロジェクト: winstonhoseadriggins/billy
    def update(self, plan, **kwargs):
        """Update a plan

        """
        now = tables.now_func()
        plan.updated_at = now
        for key in ['name', 'external_id', 'description']:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(plan, key, value)
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #13
0
    def update(self, subscription, **kwargs):
        """Update a subscription

        :param external_id: external_id to update
        """
        now = tables.now_func()
        subscription.updated_at = now
        for key in ['external_id']:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(subscription, key, value)
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(tuple(kwargs.keys())))
        self.session.flush()
コード例 #14
0
    def update(self, customer, **kwargs):
        """Update a customer

        """
        now = tables.now_func()
        customer.updated_at = now
        for key in ['processor_uri']:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            setattr(customer, key, value)
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(
                tuple(kwargs.keys())))
        self.session.flush()
コード例 #15
0
    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if reference_to is not None:
            if transaction_type not in [self.types.REFUND, self.types.REVERSE]:
                raise ValueError('reference_to can only be set to a refund '
                                 'transaction')
            if funding_instrument_uri is not None:
                raise ValueError(
                    'funding_instrument_uri cannot be set to a refund/reverse '
                    'transaction')
            if (reference_to.transaction_type
                    not in [self.types.DEBIT, self.types.CREDIT]):
                raise ValueError(
                    'Only charge/payout transaction can be refunded/reversed')

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            submit_status=self.submit_statuses.STAGED,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction
コード例 #16
0
ファイル: transaction.py プロジェクト: carriercomm/billy
    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if reference_to is not None:
            if transaction_type not in [self.types.REFUND, self.types.REVERSE]:
                raise ValueError("reference_to can only be set to a refund " "transaction")
            if funding_instrument_uri is not None:
                raise ValueError("funding_instrument_uri cannot be set to a refund/reverse " "transaction")
            if reference_to.transaction_type not in [self.types.DEBIT, self.types.CREDIT]:
                raise ValueError("Only charge/payout transaction can be refunded/reversed")

        now = tables.now_func()
        transaction = tables.Transaction(
            guid="TX" + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            submit_status=self.submit_statuses.STAGED,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction
コード例 #17
0
ファイル: plan.py プロジェクト: carriercomm/billy
    def create(self, company, plan_type, amount, frequency, interval=1, external_id=None, name=None, description=None):
        """Create a plan and return its ID

        """
        if interval < 1:
            raise ValueError("Interval can only be >= 1")
        now = tables.now_func()
        plan = tables.Plan(
            guid="PL" + make_guid(),
            company=company,
            plan_type=plan_type,
            amount=amount,
            frequency=frequency,
            interval=interval,
            external_id=external_id,
            name=name,
            description=description,
            updated_at=now,
            created_at=now,
        )
        self.session.add(plan)
        self.session.flush()
        return plan
コード例 #18
0
    def create(
        self,
        customer,
        plan,
        funding_instrument_uri=None,
        started_at=None,
        external_id=None,
        appears_on_statement_as=None,
        amount=None,
    ):
        """Create a subscription and return its id

        """
        if amount is not None and amount <= 0:
            raise ValueError('Amount should be a non-zero postive integer')
        now = tables.now_func()
        if started_at is None:
            started_at = now
        elif started_at < now:
            raise ValueError('Past started_at time is not allowed')
        subscription = tables.Subscription(
            guid='SU' + make_guid(),
            customer=customer,
            plan=plan,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            external_id=external_id,
            appears_on_statement_as=appears_on_statement_as,
            started_at=started_at,
            next_invoice_at=started_at,
            created_at=now,
            updated_at=now,
        )
        self.session.add(subscription)
        self.session.flush()
        self.yield_invoices([subscription])
        return subscription
コード例 #19
0
ファイル: invoice.py プロジェクト: winstonhoseadriggins/billy
    def cancel(self, invoice):
        """Cancel an invoice

        """
        Transaction = tables.Transaction
        now = tables.now_func()

        if invoice.status not in [
            self.statuses.STAGED,
            self.statuses.PROCESSING,
            self.statuses.FAILED,
        ]:
            raise InvalidOperationError(
                'An invoice can only be canceled when its status is one of '
                'STAGED, PROCESSING and FAILED'
            )
        self.get(invoice.guid, with_lockmode='update')
        invoice.status = self.statuses.CANCELED

        # those transactions which are still running
        running_transactions = (
            self.session.query(Transaction)
            .filter(
                Transaction.transaction_type != TransactionModel.types.REFUND,
                Transaction.submit_status.in_([
                    TransactionModel.submit_statuses.STAGED,
                    TransactionModel.submit_statuses.RETRYING,
                ])
            )
        )
        # cancel them
        running_transactions.update(dict(
            submit_status=TransactionModel.submit_statuses.CANCELED,
            updated_at=now,
        ), synchronize_session='fetch')

        self.session.flush()
コード例 #20
0
ファイル: company.py プロジェクト: Imaxinacion/billy
    def create(self, processor_key, name=None, make_callback_url=None):
        """Create a company and return

        """
        now = tables.now_func()
        company = tables.Company(
            guid='CP' + make_guid(),
            processor_key=processor_key,
            api_key=make_api_key(),
            callback_key=make_api_key(),
            name=name,
            created_at=now,
            updated_at=now,
        )
        self.session.add(company)
        self.session.flush()

        if make_callback_url is not None:
            url = make_callback_url(company)
            processor = self.factory.create_processor()
            processor.configure_api_key(company.processor_key)
            processor.register_callback(company, url)

        return company
コード例 #21
0
ファイル: invoice.py プロジェクト: winstonhoseadriggins/billy
    def create(
        self,
        amount,
        funding_instrument_uri=None,
        customer=None,
        subscription=None,
        title=None,
        items=None,
        adjustments=None,
        external_id=None,
        appears_on_statement_as=None,
        scheduled_at=None,
    ):
        """Create a invoice and return its id

        """
        from sqlalchemy.exc import IntegrityError

        if customer is not None and subscription is not None:
            raise ValueError('You can only set either customer or subscription')

        if customer is not None:
            invoice_type = self.types.CUSTOMER
            invoice_cls = tables.CustomerInvoice
            # we only support charge type for customer invoice now
            transaction_type = self.transaction_types.DEBIT
            extra_kwargs = dict(
                customer=customer,
                external_id=external_id,
            )
        elif subscription is not None:
            if scheduled_at is None:
                raise ValueError('scheduled_at cannot be None')
            invoice_type = self.types.SUBSCRIPTION
            invoice_cls = tables.SubscriptionInvoice
            plan_type = subscription.plan.plan_type
            if plan_type == PlanModel.types.DEBIT:
                transaction_type = self.transaction_types.DEBIT
            elif plan_type == PlanModel.types.CREDIT:
                transaction_type = self.transaction_types.CREDIT
            else:
                raise ValueError('Invalid plan_type {}'.format(plan_type))
            extra_kwargs = dict(
                subscription=subscription,
                scheduled_at=scheduled_at,
            )
        else:
            raise ValueError('You have to set either customer or subscription')

        if amount < 0:
            raise ValueError('Negative amount {} is not allowed'.format(amount))

        now = tables.now_func()
        invoice = invoice_cls(
            guid='IV' + make_guid(),
            invoice_type=invoice_type,
            transaction_type=transaction_type,
            status=self.statuses.STAGED,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            title=title,
            created_at=now,
            updated_at=now,
            appears_on_statement_as=appears_on_statement_as,
            **extra_kwargs
        )

        self.session.add(invoice)

        # ensure (customer_guid, external_id) is unique
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateExternalIDError(
                'Invoice {} with external_id {} already exists'
                .format(customer.guid, external_id)
            )

        if items:
            for item in items:
                record = tables.Item(
                    invoice=invoice,
                    name=item['name'],
                    amount=item['amount'],
                    type=item.get('type'),
                    quantity=item.get('quantity'),
                    unit=item.get('unit'),
                    volume=item.get('volume'),
                )
                self.session.add(record)
            self.session.flush()

        # TODO: what about an invalid adjust? say, it makes the total of invoice
        # a negative value? I think we should not allow user to create such
        # invalid invoice
        if adjustments:
            for adjustment in adjustments:
                record = tables.Adjustment(
                    invoice=invoice,
                    amount=adjustment['amount'],
                    reason=adjustment.get('reason'),
                )
                self.session.add(record)
            self.session.flush()

        # as if we set the funding_instrument_uri at very first, we want to charge it
        # immediately, so we create a transaction right away, also set the
        # status to PROCESSING
        if funding_instrument_uri is not None and invoice.amount > 0:
            invoice.status = self.statuses.PROCESSING
            self._create_transaction(invoice)
        # it is zero amount, nothing to charge, just switch to
        # SETTLED status
        elif invoice.amount == 0:
            invoice.status = self.statuses.SETTLED

        self.session.flush()
        return invoice
コード例 #22
0
    def add_event(self, transaction, status, processor_id, occurred_at):
        """Add a status updating event of transaction from callback

        """
        now = tables.now_func()
        # the latest event of this transaction
        last_event = transaction.events.first()

        event = tables.TransactionEvent(
            guid='TE' + make_guid(),
            transaction=transaction,
            processor_id=processor_id,
            occurred_at=occurred_at,
            status=status,
            created_at=now,
        )
        self.session.add(event)

        # ensure won't duplicate
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateEventError(
                'Event {} already exists for {}'.format(
                    processor_id,
                    transaction.guid,
                ), )

        # Notice: we only want to update transaction status if this event
        # is the latest one we had seen in Billy system. Why we are doing
        # here is because I think of some scenarios like
        #
        #  1. Balanced cannot reach Billy for a short while, and retry later
        #  2. Attacker want to fool us with old events
        #
        # These will lead the status of invoice to be updated incorrectly.
        # For case 1, events send to Billy like this:
        #
        #     succeeded (failed to send to Billy, retry 1 minute later)
        #     failed
        #     succeeded (retry)
        #
        # See? The final status should be `failed`, but as the `succeeded`
        # was resent later, so it became `succeded` eventually. Similarly,
        # attackers can send us an old `succeeded` event to make the invoice
        # settled.  This is why we need to ensure only the latest event can
        # affect status of invoice.
        if last_event is not None and occurred_at <= last_event.occurred_at:
            return

        old_status = transaction.status
        transaction.updated_at = now
        transaction.status = status
        # update invoice status
        invoice_model = self.factory.create_invoice_model()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice,
            transaction=transaction,
            original_status=old_status,
        )
        self.session.flush()
コード例 #23
0
class TransactionModel(BaseTableModel):

    TABLE = tables.Transaction

    #: the default maximum retry count
    DEFAULT_MAXIMUM_RETRY = 10

    types = tables.TransactionType

    submit_statuses = tables.TransactionSubmitStatus

    statuses = tables.TransactionStatus

    @property
    def maximum_retry(self):
        maximum_retry = int(
            self.factory.settings.get(
                'billy.transaction.maximum_retry',
                self.DEFAULT_MAXIMUM_RETRY,
            ))
        return maximum_retry

    def get_last_transaction(self):
        """Get last transaction

        """
        query = (self.session.query(tables.Transaction).order_by(
            tables.Transaction.created_at.desc()))
        return query.first()

    @decorate_offset_limit
    def list_by_context(self, context):
        """List transactions by a given context

        """
        Company = tables.Company
        Customer = tables.Customer
        Invoice = tables.Invoice
        Plan = tables.Plan
        Subscription = tables.Subscription
        Transaction = tables.Transaction
        SubscriptionInvoice = tables.SubscriptionInvoice
        CustomerInvoice = tables.CustomerInvoice

        # joined subscription transaction query
        basic_query = self.session.query(Transaction)
        # joined subscription invoice query
        subscription_invoice_query = (basic_query.join(
            SubscriptionInvoice,
            SubscriptionInvoice.guid == Transaction.invoice_guid,
        ))
        # joined customer invoice query
        customer_invoice_query = (basic_query.join(
            CustomerInvoice,
            CustomerInvoice.guid == Transaction.invoice_guid,
        ))
        # joined subscription query
        subscription_query = (subscription_invoice_query.join(
            Subscription,
            Subscription.guid == SubscriptionInvoice.subscription_guid,
        ))
        # joined customer query
        customer_query = (customer_invoice_query.join(
            Customer,
            Customer.guid == CustomerInvoice.customer_guid,
        ))
        # joined plan query
        plan_query = (subscription_query.join(
            Plan,
            Plan.guid == Subscription.plan_guid,
        ))

        if isinstance(context, Invoice):
            query = (basic_query.filter(Transaction.invoice == context))
        elif isinstance(context, Subscription):
            query = (subscription_invoice_query.filter(
                SubscriptionInvoice.subscription == context))
        elif isinstance(context, Customer):
            query = (customer_invoice_query.filter(
                CustomerInvoice.customer == context))
        elif isinstance(context, Plan):
            query = (subscription_query.filter(Subscription.plan == context))
        elif isinstance(context, Company):
            q1 = (plan_query.filter(Plan.company == context))
            q2 = (customer_query.filter(Customer.company == context))
            query = q1.union(q2)
        else:
            raise ValueError('Unsupported context {}'.format(context))

        query = query.order_by(Transaction.created_at.desc())
        return query

    def create(
        self,
        invoice,
        amount,
        transaction_type=None,
        funding_instrument_uri=None,
        reference_to=None,
        appears_on_statement_as=None,
    ):
        """Create a transaction and return

        """
        if transaction_type is None:
            transaction_type = invoice.transaction_type

        if reference_to is not None:
            if transaction_type not in [self.types.REFUND, self.types.REVERSE]:
                raise ValueError('reference_to can only be set to a refund '
                                 'transaction')
            if funding_instrument_uri is not None:
                raise ValueError(
                    'funding_instrument_uri cannot be set to a refund/reverse '
                    'transaction')
            if (reference_to.transaction_type
                    not in [self.types.DEBIT, self.types.CREDIT]):
                raise ValueError(
                    'Only charge/payout transaction can be refunded/reversed')

        now = tables.now_func()
        transaction = tables.Transaction(
            guid='TX' + make_guid(),
            transaction_type=transaction_type,
            amount=amount,
            funding_instrument_uri=funding_instrument_uri,
            appears_on_statement_as=appears_on_statement_as,
            submit_status=self.submit_statuses.STAGED,
            reference_to=reference_to,
            created_at=now,
            updated_at=now,
            invoice=invoice,
        )
        self.session.add(transaction)
        self.session.flush()
        return transaction

    def update(self, transaction, **kwargs):
        """Update a transaction

        """
        now = tables.now_func()
        transaction.updated_at = now
        if kwargs:
            raise TypeError('Unknown attributes {} to update'.format(
                tuple(kwargs.keys())))
        self.session.flush()

    def add_event(self, transaction, status, processor_id, occurred_at):
        """Add a status updating event of transaction from callback

        """
        now = tables.now_func()
        # the latest event of this transaction
        last_event = transaction.events.first()

        event = tables.TransactionEvent(
            guid='TE' + make_guid(),
            transaction=transaction,
            processor_id=processor_id,
            occurred_at=occurred_at,
            status=status,
            created_at=now,
        )
        self.session.add(event)

        # ensure won't duplicate
        try:
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            raise DuplicateEventError(
                'Event {} already exists for {}'.format(
                    processor_id,
                    transaction.guid,
                ), )

        # Notice: we only want to update transaction status if this event
        # is the latest one we had seen in Billy system. Why we are doing
        # here is because I think of some scenarios like
        #
        #  1. Balanced cannot reach Billy for a short while, and retry later
        #  2. Attacker want to fool us with old events
        #
        # These will lead the status of invoice to be updated incorrectly.
        # For case 1, events send to Billy like this:
        #
        #     succeeded (failed to send to Billy, retry 1 minute later)
        #     failed
        #     succeeded (retry)
        #
        # See? The final status should be `failed`, but as the `succeeded`
        # was resent later, so it became `succeded` eventually. Similarly,
        # attackers can send us an old `succeeded` event to make the invoice
        # settled.  This is why we need to ensure only the latest event can
        # affect status of invoice.
        if last_event is not None and occurred_at <= last_event.occurred_at:
            return

        old_status = transaction.status
        transaction.updated_at = now
        transaction.status = status
        # update invoice status
        invoice_model = self.factory.create_invoice_model()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice,
            transaction=transaction,
            original_status=old_status,
        )
        self.session.flush()

    def process_one(self, transaction):
        """Process one transaction

        """
        invoice_model = self.factory.create_invoice_model()

        # there is still chance we duplicate transaction, for example
        #
        #     (Thread 1)                    (Thread 2)
        #     Check existing transaction
        #                                   Check existing transaction
        #                                   Called to balanced
        #     Call to balanced
        #
        # we need to lock transaction before we process it to avoid
        # situations like that
        self.get(transaction.guid, with_lockmode='update')

        if transaction.submit_status == self.submit_statuses.DONE:
            raise ValueError('Cannot process a finished transaction {}'.format(
                transaction.guid))
        self.logger.debug('Processing transaction %s', transaction.guid)
        now = tables.now_func()

        if transaction.invoice.invoice_type == invoice_model.types.SUBSCRIPTION:
            customer = transaction.invoice.subscription.customer
        else:
            customer = transaction.invoice.customer

        processor = self.factory.create_processor()

        method = {
            self.types.DEBIT: processor.debit,
            self.types.CREDIT: processor.credit,
            self.types.REFUND: processor.refund,
        }[transaction.transaction_type]

        try:
            processor.configure_api_key(customer.company.processor_key)
            self.logger.info(
                'Preparing customer %s (processor_uri=%s)',
                customer.guid,
                customer.processor_uri,
            )
            # prepare customer (add bank account or credit card)
            processor.prepare_customer(
                customer=customer,
                funding_instrument_uri=transaction.funding_instrument_uri,
            )
            # do charge/payout/refund
            result = method(transaction)
        except (SystemExit, KeyboardInterrupt):
            raise
        except Exception, e:
            transaction.submit_status = self.submit_statuses.RETRYING
            failure_model = self.factory.create_transaction_failure_model()
            failure_model.create(
                transaction=transaction,
                error_message=unicode(e),
                # TODO: error number and code?
            )
            self.logger.error(
                'Failed to process transaction %s, '
                'failure_count=%s',
                transaction.guid,
                transaction.failure_count,
                exc_info=True)
            # the failure times exceed the limitation
            if transaction.failure_count > self.maximum_retry:
                self.logger.error(
                    'Exceed maximum retry limitation %s, '
                    'transaction %s failed', self.maximum_retry,
                    transaction.guid)
                transaction.submit_status = self.submit_statuses.FAILED

                # the transaction is failed, update invoice status
                if transaction.transaction_type in [
                        self.types.DEBIT,
                        self.types.CREDIT,
                ]:
                    transaction.invoice.status = invoice_model.statuses.FAILED
            transaction.updated_at = now
            self.session.flush()
            return

        old_status = transaction.status
        transaction.processor_uri = result['processor_uri']
        transaction.status = result['status']
        transaction.submit_status = self.submit_statuses.DONE
        transaction.updated_at = tables.now_func()
        invoice_model.transaction_status_update(
            invoice=transaction.invoice,
            transaction=transaction,
            original_status=old_status,
        )

        self.session.flush()
        self.logger.info(
            'Processed transaction %s, submit_status=%s, '
            'result=%s', transaction.guid, transaction.submit_status, result)
コード例 #24
0
    def process_one(self, transaction):
        """Process one transaction

        """
        invoice_model = self.factory.create_invoice_model()

        # there is still chance we duplicate transaction, for example
        #
        #     (Thread 1)                    (Thread 2)
        #     Check existing transaction
        #                                   Check existing transaction
        #                                   Called to balanced
        #     Call to balanced
        #
        # we need to lock transaction before we process it to avoid
        # situations like that
        self.get(transaction.guid, with_lockmode='update')

        if transaction.submit_status == self.submit_statuses.DONE:
            raise ValueError('Cannot process a finished transaction {}'.format(
                transaction.guid))
        self.logger.debug('Processing transaction %s', transaction.guid)
        now = tables.now_func()

        if transaction.invoice.invoice_type == invoice_model.types.SUBSCRIPTION:
            customer = transaction.invoice.subscription.customer
        else:
            customer = transaction.invoice.customer

        processor = self.factory.create_processor()

        method = {
            self.types.DEBIT: processor.debit,
            self.types.CREDIT: processor.credit,
            self.types.REFUND: processor.refund,
        }[transaction.transaction_type]

        try:
            processor.configure_api_key(customer.company.processor_key)
            self.logger.info(
                'Preparing customer %s (processor_uri=%s)',
                customer.guid,
                customer.processor_uri,
            )
            # prepare customer (add bank account or credit card)
            processor.prepare_customer(
                customer=customer,
                funding_instrument_uri=transaction.funding_instrument_uri,
            )
            # do charge/payout/refund
            result = method(transaction)
        except (SystemExit, KeyboardInterrupt):
            raise
        except Exception, e:
            transaction.submit_status = self.submit_statuses.RETRYING
            failure_model = self.factory.create_transaction_failure_model()
            failure_model.create(
                transaction=transaction,
                error_message=unicode(e),
                # TODO: error number and code?
            )
            self.logger.error(
                'Failed to process transaction %s, '
                'failure_count=%s',
                transaction.guid,
                transaction.failure_count,
                exc_info=True)
            # the failure times exceed the limitation
            if transaction.failure_count > self.maximum_retry:
                self.logger.error(
                    'Exceed maximum retry limitation %s, '
                    'transaction %s failed', self.maximum_retry,
                    transaction.guid)
                transaction.submit_status = self.submit_statuses.FAILED

                # the transaction is failed, update invoice status
                if transaction.transaction_type in [
                        self.types.DEBIT,
                        self.types.CREDIT,
                ]:
                    transaction.invoice.status = invoice_model.statuses.FAILED
            transaction.updated_at = now
            self.session.flush()
            return
コード例 #25
0
ファイル: transaction.py プロジェクト: carriercomm/billy
    def process_one(self, transaction):
        """Process one transaction

        """
        invoice_model = self.factory.create_invoice_model()

        # there is still chance we duplicate transaction, for example
        #
        #     (Thread 1)                    (Thread 2)
        #     Check existing transaction
        #                                   Check existing transaction
        #                                   Called to balanced
        #     Call to balanced
        #
        # we need to lock transaction before we process it to avoid
        # situations like that
        self.get(transaction.guid, with_lockmode="update")

        if transaction.submit_status == self.submit_statuses.DONE:
            raise ValueError("Cannot process a finished transaction {}".format(transaction.guid))
        self.logger.debug("Processing transaction %s", transaction.guid)
        now = tables.now_func()

        if transaction.invoice.invoice_type == invoice_model.types.SUBSCRIPTION:
            customer = transaction.invoice.subscription.customer
        else:
            customer = transaction.invoice.customer

        processor = self.factory.create_processor()

        method = {
            self.types.DEBIT: processor.debit,
            self.types.CREDIT: processor.credit,
            self.types.REFUND: processor.refund,
        }[transaction.transaction_type]

        try:
            processor.configure_api_key(customer.company.processor_key)
            self.logger.info("Preparing customer %s (processor_uri=%s)", customer.guid, customer.processor_uri)
            # prepare customer (add bank account or credit card)
            processor.prepare_customer(customer=customer, funding_instrument_uri=transaction.funding_instrument_uri)
            # do charge/payout/refund
            result = method(transaction)
        except (SystemExit, KeyboardInterrupt):
            raise
        except Exception, e:
            transaction.submit_status = self.submit_statuses.RETRYING
            failure_model = self.factory.create_transaction_failure_model()
            failure_model.create(
                transaction=transaction,
                error_message=unicode(e),
                # TODO: error number and code?
            )
            self.logger.error(
                "Failed to process transaction %s, " "failure_count=%s",
                transaction.guid,
                transaction.failure_count,
                exc_info=True,
            )
            # the failure times exceed the limitation
            if transaction.failure_count > self.maximum_retry:
                self.logger.error(
                    "Exceed maximum retry limitation %s, " "transaction %s failed", self.maximum_retry, transaction.guid
                )
                transaction.submit_status = self.submit_statuses.FAILED

                # the transaction is failed, update invoice status
                if transaction.transaction_type in [self.types.DEBIT, self.types.CREDIT]:
                    transaction.invoice.status = invoice_model.statuses.FAILED
            transaction.updated_at = now
            self.session.flush()
            return
コード例 #26
0
ファイル: invoice.py プロジェクト: winstonhoseadriggins/billy
    def update_funding_instrument_uri(self, invoice, funding_instrument_uri):
        """Update the funding_instrument_uri of an invoice, as it may yield
        transactions, we don't want to put this in `update` method

        @return: a list of yielded transaction
        """
        Transaction = tables.Transaction

        tx_model = self.factory.create_transaction_model()
        now = tables.now_func()
        invoice.updated_at = now
        invoice.funding_instrument_uri = funding_instrument_uri
        transactions = []

        # We have nothing to do if the amount is zero, just return
        if invoice.amount == 0:
            return transactions

        # think about race condition issue, what if we update the
        # funding_instrument_uri during processing previous transaction? say
        #
        #     DB Transaction A begin
        #     Call to Balanced API
        #                                   DB Transaction B begin
        #                                   Update invoice payment URI
        #                                   Update last transaction to CANCELED
        #                                   Create a new transaction
        #                                   DB Transaction B commit
        #     Update transaction to DONE
        #     DB Transaction A commit
        #     DB Transaction conflicts
        #     DB Transaction rollback
        #
        # call to balanced API is made, but we had confliction between two
        # database transactions
        #
        # to solve the problem mentioned above, we acquire a lock on the
        # invoice at begin of transaction, in this way, there will be no
        # overlap between two transaction
        self.get(invoice.guid, with_lockmode='update')

        # the invoice is just created, simply create a transaction for it
        if invoice.status == self.statuses.STAGED:
            transaction = self._create_transaction(invoice)
            transactions.append(transaction)
        # we are already processing, cancel current transaction and create
        # a new one
        elif invoice.status == self.statuses.PROCESSING:
            # find the running transaction and cancel it
            last_transaction = (
                self.session
                .query(Transaction)
                .filter(
                    Transaction.invoice == invoice,
                    Transaction.transaction_type.in_([
                        TransactionModel.types.DEBIT,
                        TransactionModel.types.CREDIT,
                    ]),
                    Transaction.submit_status.in_([
                        TransactionModel.submit_statuses.STAGED,
                        TransactionModel.submit_statuses.RETRYING,
                    ])
                )
            ).one()
            last_transaction.submit_status = tx_model.submit_statuses.CANCELED
            last_transaction.canceled_at = now
            # create a new one
            transaction = self._create_transaction(invoice)
            transactions.append(transaction)
        # the previous transaction failed, just create a new one
        elif invoice.status == self.statuses.FAILED:
            transaction = self._create_transaction(invoice)
            transactions.append(transaction)
        else:
            raise InvalidOperationError(
                'Invalid operation, you can only update funding_instrument_uri '
                'when the invoice status is one of STAGED, PROCESSING and '
                'FAILED'
            )
        invoice.status = self.statuses.PROCESSING

        self.session.flush()
        return transactions
コード例 #27
0
    def yield_invoices(self, subscriptions=None, now=None):
        """Generate new scheduled invoices from given subscriptions

        :param subscriptions: A list subscription to yield invoices
            from, if None is given, all subscriptions in the database will be
            the yielding source
        :param now: the current date time to use, now_func() will be used by
            default
        :return: a generated transaction guid list
        """
        if now is None:
            now = tables.now_func()

        invoice_model = self.factory.create_invoice_model()
        Subscription = tables.Subscription

        subscription_guids = []
        if subscriptions is not None:
            subscription_guids = [
                subscription.guid for subscription in subscriptions
            ]
        invoices = []

        # as we may have multiple new invoices for one subscription to
        # yield now, for example, we didn't run this method for a long while,
        # in this case, we need to make sure all transactions are yielded
        while True:
            # find subscriptions which should yield new invoices
            query = (
                self.session.query(Subscription)
                .filter(Subscription.next_invoice_at <= now)
                .filter(not_(Subscription.canceled))
            )
            if subscription_guids:
                query = query.filter(Subscription.guid.in_(subscription_guids))
            query = list(query)

            # okay, we have no more subscription to process, just break
            if not query:
                self.logger.info('No more subscriptions to process')
                break

            for subscription in query:
                amount = subscription.effective_amount
                # create the new transaction for this subscription
                invoice = invoice_model.create(
                    subscription=subscription,
                    funding_instrument_uri=subscription.funding_instrument_uri,
                    amount=amount,
                    scheduled_at=subscription.next_invoice_at,
                    appears_on_statement_as=subscription.appears_on_statement_as,
                )
                self.logger.info(
                    'Created subscription invoice for %s, guid=%s, '
                    'plan_type=%s, funding_instrument_uri=%s, '
                    'amount=%s, scheduled_at=%s, period=%s',
                    subscription.guid,
                    invoice.guid,
                    subscription.plan.plan_type,
                    invoice.funding_instrument_uri,
                    invoice.amount,
                    invoice.scheduled_at,
                    subscription.invoice_count - 1,
                )
                # advance the next invoice time
                subscription.next_invoice_at = next_transaction_datetime(
                    started_at=subscription.started_at,
                    frequency=subscription.plan.frequency,
                    period=subscription.invoice_count,
                    interval=subscription.plan.interval,
                )
                self.logger.info(
                    'Schedule next invoice of %s at %s (period=%s)',
                    subscription.guid,
                    subscription.next_invoice_at,
                    subscription.invoice_count,
                )
                self.session.flush()
                invoices.append(invoice)

        self.session.flush()
        return invoices