diff --git a/pinax/stripe/actions/__init__.py b/pinax/stripe/actions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pinax/stripe/actions/accounts.py b/pinax/stripe/actions/accounts.py deleted file mode 100644 index 5ca658cdf..000000000 --- a/pinax/stripe/actions/accounts.py +++ /dev/null @@ -1,225 +0,0 @@ -import datetime - -import stripe - -from .. import models, utils -from .externalaccounts import sync_bank_account_from_stripe_data - - -def create(user, country, **kwargs): - """ - Create an Account. - - Args: - country: two letter country code for where the individual lives - - Returns: - a pinax.stripe.models.Account object - """ - kwargs["country"] = country - stripe_account = stripe.Account.create(**kwargs) - return sync_account_from_stripe_data( - stripe_account, user=user - ) - - -def update(account, data): - """ - Update the given account with extra data. - - Args: - account: a pinax.stripe.models.Account object - data: dict of account fields to update via API: - - first_name -> legal_entity.first_name - last_name -> legal_entity.last_name - dob -> legal_entity.dob - personal_id_number -> legal_entity.personal_id_number - document -> legal_entity.verification.document - - Returns: - a pinax.stripe.models.Account object - """ - stripe_account = stripe.Account.retrieve(id=account.stripe_id) - - if data.get("dob"): - stripe_account.legal_entity.dob = data["dob"] - - if data.get("first_name"): - stripe_account.legal_entity.first_name = data["first_name"] - - if data.get("last_name"): - stripe_account.legal_entity.last_name = data["last_name"] - - if data.get("personal_id_number"): - stripe_account.legal_entity.personal_id_number = data["personal_id_number"] - - if data.get("document"): - response = stripe.FileUpload.create( - purpose="identity_document", - file=data["document"], - stripe_account=stripe_account.id - ) - stripe_account.legal_entity.verification.document = response["id"] - - stripe_account.save() - return sync_account_from_stripe_data(stripe_account) - - -def sync_account(account): - """ - Update the given local Account instance from remote data. - - Args: - account: a pinax.stripe.models.Account object - - Returns: - a pinax.stripe.models.Account object - """ - stripe_account = stripe.Account.retrieve(id=account.stripe_id) - return sync_account_from_stripe_data(stripe_account) - - -def sync_account_from_stripe_data(data, user=None): - """ - Create or update using the account object from a Stripe API query. - - Args: - data: the data representing an account object in the Stripe API - - Returns: - a pinax.stripe.models.Account object - """ - kwargs = {"stripe_id": data["id"]} - if user: - kwargs["user"] = user - obj, created = models.Account.objects.get_or_create( - **kwargs - ) - common_attrs = ( - "business_name", "business_url", "charges_enabled", "country", - "default_currency", "details_submitted", "display_name", - "email", "type", "statement_descriptor", "support_email", - "support_phone", "timezone", "payouts_enabled" - ) - - custom_attrs = ( - "debit_negative_balances", "metadata", "product_description", - "payout_statement_descriptor" - ) - - if data["type"] == "custom": - top_level_attrs = common_attrs + custom_attrs - else: - top_level_attrs = common_attrs - - for a in [x for x in top_level_attrs if x in data]: - setattr(obj, a, data[a]) - - # that's all we get for standard and express accounts! - if data["type"] != "custom": - obj.save() - return obj - - # otherwise we continue on to gather a range of details available - # to us on custom accounts - - # legal entity for individual accounts - le = data["legal_entity"] - address = le["address"] - obj.legal_entity_address_city = address["city"] - obj.legal_entity_address_country = address["country"] - obj.legal_entity_address_line1 = address["line1"] - obj.legal_entity_address_line2 = address["line2"] - obj.legal_entity_address_postal_code = address["postal_code"] - obj.legal_entity_address_state = address["state"] - - dob = le["dob"] - if dob: - obj.legal_entity_dob = datetime.date( - dob["year"], dob["month"], dob["day"] - ) - else: - obj.legal_entity_dob = None - - obj.legal_entity_type = le["type"] - obj.legal_entity_first_name = le["first_name"] - obj.legal_entity_last_name = le["last_name"] - obj.legal_entity_personal_id_number_provided = le["personal_id_number_provided"] - - # these attributes are not always present - obj.legal_entity_gender = le.get( - "gender", obj.legal_entity_gender - ) - obj.legal_entity_maiden_name = le.get( - "maiden_name", obj.legal_entity_maiden_name - ) - obj.legal_entity_phone_number = le.get( - "phone_number", obj.legal_entity_phone_number - ) - obj.legal_entity_ssn_last_4_provided = le.get( - "ssn_last_4_provided", obj.legal_entity_ssn_last_4_provided - ) - - verification = le["verification"] - if verification: - obj.legal_entity_verification_details = verification.get("details") - obj.legal_entity_verification_details_code = verification.get("details_code") - obj.legal_entity_verification_document = verification.get("document") - obj.legal_entity_verification_status = verification.get("status") - else: - obj.legal_entity_verification_details = None - obj.legal_entity_verification_details_code = None - obj.legal_entity_verification_document = None - obj.legal_entity_verification_status = None - - # tos state - if data["tos_acceptance"]["date"]: - obj.tos_acceptance_date = datetime.datetime.utcfromtimestamp( - data["tos_acceptance"]["date"] - ) - else: - obj.tos_acceptance_date = None - obj.tos_acceptance_ip = data["tos_acceptance"]["ip"] - obj.tos_acceptance_user_agent = data["tos_acceptance"]["user_agent"] - - # decline charge on certain conditions - obj.decline_charge_on_avs_failure = data["decline_charge_on"]["avs_failure"] - obj.decline_charge_on_cvc_failure = data["decline_charge_on"]["cvc_failure"] - - # transfer schedule to external account - ps = data["payout_schedule"] - obj.payout_schedule_interval = ps["interval"] - obj.payout_schedule_delay_days = ps.get("delay_days") - obj.payout_schedule_weekly_anchor = ps.get("weekly_anchor") - obj.payout_schedule_monthly_anchor = ps.get("monthly_anchor") - - # verification status, key to progressing account setup - obj.verification_disabled_reason = data["verification"]["disabled_reason"] - obj.verification_due_by = utils.convert_tstamp(data["verification"], "due_by") - obj.verification_fields_needed = data["verification"]["fields_needed"] - - obj.save() - - # sync any external accounts (bank accounts only for now) included - for external_account in data["external_accounts"]["data"]: - if external_account["object"] == "bank_account": - sync_bank_account_from_stripe_data(external_account) - - return obj - - -def delete(account): - """ - Delete an account both remotely and locally. - - Note that this will fail if the account's balance is - non-zero. - """ - account.stripe_account.delete() - account.delete() - - -def deauthorize(account): - account.authorized = False - account.save() diff --git a/pinax/stripe/actions/charges.py b/pinax/stripe/actions/charges.py deleted file mode 100644 index 375f8ffda..000000000 --- a/pinax/stripe/actions/charges.py +++ /dev/null @@ -1,235 +0,0 @@ -import decimal - -from django.conf import settings -from django.db.models import Q - -import stripe -from six import string_types - -from .. import hooks, models, utils - - -def calculate_refund_amount(charge, amount=None): - """ - Calculate refund amount given a charge and optional amount. - - Args: - charge: a pinax.stripe.models.Charge object - amount: optionally, the decimal.Decimal amount you wish to refund - """ - eligible_to_refund = charge.amount - (charge.amount_refunded or 0) - if amount: - return min(eligible_to_refund, amount) - return eligible_to_refund - - -def capture(charge, amount=None, idempotency_key=None): - """ - Capture the payment of an existing, uncaptured, charge. - - Args: - charge: a pinax.stripe.models.Charge object - amount: the decimal.Decimal amount of the charge to capture - idempotency_key: Any string that allows retries to be performed safely. - """ - amount = utils.convert_amount_for_api( - amount if amount else charge.amount, - charge.currency - ) - stripe_charge = stripe.Charge( - charge.stripe_id, - stripe_account=charge.stripe_account_stripe_id, - ).capture( - amount=amount, - idempotency_key=idempotency_key, - expand=["balance_transaction"], - ) - sync_charge_from_stripe_data(stripe_charge) - - -def _validate_create_params(customer, source, amount, application_fee, destination_account, destination_amount, on_behalf_of): - if not customer and not source: - raise ValueError("Must provide `customer` or `source`.") - if not isinstance(amount, decimal.Decimal): - raise ValueError( - "You must supply a decimal value for `amount`." - ) - if application_fee and not isinstance(application_fee, decimal.Decimal): - raise ValueError( - "You must supply a decimal value for `application_fee`." - ) - if application_fee and not destination_account: - raise ValueError( - "You can only specify `application_fee` with `destination_account`" - ) - if application_fee and destination_account and destination_amount: - raise ValueError( - "You can't specify `application_fee` with `destination_amount`" - ) - if destination_account and on_behalf_of: - raise ValueError( - "`destination_account` and `on_behalf_of` are mutualy exclusive") - - -def create( - amount, customer=None, source=None, currency="usd", description=None, - send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS, capture=True, - email=None, destination_account=None, destination_amount=None, - application_fee=None, on_behalf_of=None, idempotency_key=None, - stripe_account=None -): - """ - Create a charge for the given customer or source. - - If both customer and source are provided, the source must belong to the - customer. - - See https://stripe.com/docs/api#create_charge-customer. - - Args: - amount: should be a decimal.Decimal amount - customer: the Customer object to charge - source: the Stripe id of the source to charge - currency: the currency with which to charge the amount in - description: a description of the charge - send_receipt: send a receipt upon successful charge - capture: immediately capture the charge instead of doing a pre-authorization - destination_account: stripe_id of a connected account - destination_amount: amount to transfer to the `destination_account` without creating an application fee - application_fee: used with `destination_account` to add a fee destined for the platform account - on_behalf_of: Stripe account ID that these funds are intended for. Automatically set if you use the destination parameter. - idempotency_key: Any string that allows retries to be performed safely. - - Returns: - a pinax.stripe.models.Charge object - """ - # Handle customer as stripe_id for backward compatibility. - if customer and not isinstance(customer, models.Customer): - customer, _ = models.Customer.objects.get_or_create(stripe_id=customer) - _validate_create_params(customer, source, amount, application_fee, destination_account, destination_amount, on_behalf_of) - stripe_account_stripe_id = None - if stripe_account: - stripe_account_stripe_id = stripe_account.stripe_id - if customer and customer.stripe_account_stripe_id: - stripe_account_stripe_id = customer.stripe_account_stripe_id - kwargs = dict( - amount=utils.convert_amount_for_api(amount, currency), # find the final amount - currency=currency, - source=source, - customer=customer.stripe_id if customer else None, - stripe_account=stripe_account_stripe_id, - description=description, - capture=capture, - idempotency_key=idempotency_key, - ) - if destination_account: - kwargs["destination"] = {"account": destination_account} - if destination_amount: - kwargs["destination"]["amount"] = utils.convert_amount_for_api( - destination_amount, - currency - ) - if application_fee: - kwargs["application_fee"] = utils.convert_amount_for_api( - application_fee, currency - ) - elif on_behalf_of: - kwargs["on_behalf_of"] = on_behalf_of - stripe_charge = stripe.Charge.create( - **kwargs - ) - charge = sync_charge_from_stripe_data(stripe_charge) - if send_receipt: - hooks.hookset.send_receipt(charge, email) - return charge - - -def retrieve(stripe_id, stripe_account=None): - """Retrieve a Charge plus its balance info.""" - return stripe.Charge.retrieve( - stripe_id, - stripe_account=stripe_account, - expand=["balance_transaction"] - ) - - -def sync_charges_for_customer(customer): - """ - Populate database with all the charges for a customer. - - Args: - customer: a pinax.stripe.models.Customer object - """ - for charge in customer.stripe_customer.charges().data: - sync_charge_from_stripe_data(charge) - - -def sync_charge(stripe_id, stripe_account=None): - """Sync a charge given a Stripe charge ID.""" - return sync_charge_from_stripe_data( - retrieve(stripe_id, stripe_account=stripe_account) - ) - - -def sync_charge_from_stripe_data(data): - """ - Create or update the charge represented by the data from a Stripe API query. - - Args: - data: the data representing a charge object in the Stripe API - - Returns: - a pinax.stripe.models.Charge object - """ - obj, _ = models.Charge.objects.get_or_create(stripe_id=data["id"]) - obj.customer = models.Customer.objects.filter(stripe_id=data["customer"]).first() - obj.source = data["source"]["id"] - obj.currency = data["currency"] - obj.invoice = models.Invoice.objects.filter(stripe_id=data["invoice"]).first() - obj.amount = utils.convert_amount_for_db(data["amount"], obj.currency) - obj.paid = data["paid"] - obj.refunded = data["refunded"] - obj.captured = data["captured"] - obj.disputed = data["dispute"] is not None - obj.charge_created = utils.convert_tstamp(data, "created") - if data.get("description"): - obj.description = data["description"] - if data.get("amount_refunded"): - obj.amount_refunded = utils.convert_amount_for_db(data["amount_refunded"], obj.currency) - if data["refunded"]: - obj.amount_refunded = obj.amount - balance_transaction = data.get("balance_transaction") - if balance_transaction and not isinstance(balance_transaction, string_types): - obj.available = balance_transaction["status"] == "available" - obj.available_on = utils.convert_tstamp( - balance_transaction, "available_on" - ) - obj.fee = utils.convert_amount_for_db( - balance_transaction["fee"], balance_transaction["currency"] - ) - obj.fee_currency = balance_transaction["currency"] - obj.transfer_group = data.get("transfer_group") - obj.outcome = data.get("outcome") - obj.save() - return obj - - -def update_charge_availability(): - """ - Update `available` and `available_on` attributes of Charges. - - We only bother checking those Charges that can become available. - """ - charges = models.Charge.objects.filter( - paid=True, - captured=True - ).exclude( - Q(available=True) | Q(refunded=True) - ).select_related( - "customer" - ) - for c in charges.iterator(): - sync_charge( - c.stripe_id, - stripe_account=c.customer.stripe_account - ) diff --git a/pinax/stripe/actions/coupons.py b/pinax/stripe/actions/coupons.py deleted file mode 100644 index bcf6d08a7..000000000 --- a/pinax/stripe/actions/coupons.py +++ /dev/null @@ -1,32 +0,0 @@ -import stripe - -from .. import models, utils - - -def sync_coupons(): - """ - Synchronizes all coupons from the Stripe API - """ - coupons = stripe.Coupon.auto_paging_iter() - for coupon in coupons: - defaults = dict( - amount_off=( - utils.convert_amount_for_db(coupon["amount_off"], coupon["currency"]) - if coupon["amount_off"] - else None - ), - currency=coupon["currency"] or "", - duration=coupon["duration"], - duration_in_months=coupon["duration_in_months"], - max_redemptions=coupon["max_redemptions"], - metadata=coupon["metadata"], - percent_off=coupon["percent_off"], - redeem_by=utils.convert_tstamp(coupon["redeem_by"]) if coupon["redeem_by"] else None, - times_redeemed=coupon["times_redeemed"], - valid=coupon["valid"], - ) - obj, created = models.Coupon.objects.get_or_create( - stripe_id=coupon["id"], - defaults=defaults - ) - utils.update_with_defaults(obj, defaults, created) diff --git a/pinax/stripe/actions/customers.py b/pinax/stripe/actions/customers.py deleted file mode 100644 index ff82d4351..000000000 --- a/pinax/stripe/actions/customers.py +++ /dev/null @@ -1,232 +0,0 @@ -import logging - -from django.utils import timezone -from django.utils.encoding import smart_str - -import stripe - -from . import invoices, sources, subscriptions -from .. import hooks, models, utils -from ..conf import settings - -logger = logging.getLogger(__name__) - - -def can_charge(customer): - """ - Can the given customer create a charge - - Args: - customer: a pinax.stripe.models.Customer object - """ - if customer.date_purged is not None: - return False - if customer.default_source: - return True - return False - - -def _create_without_account(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): - cus = models.Customer.objects.filter(user=user).first() - if cus is not None: - try: - stripe.Customer.retrieve(cus.stripe_id) - return cus - except stripe.error.InvalidRequestError: - pass - - # At this point we maybe have a local Customer but no stripe customer - # let's create one and make the binding - trial_end = hooks.hookset.trial_period(user, plan) - stripe_customer = stripe.Customer.create( - email=user.email, - source=card, - plan=plan, - quantity=quantity, - trial_end=trial_end - ) - cus, created = models.Customer.objects.get_or_create( - user=user, - defaults={ - "stripe_id": stripe_customer["id"] - } - ) - if not created: - cus.stripe_id = stripe_customer["id"] # sync_customer will call cus.save() - sync_customer(cus, stripe_customer) - if plan and charge_immediately: - invoices.create_and_pay(cus) - return cus - - -def _create_with_account(user, stripe_account, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None): - cus = user.customers.filter(user_account__account=stripe_account).first() - if cus is not None: - try: - stripe.Customer.retrieve(cus.stripe_id, stripe_account=stripe_account.stripe_id) - return cus - except stripe.error.InvalidRequestError: - pass - - # At this point we maybe have a local Customer but no stripe customer - # let's create one and make the binding - trial_end = hooks.hookset.trial_period(user, plan) - stripe_customer = stripe.Customer.create( - email=user.email, - source=card, - plan=plan, - quantity=quantity, - trial_end=trial_end, - stripe_account=stripe_account.stripe_id, - ) - - if cus is None: - cus = models.Customer.objects.create(stripe_id=stripe_customer["id"], stripe_account=stripe_account) - models.UserAccount.objects.create(user=user, account=stripe_account, customer=cus) - else: - logger.debug("Update local customer %s with new remote customer %s for user %s, and account %s", - cus.stripe_id, stripe_customer["id"], user, stripe_account) - cus.stripe_id = stripe_customer["id"] # sync_customer() will call cus.save() - sync_customer(cus, stripe_customer) - if plan and charge_immediately: - invoices.create_and_pay(cus) - return cus - - -def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_immediately=True, quantity=None, stripe_account=None): - """ - Creates a Stripe customer. - - If a customer already exists, the existing customer will be returned. - - Args: - user: a user object - card: optionally, the token for a new card - plan: a plan to subscribe the user to - charge_immediately: whether or not the user should be immediately - charged for the subscription - quantity: the quantity (multiplier) of the subscription - stripe_account: An account object. If given, the Customer and User relation will be established for you through the UserAccount model. - Because a single User might have several Customers, one per Account. - - Returns: - the pinax.stripe.models.Customer object that was created - """ - if stripe_account is None: - return _create_without_account(user, card=card, plan=plan, charge_immediately=charge_immediately, quantity=quantity) - return _create_with_account(user, stripe_account, card=card, plan=plan, charge_immediately=charge_immediately, quantity=quantity) - - -def get_customer_for_user(user, stripe_account=None): - """ - Get a customer object for a given user - - Args: - user: a user object - stripe_account: An Account object - - Returns: - a pinax.stripe.models.Customer object - """ - if stripe_account is None: - return models.Customer.objects.filter(user=user).first() - return user.customers.filter(user_account__account=stripe_account).first() - - -def purge_local(customer): - customer.user_accounts.all().delete() - customer.user = None - customer.date_purged = timezone.now() - customer.save() - - -def purge(customer): - """ - Deletes the Stripe customer data and purges the linking of the transaction - data to the Django user. - - Args: - customer: the pinax.stripe.models.Customer object to purge - """ - try: - customer.stripe_customer.delete() - except stripe.error.InvalidRequestError as e: - if "no such customer:" not in smart_str(e).lower(): - # The exception was thrown because the customer was already - # deleted on the stripe side, ignore the exception - raise - purge_local(customer) - - -def link_customer(event): - """ - Links a customer referenced in a webhook event message to the event object - - Args: - event: the pinax.stripe.models.Event object to link - """ - cus_id = None - customer_crud_events = [ - "customer.created", - "customer.updated", - "customer.deleted" - ] - event_data_object = event.message["data"]["object"] - if event.kind in customer_crud_events: - cus_id = event_data_object["id"] - else: - cus_id = event_data_object.get("customer", None) - - if cus_id is not None: - customer, created = models.Customer.objects.get_or_create( - stripe_id=cus_id, - stripe_account=event.stripe_account, - ) - if event.kind in customer_crud_events: - sync_customer(customer, event_data_object) - - event.customer = customer - event.save() - - -def set_default_source(customer, source): - """ - Sets the default payment source for a customer - - Args: - customer: a Customer object - source: the Stripe ID of the payment source - """ - stripe_customer = customer.stripe_customer - stripe_customer.default_source = source - cu = stripe_customer.save() - sync_customer(customer, cu=cu) - - -def sync_customer(customer, cu=None): - """ - Synchronizes a local Customer object with details from the Stripe API - - Args: - customer: a Customer object - cu: optionally, data from the Stripe API representing the customer - """ - if customer.date_purged is not None: - return - - if cu is None: - cu = customer.stripe_customer - - if cu.get("deleted", False): - purge_local(customer) - return - - customer.account_balance = utils.convert_amount_for_db(cu["account_balance"], cu["currency"]) - customer.currency = cu["currency"] or "" - customer.delinquent = cu["delinquent"] - customer.default_source = cu["default_source"] or "" - customer.save() - for source in cu["sources"]["data"]: - sources.sync_payment_source_from_stripe_data(customer, source) - for subscription in cu["subscriptions"]["data"]: - subscriptions.sync_subscription_from_stripe_data(customer, subscription) diff --git a/pinax/stripe/actions/events.py b/pinax/stripe/actions/events.py deleted file mode 100644 index 6046694af..000000000 --- a/pinax/stripe/actions/events.py +++ /dev/null @@ -1,52 +0,0 @@ -from .. import models -from ..webhooks import registry - - -def add_event(stripe_id, kind, livemode, message, api_version="", - request_id="", pending_webhooks=0): - """ - Adds and processes an event from a received webhook - - Args: - stripe_id: the stripe id of the event - kind: the label of the event - livemode: True or False if the webhook was sent from livemode or not - message: the data of the webhook - api_version: the version of the Stripe API used - request_id: the id of the request that initiated the webhook - pending_webhooks: the number of pending webhooks - """ - stripe_account_id = message.get("account") - if stripe_account_id: - stripe_account, _ = models.Account.objects.get_or_create( - stripe_id=stripe_account_id - ) - else: - stripe_account = None - event = models.Event.objects.create( - stripe_account=stripe_account, - stripe_id=stripe_id, - kind=kind, - livemode=livemode, - webhook_message=message, - api_version=api_version, - request=request_id, - pending_webhooks=pending_webhooks - ) - WebhookClass = registry.get(kind) - if WebhookClass is not None: - webhook = WebhookClass(event) - webhook.process() - - -def dupe_event_exists(stripe_id): - """ - Checks if a duplicate event exists - - Args: - stripe_id: the Stripe ID of the event to check - - Returns: - True if the event already exists, False otherwise - """ - return models.Event.objects.filter(stripe_id=stripe_id).exists() diff --git a/pinax/stripe/actions/exceptions.py b/pinax/stripe/actions/exceptions.py deleted file mode 100644 index ee3b0a2ea..000000000 --- a/pinax/stripe/actions/exceptions.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys -import traceback - -from .. import models - - -def log_exception(data, exception, event=None): - """ - Log an exception that was captured as a result of processing events - - Args: - data: the data to log about the exception - exception: a string describing the exception (can be the exception - object itself - `str()` gets called on it) - event: optionally, the event object from which the exception occurred - """ - info = sys.exc_info() - info_formatted = "".join(traceback.format_exception(*info)) if info[1] is not None else "" - models.EventProcessingException.objects.create( - event=event, - data=data or "", - message=str(exception), - traceback=info_formatted - ) diff --git a/pinax/stripe/actions/externalaccounts.py b/pinax/stripe/actions/externalaccounts.py deleted file mode 100644 index a1d92cf4e..000000000 --- a/pinax/stripe/actions/externalaccounts.py +++ /dev/null @@ -1,64 +0,0 @@ -from .. import models - - -def create_bank_account(account, account_number, country, currency, **kwargs): - """ - Create a Bank Account. - - Args: - account: the stripe.Account object we're attaching - the bank account to - account_number: the Bank Account number - country: two letter country code - currency: three letter currency code - - There are additional properties that can be set, please see: - https://stripe.com/docs/api#account_create_bank_account - - Returns: - a pinax.stripe.models.BankAccount object - """ - external_account = account.external_accounts.create( - external_account=dict( - object="bank_account", - account_number=account_number, - country=country, - currency=currency, - **kwargs - ) - ) - return sync_bank_account_from_stripe_data( - external_account - ) - - -def sync_bank_account_from_stripe_data(data): - """ - Create or update using the account object from a Stripe API query. - - Args: - data: the data representing an account object in the Stripe API - - Returns: - a pinax.stripe.models.Account object - """ - account = models.Account.objects.get( - stripe_id=data["account"] - ) - kwargs = { - "stripe_id": data["id"], - "account": account - } - obj, created = models.BankAccount.objects.get_or_create( - **kwargs - ) - top_level_attrs = ( - "account_holder_name", "account_holder_type", - "bank_name", "country", "currency", "default_for_currency", - "fingerprint", "last4", "metadata", "routing_number", - "status" - ) - for a in top_level_attrs: - setattr(obj, a, data.get(a)) - obj.save() - return obj diff --git a/pinax/stripe/actions/invoices.py b/pinax/stripe/actions/invoices.py deleted file mode 100644 index 100b621c7..000000000 --- a/pinax/stripe/actions/invoices.py +++ /dev/null @@ -1,195 +0,0 @@ -import decimal - -import stripe - -from . import charges, subscriptions -from .. import hooks, models, utils -from ..conf import settings - - -def create(customer): - """ - Creates a Stripe invoice - - Args: - customer: the customer to create the invoice for (Customer) - - Returns: - the data from the Stripe API that represents the invoice object that - was created - - TODO: - We should go ahead and sync the data so the Invoice object does - not have to wait on the webhook to be received and processed for the - data to be available locally. - """ - return stripe.Invoice.create(customer=customer.stripe_id) - - -def create_and_pay(customer): - """ - Creates and and immediately pays an invoice for a customer - - Args: - customer: the customer to create the invoice for (Customer) - - Returns: - True, if invoice was created, False if there was an error - """ - try: - invoice = create(customer) - if invoice.amount_due > 0: - invoice.pay() - return True - except stripe.error.InvalidRequestError: - return False # There was nothing to Invoice - - -def pay(invoice, send_receipt=True): - """ - Cause an invoice to be paid - - Args: - invoice: the invoice object to have paid - send_receipt: if True, send the receipt as a result of paying - - Returns: - True if the invoice was paid, False if it was unable to be paid - """ - if not invoice.paid and not invoice.closed: - stripe_invoice = invoice.stripe_invoice.pay() - sync_invoice_from_stripe_data(stripe_invoice, send_receipt=send_receipt) - return True - return False - - -def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS): - """ - Synchronizes a local invoice with data from the Stripe API - - Args: - stripe_invoice: data that represents the invoice from the Stripe API - send_receipt: if True, send the receipt as a result of paying - - Returns: - the pinax.stripe.models.Invoice that was created or updated - """ - c = models.Customer.objects.get(stripe_id=stripe_invoice["customer"]) - period_end = utils.convert_tstamp(stripe_invoice, "period_end") - period_start = utils.convert_tstamp(stripe_invoice, "period_start") - date = utils.convert_tstamp(stripe_invoice, "date") - sub_id = stripe_invoice.get("subscription") - stripe_account_id = c.stripe_account_stripe_id - - if stripe_invoice.get("charge"): - charge = charges.sync_charge(stripe_invoice["charge"], stripe_account=stripe_account_id) - if send_receipt: - hooks.hookset.send_receipt(charge) - else: - charge = None - - stripe_subscription = subscriptions.retrieve(c, sub_id) - subscription = subscriptions.sync_subscription_from_stripe_data(c, stripe_subscription) if stripe_subscription else None - - defaults = dict( - customer=c, - attempted=stripe_invoice["attempted"], - attempt_count=stripe_invoice["attempt_count"], - amount_due=utils.convert_amount_for_db(stripe_invoice["amount_due"], stripe_invoice["currency"]), - closed=stripe_invoice["closed"], - paid=stripe_invoice["paid"], - period_end=period_end, - period_start=period_start, - subtotal=utils.convert_amount_for_db(stripe_invoice["subtotal"], stripe_invoice["currency"]), - tax=utils.convert_amount_for_db(stripe_invoice["tax"], stripe_invoice["currency"]) if stripe_invoice["tax"] is not None else None, - tax_percent=decimal.Decimal(stripe_invoice["tax_percent"]) if stripe_invoice["tax_percent"] is not None else None, - total=utils.convert_amount_for_db(stripe_invoice["total"], stripe_invoice["currency"]), - currency=stripe_invoice["currency"], - date=date, - charge=charge, - subscription=subscription, - receipt_number=stripe_invoice["receipt_number"] or "", - ) - invoice, created = models.Invoice.objects.get_or_create( - stripe_id=stripe_invoice["id"], - defaults=defaults - ) - if charge is not None: - charge.invoice = invoice - charge.save() - - invoice = utils.update_with_defaults(invoice, defaults, created) - sync_invoice_items(invoice, stripe_invoice["lines"].get("data", [])) - - return invoice - - -def sync_invoices_for_customer(customer): - """ - Synchronizes all invoices for a customer - - Args: - customer: the customer for whom to synchronize all invoices - """ - for invoice in customer.stripe_customer.invoices().data: - sync_invoice_from_stripe_data(invoice, send_receipt=False) - - -def sync_invoice_items(invoice, items): - """ - Synchronizes all invoice line items for a particular invoice - - This assumes line items from a Stripe invoice.lines property and not through - the invoicesitems resource calls. At least according to the documentation - the data for an invoice item is slightly different between the two calls. - - For example, going through the invoiceitems resource you don't get a "type" - field on the object. - - Args: - invoice_: the invoice objects to synchronize - items: the data from the Stripe API representing the line items - """ - for item in items: - period_end = utils.convert_tstamp(item["period"], "end") - period_start = utils.convert_tstamp(item["period"], "start") - - if item.get("plan"): - plan = models.Plan.objects.get(stripe_id=item["plan"]["id"]) - else: - plan = None - - if item["type"] == "subscription": - if invoice.subscription and invoice.subscription.stripe_id == item["id"]: - item_subscription = invoice.subscription - else: - stripe_subscription = subscriptions.retrieve( - invoice.customer, - item["id"] - ) - item_subscription = subscriptions.sync_subscription_from_stripe_data( - invoice.customer, - stripe_subscription - ) if stripe_subscription else None - if plan is None and item_subscription is not None and item_subscription.plan is not None: - plan = item_subscription.plan - else: - item_subscription = None - - defaults = dict( - amount=utils.convert_amount_for_db(item["amount"], item["currency"]), - currency=item["currency"], - proration=item["proration"], - description=item.get("description") or "", - line_type=item["type"], - plan=plan, - period_start=period_start, - period_end=period_end, - quantity=item.get("quantity"), - subscription=item_subscription - ) - inv_item, inv_item_created = invoice.items.get_or_create( - stripe_id=item["id"], - defaults=defaults - ) - utils.update_with_defaults(inv_item, defaults, inv_item_created) diff --git a/pinax/stripe/actions/plans.py b/pinax/stripe/actions/plans.py deleted file mode 100644 index b304a5110..000000000 --- a/pinax/stripe/actions/plans.py +++ /dev/null @@ -1,39 +0,0 @@ -import stripe - -from .. import models, utils - - -def sync_plans(): - """ - Synchronizes all plans from the Stripe API - """ - plans = stripe.Plan.auto_paging_iter() - for plan in plans: - sync_plan(plan) - - -def sync_plan(plan, event=None): - """ - Synchronizes a plan from the Stripe API - - Args: - plan: data from Stripe API representing a plan - event: the event associated with the plan - """ - - defaults = { - "amount": utils.convert_amount_for_db(plan["amount"], plan["currency"]), - "currency": plan["currency"] or "", - "interval": plan["interval"], - "interval_count": plan["interval_count"], - "name": plan["name"], - "statement_descriptor": plan["statement_descriptor"] or "", - "trial_period_days": plan["trial_period_days"], - "metadata": plan["metadata"] - } - - obj, created = models.Plan.objects.get_or_create( - stripe_id=plan["id"], - defaults=defaults - ) - utils.update_with_defaults(obj, defaults, created) diff --git a/pinax/stripe/actions/refunds.py b/pinax/stripe/actions/refunds.py deleted file mode 100644 index 3c0c35347..000000000 --- a/pinax/stripe/actions/refunds.py +++ /dev/null @@ -1,24 +0,0 @@ -import stripe - -from . import charges -from .. import utils - - -def create(charge, amount=None): - """ - Creates a refund for a particular charge - - Args: - charge: the charge against which to create the refund - amount: how much should the refund be, defaults to None, in which case - the full amount of the charge will be refunded - """ - if amount is None: - stripe.Refund.create(charge=charge.stripe_id, stripe_account=charge.stripe_account_stripe_id) - else: - stripe.Refund.create( - charge=charge.stripe_id, - stripe_account=charge.stripe_account_stripe_id, - amount=utils.convert_amount_for_api(charges.calculate_refund_amount(charge, amount=amount), charge.currency) - ) - charges.sync_charge_from_stripe_data(charge.stripe_charge) diff --git a/pinax/stripe/actions/sources.py b/pinax/stripe/actions/sources.py deleted file mode 100644 index 52d8b9d77..000000000 --- a/pinax/stripe/actions/sources.py +++ /dev/null @@ -1,142 +0,0 @@ -from .. import models, utils - - -def create_card(customer, token): - """ - Creates a new card for a customer - - Args: - customer: the customer to create the card for - token: the token created from Stripe.js - """ - source = customer.stripe_customer.sources.create(source=token) - return sync_payment_source_from_stripe_data(customer, source) - - -def delete_card(customer, source): - """ - Deletes a card from a customer - - Args: - customer: the customer to delete the card from - source: the Stripe ID of the payment source to delete - """ - customer.stripe_customer.sources.retrieve(source).delete() - return delete_card_object(source) - - -def delete_card_object(source): - """ - Deletes the local card object (Card) - - Args: - source: the Stripe ID of the card - """ - if source.startswith("card_"): - return models.Card.objects.filter(stripe_id=source).delete() - - -def sync_card(customer, source): - """ - Synchronizes the data for a card locally for a given customer - - Args: - customer: the customer to create or update a card for - source: data representing the card from the Stripe API - """ - defaults = dict( - customer=customer, - name=source["name"] or "", - address_line_1=source["address_line1"] or "", - address_line_1_check=source["address_line1_check"] or "", - address_line_2=source["address_line2"] or "", - address_city=source["address_city"] or "", - address_state=source["address_state"] or "", - address_country=source["address_country"] or "", - address_zip=source["address_zip"] or "", - address_zip_check=source["address_zip_check"] or "", - brand=source["brand"], - country=source["country"] or "", - cvc_check=source["cvc_check"] or "", - dynamic_last4=source["dynamic_last4"] or "", - exp_month=source["exp_month"], - exp_year=source["exp_year"], - funding=source["funding"] or "", - last4=source["last4"] or "", - fingerprint=source["fingerprint"] or "" - ) - card, created = models.Card.objects.get_or_create( - stripe_id=source["id"], - defaults=defaults - ) - return utils.update_with_defaults(card, defaults, created) - - -def sync_bitcoin(customer, source): - """ - Synchronizes the data for a Bitcoin receiver locally for a given customer - - Args: - customer: the customer to create or update a Bitcoin receiver for - source: data reprenting the Bitcoin receiver from the Stripe API - """ - defaults = dict( - customer=customer, - active=source["active"], - amount=utils.convert_amount_for_db(source["amount"], source["currency"]), - amount_received=utils.convert_amount_for_db(source["amount_received"], source["currency"]), - bitcoin_amount=source["bitcoin_amount"], - bitcoin_amount_received=source["bitcoin_amount_received"], - bitcoin_uri=source["bitcoin_uri"], - currency=source["currency"], - description=source["description"], - email=source["email"], - filled=source["filled"], - inbound_address=source["inbound_address"], - payment=source["payment"] if "payment" in source else "", - refund_address=source["refund_address"] or "", - uncaptured_funds=source["uncaptured_funds"], - used_for_payment=source["used_for_payment"] - ) - receiver, created = models.BitcoinReceiver.objects.get_or_create( - stripe_id=source["id"], - defaults=defaults - ) - return utils.update_with_defaults(receiver, defaults, created) - - -def sync_payment_source_from_stripe_data(customer, source): - """ - Synchronizes the data for a payment source locally for a given customer - - Args: - customer: the customer to create or update a Bitcoin receiver for - source: data reprenting the payment source from the Stripe API - """ - if source["object"] == "card": - return sync_card(customer, source) - # NOTE: this does not seem to be a thing anymore?! - if source["object"] == "bitcoin_receiver": - return sync_bitcoin(customer, source) - - -def update_card(customer, source, name=None, exp_month=None, exp_year=None): - """ - Updates a card for a given customer - - Args: - customer: the customer for whom to update the card - source: the Stripe ID of the card to update - name: optionally, a name to give the card - exp_month: optionally, the expiration month for the card - exp_year: optionally, the expiration year for the card - """ - stripe_source = customer.stripe_customer.sources.retrieve(source) - if name is not None: - stripe_source.name = name - if exp_month is not None: - stripe_source.exp_month = exp_month - if exp_year is not None: - stripe_source.exp_year = exp_year - s = stripe_source.save() - return sync_payment_source_from_stripe_data(customer, s) diff --git a/pinax/stripe/actions/subscriptions.py b/pinax/stripe/actions/subscriptions.py deleted file mode 100644 index 1a2e77853..000000000 --- a/pinax/stripe/actions/subscriptions.py +++ /dev/null @@ -1,199 +0,0 @@ -import datetime - -from django.db.models import Q -from django.utils import timezone - -import stripe - -from .. import hooks, models, utils - - -def cancel(subscription, at_period_end=True): - """ - Cancels a subscription - - Args: - subscription: the subscription to cancel - at_period_end: True to cancel at the end of the period, otherwise cancels immediately - """ - sub = stripe.Subscription( - subscription.stripe_id, - stripe_account=subscription.stripe_account_stripe_id, - ).delete( - at_period_end=at_period_end, - ) - return sync_subscription_from_stripe_data(subscription.customer, sub) - - -def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None): - """ - Creates a subscription for the given customer - - Args: - customer: the customer to create the subscription for - plan: the plan to subscribe to - quantity: if provided, the number to subscribe to - trial_days: if provided, the number of days to trial before starting - token: if provided, a token from Stripe.js that will be used as the - payment source for the subscription and set as the default - source for the customer, otherwise the current default source - will be used - coupon: if provided, a coupon to apply towards the subscription - tax_percent: if provided, add percentage as tax - - Returns: - the pinax.stripe.models.Subscription object (created or updated) - """ - quantity = hooks.hookset.adjust_subscription_quantity(customer=customer, plan=plan, quantity=quantity) - - subscription_params = {} - if trial_days: - subscription_params["trial_end"] = datetime.datetime.utcnow() + datetime.timedelta(days=trial_days) - if token: - subscription_params["source"] = token - - subscription_params["stripe_account"] = customer.stripe_account_stripe_id - subscription_params["customer"] = customer.stripe_id - subscription_params["plan"] = plan - subscription_params["quantity"] = quantity - subscription_params["coupon"] = coupon - subscription_params["tax_percent"] = tax_percent - resp = stripe.Subscription.create(**subscription_params) - - return sync_subscription_from_stripe_data(customer, resp) - - -def has_active_subscription(customer): - """ - Checks if the given customer has an active subscription - - Args: - customer: the customer to check - - Returns: - True, if there is an active subscription, otherwise False - """ - return models.Subscription.objects.filter( - customer=customer - ).filter( - Q(ended_at__isnull=True) | Q(ended_at__gt=timezone.now()) - ).exists() - - -def is_period_current(subscription): - """ - Tests if the provided subscription object for the current period - - Args: - subscription: a pinax.stripe.models.Subscription object to test - """ - return subscription.current_period_end > timezone.now() - - -def is_status_current(subscription): - """ - Tests if the provided subscription object has a status that means current - - Args: - subscription: a pinax.stripe.models.Subscription object to test - """ - return subscription.status in subscription.STATUS_CURRENT - - -def is_valid(subscription): - """ - Tests if the provided subscription object is valid - - Args: - subscription: a pinax.stripe.models.Subscription object to test - """ - if not is_status_current(subscription): - return False - - if subscription.cancel_at_period_end and not is_period_current(subscription): - return False - - return True - - -def retrieve(customer, sub_id): - """ - Retrieve a subscription object from Stripe's API - - Args: - customer: a legacy argument, we check that the given - subscription belongs to the given customer - sub_id: the Stripe ID of the subscription you are fetching - - Returns: - the data for a subscription object from the Stripe API - """ - if not sub_id: - return - subscription = stripe.Subscription.retrieve(sub_id, stripe_account=customer.stripe_account_stripe_id) - if subscription and subscription.customer != customer.stripe_id: - return - return subscription - - -def sync_subscription_from_stripe_data(customer, subscription): - """ - Synchronizes data from the Stripe API for a subscription - - Args: - customer: the customer who's subscription you are syncronizing - subscription: data from the Stripe API representing a subscription - - Returns: - the pinax.stripe.models.Subscription object (created or updated) - """ - defaults = dict( - customer=customer, - application_fee_percent=subscription["application_fee_percent"], - cancel_at_period_end=subscription["cancel_at_period_end"], - canceled_at=utils.convert_tstamp(subscription["canceled_at"]), - current_period_start=utils.convert_tstamp(subscription["current_period_start"]), - current_period_end=utils.convert_tstamp(subscription["current_period_end"]), - ended_at=utils.convert_tstamp(subscription["ended_at"]), - plan=models.Plan.objects.get(stripe_id=subscription["plan"]["id"]), - quantity=subscription["quantity"], - start=utils.convert_tstamp(subscription["start"]), - status=subscription["status"], - trial_start=utils.convert_tstamp(subscription["trial_start"]) if subscription["trial_start"] else None, - trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None - ) - sub, created = models.Subscription.objects.get_or_create( - stripe_id=subscription["id"], - defaults=defaults - ) - sub = utils.update_with_defaults(sub, defaults, created) - return sub - - -def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False): - """ - Updates a subscription - - Args: - subscription: the subscription to update - plan: optionally, the plan to change the subscription to - quantity: optionally, the quantity of the subscription to change - prorate: optionally, if the subscription should be prorated or not - coupon: optionally, a coupon to apply to the subscription - charge_immediately: optionally, whether or not to charge immediately - """ - stripe_subscription = subscription.stripe_subscription - if plan: - stripe_subscription.plan = plan - if quantity: - stripe_subscription.quantity = quantity - if not prorate: - stripe_subscription.prorate = False - if coupon: - stripe_subscription.coupon = coupon - if charge_immediately: - if stripe_subscription.trial_end is not None and utils.convert_tstamp(stripe_subscription.trial_end) > timezone.now(): - stripe_subscription.trial_end = "now" - sub = stripe_subscription.save() - customer = models.Customer.objects.get(pk=subscription.customer.pk) - return sync_subscription_from_stripe_data(customer, sub) diff --git a/pinax/stripe/actions/transfers.py b/pinax/stripe/actions/transfers.py deleted file mode 100644 index 1310c41ab..000000000 --- a/pinax/stripe/actions/transfers.py +++ /dev/null @@ -1,105 +0,0 @@ -import stripe - -from .. import models, utils - - -def during(year, month): - """ - Return a queryset of pinax.stripe.models.Transfer objects for the provided - year and month. - - Args: - year: 4-digit year - month: month as a integer, 1=January through 12=December - """ - return models.Transfer.objects.filter( - date__year=year, - date__month=month - ) - - -def sync_transfer(transfer, event=None): - """ - Synchronize a transfer from the Stripe API - - Args: - transfer: data from Stripe API representing transfer - event: the event associated with the transfer - """ - defaults = { - "amount": utils.convert_amount_for_db( - transfer["amount"], transfer["currency"] - ), - "amount_reversed": utils.convert_amount_for_db( - transfer["amount_reversed"], transfer["currency"] - ) if transfer.get("amount_reversed") else None, - "application_fee": utils.convert_amount_for_db( - transfer["application_fee"], transfer["currency"] - ) if transfer.get("application_fee") else None, - "created": utils.convert_tstamp(transfer["created"]) if transfer.get("created") else None, - "currency": transfer["currency"], - "date": utils.convert_tstamp(transfer.get("date")), - "description": transfer.get("description"), - "destination": transfer.get("destination"), - "destination_payment": transfer.get("destination_payment"), - "event": event, - "failure_code": transfer.get("failure_code"), - "failure_message": transfer.get("failure_message"), - "livemode": transfer.get("livemode"), - "metadata": dict(transfer.get("metadata", {})), - "method": transfer.get("method"), - "reversed": transfer.get("reversed"), - "source_transaction": transfer.get("source_transaction"), - "source_type": transfer.get("source_type"), - "statement_descriptor": transfer.get("statement_descriptor"), - "status": transfer.get("status"), - "transfer_group": transfer.get("transfer_group"), - "type": transfer.get("type") - } - obj, created = models.Transfer.objects.update_or_create( - stripe_id=transfer["id"], - defaults=defaults - ) - if not created: - obj.status = transfer["status"] - obj.save() - return obj - - -def update_status(transfer): - """ - Update the status of a pinax.stripe.models.Transfer object from Stripe API - - Args: - transfer: a pinax.stripe.models.Transfer object to update - """ - transfer.status = stripe.Transfer.retrieve(transfer.stripe_id).status - transfer.save() - - -def create(amount, currency, destination, description, transfer_group=None, - stripe_account=None, **kwargs): - """ - Create a transfer. - - Args: - amount: quantity of money to be sent - currency: currency for the transfer - destination: stripe_id of either a connected Stripe Account or Bank Account - description: an arbitrary string displayed in the webui alongside the transfer - transfer_group: a string that identifies this transfer as part of a group - stripe_account: the stripe_id of a Connect account if creating a transfer on - their behalf - """ - kwargs.update(dict( - amount=utils.convert_amount_for_api(amount, currency), - currency=currency, - destination=destination, - description=description - )) - if transfer_group: - kwargs["transfer_group"] = transfer_group - if stripe_account: - kwargs["stripe_account"] = stripe_account - transfer = stripe.Transfer.create(**kwargs) - return sync_transfer(transfer) diff --git a/pinax/stripe/admin.py b/pinax/stripe/admin.py index cbe6be417..7c5b9c656 100644 --- a/pinax/stripe/admin.py +++ b/pinax/stripe/admin.py @@ -1,142 +1,15 @@ from django.contrib import admin -from django.contrib.admin.views.main import ChangeList -from django.contrib.auth import get_user_model -from django.db.models import Count from django.utils.encoding import force_text from django.utils.translation import ugettext as _ from .models import ( - Account, - BankAccount, - BitcoinReceiver, - Card, - Charge, - Coupon, - Customer, Event, - EventProcessingException, - Invoice, - InvoiceItem, - Plan, - Subscription, - Transfer, - TransferChargeFee, - UserAccount + EventProcessingException ) -def user_search_fields(): - User = get_user_model() - fields = [ - "user__{0}".format(User.USERNAME_FIELD) - ] - if "email" in [f.name for f in User._meta.fields]: # pragma: no branch - fields += ["user__email"] - return fields - - -def customer_search_fields(): - return [ - "customer__{0}".format(field) - for field in user_search_fields() - ] - - -class CustomerHasCardListFilter(admin.SimpleListFilter): - title = "card presence" - parameter_name = "has_card" - - def lookups(self, request, model_admin): - return [ - ["yes", "Has Card"], - ["no", "Does Not Have a Card"] - ] - - def queryset(self, request, queryset): - if self.value() == "yes": - return queryset.filter(card__isnull=True) - elif self.value() == "no": - return queryset.filter(card__isnull=False) - return queryset.all() - - -class InvoiceCustomerHasCardListFilter(admin.SimpleListFilter): - title = "card presence" - parameter_name = "has_card" - - def lookups(self, request, model_admin): - return [ - ["yes", "Has Card"], - ["no", "Does Not Have a Card"] - ] - - def queryset(self, request, queryset): - if self.value() == "yes": - return queryset.filter(customer__card__isnull=True) - elif self.value() == "no": - return queryset.filter(customer__card__isnull=False) - return queryset.all() - - -class CustomerSubscriptionStatusListFilter(admin.SimpleListFilter): - title = "subscription status" - parameter_name = "sub_status" - - def lookups(self, request, model_admin): - statuses = [ - [x, x.replace("_", " ").title()] - for x in Subscription.objects.all().values_list( - "status", - flat=True - ).distinct() - ] - statuses.append(["none", "No Subscription"]) - return statuses - - def queryset(self, request, queryset): - if self.value() == "none": - # Get customers with 0 subscriptions - return queryset.annotate(subs=Count("subscription")).filter(subs=0) - elif self.value(): - # Get customer pks without a subscription with this status - customers = Subscription.objects.filter( - status=self.value()).values_list( - "customer", flat=True).distinct() - # Filter by those customers - return queryset.filter(pk__in=customers) - return queryset.all() - - -class AccountListFilter(admin.SimpleListFilter): - title = "account" - parameter_name = "stripe_account" - - def lookups(self, request, model_admin): - return [("none", "Without Account")] + [(a.pk, str(a)) for a in Account.objects.all()] - - def queryset(self, request, queryset): - if self.value() == "none": - return queryset.filter(stripe_account__isnull=True) - if self.value(): - return queryset.filter(stripe_account__pk=self.value()) - return queryset - - -class PrefetchingChangeList(ChangeList): - """A custom changelist to prefetch related fields.""" - def get_queryset(self, request): - qs = super(PrefetchingChangeList, self).get_queryset(request) - - if subscription_status in self.list_display: - qs = qs.prefetch_related("subscription_set") - if "customer" in self.list_display: - qs = qs.prefetch_related("customer") - if "user" in self.list_display: - qs = qs.prefetch_related("user") - return qs - - class ModelAdmin(admin.ModelAdmin): + def has_add_permission(self, request, obj=None): return False @@ -155,48 +28,6 @@ def has_change_permission(self, request, obj=None): return False return True - def get_changelist(self, request, **kwargs): - return PrefetchingChangeList - - -class ChargeAdmin(ModelAdmin): - list_display = [ - "stripe_id", - "customer", - "total_amount", - "description", - "paid", - "disputed", - "refunded", - "receipt_sent", - "created_at", - ] - list_select_related = [ - "customer", - ] - search_fields = [ - "stripe_id", - "customer__stripe_id", - "invoice__stripe_id", - ] + customer_search_fields() - list_filter = [ - "paid", - "disputed", - "refunded", - "created_at", - ] - raw_id_fields = [ - "customer", - "invoice", - ] - readonly_fields = [ - "stripe_account_stripe_id", - ] - - def get_queryset(self, request): - qs = super(ChargeAdmin, self).get_queryset(request) - return qs.prefetch_related("customer__user", "customer__users") - class EventProcessingExceptionAdmin(ModelAdmin): list_display = [ @@ -215,7 +46,6 @@ class EventProcessingExceptionAdmin(ModelAdmin): class EventAdmin(ModelAdmin): - raw_id_fields = ["customer", "stripe_account"] list_display = [ "stripe_id", "kind", @@ -223,259 +53,22 @@ class EventAdmin(ModelAdmin): "valid", "processed", "created_at", - "stripe_account", + "account_id", + "customer_id" ] list_filter = [ "kind", "created_at", "valid", - "processed", - AccountListFilter, + "processed" ] search_fields = [ "stripe_id", - "customer__stripe_id", + "customer_id", "validated_message", - "=stripe_account__stripe_id", - ] + customer_search_fields() - - -class SubscriptionInline(admin.TabularInline): - model = Subscription - extra = 0 - max_num = 0 - - -class CardInline(admin.TabularInline): - model = Card - extra = 0 - max_num = 0 - - -class BitcoinReceiverInline(admin.TabularInline): - model = BitcoinReceiver - extra = 0 - max_num = 0 - - -def subscription_status(obj): - return ", ".join([subscription.status for subscription in obj.subscription_set.all()]) -subscription_status.short_description = "Subscription Status" # noqa - - -class CustomerAdmin(ModelAdmin): - raw_id_fields = ["user", "stripe_account"] - list_display = [ - "stripe_id", - "user", - "account_balance", - "currency", - "delinquent", - "default_source", - subscription_status, - "date_purged", - "stripe_account", - ] - list_filter = [ - "delinquent", - CustomerHasCardListFilter, - CustomerSubscriptionStatusListFilter, - AccountListFilter, - ] - search_fields = [ - "stripe_id", - ] + user_search_fields() - inlines = [ - SubscriptionInline, - CardInline, - BitcoinReceiverInline - ] - - -class InvoiceItemInline(admin.TabularInline): - model = InvoiceItem - extra = 0 - max_num = 0 - - -def customer_has_card(obj): - return obj.customer.card_set.exclude(fingerprint="").exists() -customer_has_card.short_description = "Customer Has Card" # noqa - - -def customer_user(obj): - if not obj.customer.user: - return "" - User = get_user_model() - username = getattr(obj.customer.user, User.USERNAME_FIELD) - email = getattr(obj, "email", "") - return "{0} <{1}>".format( - username, - email - ) -customer_user.short_description = "Customer" # noqa - - -class InvoiceAdmin(ModelAdmin): - raw_id_fields = ["customer"] - list_display = [ - "stripe_id", - "paid", - "closed", - customer_user, - customer_has_card, - "period_start", - "period_end", - "subtotal", - "total" - ] - search_fields = [ - "stripe_id", - "customer__stripe_id", - ] + customer_search_fields() - list_filter = [ - InvoiceCustomerHasCardListFilter, - "paid", - "closed", - "attempted", - "attempt_count", - "created_at", - "date", - "period_end", - "total" - ] - inlines = [ - InvoiceItemInline - ] - readonly_fields = [ - "stripe_account_stripe_id", - ] - - -class PlanAdmin(ModelAdmin): - raw_id_fields = ["stripe_account"] - list_display = [ - "stripe_id", - "name", - "amount", - "currency", - "interval", - "interval_count", - "trial_period_days", - "stripe_account", - ] - search_fields = [ - "stripe_id", - "name", - "=stripe_account__stripe_id", - ] + customer_search_fields() - list_filter = [ - "currency", - AccountListFilter, - ] - - -class CouponAdmin(ModelAdmin): - list_display = [ - "stripe_id", - "amount_off", - "currency", - "percent_off", - "duration", - "duration_in_months", - "redeem_by", - "valid" - ] - search_fields = [ - "stripe_id", - ] - list_filter = [ - "currency", - "valid", - ] - - -class TransferChargeFeeInline(admin.TabularInline): - model = TransferChargeFee - extra = 0 - max_num = 0 - - -class TransferAdmin(ModelAdmin): - Transfer - raw_id_fields = ["event", "stripe_account"] - list_display = [ - "stripe_id", - "amount", - "status", - "date", - "description", - "stripe_account", - ] - search_fields = [ - "stripe_id", - "event__stripe_id", - "=stripe_account__stripe_id", - ] - inlines = [ - TransferChargeFeeInline - ] - list_filter = [ - AccountListFilter, - ] - - -class AccountAdmin(ModelAdmin): - raw_id_fields = ["user"] - list_display = [ - "display_name", - "type", - "country", - "payouts_enabled", - "charges_enabled", - "stripe_id", - "created_at", - ] - search_fields = [ - "display_name", - "stripe_id", - ] - - -class BankAccountAdmin(ModelAdmin): - raw_id_fields = ["account"] - list_display = [ - "stripe_id", - "account", - "account_holder_type", - "account_holder_name", - "currency", - "default_for_currency", - "bank_name", - "country", - "last4" - ] - search_fields = [ - "stripe_id", - ] - - -class UserAccountAdmin(ModelAdmin): - raw_id_fields = ["user", "customer"] - list_display = ["user", "customer"] - search_fields = [ - "=customer__stripe_id", - "=user__email", + "account_id", ] -admin.site.register(Account, AccountAdmin) -admin.site.register(BankAccount, BankAccountAdmin) -admin.site.register(Charge, ChargeAdmin) -admin.site.register(Coupon, CouponAdmin) admin.site.register(Event, EventAdmin) admin.site.register(EventProcessingException, EventProcessingExceptionAdmin) -admin.site.register(Invoice, InvoiceAdmin) -admin.site.register(Customer, CustomerAdmin) -admin.site.register(Plan, PlanAdmin) -admin.site.register(UserAccount, UserAccountAdmin) diff --git a/pinax/stripe/conf.py b/pinax/stripe/conf.py index c7c2fc6f5..ff71e7054 100644 --- a/pinax/stripe/conf.py +++ b/pinax/stripe/conf.py @@ -1,43 +1,14 @@ -import importlib - from django.conf import settings # noqa -from django.core.exceptions import ImproperlyConfigured import stripe from appconf import AppConf -def load_path_attr(path): - i = path.rfind(".") - module, attr = path[:i], path[i + 1:] - try: - mod = importlib.import_module(module) - except ImportError as e: - raise ImproperlyConfigured( - "Error importing {0}: '{1}'".format(module, e) - ) - try: - attr = getattr(mod, attr) - except AttributeError: - raise ImproperlyConfigured( - "Module '{0}' does not define a '{1}'".format(module, attr) - ) - return attr - - class PinaxStripeAppConf(AppConf): PUBLIC_KEY = None SECRET_KEY = None API_VERSION = "2015-10-16" - INVOICE_FROM_EMAIL = "billing@example.com" - DEFAULT_PLAN = None - HOOKSET = "pinax.stripe.hooks.DefaultHookSet" - SEND_EMAIL_RECEIPTS = True - SUBSCRIPTION_REQUIRED_EXCEPTION_URLS = [] - SUBSCRIPTION_REQUIRED_REDIRECT = None - SUBSCRIPTION_TAX_PERCENT = None - DOCUMENT_MAX_SIZE_KB = 20 * 1024 * 1024 class Meta: prefix = "pinax_stripe" @@ -50,6 +21,3 @@ def configure_api_version(self, value): def configure_secret_key(self, value): stripe.api_key = value return value - - def configure_hookset(self, value): - return load_path_attr(value)() diff --git a/pinax/stripe/forms.py b/pinax/stripe/forms.py index 09f3eb28d..6019dc39a 100644 --- a/pinax/stripe/forms.py +++ b/pinax/stripe/forms.py @@ -7,20 +7,7 @@ import stripe from ipware.ip import get_ip, get_real_ip -from .actions import accounts from .conf import settings -from .models import Plan - - -class PaymentMethodForm(forms.Form): - - expMonth = forms.IntegerField(min_value=1, max_value=12) - expYear = forms.IntegerField(min_value=2015, max_value=9999) - - -class PlanForm(forms.Form): - plan = forms.ModelChoiceField(queryset=Plan.objects.all()) - """ The Connect forms here are designed to get users through the multi-stage @@ -306,6 +293,10 @@ def get_ipaddress(self): def get_user_agent(self): return self.request.META.get("HTTP_USER_AGENT") + def account_create(self, user, **kwargs): + stripe_account = stripe.Account.create(**kwargs) + return stripe_account + def save(self): """ Create a custom account, handling Stripe errors. @@ -317,7 +308,7 @@ def save(self): """ data = self.cleaned_data try: - return accounts.create( + return self.account_create( self.request.user, country=data["address_country"], type="custom", @@ -392,6 +383,10 @@ class AdditionalCustomAccountForm(DynamicManagedAccountForm): dob = forms.DateField() def __init__(self, *args, **kwargs): + """ + Assumes you are instantiating with an instance of a model that represents + a local cache of a Stripe Account with a `stripe_id` property. + """ self.account = kwargs.pop("account") kwargs.update( { @@ -405,11 +400,34 @@ def __init__(self, *args, **kwargs): self.fields["last_name"].initial = self.account.legal_entity_last_name self.fields["dob"].initial = self.account.legal_entity_dob + def account_update(self, data): + stripe_account = stripe.Account.retrieve(id=self.account.stripe_id) + if data.get("dob"): + stripe_account.legal_entity.dob = data["dob"] + + if data.get("first_name"): + stripe_account.legal_entity.first_name = data["first_name"] + + if data.get("last_name"): + stripe_account.legal_entity.last_name = data["last_name"] + + if data.get("personal_id_number"): + stripe_account.legal_entity.personal_id_number = data["personal_id_number"] + + if data.get("document"): + response = stripe.FileUpload.create( + purpose="identity_document", + file=data["document"], + stripe_account=stripe_account.id + ) + stripe_account.legal_entity.verification.document = response["id"] + stripe_account.save() + return stripe_account + def save(self): data = self.cleaned_data try: - return accounts.update( - self.account, + return self.account_update( { "dob": { "day": data["dob"].day, diff --git a/pinax/stripe/hooks.py b/pinax/stripe/hooks.py deleted file mode 100644 index 671cc6b79..000000000 --- a/pinax/stripe/hooks.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.core.mail import EmailMessage -from django.template.loader import render_to_string - - -class DefaultHookSet(object): - - def adjust_subscription_quantity(self, customer, plan, quantity): - """ - Given a customer, plan, and quantity, when calling Customer.subscribe - you have the opportunity to override the quantity that was specified. - - Previously this was handled in the setting `PAYMENTS_PLAN_QUANTITY_CALLBACK` - and was only passed a customer object. - """ - if quantity is None: - quantity = 1 - return quantity - - def trial_period(self, user, plan): - """ - Given a user and plan, return an end date for a trial period, or None - for no trial period. - - Was previously in the setting `TRIAL_PERIOD_FOR_USER_CALLBACK` - """ - return None - - def send_receipt(self, charge, email=None): - from django.conf import settings - if not charge.receipt_sent: - # Import here to not add a hard dependency on the Sites framework - from django.contrib.sites.models import Site - - site = Site.objects.get_current() - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") - ctx = { - "charge": charge, - "site": site, - "protocol": protocol, - } - subject = render_to_string("pinax/stripe/email/subject.txt", ctx) - subject = subject.strip() - message = render_to_string("pinax/stripe/email/body.txt", ctx) - - if not email and charge.customer: - email = charge.customer.user.email - - num_sent = EmailMessage( - subject, - message, - to=[email], - from_email=settings.PINAX_STRIPE_INVOICE_FROM_EMAIL - ).send() - charge.receipt_sent = num_sent and num_sent > 0 - charge.save() - - -class HookProxy(object): - - def __getattr__(self, attr): - from .conf import settings # if put globally there is a race condition - return getattr(settings.PINAX_STRIPE_HOOKSET, attr) - - -hookset = HookProxy() diff --git a/pinax/stripe/management/__init__.py b/pinax/stripe/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pinax/stripe/management/commands/__init__.py b/pinax/stripe/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pinax/stripe/management/commands/init_customers.py b/pinax/stripe/management/commands/init_customers.py deleted file mode 100644 index 8bc917e97..000000000 --- a/pinax/stripe/management/commands/init_customers.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand - -from ...actions import customers - - -class Command(BaseCommand): - - help = "Create customer objects for existing users that do not have one" - - def handle(self, *args, **options): - User = get_user_model() - for user in User.objects.filter(customer__isnull=True): - customers.create(user=user, charge_immediately=False) - self.stdout.write("Created customer for {0}\n".format(user.email)) diff --git a/pinax/stripe/management/commands/sync_coupons.py b/pinax/stripe/management/commands/sync_coupons.py deleted file mode 100644 index 4f5f068b2..000000000 --- a/pinax/stripe/management/commands/sync_coupons.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.core.management.base import BaseCommand - -from ...actions import coupons - - -class Command(BaseCommand): - - help = "Make sure your Stripe account has the coupons" - - def handle(self, *args, **options): - coupons.sync_coupons() diff --git a/pinax/stripe/management/commands/sync_customers.py b/pinax/stripe/management/commands/sync_customers.py deleted file mode 100644 index d83d1ea98..000000000 --- a/pinax/stripe/management/commands/sync_customers.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand - -from stripe.error import InvalidRequestError - -from ...actions import charges, customers, invoices - - -class Command(BaseCommand): - - help = "Sync customer data" - - def handle(self, *args, **options): - User = get_user_model() - qs = User.objects.exclude(customer__isnull=True) - count = 0 - total = qs.count() - for user in qs: - count += 1 - perc = int(round(100 * (float(count) / float(total)))) - username = getattr(user, user.USERNAME_FIELD) - self.stdout.write(u"[{0}/{1} {2}%] Syncing {3} [{4}]\n".format( - count, total, perc, username, user.pk - )) - customer = customers.get_customer_for_user(user) - try: - customers.sync_customer(customer) - except InvalidRequestError as exc: - if exc.http_status == 404: # pragma: no branch - # This user doesn't exist (might be in test mode) - continue - raise exc - - if customer.date_purged is None: - invoices.sync_invoices_for_customer(customer) - charges.sync_charges_for_customer(customer) diff --git a/pinax/stripe/management/commands/sync_plans.py b/pinax/stripe/management/commands/sync_plans.py deleted file mode 100644 index ce3f1203c..000000000 --- a/pinax/stripe/management/commands/sync_plans.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.core.management.base import BaseCommand - -from ...actions import plans - - -class Command(BaseCommand): - - help = "Make sure your Stripe account has the plans" - - def handle(self, *args, **options): - plans.sync_plans() diff --git a/pinax/stripe/management/commands/update_charge_availability.py b/pinax/stripe/management/commands/update_charge_availability.py deleted file mode 100644 index 31e1de96d..000000000 --- a/pinax/stripe/management/commands/update_charge_availability.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.core.management.base import BaseCommand - -from ...actions import charges - - -class Command(BaseCommand): - - help = "Check for newly available Charges." - - def handle(self, *args, **options): - charges.update_charge_availability() diff --git a/pinax/stripe/managers.py b/pinax/stripe/managers.py deleted file mode 100644 index bfc849926..000000000 --- a/pinax/stripe/managers.py +++ /dev/null @@ -1,73 +0,0 @@ -import decimal - -from django.db import models - - -class CustomerManager(models.Manager): - - def started_during(self, year, month): - return self.exclude( - subscription__status="trialing" - ).filter( - subscription__start__year=year, - subscription__start__month=month - ) - - def active(self): - return self.filter( - subscription__status="active" - ) - - def canceled(self): - return self.filter( - subscription__status="canceled" - ) - - def canceled_during(self, year, month): - return self.canceled().filter( - subscription__canceled_at__year=year, - subscription__canceled_at__month=month, - ) - - def started_plan_summary_for(self, year, month): - return self.started_during(year, month).values( - "subscription__plan" - ).order_by().annotate( - count=models.Count("subscription__plan") - ) - - def active_plan_summary(self): - return self.active().values( - "subscription__plan" - ).order_by().annotate( - count=models.Count("subscription__plan") - ) - - def canceled_plan_summary_for(self, year, month): - return self.canceled_during(year, month).values( - "subscription__plan" - ).order_by().annotate( - count=models.Count("subscription__plan") - ) - - def churn(self): - canceled = self.canceled().count() - active = self.active().count() - return decimal.Decimal(str(canceled)) / decimal.Decimal(str(active)) - - -class ChargeManager(models.Manager): - - def during(self, year, month): - return self.filter( - charge_created__year=year, - charge_created__month=month - ) - - def paid_totals_for(self, year, month): - return self.during(year, month).filter( - paid=True - ).aggregate( - total_amount=models.Sum("amount"), - total_refunded=models.Sum("amount_refunded") - ) diff --git a/pinax/stripe/middleware.py b/pinax/stripe/middleware.py deleted file mode 100644 index 38687909d..000000000 --- a/pinax/stripe/middleware.py +++ /dev/null @@ -1,32 +0,0 @@ -import django -from django.shortcuts import redirect - -from .actions import customers, subscriptions -from .conf import settings - -try: - from django.urls import resolve -except ImportError: - from django.core.urlresolvers import resolve - -try: - from django.utils.deprecation import MiddlewareMixin as MixinorObject -except ImportError: - MixinorObject = object - - -class ActiveSubscriptionMiddleware(MixinorObject): - - def process_request(self, request): - is_authenticated = request.user.is_authenticated - if django.VERSION < (1, 10): - is_authenticated = is_authenticated() - - if is_authenticated and not request.user.is_staff: - url_name = resolve(request.path).url_name - if url_name not in settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS: - customer = customers.get_customer_for_user(request.user) - if not subscriptions.has_active_subscription(customer): - return redirect( - settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_REDIRECT - ) diff --git a/pinax/stripe/mixins.py b/pinax/stripe/mixins.py deleted file mode 100644 index 114f96a9a..000000000 --- a/pinax/stripe/mixins.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.utils.decorators import method_decorator - -from .actions import customers -from .conf import settings - -try: - from account.decorators import login_required -except ImportError: - from django.contrib.auth.decorators import login_required - - -class LoginRequiredMixin(object): - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) - - -class CustomerMixin(object): - - @property - def customer(self): - if not hasattr(self, "_customer"): - self._customer = customers.get_customer_for_user(self.request.user) - return self._customer - - def get_queryset(self): - return super(CustomerMixin, self).get_queryset().filter( - customer=self.customer - ) - - -class PaymentsContextMixin(object): - - def get_context_data(self, **kwargs): - context = super(PaymentsContextMixin, self).get_context_data(**kwargs) - context.update({ - "PINAX_STRIPE_PUBLIC_KEY": settings.PINAX_STRIPE_PUBLIC_KEY - }) - return context diff --git a/pinax/stripe/models.py b/pinax/stripe/models.py index 584f34e56..49f409c32 100644 --- a/pinax/stripe/models.py +++ b/pinax/stripe/models.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import decimal - -from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible -from django.utils.functional import cached_property -from django.utils.translation import ugettext_lazy as _ -import stripe from jsonfield.fields import JSONField -from .conf import settings -from .managers import ChargeManager, CustomerManager -from .utils import CURRENCY_SYMBOLS - class StripeObject(models.Model): @@ -27,130 +17,13 @@ class Meta: abstract = True -class AccountRelatedStripeObjectMixin(models.Model): - - stripe_account = models.ForeignKey( - "pinax_stripe.Account", - on_delete=models.CASCADE, - null=True, - default=None, - blank=True, - ) - - @property - def stripe_account_stripe_id(self): - return getattr(self.stripe_account, "stripe_id", None) - stripe_account_stripe_id.fget.short_description = "Stripe Account" - - class Meta: - abstract = True - - -class AccountRelatedStripeObject(AccountRelatedStripeObjectMixin, StripeObject): - """Uses a mixin to support Django 1.8 (name clash for stripe_id)""" - - class Meta: - abstract = True - - -class UniquePerAccountStripeObject(AccountRelatedStripeObjectMixin): - stripe_id = models.CharField(max_length=191) - created_at = models.DateTimeField(default=timezone.now) - - class Meta: - abstract = True - unique_together = ("stripe_id", "stripe_account") - - -class StripeAccountFromCustomerMixin(object): - @property - def stripe_account(self): - customer = getattr(self, "customer", None) - return customer.stripe_account if customer else None - - @property - def stripe_account_stripe_id(self): - return self.stripe_account.stripe_id if self.stripe_account else None - stripe_account_stripe_id.fget.short_description = "Stripe Account" - - -@python_2_unicode_compatible -class Plan(UniquePerAccountStripeObject): - amount = models.DecimalField(decimal_places=2, max_digits=9) - currency = models.CharField(max_length=15, blank=False) - interval = models.CharField(max_length=15) - interval_count = models.IntegerField() - name = models.CharField(max_length=150) - statement_descriptor = models.TextField(blank=True) - trial_period_days = models.IntegerField(null=True, blank=True) - metadata = JSONField(null=True, blank=True) - - def __str__(self): - return "{} ({}{})".format(self.name, CURRENCY_SYMBOLS.get(self.currency, ""), self.amount) - - def __repr__(self): - return "Plan(pk={!r}, name={!r}, amount={!r}, currency={!r}, interval={!r}, interval_count={!r}, trial_period_days={!r}, stripe_id={!r})".format( - self.pk, - self.name, - self.amount, - self.currency, - self.interval, - self.interval_count, - self.trial_period_days, - self.stripe_id, - ) - - @property - def stripe_plan(self): - return stripe.Plan.retrieve( - self.stripe_id, - stripe_account=self.stripe_account_stripe_id, - ) - - -@python_2_unicode_compatible -class Coupon(StripeObject): - - amount_off = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - currency = models.CharField(max_length=10, default="usd") - duration = models.CharField(max_length=10, default="once") - duration_in_months = models.PositiveIntegerField(null=True, blank=True) - livemode = models.BooleanField(default=False) - max_redemptions = models.PositiveIntegerField(null=True, blank=True) - metadata = JSONField(null=True, blank=True) - percent_off = models.PositiveIntegerField(null=True, blank=True) - redeem_by = models.DateTimeField(null=True, blank=True) - times_redeemed = models.PositiveIntegerField(null=True, blank=True) - valid = models.BooleanField(default=False) - - def __str__(self): - if self.amount_off is None: - description = "{}% off".format(self.percent_off,) - else: - description = "{}{}".format(CURRENCY_SYMBOLS.get(self.currency, ""), self.amount_off) - - return "Coupon for {}, {}".format(description, self.duration) - - @python_2_unicode_compatible -class EventProcessingException(models.Model): - - event = models.ForeignKey("Event", null=True, blank=True, on_delete=models.CASCADE) - data = models.TextField() - message = models.CharField(max_length=500) - traceback = models.TextField() - created_at = models.DateTimeField(default=timezone.now) - - def __str__(self): - return "<{}, pk={}, Event={}>".format(self.message, self.pk, self.event) - - -@python_2_unicode_compatible -class Event(AccountRelatedStripeObject): +class Event(StripeObject): kind = models.CharField(max_length=250) livemode = models.BooleanField(default=False) - customer = models.ForeignKey("Customer", null=True, blank=True, on_delete=models.CASCADE) + customer_id = models.CharField(max_length=200, blank=True) + account_id = models.CharField(max_length=200, blank=True) webhook_message = JSONField() validated_message = JSONField(null=True, blank=True) valid = models.NullBooleanField(null=True, blank=True) @@ -170,486 +43,21 @@ def __repr__(self): return "Event(pk={!r}, kind={!r}, customer={!r}, valid={!r}, created_at={!s}, stripe_id={!r})".format( self.pk, self.kind, - self.customer, + self.customer_id, self.valid, self.created_at.replace(microsecond=0).isoformat(), self.stripe_id, ) -class Transfer(AccountRelatedStripeObject): - - amount = models.DecimalField(decimal_places=2, max_digits=9) - amount_reversed = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - application_fee = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - created = models.DateTimeField(null=True, blank=True) - currency = models.CharField(max_length=25, default="usd") - date = models.DateTimeField() - description = models.TextField(null=True, blank=True) - destination = models.TextField(null=True, blank=True) - destination_payment = models.TextField(null=True, blank=True) - event = models.ForeignKey( - Event, related_name="transfers", - on_delete=models.CASCADE, - null=True, - blank=True - ) - failure_code = models.TextField(null=True, blank=True) - failure_message = models.TextField(null=True, blank=True) - livemode = models.BooleanField(default=False) - metadata = JSONField(null=True, blank=True) - method = models.TextField(null=True, blank=True) - reversed = models.BooleanField(default=False) - source_transaction = models.TextField(null=True, blank=True) - source_type = models.TextField(null=True, blank=True) - statement_descriptor = models.TextField(null=True, blank=True) - status = models.CharField(max_length=25) - transfer_group = models.TextField(null=True, blank=True) - type = models.TextField(null=True, blank=True) - - @property - def stripe_transfer(self): - return stripe.Transfer.retrieve( - self.stripe_id, - stripe_account=self.stripe_account_stripe_id, - ) - - -class TransferChargeFee(models.Model): - - transfer = models.ForeignKey(Transfer, related_name="charge_fee_details", on_delete=models.CASCADE) - amount = models.DecimalField(decimal_places=2, max_digits=9) - currency = models.CharField(max_length=10, default="usd") - application = models.TextField(null=True, blank=True) - description = models.TextField(null=True, blank=True) - kind = models.CharField(max_length=150) - created_at = models.DateTimeField(default=timezone.now) - - -class UserAccount(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, - related_name="user_accounts", - related_query_name="user_account", - on_delete=models.CASCADE) - account = models.ForeignKey("pinax_stripe.Account", - related_name="user_accounts", - related_query_name="user_account", - on_delete=models.CASCADE) - customer = models.ForeignKey("pinax_stripe.Customer", - related_name="user_accounts", - related_query_name="user_account", - on_delete=models.CASCADE) - - class Meta: - unique_together = ("user", "account") - - def clean(self): - if not self.customer.stripe_account == self.account: - raise ValidationError(_("customer.stripe_account must be account.")) - return super(UserAccount, self).clean() - - def save(self, *args, **kwargs): - self.full_clean() - return super(UserAccount, self).save(*args, **kwargs) - - def __repr__(self): - return "UserAccount(pk={self.pk!r}, user={self.user!r}, account={self.account!r}, customer={self.customer!r})".format(self=self) - - @python_2_unicode_compatible -class Customer(AccountRelatedStripeObject): - - user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE) - users = models.ManyToManyField(settings.AUTH_USER_MODEL, through=UserAccount, - related_name="customers", - related_query_name="customers") - account_balance = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - currency = models.CharField(max_length=10, default="usd", blank=True) - delinquent = models.BooleanField(default=False) - default_source = models.TextField(blank=True) - date_purged = models.DateTimeField(null=True, blank=True, editable=False) - - objects = CustomerManager() - - @cached_property - def stripe_customer(self): - return stripe.Customer.retrieve( - self.stripe_id, - stripe_account=self.stripe_account_stripe_id, - ) - - def __str__(self): - if self.user: - return str(self.user) - elif self.id: - users = self.users.all() - if users: - return ", ".join(str(user) for user in users) - if self.stripe_id: - return "No User(s) ({})".format(self.stripe_id) - return "No User(s)" - - def __repr__(self): - if self.user: - return "Customer(pk={!r}, user={!r}, stripe_id={!r})".format( - self.pk, - self.user, - self.stripe_id, - ) - elif self.id: - return "Customer(pk={!r}, users={}, stripe_id={!r})".format( - self.pk, - ", ".join(repr(user) for user in self.users.all()), - self.stripe_id, - ) - return "Customer(pk={!r}, stripe_id={!r})".format(self.pk, self.stripe_id) - - -class Card(StripeObject): - - customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - name = models.TextField(blank=True) - address_line_1 = models.TextField(blank=True) - address_line_1_check = models.CharField(max_length=15) - address_line_2 = models.TextField(blank=True) - address_city = models.TextField(blank=True) - address_state = models.TextField(blank=True) - address_country = models.TextField(blank=True) - address_zip = models.TextField(blank=True) - address_zip_check = models.CharField(max_length=15) - brand = models.TextField(blank=True) - country = models.CharField(max_length=2, blank=True) - cvc_check = models.CharField(max_length=15, blank=True) - dynamic_last4 = models.CharField(max_length=4, blank=True) - tokenization_method = models.CharField(max_length=15, blank=True) - exp_month = models.IntegerField() - exp_year = models.IntegerField() - funding = models.CharField(max_length=15) - last4 = models.CharField(max_length=4, blank=True) - fingerprint = models.TextField() - - def __repr__(self): - return "Card(pk={!r}, customer={!r})".format( - self.pk, - getattr(self, "customer", None), - ) - - -class BitcoinReceiver(StripeObject): - - customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - active = models.BooleanField(default=False) - amount = models.DecimalField(decimal_places=2, max_digits=9) - amount_received = models.DecimalField(decimal_places=2, max_digits=9, default=decimal.Decimal("0")) - bitcoin_amount = models.PositiveIntegerField() # Satoshi (10^8 Satoshi in one bitcoin) - bitcoin_amount_received = models.PositiveIntegerField(default=0) - bitcoin_uri = models.TextField(blank=True) - currency = models.CharField(max_length=10, default="usd") - description = models.TextField(blank=True) - email = models.TextField(blank=True) - filled = models.BooleanField(default=False) - inbound_address = models.TextField(blank=True) - payment = models.TextField(blank=True) - refund_address = models.TextField(blank=True) - uncaptured_funds = models.BooleanField(default=False) - used_for_payment = models.BooleanField(default=False) - - -class Subscription(StripeAccountFromCustomerMixin, StripeObject): - - STATUS_CURRENT = ["trialing", "active"] - - customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - application_fee_percent = models.DecimalField(decimal_places=2, max_digits=3, default=None, null=True, blank=True) - cancel_at_period_end = models.BooleanField(default=False) - canceled_at = models.DateTimeField(null=True, blank=True) - current_period_end = models.DateTimeField(null=True, blank=True) - current_period_start = models.DateTimeField(null=True, blank=True) - ended_at = models.DateTimeField(null=True, blank=True) - plan = models.ForeignKey(Plan, on_delete=models.CASCADE) - quantity = models.IntegerField() - start = models.DateTimeField() - status = models.CharField(max_length=25) # trialing, active, past_due, canceled, or unpaid - trial_end = models.DateTimeField(null=True, blank=True) - trial_start = models.DateTimeField(null=True, blank=True) - - @property - def stripe_subscription(self): - return stripe.Subscription.retrieve(self.stripe_id, stripe_account=self.stripe_account_stripe_id) - - @property - def total_amount(self): - return self.plan.amount * self.quantity - - def plan_display(self): - return self.plan.name - - def status_display(self): - return self.status.replace("_", " ").title() - - def delete(self, using=None): - """ - Set values to None while deleting the object so that any lingering - references will not show previous values (such as when an Event - signal is triggered after a subscription has been deleted) - """ - super(Subscription, self).delete(using=using) - self.status = None - self.quantity = 0 - self.amount = 0 - - def __repr__(self): - return "Subscription(pk={!r}, customer={!r}, plan={!r}, status={!r}, stripe_id={!r})".format( - self.pk, - getattr(self, "customer", None), - getattr(self, "plan", None), - self.status, - self.stripe_id, - ) - - -class Invoice(StripeAccountFromCustomerMixin, StripeObject): - - customer = models.ForeignKey(Customer, related_name="invoices", on_delete=models.CASCADE) - amount_due = models.DecimalField(decimal_places=2, max_digits=9) - attempted = models.NullBooleanField() - attempt_count = models.PositiveIntegerField(null=True, blank=True) - charge = models.ForeignKey("Charge", null=True, blank=True, related_name="invoices", on_delete=models.CASCADE) - subscription = models.ForeignKey(Subscription, null=True, blank=True, on_delete=models.CASCADE) - statement_descriptor = models.TextField(blank=True) - currency = models.CharField(max_length=10, default="usd") - closed = models.BooleanField(default=False) - description = models.TextField(blank=True) - paid = models.BooleanField(default=False) - receipt_number = models.TextField(blank=True) - period_end = models.DateTimeField() - period_start = models.DateTimeField() - subtotal = models.DecimalField(decimal_places=2, max_digits=9) - tax = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - tax_percent = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - total = models.DecimalField(decimal_places=2, max_digits=9) - date = models.DateTimeField() - webhooks_delivered_at = models.DateTimeField(null=True, blank=True) - - @property - def status(self): - return "Paid" if self.paid else "Open" - - @property - def stripe_invoice(self): - return stripe.Invoice.retrieve( - self.stripe_id, - stripe_account=self.stripe_account_stripe_id, - ) - - -class InvoiceItem(models.Model): +class EventProcessingException(models.Model): - stripe_id = models.CharField(max_length=255) + event = models.ForeignKey("Event", null=True, blank=True, on_delete=models.CASCADE) + data = models.TextField() + message = models.CharField(max_length=500) + traceback = models.TextField() created_at = models.DateTimeField(default=timezone.now) - invoice = models.ForeignKey(Invoice, related_name="items", on_delete=models.CASCADE) - amount = models.DecimalField(decimal_places=2, max_digits=9) - currency = models.CharField(max_length=10, default="usd") - kind = models.CharField(max_length=25, blank=True) - subscription = models.ForeignKey(Subscription, null=True, blank=True, on_delete=models.CASCADE) - period_start = models.DateTimeField() - period_end = models.DateTimeField() - proration = models.BooleanField(default=False) - line_type = models.CharField(max_length=50) - description = models.CharField(max_length=200, blank=True) - plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE) - quantity = models.IntegerField(null=True, blank=True) - - def plan_display(self): - return self.plan.name if self.plan else "" - - -class Charge(StripeAccountFromCustomerMixin, StripeObject): - - customer = models.ForeignKey(Customer, null=True, blank=True, related_name="charges", on_delete=models.CASCADE) - invoice = models.ForeignKey(Invoice, null=True, blank=True, related_name="charges", on_delete=models.CASCADE) - source = models.CharField(max_length=100, blank=True) - currency = models.CharField(max_length=10, default="usd") - amount = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - amount_refunded = models.DecimalField(decimal_places=2, max_digits=9, null=True, blank=True) - description = models.TextField(blank=True) - paid = models.NullBooleanField(null=True, blank=True) - disputed = models.NullBooleanField(null=True, blank=True) - refunded = models.NullBooleanField(null=True, blank=True) - captured = models.NullBooleanField(null=True, blank=True) - receipt_sent = models.BooleanField(default=False) - charge_created = models.DateTimeField(null=True, blank=True) - - # These fields are extracted from the BalanceTransaction for the - # charge and help us to know when funds from a charge are added to - # our Stripe account's balance. - available = models.BooleanField(default=False) - available_on = models.DateTimeField(null=True, blank=True) - fee = models.DecimalField( - decimal_places=2, max_digits=9, null=True, blank=True - ) - fee_currency = models.CharField(max_length=10, null=True, blank=True) - - transfer_group = models.TextField(null=True, blank=True) - outcome = JSONField(null=True, blank=True) - - objects = ChargeManager() - - def __str__(self): - info = [] - if not self.paid: - info += ["unpaid"] - if not self.captured: - info += ["uncaptured"] - if self.refunded: - info += ["refunded"] - currency = CURRENCY_SYMBOLS.get(self.currency, "") - return "{}{}{}".format( - currency, - self.total_amount, - " ({})".format(", ".join(info)) if info else "", - ) - - def __repr__(self): - return "Charge(pk={!r}, customer={!r}, source={!r}, amount={!r}, captured={!r}, paid={!r}, stripe_id={!r})".format( - self.pk, - self.customer, - self.source, - self.amount, - self.captured, - self.paid, - self.stripe_id, - ) - - @property - def total_amount(self): - amount = self.amount if self.amount else 0 - amount_refunded = self.amount_refunded if self.amount_refunded else 0 - return amount - amount_refunded - total_amount.fget.short_description = "Σ amount" - - @property - def stripe_charge(self): - return stripe.Charge.retrieve( - self.stripe_id, - stripe_account=self.stripe_account_stripe_id, - expand=["balance_transaction"] - ) - - @property - def card(self): - return Card.objects.filter(stripe_id=self.source).first() - - -@python_2_unicode_compatible -class Account(StripeObject): - - INTERVAL_CHOICES = ( - ("Manual", "manual"), - ("Daily", "daily"), - ("Weekly", "weekly"), - ("Monthly", "monthly"), - ) - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE, related_name="stripe_accounts") - - business_name = models.TextField(null=True, blank=True) - business_url = models.TextField(null=True, blank=True) - - charges_enabled = models.BooleanField(default=False) - country = models.CharField(max_length=2) - debit_negative_balances = models.BooleanField(default=False) - decline_charge_on_avs_failure = models.BooleanField(default=False) - decline_charge_on_cvc_failure = models.BooleanField(default=False) - default_currency = models.CharField(max_length=3) - details_submitted = models.BooleanField(default=False) - display_name = models.TextField(blank=True, null=True) - email = models.TextField(null=True, blank=True) - - legal_entity_address_city = models.TextField(null=True, blank=True) - legal_entity_address_country = models.TextField(null=True, blank=True) - legal_entity_address_line1 = models.TextField(null=True, blank=True) - legal_entity_address_line2 = models.TextField(null=True, blank=True) - legal_entity_address_postal_code = models.TextField(null=True, blank=True) - legal_entity_address_state = models.TextField(null=True, blank=True) - legal_entity_dob = models.DateField(null=True, blank=True) - legal_entity_first_name = models.TextField(null=True, blank=True) - legal_entity_gender = models.TextField(null=True, blank=True) - legal_entity_last_name = models.TextField(null=True, blank=True) - legal_entity_maiden_name = models.TextField(null=True, blank=True) - legal_entity_personal_id_number_provided = models.BooleanField(default=False) - legal_entity_phone_number = models.TextField(null=True, blank=True) - legal_entity_ssn_last_4_provided = models.BooleanField(default=False) - legal_entity_type = models.TextField(null=True, blank=True) - legal_entity_verification_details = models.TextField(null=True, blank=True) - legal_entity_verification_details_code = models.TextField(null=True, blank=True) - legal_entity_verification_document = models.TextField(null=True, blank=True) - legal_entity_verification_status = models.TextField(null=True, blank=True) - - # The type of the Stripe account. Can be "standard", "express", or "custom". - type = models.TextField(null=True, blank=True) - - metadata = JSONField(null=True, blank=True) - - stripe_publishable_key = models.CharField(null=True, blank=True, max_length=100) - - product_description = models.TextField(null=True, blank=True) - statement_descriptor = models.TextField(null=True, blank=True) - support_email = models.TextField(null=True, blank=True) - support_phone = models.TextField(null=True, blank=True) - - timezone = models.TextField(null=True, blank=True) - - tos_acceptance_date = models.DateField(null=True, blank=True) - tos_acceptance_ip = models.TextField(null=True, blank=True) - tos_acceptance_user_agent = models.TextField(null=True, blank=True) - - payout_schedule_delay_days = models.PositiveSmallIntegerField(null=True, blank=True) - payout_schedule_interval = models.CharField(max_length=7, choices=INTERVAL_CHOICES, null=True, blank=True) - payout_schedule_monthly_anchor = models.PositiveSmallIntegerField(null=True, blank=True) - payout_schedule_weekly_anchor = models.TextField(null=True, blank=True) - payout_statement_descriptor = models.TextField(null=True, blank=True) - payouts_enabled = models.BooleanField(default=False) - - verification_disabled_reason = models.TextField(null=True, blank=True) - verification_due_by = models.DateTimeField(null=True, blank=True) - verification_timestamp = models.DateTimeField(null=True, blank=True) - verification_fields_needed = JSONField(null=True, blank=True) - authorized = models.BooleanField(default=True) - - @property - def stripe_account(self): - return stripe.Account.retrieve(self.stripe_id) def __str__(self): - return "{} - {}".format(self.display_name or "", self.stripe_id) - - def __repr__(self): - return "Account(pk={!r}, display_name={!r}, type={!r}, authorized={!r}, stripe_id={!r})".format( - self.pk, - self.display_name or "", - self.type, - self.authorized, - self.stripe_id, - ) - - -class BankAccount(StripeObject): - - account = models.ForeignKey(Account, related_name="bank_accounts", on_delete=models.CASCADE) - account_holder_name = models.TextField() - account_holder_type = models.TextField() - bank_name = models.TextField(null=True, blank=True) - country = models.TextField() - currency = models.TextField() - default_for_currency = models.BooleanField(default=False) - fingerprint = models.TextField() - last4 = models.CharField(max_length=4) - metadata = JSONField(null=True, blank=True) - routing_number = models.TextField() - status = models.TextField() - - @property - def stripe_bankaccount(self): - return self.account.stripe_account.external_accounts.retrieve( - self.stripe_id - ) + return "<{}, pk={}, Event={}>".format(self.message, self.pk, self.event) diff --git a/pinax/stripe/templates/pinax/stripe/email/body.txt b/pinax/stripe/templates/pinax/stripe/email/body.txt deleted file mode 100644 index dba968e32..000000000 --- a/pinax/stripe/templates/pinax/stripe/email/body.txt +++ /dev/null @@ -1 +0,0 @@ -{% extends "pinax/stripe/email/body_base.txt" %} diff --git a/pinax/stripe/templates/pinax/stripe/email/body_base.txt b/pinax/stripe/templates/pinax/stripe/email/body_base.txt deleted file mode 100644 index 56f1dc329..000000000 --- a/pinax/stripe/templates/pinax/stripe/email/body_base.txt +++ /dev/null @@ -1,27 +0,0 @@ -{% if charge.paid %}Your {{ site.name }} account was successfully charged ${{ charge.amount|floatformat:2 }} to the credit card ending in {{ charge.card.last4 }}. The invoice below is for your records. - - -======================================================== -INVOICE #{{ charge.pk }} {{ charge.created_at|date:"F d, Y" }} -........................................................ - - -CUSTOMER: {% block customer_name %}{{ charge.customer.user }}{% endblock %} - - -DETAILS -------- -{{ charge.customer.current_subscription.plan_display }} - ${{ charge.amount|floatformat:2 }} - -TOTAL: ${{ charge.amount|floatformat:2 }} USD -PAID BY CREDIT CARD: -${{ charge.amount|floatformat:2 }} -======================================================== -{% else %}{% if charge.refunded %}Your credit card ending in {{ charge.card.last4 }} was refunded ${{ charge.amount|floatformat:2 }}. -{% else %}We are sorry, but we failed to charge your credit card ending in {{ charge.card.last4 }} for the amount ${{ charge.amount|floatformat:2 }}. -{% endif %}{% endif %} - -Please contact us with any questions regarding this invoice. - ---- -Your {{ site.name }} Team -{{ protocol }}://{{ site.domain }} diff --git a/pinax/stripe/templates/pinax/stripe/email/subject.txt b/pinax/stripe/templates/pinax/stripe/email/subject.txt deleted file mode 100644 index be4a37b62..000000000 --- a/pinax/stripe/templates/pinax/stripe/email/subject.txt +++ /dev/null @@ -1 +0,0 @@ -[{{ site.name }}] Payment Receipt (Invoice #{{ charge.pk }}) \ No newline at end of file diff --git a/pinax/stripe/tests/hooks.py b/pinax/stripe/tests/hooks.py deleted file mode 100644 index 24c817db7..000000000 --- a/pinax/stripe/tests/hooks.py +++ /dev/null @@ -1,28 +0,0 @@ -from datetime import timedelta - -from django.utils import timezone - -from ..hooks import DefaultHookSet - - -class TestHookSet(DefaultHookSet): - - def adjust_subscription_quantity(self, customer, plan, quantity): - """ - Given a customer, plan, and quantity, when calling Customer.subscribe - you have the opportunity to override the quantity that was specified. - - Previously this was handled in the setting `PAYMENTS_PLAN_QUANTITY_CALLBACK` - and was only passed a customer object. - """ - return quantity or 4 - - def trial_period(self, user, plan): - """ - Given a user and plan, return an end date for a trial period, or None - for no trial period. - - Was previously in the setting `TRIAL_PERIOD_FOR_USER_CALLBACK` - """ - if plan is not None: - return timezone.now() + timedelta(days=3) diff --git a/pinax/stripe/tests/settings.py b/pinax/stripe/tests/settings.py index 5dbee04ce..bf0faa023 100644 --- a/pinax/stripe/tests/settings.py +++ b/pinax/stripe/tests/settings.py @@ -33,9 +33,6 @@ SITE_ID = 1 PINAX_STRIPE_PUBLIC_KEY = "" PINAX_STRIPE_SECRET_KEY = "sk_test_01234567890123456789abcd" -PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS = ["pinax_stripe_subscription_create"] -PINAX_STRIPE_SUBSCRIPTION_REQUIRED_REDIRECT = "pinax_stripe_subscription_create" -PINAX_STRIPE_HOOKSET = "pinax.stripe.tests.hooks.TestHookSet" TEMPLATES = [{ "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [ diff --git a/pinax/stripe/tests/test_actions.py b/pinax/stripe/tests/test_actions.py deleted file mode 100644 index 4d5dc1b40..000000000 --- a/pinax/stripe/tests/test_actions.py +++ /dev/null @@ -1,3299 +0,0 @@ -import datetime -import decimal -import json -import time -from unittest import skipIf - -import django -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.utils import timezone - -import stripe -from mock import Mock, patch - -from ..actions import ( - accounts, - charges, - customers, - events, - externalaccounts, - invoices, - plans, - refunds, - sources, - subscriptions, - transfers -) -from ..models import ( - Account, - BitcoinReceiver, - Card, - Charge, - Customer, - Event, - Invoice, - Plan, - Subscription, - Transfer, - UserAccount -) - - -class ChargesTests(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - - def test_calculate_refund_amount(self): - charge = Charge(amount=decimal.Decimal("100"), amount_refunded=decimal.Decimal("50")) - expected = decimal.Decimal("50") - actual = charges.calculate_refund_amount(charge) - self.assertEqual(expected, actual) - - def test_calculate_refund_amount_with_amount_under(self): - charge = Charge(amount=decimal.Decimal("100"), amount_refunded=decimal.Decimal("50")) - expected = decimal.Decimal("25") - actual = charges.calculate_refund_amount(charge, amount=decimal.Decimal("25")) - self.assertEqual(expected, actual) - - def test_calculate_refund_amount_with_amount_over(self): - charge = Charge(amount=decimal.Decimal("100"), amount_refunded=decimal.Decimal("50")) - expected = decimal.Decimal("50") - actual = charges.calculate_refund_amount(charge, amount=decimal.Decimal("100")) - self.assertEqual(expected, actual) - - def test_create_amount_not_decimal_raises_error(self): - with self.assertRaises(ValueError): - charges.create(customer=self.customer, amount=10) - - def test_create_no_customer_nor_source_raises_error(self): - with self.assertRaises(ValueError) as exc: - charges.create(amount=decimal.Decimal("10"), - customer=None) - self.assertEqual(exc.exception.args, ("Must provide `customer` or `source`.",)) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_send_receipt_False_skips_sending_receipt(self, CreateMock, SyncMock, SendReceiptMock): - charges.create(amount=decimal.Decimal("10"), customer=self.customer, send_receipt=False) - self.assertTrue(CreateMock.called) - self.assertTrue(SyncMock.called) - self.assertFalse(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_customer(self, CreateMock, SyncMock, SendReceiptMock): - charges.create(amount=decimal.Decimal("10"), customer=self.customer) - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs, { - "amount": 1000, - "currency": "usd", - "source": None, - "customer": "cus_xxxxxxxxxxxxxxx", - "stripe_account": None, - "description": None, - "capture": True, - "idempotency_key": None, - }) - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_customer_id(self, CreateMock, SyncMock, SendReceiptMock): - charges.create(amount=decimal.Decimal("10"), customer=self.customer.stripe_id) - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs, { - "amount": 1000, - "currency": "usd", - "source": None, - "customer": "cus_xxxxxxxxxxxxxxx", - "stripe_account": None, - "description": None, - "capture": True, - "idempotency_key": None, - }) - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_new_customer_id(self, CreateMock, SyncMock, SendReceiptMock): - charges.create(amount=decimal.Decimal("10"), customer="cus_NEW") - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs, { - "amount": 1000, - "currency": "usd", - "source": None, - "customer": "cus_NEW", - "stripe_account": None, - "description": None, - "capture": True, - "idempotency_key": None, - }) - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - self.assertTrue(Customer.objects.get(stripe_id="cus_NEW")) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_idempotency_key(self, CreateMock, SyncMock, SendReceiptMock): - charges.create(amount=decimal.Decimal("10"), customer=self.customer.stripe_id, idempotency_key="a") - CreateMock.assert_called_once_with( - amount=1000, - capture=True, - customer=self.customer.stripe_id, - stripe_account=self.customer.stripe_account_stripe_id, - idempotency_key="a", - description=None, - currency="usd", - source=None, - ) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_app_fee(self, CreateMock, SyncMock, SendReceiptMock): - charges.create( - amount=decimal.Decimal("10"), - customer=self.customer, - destination_account="xxx", - application_fee=decimal.Decimal("25") - ) - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["application_fee"], 2500) - self.assertEqual(kwargs["destination"]["account"], "xxx") - self.assertEqual(kwargs["destination"].get("amount"), None) - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_destination(self, CreateMock, SyncMock, SendReceiptMock): - charges.create( - amount=decimal.Decimal("10"), - customer=self.customer, - destination_account="xxx", - destination_amount=decimal.Decimal("45") - ) - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["destination"]["account"], "xxx") - self.assertEqual(kwargs["destination"]["amount"], 4500) - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_on_behalf_of(self, CreateMock, SyncMock, SendReceiptMock): - charges.create( - amount=decimal.Decimal("10"), - customer=self.customer, - on_behalf_of="account", - ) - self.assertTrue(CreateMock.called) - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["on_behalf_of"], "account") - self.assertTrue(SyncMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.create") - def test_create_with_destination_and_on_behalf_of(self, CreateMock, SyncMock, SendReceiptMock): - with self.assertRaises(ValueError): - charges.create( - amount=decimal.Decimal("10"), - customer=self.customer, - destination_account="xxx", - on_behalf_of="account", - ) - - @patch("stripe.Charge.create") - def test_create_not_decimal_raises_exception(self, CreateMock): - with self.assertRaises(ValueError): - charges.create( - amount=decimal.Decimal("100"), - customer=self.customer, - application_fee=10 - ) - - @patch("stripe.Charge.create") - def test_create_app_fee_no_dest_raises_exception(self, CreateMock): - with self.assertRaises(ValueError): - charges.create( - amount=decimal.Decimal("100"), - customer=self.customer, - application_fee=decimal.Decimal("10") - ) - - @patch("stripe.Charge.create") - def test_create_app_fee_dest_acct_and_dest_amt_raises_exception(self, CreateMock): - with self.assertRaises(ValueError): - charges.create( - amount=decimal.Decimal("100"), - customer=self.customer, - application_fee=decimal.Decimal("10"), - destination_account="xxx", - destination_amount=decimal.Decimal("15") - ) - - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.capture") - def test_capture(self, CaptureMock, SyncMock): - charges.capture(Charge(stripe_id="ch_A", amount=decimal.Decimal("100"), currency="usd")) - self.assertTrue(CaptureMock.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.capture") - def test_capture_with_amount(self, CaptureMock, SyncMock): - charge = Charge(stripe_id="ch_A", amount=decimal.Decimal("100"), currency="usd") - charges.capture(charge, amount=decimal.Decimal("50"), idempotency_key="IDEM") - self.assertTrue(CaptureMock.called) - _, kwargs = CaptureMock.call_args - self.assertEqual(kwargs["amount"], 5000) - self.assertEqual(kwargs["idempotency_key"], "IDEM") - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Charge.capture") - def test_capture_with_connect(self, CaptureMock, SyncMock): - account = Account(stripe_id="acc_001") - customer = Customer(stripe_id="cus_001", stripe_account=account) - charges.capture(Charge(stripe_id="ch_A", amount=decimal.Decimal("100"), currency="usd", customer=customer)) - self.assertTrue(CaptureMock.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.charges.sync_charge") - def test_update_availability(self, SyncMock): - Charge.objects.create(customer=self.customer, amount=decimal.Decimal("100"), currency="usd", paid=True, captured=True, available=False, refunded=False) - charges.update_charge_availability() - self.assertTrue(SyncMock.called) - - -class CustomersTests(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.plan = Plan.objects.create( - stripe_id="p1", - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - - def test_get_customer_for_user(self): - expected = Customer.objects.create(stripe_id="x", user=self.user) - actual = customers.get_customer_for_user(self.user) - self.assertEqual(expected, actual) - - def test_get_customer_for_user_not_exists(self): - actual = customers.get_customer_for_user(self.user) - self.assertIsNone(actual) - - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.retrieve") - def test_set_default_source(self, RetrieveMock, SyncMock): - customers.set_default_source(Customer(), "the source") - self.assertEqual(RetrieveMock().default_source, "the source") - self.assertTrue(RetrieveMock().save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_user_only(self, CreateMock, SyncMock): - CreateMock.return_value = dict(id="cus_XXXXX") - customer = customers.create(self.user) - self.assertEqual(customer.user, self.user) - self.assertEqual(customer.stripe_id, "cus_XXXXX") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertIsNone(kwargs["source"]) - self.assertIsNone(kwargs["plan"]) - self.assertIsNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - - @patch("stripe.Customer.retrieve") - @patch("stripe.Customer.create") - def test_customer_create_user_duplicate(self, CreateMock, RetrieveMock): - # Create an existing database customer for this user - original = Customer.objects.create(user=self.user, stripe_id="cus_XXXXX") - - new_customer = Mock() - RetrieveMock.return_value = new_customer - customer = customers.create(self.user) - - # But only one customer will exist - the original one - self.assertEqual(Customer.objects.count(), 1) - self.assertEqual(customer.stripe_id, original.stripe_id) - - # Check that the customer hasn't been modified - self.assertEqual(customer.user, self.user) - self.assertEqual(customer.stripe_id, "cus_XXXXX") - CreateMock.assert_not_called() - - @patch("stripe.Customer.retrieve") - @patch("stripe.Customer.create") - def test_customer_create_local_customer_but_no_remote(self, CreateMock, RetrieveMock): - # Create an existing database customer for this user - Customer.objects.create(user=self.user, stripe_id="cus_XXXXX") - - RetrieveMock.side_effect = stripe.error.InvalidRequestError( - message="invalid", param=None) - - # customers.Create will return a new customer instance - CreateMock.return_value = { - "id": "cus_YYYYY", - "account_balance": 0, - "currency": "us", - "delinquent": False, - "default_source": "", - "sources": {"data": []}, - "subscriptions": {"data": []}, - } - customer = customers.create(self.user) - - # But a customer *was* retrieved, but not found - RetrieveMock.assert_called_once_with("cus_XXXXX") - - # But only one customer will exist - the original one - self.assertEqual(Customer.objects.count(), 1) - self.assertEqual(customer.stripe_id, "cus_YYYYY") - - # Check that the customer hasn't been modified - self.assertEqual(customer.user, self.user) - self.assertEqual(customer.stripe_id, "cus_YYYYY") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertIsNone(kwargs["source"]) - self.assertIsNone(kwargs["plan"]) - self.assertIsNone(kwargs["trial_end"]) - - @patch("pinax.stripe.actions.invoices.create_and_pay") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_user_with_plan(self, CreateMock, SyncMock, CreateAndPayMock): - Plan.objects.create( - stripe_id="pro-monthly", - name="Pro ($19.99/month)", - amount=19.99, - interval="monthly", - interval_count=1, - currency="usd" - ) - CreateMock.return_value = dict(id="cus_YYYYYYYYYYYYY") - customer = customers.create(self.user, card="token232323", plan=self.plan) - self.assertEqual(customer.user, self.user) - self.assertEqual(customer.stripe_id, "cus_YYYYYYYYYYYYY") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertEqual(kwargs["source"], "token232323") - self.assertEqual(kwargs["plan"], self.plan) - self.assertIsNotNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - self.assertTrue(CreateAndPayMock.called) - - @patch("pinax.stripe.actions.invoices.create_and_pay") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_user_with_plan_and_quantity(self, CreateMock, SyncMock, CreateAndPayMock): - Plan.objects.create( - stripe_id="pro-monthly", - name="Pro ($19.99/month each)", - amount=19.99, - interval="monthly", - interval_count=1, - currency="usd" - ) - CreateMock.return_value = dict(id="cus_YYYYYYYYYYYYY") - customer = customers.create(self.user, card="token232323", plan=self.plan, quantity=42) - self.assertEqual(customer.user, self.user) - self.assertEqual(customer.stripe_id, "cus_YYYYYYYYYYYYY") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertEqual(kwargs["source"], "token232323") - self.assertEqual(kwargs["plan"], self.plan) - self.assertEqual(kwargs["quantity"], 42) - self.assertIsNotNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - self.assertTrue(CreateAndPayMock.called) - - @patch("stripe.Customer.retrieve") - def test_purge(self, RetrieveMock): - customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - customers.purge(customer) - self.assertTrue(RetrieveMock().delete.called) - self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) - self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) - - @patch("stripe.Customer.retrieve") - def test_purge_connected(self, RetrieveMock): - account = Account.objects.create(stripe_id="acc_XXX") - customer = Customer.objects.create( - user=self.user, - stripe_account=account, - stripe_id="cus_xxxxxxxxxxxxxxx", - ) - UserAccount.objects.create(user=self.user, account=account, customer=customer) - customers.purge(customer) - self.assertTrue(RetrieveMock().delete.called) - self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) - self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) - self.assertFalse(UserAccount.objects.exists()) - self.assertTrue(self.User.objects.exists()) - - @patch("stripe.Customer.retrieve") - def test_purge_already_deleted(self, RetrieveMock): - RetrieveMock().delete.side_effect = stripe.error.InvalidRequestError("No such customer:", "error") - customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - customers.purge(customer) - self.assertTrue(RetrieveMock().delete.called) - self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) - self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) - - @patch("stripe.Customer.retrieve") - def test_purge_already_some_other_error(self, RetrieveMock): - RetrieveMock().delete.side_effect = stripe.error.InvalidRequestError("Bad", "error") - customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - with self.assertRaises(stripe.error.InvalidRequestError): - customers.purge(customer) - self.assertTrue(RetrieveMock().delete.called) - self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).user) - self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) - - def test_can_charge(self): - customer = Customer(default_source="card_001") - self.assertTrue(customers.can_charge(customer)) - - def test_can_charge_false_purged(self): - customer = Customer(default_source="card_001", date_purged=timezone.now()) - self.assertFalse(customers.can_charge(customer)) - - def test_can_charge_false_no_default_source(self): - customer = Customer() - self.assertFalse(customers.can_charge(customer)) - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_link_customer(self, SyncMock): - Customer.objects.create(stripe_id="cu_123") - message = dict(data=dict(object=dict(id="cu_123"))) - event = Event.objects.create(validated_message=message, kind="customer.created") - customers.link_customer(event) - self.assertEqual(event.customer.stripe_id, "cu_123") - self.assertTrue(SyncMock.called) - - def test_link_customer_non_customer_event(self): - Customer.objects.create(stripe_id="cu_123") - message = dict(data=dict(object=dict(customer="cu_123"))) - event = Event.objects.create(validated_message=message, kind="invoice.created") - customers.link_customer(event) - self.assertEqual(event.customer.stripe_id, "cu_123") - - def test_link_customer_non_customer_event_no_customer(self): - Customer.objects.create(stripe_id="cu_123") - message = dict(data=dict(object=dict())) - event = Event.objects.create(validated_message=message, kind="transfer.created") - customers.link_customer(event) - self.assertIsNone(event.customer, "cu_123") - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_link_customer_does_not_exist(self, SyncMock): - message = dict(data=dict(object=dict(id="cu_123"))) - event = Event.objects.create(stripe_id="evt_1", validated_message=message, kind="customer.created") - customers.link_customer(event) - Customer.objects.get(stripe_id="cu_123") - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_link_customer_does_not_exist_connected(self, SyncMock): - message = dict(data=dict(object=dict(id="cu_123"))) - account = Account.objects.create(stripe_id="acc_XXX") - event = Event.objects.create(stripe_id="evt_1", validated_message=message, kind="customer.created", stripe_account=account) - customers.link_customer(event) - Customer.objects.get(stripe_id="cu_123", stripe_account=account) - self.assertTrue(SyncMock.called) - - -class CustomersWithConnectTests(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.plan = Plan.objects.create( - stripe_id="p1", - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - self.account = Account.objects.create( - stripe_id="acc_XXX" - ) - - def test_get_customer_for_user_with_stripe_account(self): - expected = Customer.objects.create( - stripe_id="x", - stripe_account=self.account) - UserAccount.objects.create(user=self.user, account=self.account, customer=expected) - actual = customers.get_customer_for_user( - self.user, stripe_account=self.account) - self.assertEqual(expected, actual) - - def test_get_customer_for_user_with_stripe_account_and_legacy_customer(self): - Customer.objects.create(user=self.user, stripe_id="x") - self.assertIsNone(customers.get_customer_for_user( - self.user, stripe_account=self.account)) - - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_with_connect(self, CreateMock, SyncMock): - CreateMock.return_value = dict(id="cus_XXXXX") - customer = customers.create(self.user, stripe_account=self.account) - self.assertIsNone(customer.user) - self.assertEqual(customer.stripe_id, "cus_XXXXX") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertEqual(kwargs["stripe_account"], self.account.stripe_id) - self.assertIsNone(kwargs["source"]) - self.assertIsNone(kwargs["plan"]) - self.assertIsNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_with_connect_and_stale_user_account(self, CreateMock, SyncMock, RetrieveMock): - CreateMock.return_value = dict(id="cus_XXXXX") - RetrieveMock.side_effect = stripe.error.InvalidRequestError( - message="Not Found", param="stripe_id" - ) - ua = UserAccount.objects.create( - user=self.user, - account=self.account, - customer=Customer.objects.create(stripe_id="cus_Z", stripe_account=self.account)) - customer = customers.create(self.user, stripe_account=self.account) - self.assertIsNone(customer.user) - self.assertEqual(customer.stripe_id, "cus_XXXXX") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertEqual(kwargs["stripe_account"], self.account.stripe_id) - self.assertIsNone(kwargs["source"]) - self.assertIsNone(kwargs["plan"]) - self.assertIsNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - self.assertEqual(self.user.user_accounts.get(), ua) - self.assertEqual(ua.customer, customer) - RetrieveMock.assert_called_once_with("cus_Z", stripe_account=self.account.stripe_id) - - @patch("stripe.Customer.retrieve") - def test_customer_create_with_connect_with_existing_customer(self, RetrieveMock): - expected = Customer.objects.create( - stripe_id="x", - stripe_account=self.account) - UserAccount.objects.create(user=self.user, account=self.account, customer=expected) - customer = customers.create(self.user, stripe_account=self.account) - self.assertEqual(customer, expected) - RetrieveMock.assert_called_once_with("x", stripe_account=self.account.stripe_id) - - @patch("pinax.stripe.actions.invoices.create_and_pay") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Customer.create") - def test_customer_create_user_with_plan(self, CreateMock, SyncMock, CreateAndPayMock): - Plan.objects.create( - stripe_id="pro-monthly", - name="Pro ($19.99/month)", - amount=19.99, - interval="monthly", - interval_count=1, - currency="usd" - ) - CreateMock.return_value = dict(id="cus_YYYYYYYYYYYYY") - customer = customers.create(self.user, card="token232323", plan=self.plan, stripe_account=self.account) - self.assertEqual(customer.stripe_id, "cus_YYYYYYYYYYYYY") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["email"], self.user.email) - self.assertEqual(kwargs["source"], "token232323") - self.assertEqual(kwargs["plan"], self.plan) - self.assertIsNotNone(kwargs["trial_end"]) - self.assertTrue(SyncMock.called) - self.assertTrue(CreateAndPayMock.called) - - -class EventsTests(TestCase): - - @classmethod - def setUpClass(cls): - super(EventsTests, cls).setUpClass() - cls.account = Account.objects.create(stripe_id="acc_001") - - def test_dupe_event_exists(self): - Event.objects.create(stripe_id="evt_003", kind="foo", livemode=True, webhook_message="{}", api_version="", request="", pending_webhooks=0) - self.assertTrue(events.dupe_event_exists("evt_003")) - - @patch("pinax.stripe.webhooks.AccountUpdatedWebhook.process") - def test_add_event(self, ProcessMock): - events.add_event(stripe_id="evt_001", kind="account.updated", livemode=True, message={}) - event = Event.objects.get(stripe_id="evt_001") - self.assertEqual(event.kind, "account.updated") - self.assertTrue(ProcessMock.called) - - @patch("pinax.stripe.webhooks.AccountUpdatedWebhook.process") - def test_add_event_connect(self, ProcessMock): - events.add_event(stripe_id="evt_001", kind="account.updated", livemode=True, message={"account": self.account.stripe_id}) - event = Event.objects.get(stripe_id="evt_001", stripe_account=self.account) - self.assertEqual(event.kind, "account.updated") - self.assertTrue(ProcessMock.called) - - @patch("pinax.stripe.webhooks.AccountUpdatedWebhook.process") - def test_add_event_missing_account_connect(self, ProcessMock): - events.add_event(stripe_id="evt_001", kind="account.updated", livemode=True, message={"account": "acc_NEW"}) - event = Event.objects.get(stripe_id="evt_001", stripe_account=Account.objects.get(stripe_id="acc_NEW")) - self.assertEqual(event.kind, "account.updated") - self.assertTrue(ProcessMock.called) - - def test_add_event_new_webhook_kind(self): - events.add_event(stripe_id="evt_002", kind="patrick.got.coffee", livemode=True, message={}) - event = Event.objects.get(stripe_id="evt_002") - self.assertEqual(event.processed, False) - self.assertIsNone(event.validated_message) - - -class InvoicesTests(TestCase): - - @patch("stripe.Invoice.create") - def test_create(self, CreateMock): - invoices.create(Mock()) - self.assertTrue(CreateMock.called) - - @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") - def test_pay(self, SyncMock): - invoice = Mock() - invoice.paid = False - invoice.closed = False - self.assertTrue(invoices.pay(invoice)) - self.assertTrue(invoice.stripe_invoice.pay.called) - self.assertTrue(SyncMock.called) - - def test_pay_invoice_paid(self): - invoice = Mock() - invoice.paid = True - invoice.closed = False - self.assertFalse(invoices.pay(invoice)) - self.assertFalse(invoice.stripe_invoice.pay.called) - - def test_pay_invoice_closed(self): - invoice = Mock() - invoice.paid = False - invoice.closed = True - self.assertFalse(invoices.pay(invoice)) - self.assertFalse(invoice.stripe_invoice.pay.called) - - @patch("stripe.Invoice.create") - def test_create_and_pay(self, CreateMock): - invoice = CreateMock() - invoice.amount_due = 100 - self.assertTrue(invoices.create_and_pay(Mock())) - self.assertTrue(invoice.pay.called) - - @patch("stripe.Invoice.create") - def test_create_and_pay_amount_due_0(self, CreateMock): - invoice = CreateMock() - invoice.amount_due = 0 - self.assertTrue(invoices.create_and_pay(Mock())) - self.assertFalse(invoice.pay.called) - - @patch("stripe.Invoice.create") - def test_create_and_pay_invalid_request_error(self, CreateMock): - invoice = CreateMock() - invoice.amount_due = 100 - invoice.pay.side_effect = stripe.error.InvalidRequestError("Bad", "error") - self.assertFalse(invoices.create_and_pay(Mock())) - self.assertTrue(invoice.pay.called) - - @patch("stripe.Invoice.create") - def test_create_and_pay_invalid_request_error_on_create(self, CreateMock): - CreateMock.side_effect = stripe.error.InvalidRequestError("Bad", "error") - self.assertFalse(invoices.create_and_pay(Mock())) - - -class RefundsTests(TestCase): - - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Refund.create") - def test_create_amount_none(self, RefundMock, SyncMock): - refunds.create(Mock()) - self.assertTrue(RefundMock.called) - _, kwargs = RefundMock.call_args - self.assertFalse("amount" in kwargs) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.charges.calculate_refund_amount") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Refund.create") - def test_create_with_amount(self, RefundMock, SyncMock, CalcMock): - ChargeMock = Mock() - CalcMock.return_value = decimal.Decimal("10") - refunds.create(ChargeMock, amount=decimal.Decimal("10")) - self.assertTrue(RefundMock.called) - _, kwargs = RefundMock.call_args - self.assertTrue("amount" in kwargs) - self.assertEqual(kwargs["amount"], 1000) - self.assertTrue(SyncMock.called) - - -class SourcesTests(TestCase): - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_create_card(self, SyncMock): - CustomerMock = Mock() - result = sources.create_card(CustomerMock, token="token") - self.assertTrue(result is not None) - self.assertTrue(CustomerMock.stripe_customer.sources.create.called) - _, kwargs = CustomerMock.stripe_customer.sources.create.call_args - self.assertEqual(kwargs["source"], "token") - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_update_card(self, SyncMock): - CustomerMock = Mock() - SourceMock = CustomerMock.stripe_customer.sources.retrieve() - result = sources.update_card(CustomerMock, "") - self.assertTrue(result is not None) - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve.called) - self.assertTrue(SourceMock.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_update_card_name_not_none(self, SyncMock): - CustomerMock = Mock() - SourceMock = CustomerMock.stripe_customer.sources.retrieve() - sources.update_card(CustomerMock, "", name="My Visa") - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve.called) - self.assertTrue(SourceMock.save.called) - self.assertEqual(SourceMock.name, "My Visa") - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_update_card_exp_month_not_none(self, SyncMock): - CustomerMock = Mock() - SourceMock = CustomerMock.stripe_customer.sources.retrieve() - sources.update_card(CustomerMock, "", exp_month="My Visa") - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve.called) - self.assertTrue(SourceMock.save.called) - self.assertEqual(SourceMock.exp_month, "My Visa") - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_update_card_exp_year_not_none(self, SyncMock): - CustomerMock = Mock() - SourceMock = CustomerMock.stripe_customer.sources.retrieve() - sources.update_card(CustomerMock, "", exp_year="My Visa") - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve.called) - self.assertTrue(SourceMock.save.called) - self.assertEqual(SourceMock.exp_year, "My Visa") - self.assertTrue(SyncMock.called) - - @skipIf(django.VERSION < (1, 9), "Only for django 1.9+") - def test_delete_card_dj19(self): - CustomerMock = Mock() - result = sources.delete_card(CustomerMock, source="card_token") - self.assertEqual(result, (0, {"pinax_stripe.Card": 0})) - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve().delete.called) - - @skipIf(django.VERSION >= (1, 9), "Only for django before 1.9") - def test_delete_card(self): - CustomerMock = Mock() - result = sources.delete_card(CustomerMock, source="card_token") - self.assertTrue(result is None) - self.assertTrue(CustomerMock.stripe_customer.sources.retrieve().delete.called) - - def test_delete_card_object(self): - User = get_user_model() - user = User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - customer = Customer.objects.create( - user=user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - card = Card.objects.create( - customer=customer, - stripe_id="card_stripe", - address_line_1_check="check", - address_zip_check="check", - country="us", - cvc_check="check", - exp_month=1, - exp_year=2000, - funding="funding", - fingerprint="fingerprint" - ) - pk = card.pk - sources.delete_card_object("card_stripe") - self.assertFalse(Card.objects.filter(pk=pk).exists()) - - def test_delete_card_object_not_card(self): - User = get_user_model() - user = User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - customer = Customer.objects.create( - user=user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - card = Card.objects.create( - customer=customer, - stripe_id="bitcoin_stripe", - address_line_1_check="check", - address_zip_check="check", - country="us", - cvc_check="check", - exp_month=1, - exp_year=2000, - funding="funding", - fingerprint="fingerprint" - ) - pk = card.pk - sources.delete_card_object("bitcoin_stripe") - self.assertTrue(Card.objects.filter(pk=pk).exists()) - - -class SubscriptionsTests(TestCase): - - @classmethod - def setUpClass(cls): - super(SubscriptionsTests, cls).setUpClass() - cls.User = get_user_model() - cls.user = cls.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - cls.customer = Customer.objects.create( - user=cls.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - cls.plan = Plan.objects.create( - stripe_id="the-plan", - amount=2, - interval_count=1, - ) - cls.account = Account.objects.create(stripe_id="acct_xx") - cls.connected_customer = Customer.objects.create( - stripe_id="cus_yyyyyyyyyyyyyyy", - stripe_account=cls.account, - ) - UserAccount.objects.create(user=cls.user, - customer=cls.connected_customer, - account=cls.account) - - def test_has_active_subscription(self): - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - Subscription.objects.create( - customer=self.customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active", - cancel_at_period_end=False - ) - self.assertTrue(subscriptions.has_active_subscription(self.customer)) - - def test_has_active_subscription_False_no_subscription(self): - self.assertFalse(subscriptions.has_active_subscription(self.customer)) - - def test_has_active_subscription_False_expired(self): - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - Subscription.objects.create( - customer=self.customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active", - cancel_at_period_end=False, - ended_at=timezone.now() - datetime.timedelta(days=3) - ) - self.assertFalse(subscriptions.has_active_subscription(self.customer)) - - def test_has_active_subscription_ended_but_not_expired(self): - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - Subscription.objects.create( - customer=self.customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active", - cancel_at_period_end=False, - ended_at=timezone.now() + datetime.timedelta(days=3) - ) - self.assertTrue(subscriptions.has_active_subscription(self.customer)) - - @patch("stripe.Subscription") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_cancel_subscription(self, SyncMock, StripeSubMock): - subscription = Subscription(stripe_id="sub_X", customer=self.customer) - obj = object() - SyncMock.return_value = obj - sub = subscriptions.cancel(subscription) - self.assertIs(sub, obj) - self.assertTrue(SyncMock.called) - _, kwargs = StripeSubMock.call_args - self.assertEqual(kwargs["stripe_account"], None) - - @patch("stripe.Subscription") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_cancel_subscription_with_account(self, SyncMock, StripeSubMock): - subscription = Subscription(stripe_id="sub_X", customer=self.connected_customer) - subscriptions.cancel(subscription) - _, kwargs = StripeSubMock.call_args - self.assertEqual(kwargs["stripe_account"], self.account.stripe_id) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - obj = object() - SyncMock.return_value = obj - sub = subscriptions.update(SubMock) - self.assertIs(sub, obj) - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - subscriptions.update(SubMock, plan="test_value") - self.assertEqual(SubMock.stripe_subscription.plan, "test_value") - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan_quantity(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - subscriptions.update(SubMock, quantity="test_value") - self.assertEqual(SubMock.stripe_subscription.quantity, "test_value") - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan_prorate(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - subscriptions.update(SubMock, prorate=False) - self.assertEqual(SubMock.stripe_subscription.prorate, False) - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan_coupon(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - subscriptions.update(SubMock, coupon="test_value") - self.assertEqual(SubMock.stripe_subscription.coupon, "test_value") - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan_charge_now(self, SyncMock): - SubMock = Mock() - SubMock.customer = self.customer - SubMock.stripe_subscription.trial_end = time.time() + 1000000.0 - - subscriptions.update(SubMock, charge_immediately=True) - self.assertEqual(SubMock.stripe_subscription.trial_end, "now") - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_update_plan_charge_now_old_trial(self, SyncMock): - trial_end = time.time() - 1000000.0 - SubMock = Mock() - SubMock.customer = self.customer - SubMock.stripe_subscription.trial_end = trial_end - - subscriptions.update(SubMock, charge_immediately=True) - # Trial end date hasn't changed - self.assertEqual(SubMock.stripe_subscription.trial_end, trial_end) - self.assertTrue(SubMock.stripe_subscription.save.called) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Subscription.create") - def test_subscription_create(self, SubscriptionCreateMock, SyncMock): - subscriptions.create(self.customer, "the-plan") - self.assertTrue(SyncMock.called) - self.assertTrue(SubscriptionCreateMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Subscription.create") - def test_subscription_create_with_trial(self, SubscriptionCreateMock, SyncMock): - subscriptions.create(self.customer, "the-plan", trial_days=3) - self.assertTrue(SubscriptionCreateMock.called) - _, kwargs = SubscriptionCreateMock.call_args - self.assertEqual(kwargs["trial_end"].date(), (datetime.datetime.utcnow() + datetime.timedelta(days=3)).date()) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Subscription.create") - def test_subscription_create_token(self, SubscriptionCreateMock, CustomerMock): - subscriptions.create(self.customer, "the-plan", token="token") - self.assertTrue(SubscriptionCreateMock.called) - _, kwargs = SubscriptionCreateMock.call_args - self.assertEqual(kwargs["source"], "token") - - @patch("stripe.Subscription.create") - def test_subscription_create_with_connect(self, SubscriptionCreateMock): - SubscriptionCreateMock.return_value = { - "object": "subscription", - "id": "sub_XX", - "application_fee_percent": None, - "cancel_at_period_end": False, - "canceled_at": None, - "current_period_start": 1509978774, - "current_period_end": 1512570774, - "ended_at": None, - "quantity": 1, - "start": 1509978774, - "status": "active", - "trial_start": None, - "trial_end": None, - "plan": { - "id": self.plan.stripe_id, - }} - subscriptions.create(self.connected_customer, self.plan.stripe_id) - SubscriptionCreateMock.assert_called_once_with( - coupon=None, - customer=self.connected_customer.stripe_id, - plan="the-plan", - quantity=4, - stripe_account="acct_xx", - tax_percent=None) - subscription = Subscription.objects.get() - self.assertEqual(subscription.customer, self.connected_customer) - - @patch("stripe.Subscription.retrieve") - @patch("stripe.Subscription.create") - def test_retrieve_subscription_with_connect(self, CreateMock, RetrieveMock): - CreateMock.return_value = { - "object": "subscription", - "id": "sub_XX", - "application_fee_percent": None, - "cancel_at_period_end": False, - "canceled_at": None, - "current_period_start": 1509978774, - "current_period_end": 1512570774, - "ended_at": None, - "quantity": 1, - "start": 1509978774, - "status": "active", - "trial_start": None, - "trial_end": None, - "plan": { - "id": self.plan.stripe_id, - }} - subscriptions.create(self.connected_customer, self.plan.stripe_id) - subscriptions.retrieve(self.connected_customer, "sub_XX") - RetrieveMock.assert_called_once_with("sub_XX", stripe_account=self.account.stripe_id) - - def test_is_period_current(self): - sub = Subscription(current_period_end=(timezone.now() + datetime.timedelta(days=2))) - self.assertTrue(subscriptions.is_period_current(sub)) - - def test_is_period_current_false(self): - sub = Subscription(current_period_end=(timezone.now() - datetime.timedelta(days=2))) - self.assertFalse(subscriptions.is_period_current(sub)) - - def test_is_status_current(self): - sub = Subscription(status="trialing") - self.assertTrue(subscriptions.is_status_current(sub)) - - def test_is_status_current_false(self): - sub = Subscription(status="canceled") - self.assertFalse(subscriptions.is_status_current(sub)) - - def test_is_valid(self): - sub = Subscription(status="trialing") - self.assertTrue(subscriptions.is_valid(sub)) - - def test_is_valid_false(self): - sub = Subscription(status="canceled") - self.assertFalse(subscriptions.is_valid(sub)) - - def test_is_valid_false_canceled(self): - sub = Subscription(status="trialing", cancel_at_period_end=True, current_period_end=(timezone.now() - datetime.timedelta(days=2))) - self.assertFalse(subscriptions.is_valid(sub)) - - -class SyncsTests(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - - @patch("stripe.Plan.auto_paging_iter", create=True) - def test_sync_plans(self, PlanAutoPagerMock): - PlanAutoPagerMock.return_value = [ - { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - { - "id": "simple1", - "object": "plan", - "amount": 999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Simple Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - ] - plans.sync_plans() - self.assertTrue(Plan.objects.all().count(), 2) - self.assertEqual(Plan.objects.get(stripe_id="simple1").amount, decimal.Decimal("9.99")) - - @patch("stripe.Plan.auto_paging_iter", create=True) - def test_sync_plans_update(self, PlanAutoPagerMock): - PlanAutoPagerMock.return_value = [ - { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - { - "id": "simple1", - "object": "plan", - "amount": 999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "The Simple Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - ] - plans.sync_plans() - self.assertTrue(Plan.objects.all().count(), 2) - self.assertEqual(Plan.objects.get(stripe_id="simple1").amount, decimal.Decimal("9.99")) - PlanAutoPagerMock.return_value[1].update({"amount": 499}) - plans.sync_plans() - self.assertEqual(Plan.objects.get(stripe_id="simple1").amount, decimal.Decimal("4.99")) - - def test_sync_plan(self): - """ - Test that a single Plan is updated - """ - Plan.objects.create( - stripe_id="pro2", - name="Plan Plan", - interval="month", - interval_count=1, - amount=decimal.Decimal("19.99") - ) - plan = { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": {}, - "name": "Gold Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - } - plans.sync_plan(plan) - self.assertTrue(Plan.objects.all().count(), 1) - self.assertEqual(Plan.objects.get(stripe_id="pro2").name, plan["name"]) - - def test_sync_payment_source_from_stripe_data_card(self): - source = { - "id": "card_17AMEBI10iPhvocM1LnJ0dBc", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "MasterCard", - "country": "US", - "customer": "cus_7PAYYALEwPuDJE", - "cvc_check": "pass", - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2018, - "funding": "credit", - "last4": "4444", - "metadata": { - }, - "name": None, - "tokenization_method": None, - "fingerprint": "xyz" - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(Card.objects.get(stripe_id=source["id"]).exp_year, 2018) - - def test_sync_payment_source_from_stripe_data_card_blank_cvc_check(self): - source = { - "id": "card_17AMEBI10iPhvocM1LnJ0dBc", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "MasterCard", - "country": "US", - "customer": "cus_7PAYYALEwPuDJE", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2018, - "funding": "credit", - "last4": "4444", - "metadata": { - }, - "name": None, - "tokenization_method": None, - "fingerprint": "xyz" - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(Card.objects.get(stripe_id=source["id"]).cvc_check, "") - - def test_sync_payment_source_from_stripe_data_card_blank_country(self): - source = { - "id": "card_17AMEBI10iPhvocM1LnJ0dBc", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "MasterCard", - "country": None, - "customer": "cus_7PAYYALEwPuDJE", - "cvc_check": "pass", - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2018, - "funding": "credit", - "last4": "4444", - "metadata": { - }, - "name": None, - "tokenization_method": None, - "fingerprint": "xyz" - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(Card.objects.get(stripe_id=source["id"]).country, "") - - def test_sync_payment_source_from_stripe_data_card_updated(self): - source = { - "id": "card_17AMEBI10iPhvocM1LnJ0dBc", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "MasterCard", - "country": "US", - "customer": "cus_7PAYYALEwPuDJE", - "cvc_check": "pass", - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2018, - "funding": "credit", - "last4": "4444", - "metadata": { - }, - "name": None, - "tokenization_method": None, - "fingerprint": "xyz" - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(Card.objects.get(stripe_id=source["id"]).exp_year, 2018) - source.update({"exp_year": 2022}) - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(Card.objects.get(stripe_id=source["id"]).exp_year, 2022) - - def test_sync_payment_source_from_stripe_data_source_card(self): - source = { - "id": "src_123", - "object": "source", - "amount": None, - "client_secret": "src_client_secret_123", - "created": 1483575790, - "currency": None, - "flow": "none", - "livemode": False, - "metadata": {}, - "owner": { - "address": None, - "email": None, - "name": None, - "phone": None, - "verified_address": None, - "verified_email": None, - "verified_name": None, - "verified_phone": None, - }, - "status": "chargeable", - "type": "card", - "usage": "reusable", - "card": { - "brand": "Visa", - "country": "US", - "exp_month": 12, - "exp_year": 2034, - "funding": "debit", - "last4": "5556", - "three_d_secure": "not_supported" - } - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertFalse(Card.objects.exists()) - - def test_sync_payment_source_from_stripe_data_bitcoin(self): - source = { - "id": "btcrcv_17BE32I10iPhvocMqViUU1w4", - "object": "bitcoin_receiver", - "active": False, - "amount": 100, - "amount_received": 0, - "bitcoin_amount": 1757908, - "bitcoin_amount_received": 0, - "bitcoin_uri": "bitcoin:test_7i9Fo4b5wXcUAuoVBFrc7nc9HDxD1?amount=0.01757908", - "created": 1448499344, - "currency": "usd", - "description": "Receiver for John Doe", - "email": "test@example.com", - "filled": False, - "inbound_address": "test_7i9Fo4b5wXcUAuoVBFrc7nc9HDxD1", - "livemode": False, - "metadata": { - }, - "refund_address": None, - "uncaptured_funds": False, - "used_for_payment": False - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(BitcoinReceiver.objects.get(stripe_id=source["id"]).bitcoin_amount, 1757908) - - def test_sync_payment_source_from_stripe_data_bitcoin_updated(self): - source = { - "id": "btcrcv_17BE32I10iPhvocMqViUU1w4", - "object": "bitcoin_receiver", - "active": False, - "amount": 100, - "amount_received": 0, - "bitcoin_amount": 1757908, - "bitcoin_amount_received": 0, - "bitcoin_uri": "bitcoin:test_7i9Fo4b5wXcUAuoVBFrc7nc9HDxD1?amount=0.01757908", - "created": 1448499344, - "currency": "usd", - "description": "Receiver for John Doe", - "email": "test@example.com", - "filled": False, - "inbound_address": "test_7i9Fo4b5wXcUAuoVBFrc7nc9HDxD1", - "livemode": False, - "metadata": { - }, - "refund_address": None, - "uncaptured_funds": False, - "used_for_payment": False - } - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(BitcoinReceiver.objects.get(stripe_id=source["id"]).bitcoin_amount, 1757908) - source.update({"bitcoin_amount": 1886800}) - sources.sync_payment_source_from_stripe_data(self.customer, source) - self.assertEqual(BitcoinReceiver.objects.get(stripe_id=source["id"]).bitcoin_amount, 1886800) - - def test_sync_subscription_from_stripe_data(self): - Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = { - "id": "sub_7Q4BX0HMfqTpN8", - "object": "subscription", - "application_fee_percent": None, - "cancel_at_period_end": False, - "canceled_at": None, - "current_period_end": 1448758544, - "current_period_start": 1448499344, - "customer": self.customer.stripe_id, - "discount": None, - "ended_at": None, - "metadata": { - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "quantity": 1, - "start": 1448499344, - "status": "trialing", - "tax_percent": None, - "trial_end": 1448758544, - "trial_start": 1448499344 - } - sub = subscriptions.sync_subscription_from_stripe_data(self.customer, subscription) - self.assertEqual(Subscription.objects.get(stripe_id=subscription["id"]), sub) - self.assertEqual(sub.status, "trialing") - - def test_sync_subscription_from_stripe_data_updated(self): - Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = { - "id": "sub_7Q4BX0HMfqTpN8", - "object": "subscription", - "application_fee_percent": None, - "cancel_at_period_end": False, - "canceled_at": None, - "current_period_end": 1448758544, - "current_period_start": 1448499344, - "customer": self.customer.stripe_id, - "discount": None, - "ended_at": None, - "metadata": { - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "quantity": 1, - "start": 1448499344, - "status": "trialing", - "tax_percent": None, - "trial_end": 1448758544, - "trial_start": 1448499344 - } - subscriptions.sync_subscription_from_stripe_data(self.customer, subscription) - self.assertEqual(Subscription.objects.get(stripe_id=subscription["id"]).status, "trialing") - subscription.update({"status": "active"}) - subscriptions.sync_subscription_from_stripe_data(self.customer, subscription) - self.assertEqual(Subscription.objects.get(stripe_id=subscription["id"]).status, "active") - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - @patch("stripe.Customer.retrieve") - def test_sync_customer(self, RetreiveMock, SyncPaymentSourceMock, SyncSubscriptionMock): - RetreiveMock.return_value = dict( - account_balance=1999, - currency="usd", - delinquent=False, - default_source=None, - sources=dict(data=[Mock()]), - subscriptions=dict(data=[Mock()]) - ) - customers.sync_customer(self.customer) - customer = Customer.objects.get(user=self.user) - self.assertEqual(customer.account_balance, decimal.Decimal("19.99")) - self.assertEqual(customer.currency, "usd") - self.assertEqual(customer.delinquent, False) - self.assertEqual(customer.default_source, "") - self.assertTrue(SyncPaymentSourceMock.called) - self.assertTrue(SyncSubscriptionMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_sync_customer_no_cu_provided(self, SyncPaymentSourceMock, SyncSubscriptionMock): - cu = dict( - account_balance=1999, - currency="usd", - delinquent=False, - default_source=None, - sources=dict(data=[Mock()]), - subscriptions=dict(data=[Mock()]) - ) - customers.sync_customer(self.customer, cu=cu) - customer = Customer.objects.get(user=self.user) - self.assertEqual(customer.account_balance, decimal.Decimal("19.99")) - self.assertEqual(customer.currency, "usd") - self.assertEqual(customer.delinquent, False) - self.assertEqual(customer.default_source, "") - self.assertTrue(SyncPaymentSourceMock.called) - self.assertTrue(SyncSubscriptionMock.called) - - @patch("pinax.stripe.actions.customers.purge_local") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - @patch("stripe.Customer.retrieve") - def test_sync_customer_purged_locally(self, RetrieveMock, SyncPaymentSourceMock, SyncSubscriptionMock, PurgeLocalMock): - self.customer.date_purged = timezone.now() - customers.sync_customer(self.customer) - self.assertFalse(RetrieveMock.called) - self.assertFalse(SyncPaymentSourceMock.called) - self.assertFalse(SyncSubscriptionMock.called) - self.assertFalse(PurgeLocalMock.called) - - @patch("pinax.stripe.actions.customers.purge_local") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - @patch("stripe.Customer.retrieve") - def test_sync_customer_purged_remotely_not_locally(self, RetrieveMock, SyncPaymentSourceMock, SyncSubscriptionMock, PurgeLocalMock): - RetrieveMock.return_value = dict( - deleted=True - ) - customers.sync_customer(self.customer) - self.assertFalse(SyncPaymentSourceMock.called) - self.assertFalse(SyncSubscriptionMock.called) - self.assertTrue(PurgeLocalMock.called) - - @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") - @patch("stripe.Customer.retrieve") - def test_sync_invoices_for_customer(self, RetreiveMock, SyncMock): - RetreiveMock().invoices().data = [Mock()] - invoices.sync_invoices_for_customer(self.customer) - self.assertTrue(SyncMock.called) - - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("stripe.Customer.retrieve") - def test_sync_charges_for_customer(self, RetreiveMock, SyncMock): - RetreiveMock().charges().data = [Mock()] - charges.sync_charges_for_customer(self.customer) - self.assertTrue(SyncMock.called) - - def test_sync_charge_from_stripe_data(self): - data = { - "id": "ch_17A1dUI10iPhvocMOecpvQlI", - "object": "charge", - "amount": 200, - "amount_refunded": 0, - "application_fee": None, - "balance_transaction": "txn_179l3zI10iPhvocMhvKxAer7", - "captured": True, - "created": 1448213304, - "currency": "usd", - "customer": self.customer.stripe_id, - "description": None, - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": "in_17A1dUI10iPhvocMSGtIfUDF", - "livemode": False, - "metadata": { - }, - "paid": True, - "receipt_email": None, - "receipt_number": None, - "refunded": False, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_17A1dUI10iPhvocMOecpvQlI/refunds" - }, - "shipping": None, - "source": { - "id": "card_179o0lI10iPhvocMZgdPiR5M", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": "cus_7ObCqsp1NGVT6o", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2019, - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "statement_descriptor": "A descriptor", - "status": "succeeded" - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(customer=self.customer, stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - - def test_sync_charge_from_stripe_data_balance_transaction(self): - data = { - "id": "ch_17A1dUI10iPhvocMOecpvQlI", - "object": "charge", - "amount": 200, - "amount_refunded": 0, - "application_fee": None, - "balance_transaction": { - "id": "txn_19XJJ02eZvKYlo2ClwuJ1rbA", - "object": "balance_transaction", - "amount": 999, - "available_on": 1483920000, - "created": 1483315442, - "currency": "usd", - "description": None, - "fee": 59, - "fee_details": [ - { - "amount": 59, - "application": None, - "currency": "usd", - "description": "Stripe processing fees", - "type": "stripe_fee" - } - ], - "net": 940, - "source": "ch_19XJJ02eZvKYlo2CHfSUsSpl", - "status": "pending", - "type": "charge" - }, - "captured": True, - "created": 1448213304, - "currency": "usd", - "customer": self.customer.stripe_id, - "description": None, - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": "in_17A1dUI10iPhvocMSGtIfUDF", - "livemode": False, - "metadata": { - }, - "paid": True, - "receipt_email": None, - "receipt_number": None, - "refunded": False, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_17A1dUI10iPhvocMOecpvQlI/refunds" - }, - "shipping": None, - "source": { - "id": "card_179o0lI10iPhvocMZgdPiR5M", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": "cus_7ObCqsp1NGVT6o", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2019, - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "statement_descriptor": "A descriptor", - "status": "succeeded" - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(customer=self.customer, stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - self.assertEqual(charge.available, False) - self.assertEqual(charge.fee, decimal.Decimal("0.59")) - self.assertEqual(charge.currency, "usd") - - def test_sync_charge_from_stripe_data_description(self): - data = { - "id": "ch_17A1dUI10iPhvocMOecpvQlI", - "object": "charge", - "amount": 200, - "amount_refunded": 0, - "application_fee": None, - "balance_transaction": "txn_179l3zI10iPhvocMhvKxAer7", - "captured": True, - "created": 1448213304, - "currency": "usd", - "customer": self.customer.stripe_id, - "description": "This was a charge for awesome.", - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": "in_17A1dUI10iPhvocMSGtIfUDF", - "livemode": False, - "metadata": { - }, - "paid": True, - "receipt_email": None, - "receipt_number": None, - "refunded": False, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_17A1dUI10iPhvocMOecpvQlI/refunds" - }, - "shipping": None, - "source": { - "id": "card_179o0lI10iPhvocMZgdPiR5M", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": "cus_7ObCqsp1NGVT6o", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2019, - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "statement_descriptor": "A descriptor", - "status": "succeeded" - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(customer=self.customer, stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - self.assertEqual(charge.description, "This was a charge for awesome.") - - def test_sync_charge_from_stripe_data_amount_refunded(self): - data = { - "id": "ch_17A1dUI10iPhvocMOecpvQlI", - "object": "charge", - "amount": 200, - "amount_refunded": 10000, - "application_fee": None, - "balance_transaction": "txn_179l3zI10iPhvocMhvKxAer7", - "captured": True, - "created": 1448213304, - "currency": "usd", - "customer": self.customer.stripe_id, - "description": None, - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": "in_17A1dUI10iPhvocMSGtIfUDF", - "livemode": False, - "metadata": { - }, - "paid": True, - "receipt_email": None, - "receipt_number": None, - "refunded": False, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_17A1dUI10iPhvocMOecpvQlI/refunds" - }, - "shipping": None, - "source": { - "id": "card_179o0lI10iPhvocMZgdPiR5M", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": "cus_7ObCqsp1NGVT6o", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2019, - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "statement_descriptor": "A descriptor", - "status": "succeeded" - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(customer=self.customer, stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - self.assertEqual(charge.amount_refunded, decimal.Decimal("100")) - - def test_sync_charge_from_stripe_data_refunded(self): - data = { - "id": "ch_17A1dUI10iPhvocMOecpvQlI", - "object": "charge", - "amount": 200, - "amount_refunded": 0, - "application_fee": None, - "balance_transaction": "txn_179l3zI10iPhvocMhvKxAer7", - "captured": True, - "created": 1448213304, - "currency": "usd", - "customer": self.customer.stripe_id, - "description": None, - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": "in_17A1dUI10iPhvocMSGtIfUDF", - "livemode": False, - "metadata": { - }, - "paid": True, - "receipt_email": None, - "receipt_number": None, - "refunded": True, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_17A1dUI10iPhvocMOecpvQlI/refunds" - }, - "shipping": None, - "source": { - "id": "card_179o0lI10iPhvocMZgdPiR5M", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": "cus_7ObCqsp1NGVT6o", - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 10, - "exp_year": 2019, - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "statement_descriptor": "A descriptor", - "status": "succeeded" - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(customer=self.customer, stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - self.assertEqual(charge.refunded, True) - - def test_sync_charge_from_stripe_data_failed(self): - data = { - "id": "ch_xxxxxxxxxxxxxxxxxxxxxxxx", - "object": "charge", - "amount": 200, - "amount_refunded": 0, - "application": None, - "application_fee": None, - "balance_transaction": None, - "captured": False, - "created": 1488208611, - "currency": "usd", - "customer": None, - "description": None, - "destination": None, - "dispute": None, - "failure_code": "card_declined", - "failure_message": "Your card was declined.", - "fraud_details": {}, - "invoice": None, - "livemode": False, - "metadata": {}, - "on_behalf_of": None, - "order": None, - "outcome": { - "network_status": "declined_by_network", - "reason": "generic_decline", - "risk_level": "normal", - "seller_message": "The bank did not return any further details with this decline.", - "type": "issuer_declined" - }, - "paid": False, - "receipt_email": None, - "receipt_number": None, - "refunded": False, - "refunds": { - "object": "list", - "data": [], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_xxxxxxxxxxxxxxxxxxxxxxxx/refunds" - }, - "review": None, - "shipping": None, - "source": { - "id": "card_xxxxxxxxxxxxxxxxxxxxxxxx", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": "424", - "address_zip_check": "pass", - "brand": "Visa", - "country": "US", - "customer": None, - "cvc_check": "pass", - "dynamic_last4": None, - "exp_month": 4, - "exp_year": 2024, - "fingerprint": "xxxxxxxxxxxxxxxx", - "funding": "credit", - "last4": "0341", - "metadata": {}, - "name": "example@example.com", - "tokenization_method": None - }, - "source_transfer": None, - "statement_descriptor": None, - "status": "failed", - "transfer_group": None - } - charges.sync_charge_from_stripe_data(data) - charge = Charge.objects.get(stripe_id=data["id"]) - self.assertEqual(charge.amount, decimal.Decimal("2")) - self.assertEqual(charge.customer, None) - self.assertEqual(charge.outcome["risk_level"], "normal") - - @patch("stripe.Subscription.retrieve") - def test_retrieve_stripe_subscription(self, RetrieveMock): - RetrieveMock.return_value = stripe.Subscription( - customer="cus_xxxxxxxxxxxxxxx" - ) - value = subscriptions.retrieve(self.customer, "sub id") - self.assertEqual(value, RetrieveMock.return_value) - - def test_retrieve_stripe_subscription_no_sub_id(self): - value = subscriptions.retrieve(self.customer, None) - self.assertIsNone(value) - - @patch("stripe.Subscription.retrieve") - def test_retrieve_stripe_subscription_diff_customer(self, RetrieveMock): - class Subscription: - customer = "cus_xxxxxxxxxxxxZZZ" - - RetrieveMock.return_value = Subscription() - - value = subscriptions.retrieve(self.customer, "sub_id") - self.assertIsNone(value) - - @patch("stripe.Subscription.retrieve") - def test_retrieve_stripe_subscription_missing_subscription(self, RetrieveMock): - RetrieveMock.return_value = None - value = subscriptions.retrieve(self.customer, "sub id") - self.assertIsNone(value) - - @patch("stripe.Subscription.retrieve") - def test_retrieve_stripe_subscription_invalid_request(self, RetrieveMock): - def bad_request(*args, **kwargs): - raise stripe.error.InvalidRequestError("Bad", "error") - RetrieveMock.side_effect = bad_request - with self.assertRaises(stripe.error.InvalidRequestError): - subscriptions.retrieve(self.customer, "sub id") - - def test_sync_invoice_items(self): - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - invoice = Invoice.objects.create( - stripe_id="inv_001", - customer=self.customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now(), - subscription=subscription - ) - items = [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }] - invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 1) - - def test_sync_invoice_items_no_plan(self): - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - invoice = Invoice.objects.create( - stripe_id="inv_001", - customer=self.customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now(), - subscription=subscription - ) - items = [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }] - invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 1) - self.assertEqual(invoice.items.all()[0].plan, plan) - - def test_sync_invoice_items_type_not_subscription(self): - invoice = Invoice.objects.create( - stripe_id="inv_001", - customer=self.customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now() - ) - items = [{ - "id": "ii_23lkj2lkj", - "object": "line_item", - "amount": 2000, - "currency": "usd", - "description": "Something random", - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "line_item" - }] - invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 1) - self.assertEqual(invoice.items.all()[0].description, "Something random") - self.assertEqual(invoice.items.all()[0].amount, decimal.Decimal("20")) - - @patch("pinax.stripe.actions.subscriptions.retrieve") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - def test_sync_invoice_items_different_stripe_id_than_invoice(self, SyncMock, RetrieveSubscriptionMock): # two subscriptions on invoice? - Plan.objects.create(stripe_id="simple", interval="month", interval_count=1, amount=decimal.Decimal("9.99")) - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - invoice = Invoice.objects.create( - stripe_id="inv_001", - customer=self.customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now(), - subscription=subscription - ) - SyncMock.return_value = subscription - items = [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }, { - "id": "sub_7Q4BX0HMfqTpN9", - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "simple", - "object": "plan", - "amount": 999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Simple Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }] - invoices.sync_invoice_items(invoice, items) - self.assertTrue(invoice.items.all().count(), 2) - - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_items_updating(self, RetrieveSubscriptionMock): - RetrieveSubscriptionMock.return_value = None - Plan.objects.create(stripe_id="simple", interval="month", interval_count=1, amount=decimal.Decimal("9.99")) - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - invoice = Invoice.objects.create( - stripe_id="inv_001", - customer=self.customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now(), - subscription=subscription - ) - items = [{ - "id": subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }, { - "id": "sub_7Q4BX0HMfqTpN9", - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "simple", - "object": "plan", - "amount": 999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Simple Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }] - invoices.sync_invoice_items(invoice, items) - self.assertEqual(invoice.items.count(), 2) - - items[1].update({"description": "This is your second subscription"}) - invoices.sync_invoice_items(invoice, items) - self.assertEqual(invoice.items.count(), 2) - self.assertEqual(invoice.items.get(stripe_id="sub_7Q4BX0HMfqTpN9").description, "This is your second subscription") - - -class InvoiceSyncsTests(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - - plan = Plan.objects.create(stripe_id="pro2", interval="month", interval_count=1, amount=decimal.Decimal("19.99")) - self.subscription = Subscription.objects.create( - stripe_id="sub_7Q4BX0HMfqTpN8", - customer=self.customer, - plan=plan, - quantity=1, - status="active", - start=timezone.now() - ) - self.invoice_data = { - "id": "in_17B6e8I10iPhvocMGtYd4hDD", - "object": "invoice", - "amount_due": 1999, - "application_fee": None, - "attempt_count": 0, - "attempted": False, - "charge": None, - "closed": False, - "currency": "usd", - "customer": self.customer.stripe_id, - "date": 1448470892, - "description": None, - "discount": None, - "ending_balance": None, - "forgiven": False, - "lines": { - "data": [{ - "id": self.subscription.stripe_id, - "object": "line_item", - "amount": 0, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "plan": { - "id": "pro2", - "object": "plan", - "amount": 1999, - "created": 1448121054, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": False, - "metadata": { - }, - "name": "The Pro Plan", - "statement_descriptor": "ALTMAN", - "trial_period_days": 3 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "subscription" - }], - "total_count": 1, - "object": "list", - "url": "/v1/invoices/in_17B6e8I10iPhvocMGtYd4hDD/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": 1448474492, - "paid": False, - "period_end": 1448470739, - "period_start": 1448211539, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "subscription": self.subscription.stripe_id, - "subtotal": 1999, - "tax": None, - "tax_percent": None, - "total": 1999, - "webhooks_delivered_at": None - } - self.account = Account.objects.create(stripe_id="acct_X") - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False - ) - self.invoice_data["charge"] = charge.stripe_id - SyncChargeMock.return_value = charge - SyncSubscriptionMock.return_value = self.subscription - invoices.sync_invoice_from_stripe_data(self.invoice_data) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - self.assertTrue(ChargeFetchMock.called) - self.assertTrue(SyncChargeMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data_no_send_receipt(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False - ) - self.invoice_data["charge"] = charge.stripe_id - SyncChargeMock.return_value = charge - SyncSubscriptionMock.return_value = self.subscription - invoices.sync_invoice_from_stripe_data(self.invoice_data, send_receipt=False) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - self.assertTrue(ChargeFetchMock.called) - self.assertTrue(SyncChargeMock.called) - self.assertFalse(SendReceiptMock.called) - - @patch("pinax.stripe.hooks.hookset.send_receipt") - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data_connect(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncChargeMock, ChargeFetchMock, SyncSubscriptionMock, SendReceiptMock): - self.invoice_data["charge"] = "ch_XXXXXX" - self.customer.stripe_account = self.account - self.customer.save() - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False - ) - SyncChargeMock.return_value = charge - SyncSubscriptionMock.return_value = self.subscription - invoices.sync_invoice_from_stripe_data(self.invoice_data) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - self.assertTrue(ChargeFetchMock.called) - args, kwargs = ChargeFetchMock.call_args - self.assertEqual(args, ("ch_XXXXXX",)) - self.assertEqual(kwargs, {"stripe_account": "acct_X", - "expand": ["balance_transaction"]}) - self.assertTrue(SyncChargeMock.called) - self.assertTrue(SendReceiptMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data_no_charge(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncSubscriptionMock): - SyncSubscriptionMock.return_value = self.subscription - self.invoice_data["charge"] = None - invoices.sync_invoice_from_stripe_data(self.invoice_data) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data_no_subscription(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncSubscriptionMock): - SyncSubscriptionMock.return_value = None - data = { - "id": "in_17B6e8I10iPhvocMGtYd4hDD", - "object": "invoice", - "amount_due": 1999, - "application_fee": None, - "attempt_count": 0, - "attempted": False, - "charge": None, - "closed": False, - "currency": "usd", - "customer": self.customer.stripe_id, - "date": 1448470892, - "description": None, - "discount": None, - "ending_balance": None, - "forgiven": False, - "lines": { - "data": [{ - "id": "ii_2342342", - "object": "line_item", - "amount": 2000, - "currency": "usd", - "description": None, - "discountable": True, - "livemode": True, - "metadata": { - }, - "period": { - "start": 1448499344, - "end": 1448758544 - }, - "proration": False, - "quantity": 1, - "subscription": None, - "type": "line_item" - }], - "total_count": 1, - "object": "list", - "url": "/v1/invoices/in_17B6e8I10iPhvocMGtYd4hDD/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": 1448474492, - "paid": False, - "period_end": 1448470739, - "period_start": 1448211539, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "subscription": "", - "subtotal": 2000, - "tax": None, - "tax_percent": None, - "total": 2000, - "webhooks_delivered_at": None - } - invoices.sync_invoice_from_stripe_data(data) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - self.assertIsNone(Invoice.objects.filter(customer=self.customer)[0].subscription) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.invoices.sync_invoice_items") - @patch("pinax.stripe.actions.subscriptions.retrieve") - def test_sync_invoice_from_stripe_data_updated(self, RetrieveSubscriptionMock, SyncInvoiceItemsMock, SyncSubscriptionMock): - SyncSubscriptionMock.return_value = self.subscription - data = self.invoice_data - invoices.sync_invoice_from_stripe_data(data) - self.assertTrue(SyncInvoiceItemsMock.called) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - data.update({"paid": True}) - invoices.sync_invoice_from_stripe_data(data) - self.assertEqual(Invoice.objects.filter(customer=self.customer).count(), 1) - self.assertEqual(Invoice.objects.filter(customer=self.customer)[0].paid, True) - - -class TransfersTests(TestCase): - - def setUp(self): - self.data = { - "id": "tr_17BE31I10iPhvocMDwiBi4Pk", - "object": "transfer", - "amount": 1100, - "amount_reversed": 0, - "application_fee": None, - "balance_transaction": "txn_179l3zI10iPhvocMhvKxAer7", - "created": 1448499343, - "currency": "usd", - "date": 1448499343, - "description": "Transfer to test@example.com", - "destination": "ba_17BE31I10iPhvocMOUp6E9If", - "failure_code": None, - "failure_message": None, - "livemode": False, - "metadata": { - }, - "recipient": "rp_17BE31I10iPhvocM14ZKPFfR", - "reversals": { - "object": "list", - "data": [ - - ], - "has_more": False, - "total_count": 0, - "url": "/v1/transfers/tr_17BE31I10iPhvocMDwiBi4Pk/reversals" - }, - "reversed": False, - "source_transaction": None, - "statement_descriptor": None, - "status": "in_transit", - "type": "bank_account" - } - self.event = Event.objects.create( - stripe_id="evt_001", - kind="transfer.paid", - webhook_message={"data": {"object": self.data}}, - validated_message={"data": {"object": self.data}}, - valid=True, - processed=False - ) - - def test_sync_transfer(self): - transfers.sync_transfer(self.data, self.event) - qs = Transfer.objects.filter(stripe_id=self.event.message["data"]["object"]["id"]) - self.assertEqual(qs.count(), 1) - self.assertEqual(qs[0].event, self.event) - - def test_sync_transfer_update(self): - transfers.sync_transfer(self.data, self.event) - qs = Transfer.objects.filter(stripe_id=self.event.message["data"]["object"]["id"]) - self.assertEqual(qs.count(), 1) - self.assertEqual(qs[0].event, self.event) - self.event.validated_message["data"]["object"]["status"] = "paid" - transfers.sync_transfer(self.event.message["data"]["object"], self.event) - qs = Transfer.objects.filter(stripe_id=self.event.message["data"]["object"]["id"]) - self.assertEqual(qs[0].status, "paid") - - def test_transfer_during(self): - Transfer.objects.create( - stripe_id="tr_002", - event=Event.objects.create(kind="transfer.created", webhook_message={}), - amount=decimal.Decimal("100"), - status="pending", - date=timezone.now().replace(year=2015, month=1) - ) - qs = transfers.during(2015, 1) - self.assertEqual(qs.count(), 1) - - @patch("stripe.Transfer.retrieve") - def test_transfer_update_status(self, RetrieveMock): - RetrieveMock().status = "complete" - transfer = Transfer.objects.create( - stripe_id="tr_001", - event=Event.objects.create(kind="transfer.created", webhook_message={}), - amount=decimal.Decimal("100"), - status="pending", - date=timezone.now().replace(year=2015, month=1) - ) - transfers.update_status(transfer) - self.assertEqual(transfer.status, "complete") - - @patch("stripe.Transfer.create") - def test_transfer_create(self, CreateMock): - CreateMock.return_value = self.data - transfers.create(decimal.Decimal("100"), "usd", None, None) - self.assertTrue(CreateMock.called) - - @patch("stripe.Transfer.create") - def test_transfer_create_with_transfer_group(self, CreateMock): - CreateMock.return_value = self.data - transfers.create(decimal.Decimal("100"), "usd", None, None, transfer_group="foo") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["transfer_group"], "foo") - - @patch("stripe.Transfer.create") - def test_transfer_create_with_stripe_account(self, CreateMock): - CreateMock.return_value = self.data - transfers.create(decimal.Decimal("100"), "usd", None, None, stripe_account="foo") - _, kwargs = CreateMock.call_args - self.assertEqual(kwargs["stripe_account"], "foo") - - -class AccountsSyncTestCase(TestCase): - - @classmethod - def setUpClass(cls): - super(AccountsSyncTestCase, cls).setUpClass() - - cls.custom_account_data = json.loads( - """{ - "type":"custom", - "tos_acceptance":{ - "date":1490903452, - "ip":"123.107.1.28", - "user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" - }, - "business_logo":null, - "email":"operations@someurl.com", - "timezone":"Etc/UTC", - "statement_descriptor":"SOME COMP", - "default_currency":"cad", - "payout_schedule":{ - "delay_days":3, - "interval":"manual" - }, - "display_name":"Some Company", - "payout_statement_descriptor": "For reals", - "id":"acct_1A39IGDwqdd5icDO", - "payouts_enabled":true, - "external_accounts":{ - "has_more":false, - "total_count":1, - "object":"list", - "data":[ - { - "routing_number":"11000-000", - "bank_name":"SOME CREDIT UNION", - "account":"acct_1A39IGDwqdd5icDO", - "object":"bank_account", - "currency":"cad", - "country":"CA", - "account_holder_name":"Luke Burden", - "last4":"6789", - "status":"new", - "fingerprint":"bZJnuqqS4qIX0SX0", - "account_holder_type":"individual", - "default_for_currency":true, - "id":"ba_1A39IGDwqdd5icDOn9VrFXlQ", - "metadata":{} - } - ], - "url":"/v1/accounts/acct_1A39IGDwqdd5icDO/external_accounts" - }, - "support_email":"support@someurl.com", - "metadata":{ - "user_id":"9428" - }, - "support_phone":"7788188181", - "business_name":"Woop Woop", - "object":"account", - "charges_enabled":true, - "business_name":"Woop Woop", - "debit_negative_balances":false, - "country":"CA", - "decline_charge_on":{ - "avs_failure":true, - "cvc_failure":true - }, - "product_description":"Monkey Magic", - "legal_entity":{ - "personal_id_number_provided":false, - "first_name":"Luke", - "last_name":"Baaard", - "dob":{ - "month":2, - "day":3, - "year":1999 - }, - "personal_address":{ - "city":null, - "country":"CA", - "line2":null, - "line1":null, - "state":null, - "postal_code":null - }, - "business_tax_id_provided":false, - "verification":{ - "status":"unverified", - "details_code":"failed_keyed_identity", - "document":null, - "details":"Provided identity information could not be verified" - }, - "address":{ - "city":"Vancouver", - "country":"CA", - "line2":null, - "line1":"14 Alberta St", - "state":"BC", - "postal_code":"V5Y4Z2" - }, - "business_name":null, - "type":"individual" - }, - "details_submitted":true, - "verification":{ - "due_by":null, - "fields_needed":[ - "legal_entity.personal_id_number" - ], - "disabled_reason":null - } - }""") - cls.custom_account_data_no_dob_no_verification_no_tosacceptance = json.loads( - """{ - "type":"custom", - "tos_acceptance":{ - "date":null, - "ip":"123.107.1.28", - "user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" - }, - "business_logo":null, - "email":"operations@someurl.com", - "timezone":"Etc/UTC", - "statement_descriptor":"SOME COMP", - "default_currency":"cad", - "payout_schedule":{ - "delay_days":3, - "interval":"manual" - }, - "display_name":"Some Company", - "payout_statement_descriptor": "For reals", - "id":"acct_1A39IGDwqdd5icDO", - "payouts_enabled":true, - "external_accounts":{ - "has_more":false, - "total_count":1, - "object":"list", - "data":[ - { - "routing_number":"11000-000", - "bank_name":"SOME CREDIT UNION", - "account":"acct_1A39IGDwqdd5icDO", - "object":"other", - "currency":"cad", - "country":"CA", - "account_holder_name":"Luke Burden", - "last4":"6789", - "status":"new", - "fingerprint":"bZJnuqqS4qIX0SX0", - "account_holder_type":"individual", - "default_for_currency":true, - "id":"ba_1A39IGDwqdd5icDOn9VrFXlQ", - "metadata":{} - } - ], - "url":"/v1/accounts/acct_1A39IGDwqdd5icDO/external_accounts" - }, - "support_email":"support@someurl.com", - "metadata":{ - "user_id":"9428" - }, - "support_phone":"7788188181", - "business_name":"Woop Woop", - "object":"account", - "charges_enabled":true, - "business_name":"Woop Woop", - "debit_negative_balances":false, - "country":"CA", - "decline_charge_on":{ - "avs_failure":true, - "cvc_failure":true - }, - "product_description":"Monkey Magic", - "legal_entity":{ - "dob": null, - "verification": null, - "personal_id_number_provided":false, - "first_name":"Luke", - "last_name":"Baaard", - "personal_address":{ - "city":null, - "country":"CA", - "line2":null, - "line1":null, - "state":null, - "postal_code":null - }, - "business_tax_id_provided":false, - "address":{ - "city":"Vancouver", - "country":"CA", - "line2":null, - "line1":"14 Alberta St", - "state":"BC", - "postal_code":"V5Y4Z2" - }, - "business_name":null, - "type":"individual" - }, - "details_submitted":true, - "verification":{ - "due_by":null, - "fields_needed":[ - "legal_entity.personal_id_number" - ], - "disabled_reason":null - } - }""") - cls.not_custom_account_data = json.loads( - """{ - "business_logo":null, - "business_name":"Woop Woop", - "business_url":"https://www.someurl.com", - "charges_enabled":true, - "country":"CA", - "default_currency":"cad", - "details_submitted":true, - "display_name":"Some Company", - "email":"operations@someurl.com", - "id":"acct_102t2K2m3chDH8uL", - "object":"account", - "payouts_enabled": true, - "statement_descriptor":"SOME COMP", - "support_address": { - "city": null, - "country": "DE", - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "support_email":"support@someurl.com", - "support_phone":"7788188181", - "support_url":"https://support.someurl.com", - "timezone":"Etc/UTC", - "type":"standard" - }""") - - def assert_common_attributes(self, account): - self.assertEqual(account.support_phone, "7788188181") - self.assertEqual(account.business_name, "Woop Woop") - self.assertEqual(account.country, "CA") - self.assertEqual(account.charges_enabled, True) - self.assertEqual(account.support_email, "support@someurl.com") - self.assertEqual(account.details_submitted, True) - self.assertEqual(account.email, "operations@someurl.com") - self.assertEqual(account.timezone, "Etc/UTC") - self.assertEqual(account.display_name, "Some Company") - self.assertEqual(account.statement_descriptor, "SOME COMP") - self.assertEqual(account.default_currency, "cad") - - def assert_custom_attributes(self, account, dob=None, verification=None, acceptance_date=None, bank_accounts=0): - - # extra top level attributes - self.assertEqual(account.debit_negative_balances, False) - self.assertEqual(account.product_description, "Monkey Magic") - self.assertEqual(account.metadata, {"user_id": "9428"}) - self.assertEqual(account.payout_statement_descriptor, "For reals") - - # legal entity - self.assertEqual(account.legal_entity_address_city, "Vancouver") - self.assertEqual(account.legal_entity_address_country, "CA") - self.assertEqual(account.legal_entity_address_line1, "14 Alberta St") - self.assertEqual(account.legal_entity_address_line2, None) - self.assertEqual(account.legal_entity_address_postal_code, "V5Y4Z2") - self.assertEqual(account.legal_entity_address_state, "BC") - self.assertEqual(account.legal_entity_dob, dob) - self.assertEqual(account.legal_entity_type, "individual") - self.assertEqual(account.legal_entity_first_name, "Luke") - self.assertEqual(account.legal_entity_last_name, "Baaard") - self.assertEqual(account.legal_entity_personal_id_number_provided, False) - - # verification - if verification is not None: - self.assertEqual( - account.legal_entity_verification_details, - "Provided identity information could not be verified" - ) - self.assertEqual( - account.legal_entity_verification_details_code, "failed_keyed_identity" - ) - self.assertEqual(account.legal_entity_verification_document, None) - self.assertEqual(account.legal_entity_verification_status, "unverified") - - self.assertEqual( - account.tos_acceptance_date, - acceptance_date - ) - - self.assertEqual(account.tos_acceptance_ip, "123.107.1.28") - self.assertEqual( - account.tos_acceptance_user_agent, - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" - ) - - # decline charge on certain conditions - self.assertEqual(account.decline_charge_on_avs_failure, True) - self.assertEqual(account.decline_charge_on_cvc_failure, True) - - # Payout schedule - self.assertEqual(account.payout_schedule_interval, "manual") - self.assertEqual(account.payout_schedule_delay_days, 3) - self.assertEqual(account.payout_schedule_weekly_anchor, None) - self.assertEqual(account.payout_schedule_monthly_anchor, None) - - # verification status, key to progressing account setup - self.assertEqual(account.verification_disabled_reason, None) - self.assertEqual(account.verification_due_by, None) - self.assertEqual( - account.verification_fields_needed, - [ - "legal_entity.personal_id_number" - ] - ) - - # external accounts should be sync'd - leave the detail check to - # its own test - self.assertEqual( - account.bank_accounts.all().count(), bank_accounts - ) - - def test_sync_custom_account(self): - User = get_user_model() - user = User.objects.create_user( - username="snuffle", - email="upagus@test" - ) - account = accounts.sync_account_from_stripe_data( - self.custom_account_data, user=user - ) - self.assertEqual(account.type, "custom") - self.assert_common_attributes(account) - self.assert_custom_attributes( - account, - dob=datetime.date(1999, 2, 3), - verification="full", - acceptance_date=datetime.datetime(2017, 3, 30, 19, 50, 52), - bank_accounts=1 - ) - - @patch("pinax.stripe.actions.externalaccounts.sync_bank_account_from_stripe_data") - def test_sync_custom_account_no_dob_no_verification(self, SyncMock): - User = get_user_model() - user = User.objects.create_user( - username="snuffle", - email="upagus@test" - ) - account = accounts.sync_account_from_stripe_data( - self.custom_account_data_no_dob_no_verification_no_tosacceptance, user=user - ) - self.assertEqual(account.type, "custom") - self.assert_common_attributes(account) - self.assert_custom_attributes(account) - self.assertFalse(SyncMock.called) - - def test_sync_not_custom_account(self): - account = accounts.sync_account_from_stripe_data( - self.not_custom_account_data - ) - self.assertNotEqual(account.type, "custom") - self.assert_common_attributes(account) - - def test_deauthorize_account(self): - account = accounts.sync_account_from_stripe_data( - self.not_custom_account_data - ) - accounts.deauthorize(account) - self.assertFalse(account.authorized) - - -class BankAccountsSyncTestCase(TestCase): - - def setUp(self): - self.data = json.loads( - """{ - "id": "ba_19VZfo2m3chDH8uLo0r6WCia", - "object": "bank_account", - "account": "acct_102t2K2m3chDH8uL", - "account_holder_name": "Jane Austen", - "account_holder_type": "individual", - "bank_name": "STRIPE TEST BANK", - "country": "US", - "currency": "cad", - "default_for_currency": false, - "fingerprint": "ObHHcvjOGrhaeWhC", - "last4": "6789", - "metadata": { - }, - "routing_number": "110000000", - "status": "new" -} -""") - - def test_sync(self): - User = get_user_model() - user = User.objects.create_user( - username="snuffle", - email="upagus@test" - ) - account = Account.objects.create( - stripe_id="acct_102t2K2m3chDH8uL", - type="custom", - user=user - ) - bankaccount = externalaccounts.sync_bank_account_from_stripe_data( - self.data - ) - self.assertEqual(bankaccount.account_holder_name, "Jane Austen") - self.assertEqual(bankaccount.account, account) - - @patch("pinax.stripe.actions.externalaccounts.sync_bank_account_from_stripe_data") - def test_create_bank_account(self, SyncMock): - account = Mock() - externalaccounts.create_bank_account(account, 123455, "US", "usd") - self.assertTrue(account.external_accounts.create.called) - self.assertTrue(SyncMock.called) diff --git a/pinax/stripe/tests/test_admin.py b/pinax/stripe/tests/test_admin.py deleted file mode 100644 index 742979216..000000000 --- a/pinax/stripe/tests/test_admin.py +++ /dev/null @@ -1,216 +0,0 @@ -import datetime - -from django.contrib.auth import get_user_model -from django.db import connection -from django.test import Client, RequestFactory, SimpleTestCase, TestCase -from django.test.utils import CaptureQueriesContext -from django.utils import timezone - -from ..models import Account, Customer, Invoice, Plan, Subscription - -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - - -User = get_user_model() - - -class AdminTestCase(TestCase): - - @classmethod - def setUpClass(cls): - super(AdminTestCase, cls).setUpClass() - - # create customers and current subscription records - period_start = datetime.datetime(2013, 4, 1, tzinfo=timezone.utc) - period_end = datetime.datetime(2013, 4, 30, tzinfo=timezone.utc) - start = datetime.datetime(2013, 1, 1, tzinfo=timezone.utc) - cls.plan = Plan.objects.create( - stripe_id="p1", - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - cls.plan2 = Plan.objects.create( - stripe_id="p2", - amount=5, - currency="usd", - interval="monthly", - interval_count=1, - name="Light" - ) - for i in range(10): - customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(i)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(i) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(i), - customer=customer, - plan=cls.plan, - current_period_start=period_start, - current_period_end=period_end, - status="active", - start=start, - quantity=1 - ) - customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(11)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(11) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(11), - customer=customer, - plan=cls.plan, - current_period_start=period_start, - current_period_end=period_end, - status="canceled", - canceled_at=period_end, - start=start, - quantity=1 - ) - cls.customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(12)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(12) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(12), - customer=customer, - plan=cls.plan2, - current_period_start=period_start, - current_period_end=period_end, - status="active", - start=start, - quantity=1 - ) - Invoice.objects.create( - customer=customer, - date=timezone.now(), - amount_due=100, - subtotal=100, - total=100, - period_end=period_end, - period_start=period_start - ) - cls.user = User.objects.create_superuser( - username="admin", email="admin@test.com", password="admin") - cls.account = Account.objects.create(stripe_id="acc_abcd") - cls.client = Client() - - def setUp(self): - try: - self.client.force_login(self.user) - except AttributeError: - # Django 1.8 - self.client.login(username="admin", password="admin") - - def test_readonly_change_form(self): - url = reverse("admin:pinax_stripe_customer_change", args=(self.customer.pk,)) - response = self.client.get(url) - self.assertNotContains(response, "submit-row") - - response = self.client.post(url) - self.assertEqual(response.status_code, 403) - - def test_customer_admin(self): - """Make sure we get good responses for all filter options""" - url = reverse("admin:pinax_stripe_customer_changelist") - - response = self.client.get(url + "?sub_status=active") - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?sub_status=cancelled") - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?sub_status=none") - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?has_card=yes") - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?has_card=no") - self.assertEqual(response.status_code, 200) - - def test_customer_admin_prefetch(self): - url = reverse("admin:pinax_stripe_customer_changelist") - - with CaptureQueriesContext(connection) as captured: - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(13)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(13) - ) - with self.assertNumQueries(len(captured)): - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_invoice_admin(self): - url = reverse("admin:pinax_stripe_invoice_changelist") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?has_card=no") - self.assertEqual(response.status_code, 200) - - response = self.client.get(url + "?has_card=yes") - self.assertEqual(response.status_code, 200) - - def test_plan_admin(self): - url = reverse("admin:pinax_stripe_plan_changelist") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_charge_admin(self): - url = reverse("admin:pinax_stripe_charge_changelist") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_account_filter(self): - url = reverse("admin:pinax_stripe_customer_changelist") - response = self.client.get(url + "?stripe_account={}".format(self.account.pk)) - self.assertEqual(response.status_code, 200) - response = self.client.get(url + "?stripe_account=none") - self.assertEqual(response.status_code, 200) - - @classmethod - def get_changelist(cls, model_class, data=None): - from django.contrib.admin.sites import AdminSite - from django.utils.module_loading import import_string - - admin_class = import_string("pinax.stripe.admin.{}Admin".format( - model_class.__name__)) - - ma = admin_class(model_class, AdminSite()) - - info = ma.model._meta.app_label, ma.model._meta.model_name - url = reverse("admin:%s_%s_changelist" % info) - request = RequestFactory().get(url, data=data) - request.user = cls.user - return ma.changelist_view(request).context_data["cl"] - - def test_account_search(self): - cl = self.get_changelist(Account) - self.assertEqual(list(cl.queryset), [self.account]) - - cl = self.get_changelist(Account, {"q": "acc_doesnotexist"}) - self.assertEqual(list(cl.queryset), []) - - -class AdminSimpleTestCase(SimpleTestCase): - - def test_customer_user_without_user(self): - from ..admin import customer_user - - class CustomerWithoutUser(object): - user = None - - class Obj(object): - customer = CustomerWithoutUser() - - self.assertEqual(customer_user(Obj()), "") diff --git a/pinax/stripe/tests/test_commands.py b/pinax/stripe/tests/test_commands.py deleted file mode 100644 index 51f520e02..000000000 --- a/pinax/stripe/tests/test_commands.py +++ /dev/null @@ -1,160 +0,0 @@ -import decimal - -from django.contrib.auth import get_user_model -from django.core import management -from django.test import TestCase - -from mock import patch -from stripe.error import InvalidRequestError - -from ..models import Coupon, Customer, Plan - - -class CommandTests(TestCase): - - def setUp(self): - User = get_user_model() - self.user = User.objects.create_user(username="patrick") - - @patch("stripe.Customer.retrieve") - @patch("stripe.Customer.create") - def test_init_customer_creates_customer(self, CreateMock, RetrieveMock): - CreateMock.return_value = dict( - account_balance=0, - delinquent=False, - default_source="card_178Zqj2eZvKYlo2Cr2fUZZz7", - currency="usd", - id="cus_XXXXX", - sources=dict(data=[]), - subscriptions=dict(data=[]), - ) - management.call_command("init_customers") - customer = Customer.objects.get(user=self.user) - self.assertEqual(customer.stripe_id, "cus_XXXXX") - - @patch("stripe.Plan.auto_paging_iter", create=True) - def test_plans_create(self, PlanAutoPagerMock): - PlanAutoPagerMock.return_value = [{ - "id": "entry-monthly", - "amount": 954, - "interval": "monthly", - "interval_count": 1, - "currency": None, - "statement_descriptor": None, - "trial_period_days": None, - "name": "Pro", - "metadata": {} - }] - management.call_command("sync_plans") - self.assertEqual(Plan.objects.count(), 1) - self.assertEqual(Plan.objects.all()[0].stripe_id, "entry-monthly") - self.assertEqual(Plan.objects.all()[0].amount, decimal.Decimal("9.54")) - - @patch("stripe.Coupon.auto_paging_iter", create=True) - def test_coupons_create(self, CouponAutoPagerMock): - CouponAutoPagerMock.return_value = [{ - "id": "test-coupon", - "object": "coupon", - "amount_off": None, - "created": 1482132502, - "currency": "aud", - "duration": "repeating", - "duration_in_months": 3, - "livemode": False, - "max_redemptions": None, - "metadata": { - }, - "percent_off": 25, - "redeem_by": None, - "times_redeemed": 0, - "valid": True - }] - management.call_command("sync_coupons") - self.assertEqual(Coupon.objects.count(), 1) - self.assertEqual(Coupon.objects.all()[0].stripe_id, "test-coupon") - self.assertEqual(Coupon.objects.all()[0].percent_off, 25) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("pinax.stripe.actions.invoices.sync_invoices_for_customer") - @patch("pinax.stripe.actions.charges.sync_charges_for_customer") - def test_sync_customers(self, SyncChargesMock, SyncInvoicesMock, SyncMock, RetrieveMock): - user2 = get_user_model().objects.create_user(username="thomas") - get_user_model().objects.create_user(username="altman") - Customer.objects.create(stripe_id="cus_XXXXX", user=self.user) - Customer.objects.create(stripe_id="cus_YYYYY", user=user2) - management.call_command("sync_customers") - self.assertEqual(SyncChargesMock.call_count, 2) - self.assertEqual(SyncInvoicesMock.call_count, 2) - self.assertEqual(SyncMock.call_count, 2) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("pinax.stripe.actions.invoices.sync_invoices_for_customer") - @patch("pinax.stripe.actions.charges.sync_charges_for_customer") - def test_sync_customers_with_test_customer(self, SyncChargesMock, SyncInvoicesMock, SyncMock, RetrieveMock): - user2 = get_user_model().objects.create_user(username="thomas") - get_user_model().objects.create_user(username="altman") - Customer.objects.create(stripe_id="cus_XXXXX", user=self.user) - Customer.objects.create(stripe_id="cus_YYYYY", user=user2) - - SyncMock.side_effect = InvalidRequestError("Unknown customer", None, http_status=404) - - management.call_command("sync_customers") - self.assertEqual(SyncChargesMock.call_count, 0) - self.assertEqual(SyncInvoicesMock.call_count, 0) - self.assertEqual(SyncMock.call_count, 2) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("pinax.stripe.actions.invoices.sync_invoices_for_customer") - @patch("pinax.stripe.actions.charges.sync_charges_for_customer") - def test_sync_customers_with_test_customer_unknown_error(self, SyncChargesMock, SyncInvoicesMock, SyncMock, RetrieveMock): - user2 = get_user_model().objects.create_user(username="thomas") - get_user_model().objects.create_user(username="altman") - Customer.objects.create(stripe_id="cus_XXXXX", user=self.user) - Customer.objects.create(stripe_id="cus_YYYYY", user=user2) - - SyncMock.side_effect = InvalidRequestError("Unknown error", None, http_status=500) - - with self.assertRaises(InvalidRequestError): - management.call_command("sync_customers") - self.assertEqual(SyncChargesMock.call_count, 0) - self.assertEqual(SyncInvoicesMock.call_count, 0) - self.assertEqual(SyncMock.call_count, 1) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("pinax.stripe.actions.invoices.sync_invoices_for_customer") - @patch("pinax.stripe.actions.charges.sync_charges_for_customer") - def test_sync_customers_with_unicode_username(self, SyncChargesMock, SyncInvoicesMock, SyncMock, RetrieveMock): - user2 = get_user_model().objects.create_user(username=u"tom\xe1s") - Customer.objects.create(stripe_id="cus_YYYYY", user=user2) - management.call_command("sync_customers") - self.assertEqual(SyncChargesMock.call_count, 1) - self.assertEqual(SyncInvoicesMock.call_count, 1) - self.assertEqual(SyncMock.call_count, 1) - - @patch("stripe.Customer.retrieve") - @patch("pinax.stripe.actions.invoices.sync_invoices_for_customer") - @patch("pinax.stripe.actions.charges.sync_charges_for_customer") - def test_sync_customers_with_remotely_purged_customer(self, SyncChargesMock, SyncInvoicesMock, RetrieveMock): - customer = Customer.objects.create( - user=self.user, - stripe_id="cus_XXXXX" - ) - - RetrieveMock.return_value = dict( - deleted=True - ) - - management.call_command("sync_customers") - self.assertIsNone(Customer.objects.get(stripe_id=customer.stripe_id).user) - self.assertIsNotNone(Customer.objects.get(stripe_id=customer.stripe_id).date_purged) - self.assertEqual(SyncChargesMock.call_count, 0) - self.assertEqual(SyncInvoicesMock.call_count, 0) - - @patch("pinax.stripe.actions.charges.update_charge_availability") - def test_update_charge_availability(self, UpdateChargeMock): - management.call_command("update_charge_availability") - self.assertEqual(UpdateChargeMock.call_count, 1) diff --git a/pinax/stripe/tests/test_email.py b/pinax/stripe/tests/test_email.py deleted file mode 100644 index 0b25ca8fd..000000000 --- a/pinax/stripe/tests/test_email.py +++ /dev/null @@ -1,68 +0,0 @@ -import decimal - -from django.contrib.auth import get_user_model -from django.core import mail -from django.test import TestCase - -from mock import patch - -from ..actions import charges -from ..models import Customer - - -class EmailReceiptTest(TestCase): - - def setUp(self): - User = get_user_model() - self.user = User.objects.create_user(username="patrick", email="user@test.com") - self.customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - - @patch("stripe.Charge.create") - def test_email_receipt_renders_amount_properly(self, ChargeMock): - ChargeMock.return_value = { - "id": "ch_XXXXXX", - "source": { - "id": "card_01" - }, - "amount": 40000, - "currency": "usd", - "paid": True, - "refunded": False, - "invoice": None, - "captured": True, - "dispute": None, - "created": 1363911708, - "customer": "cus_xxxxxxxxxxxxxxx" - } - charges.create( - customer=self.customer, - amount=decimal.Decimal("400.00") - ) - self.assertTrue("$400.00" in mail.outbox[0].body) - - @patch("stripe.Charge.create") - def test_email_receipt_renders_amount_in_JPY_properly(self, ChargeMock): - ChargeMock.return_value = { - "id": "ch_XXXXXX", - "source": { - "id": "card_01" - }, - "amount": 40000, - "currency": "jpy", - "paid": True, - "refunded": False, - "invoice": None, - "captured": True, - "dispute": None, - "created": 1363911708, - "customer": "cus_xxxxxxxxxxxxxxx" - } - charges.create( - customer=self.customer, - amount=decimal.Decimal("40000"), - currency="jpy" - ) - self.assertTrue("$40000.00" in mail.outbox[0].body) diff --git a/pinax/stripe/tests/test_event.py b/pinax/stripe/tests/test_event.py deleted file mode 100644 index 5d511c110..000000000 --- a/pinax/stripe/tests/test_event.py +++ /dev/null @@ -1,321 +0,0 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.utils import timezone - -from mock import patch - -from ..actions import customers -from ..models import Customer, Event, Plan, Subscription -from ..signals import WEBHOOK_SIGNALS -from ..webhooks import registry - - -class TestEventMethods(TestCase): - def setUp(self): - User = get_user_model() - self.user = User.objects.create_user(username="testuser") - self.user.save() - self.customer = Customer.objects.create( - stripe_id="cus_xxxxxxxxxxxxxxx", - user=self.user - ) - self.plan = Plan.objects.create( - stripe_id="p1", - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - - def test_link_customer_customer_created(self): - msg = { - "created": 1363911708, - "data": { - "object": { - "account_balance": 0, - "active_card": None, - "created": 1363911708, - "currency": None, - "default_source": None, - "delinquent": False, - "description": None, - "discount": None, - "email": "xxxxxxxxxx@yahoo.com", - "id": "cus_xxxxxxxxxxxxxxx", - "livemode": True, - "object": "customer", - "sources": { - "data": [], - }, - "subscriptions": { - "data": [], - }, - } - }, - "id": "evt_xxxxxxxxxxxxx", - "livemode": True, - "object": "event", - "pending_webhooks": 1, - "type": "customer.created" - } - event = Event.objects.create( - stripe_id=msg["id"], - kind="customer.created", - livemode=True, - webhook_message=msg, - validated_message=msg - ) - self.assertIsNone(self.customer.account_balance) - customers.link_customer(event) - self.assertEqual(event.customer, self.customer) - self.customer.refresh_from_db() - self.assertEqual(self.customer.account_balance, 0) - - def test_link_customer_customer_updated(self): - msg = { - "created": 1346855599, - "data": { - "object": { - "account_balance": 0, - "active_card": { - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "country": "MX", - "cvc_check": "pass", - "exp_month": 1, - "exp_year": 2014, - "fingerprint": "XXXXXXXXXXX", - "last4": "7992", - "name": None, - "object": "card", - "type": "MasterCard" - }, - "created": 1346855596, - "currency": None, - "default_source": None, - "delinquent": False, - "description": None, - "discount": None, - "email": "xxxxxxxxxx@yahoo.com", - "id": "cus_xxxxxxxxxxxxxxx", - "livemode": True, - "object": "customer", - "sources": { - "data": [], - }, - "subscriptions": { - "data": [], - }, - }, - "previous_attributes": { - "active_card": None - } - }, - "id": "evt_xxxxxxxxxxxxx", - "livemode": True, - "object": "event", - "pending_webhooks": 1, - "type": "customer.updated" - } - event = Event.objects.create( - stripe_id=msg["id"], - kind="customer.updated", - livemode=True, - webhook_message=msg, - validated_message=msg - ) - customers.link_customer(event) - self.assertEqual(event.customer, self.customer) - - def test_link_customer_customer_deleted(self): - msg = { - "created": 1348286560, - "data": { - "object": { - "account_balance": 0, - "active_card": None, - "created": 1348286302, - "currency": None, - "default_source": None, - "delinquent": False, - "description": None, - "discount": None, - "email": "paltman+test@gmail.com", - "id": "cus_xxxxxxxxxxxxxxx", - "livemode": True, - "object": "customer", - "sources": { - "data": [], - }, - "subscriptions": { - "data": [], - }, - } - }, - "id": "evt_xxxxxxxxxxxxx", - "livemode": True, - "object": "event", - "pending_webhooks": 1, - "type": "customer.deleted" - } - event = Event.objects.create( - stripe_id=msg["id"], - kind="customer.deleted", - livemode=True, - webhook_message=msg, - validated_message=msg - ) - customers.link_customer(event) - self.assertEqual(event.customer, self.customer) - - @patch("stripe.Event.retrieve") - @patch("stripe.Customer.retrieve") - def test_process_customer_deleted(self, CustomerMock, EventMock): - ev = EventMock() - msg = { - "created": 1348286560, - "data": { - "object": { - "account_balance": 0, - "active_card": None, - "created": 1348286302, - "currency": None, - "default_source": None, - "delinquent": False, - "description": None, - "discount": None, - "email": "paltman+test@gmail.com", - "id": "cus_xxxxxxxxxxxxxxx", - "livemode": True, - "object": "customer", - "sources": { - "data": [], - }, - "subscriptions": { - "data": [], - } - } - }, - "id": "evt_xxxxxxxxxxxxx", - "livemode": True, - "object": "event", - "pending_webhooks": 1, - "type": "customer.deleted" - } - ev.to_dict.return_value = msg - event = Event.objects.create( - stripe_id=msg["id"], - kind="customer.deleted", - livemode=True, - webhook_message=msg, - validated_message=msg, - valid=True - ) - registry.get(event.kind)(event).process() - self.assertEqual(event.customer, self.customer) - self.assertEqual(event.customer.user, None) - - @staticmethod - def send_signal(customer, kind): - event = Event(customer=customer, kind=kind) - signal = WEBHOOK_SIGNALS.get(kind) - signal.send(sender=Event, event=event) - - @staticmethod - def connect_webhook_signal(kind, func, **kwargs): - signal = WEBHOOK_SIGNALS.get(kind) - signal.connect(func, **kwargs) - - @staticmethod - def disconnect_webhook_signal(kind, func, **kwargs): - signal = WEBHOOK_SIGNALS.get(kind) - signal.disconnect(func, **kwargs) - - @patch("pinax.stripe.actions.customers.sync_customer") - @patch("stripe.Event.retrieve") - @patch("stripe.Customer.retrieve") - def test_customer_subscription_deleted(self, CustomerMock, EventMock, SyncMock): - """ - Tests to make sure downstream signal handlers do not see stale Subscription object properties - after a customer.subscription.deleted event occurs. While the delete method is called - on the affected Subscription object's properties are still accessible (unless the - Customer object for the event gets refreshed before sending the complimentary signal) - """ - ev = EventMock() - cm = CustomerMock() - cm.currency = "usd" - cm.delinquent = False - cm.default_source = "" - cm.account_balance = 0 - kind = "customer.subscription.deleted" - plan = self.plan - - cs = Subscription(stripe_id="su_2ZDdGxJ3EQQc7Q", customer=self.customer, quantity=1, start=timezone.now(), plan=plan) - cs.save() - customer = Customer.objects.get(pk=self.customer.pk) - - # Stripe objects will not have this attribute so we must delete it from the mocked object - del customer.stripe_customer.subscription - self.assertIsNotNone(customer.subscription_set.all()[0]) - - # This is the expected format of a customer.subscription.delete message - msg = { - "id": "evt_2eRjeAlnH1XMe8", - "created": 1380317537, - "livemode": True, - "type": kind, - "data": { - "object": { - "id": "su_2ZDdGxJ3EQQc7Q", - "plan": { - "interval": "month", - "name": "xxx", - "amount": 200, - "currency": "usd", - "id": plan.stripe_id, - "object": "plan", - "livemode": True, - "interval_count": 1, - "trial_period_days": None - }, - "object": "subscription", - "start": 1379111889, - "status": "canceled", - "customer": self.customer.stripe_id, - "cancel_at_period_end": False, - "current_period_start": 1378738246, - "current_period_end": 1381330246, - "ended_at": 1380317537, - "trial_start": None, - "trial_end": None, - "canceled_at": 1380317537, - "quantity": 1, - "application_fee_percent": None - } - }, - "object": "event", - "pending_webhooks": 1, - "request": "iar_2eRjQZmn0i3G9M" - } - ev.to_dict.return_value = msg - - # Create a test event for the message - test_event = Event.objects.create( - stripe_id=msg["id"], - kind=kind, - livemode=msg["livemode"], - webhook_message=msg, - validated_message=msg, - valid=True, - customer=customer, - ) - - registry.get(test_event.kind)(test_event).process() - self.assertTrue(SyncMock.called) diff --git a/pinax/stripe/tests/test_forms.py b/pinax/stripe/tests/test_forms.py deleted file mode 100644 index dacfef29c..000000000 --- a/pinax/stripe/tests/test_forms.py +++ /dev/null @@ -1,398 +0,0 @@ -import datetime -import json -from base64 import b64decode -from copy import copy - -from django import forms -from django.contrib.auth import get_user_model -from django.core.files.uploadedfile import InMemoryUploadedFile -from django.test import TestCase -from django.test.client import RequestFactory -from django.test.utils import override_settings -from django.utils import timezone - -from mock import patch -from stripe.error import InvalidRequestError - -from ..forms import ( - AdditionalCustomAccountForm, - InitialCustomAccountForm, - extract_ipaddress -) -from ..models import Account - - -def get_stripe_error(field_name=None, message=None): - if field_name is None: - field_name = u"legal_entity[dob][year]" - if message is None: - message = u"This value must be greater than 1900 (it currently is '1800')." - json_body = { - "error": { - "type": "invalid_request_error", - "message": message, - "param": field_name - } - } - http_body = json.dumps(json_body) - return InvalidRequestError( - message, - field_name, - http_body=http_body, - json_body=json_body - ) - - -def get_image(name=None, _type=None): - # https://raw.githubusercontent.com/mathiasbynens/small/master/jpeg.jpg - if _type is None: - _type = "image/jpeg" - if name is None: - name = "random-name.jpg" - image = b64decode( - "/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOC" - "wkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQ" - "EAAD8A0s8g/9k=" - ) - return InMemoryUploadedFile( - image, None, name, _type, len(image), None - ) - - -class InitialCustomAccountFormTestCase(TestCase): - - def setUp(self): - self.user = get_user_model().objects.create_user( - username="luke", - email="luke@example.com" - ) - self.data = { - "first_name": "Donkey", - "last_name": "McGee", - "dob": "1980-01-01", - "address_line1": "2993 Steve St", - "address_city": "Fake Town", - "address_state": "CA", - "address_country": "US", - "address_postal_code": "V5Y3Z9", - "routing_number": "11000-000", - "account_number": "12345678900", - "tos_accepted": "true", - "currency": "USD" - } - self.request = RequestFactory().get("/user/account/create") - self.request.user = self.user - - def test_conditional_state_field(self): - form = InitialCustomAccountForm( - self.data, - request=self.request, - country="CA" - ) - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors["address_state"][0], - "Select a valid choice. CA is not one of the available choices." - ) - - def test_fields_needed(self): - form = InitialCustomAccountForm( - self.data, - request=self.request, - country="CA", - fields_needed=["legal_entity.verification.document"] - ) - self.assertTrue("document" in form.fields) - - def test_conditional_currency_field(self): - data = copy(self.data) - data["currency"] = "AUD" - form = InitialCustomAccountForm( - data, - request=self.request, - country="US" - ) - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors["currency"][0], - "Select a valid choice. AUD is not one of the available choices." - ) - - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - @patch("stripe.Account.create") - def test_save(self, create_mock, sync_mock): - form = InitialCustomAccountForm( - self.data, - request=self.request, - country="US" - ) - self.assertTrue(form.is_valid()) - form.save() - self.assertTrue(create_mock.called) - self.assertTrue(sync_mock.called) - - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - @patch("stripe.Account.create") - def test_save_with_stripe_error(self, create_mock, sync_mock): - create_mock.side_effect = get_stripe_error() - form = InitialCustomAccountForm( - self.data, - request=self.request, - country="US" - ) - self.assertTrue(form.is_valid()) - with self.assertRaises(InvalidRequestError): - form.save() - self.assertTrue(create_mock.called) - self.assertFalse(sync_mock.called) - self.assertEqual( - form.errors["dob"], - [u"This value must be greater than 1900 (it currently is '1800')."] - ) - - @patch("stripe.Account.create") - def test_save_with_stripe_error_unknown_field(self, create_mock): - create_mock.side_effect = get_stripe_error( - field_name="unknown", - message="Oopsie daisy" - ) - form = InitialCustomAccountForm( - self.data, - request=self.request, - country="US" - ) - self.assertTrue(form.is_valid()) - with self.assertRaises(InvalidRequestError): - form.save() - self.assertEqual( - form.non_field_errors()[0], - "Oopsie daisy" - ) - - @override_settings(DEBUG=True) - @patch("ipware.ip.get_real_ip") - @patch("ipware.ip.get_ip") - def test_extract_ipaddress(self, ip_mock, ip_real_mock): - # force hit of get_ip when get_real_ip returns None - ip_real_mock.return_value = None - ip_mock.return_value = "192.168.0.1" - ip = extract_ipaddress(self.request) - self.assertEqual(ip, "192.168.0.1") - - -class AdditionalCustomAccountFormTestCase(TestCase): - - def setUp(self): - self.user = get_user_model().objects.create_user( - username="luke", - email="luke@example.com" - ) - self.data = { - "first_name": "Donkey", - "last_name": "McGee", - "dob": "1980-01-01", - "personal_id_number": "123123123" - } - self.account = Account.objects.create( - user=self.user, - stripe_id="acct_123123", - country="US", - legal_entity_first_name="Donkey", - legal_entity_last_name="McGee", - legal_entity_dob=datetime.datetime(1980, 1, 1), - type="custom", - verification_due_by=timezone.now() + datetime.timedelta(days=2), - verification_fields_needed=["legal_entity.personal_id_number"], - ) - - def test_initial_data_from_account(self): - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertEqual( - form.fields["first_name"].initial, - self.account.legal_entity_first_name - ) - self.assertEqual( - form.fields["last_name"].initial, - self.account.legal_entity_last_name - ) - self.assertEqual( - form.fields["dob"].initial, - self.account.legal_entity_dob - ) - - def test_country_from_account(self): - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertEqual( - form.country, self.account.country - ) - - def test_fields_needed_from_account(self): - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertEqual( - form.fields_needed, self.account.verification_fields_needed - ) - - def test_dynamic_personal_id_field_added(self): - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertIn("personal_id_number", form.fields) - self.assertTrue( - isinstance(form.fields["personal_id_number"], forms.CharField) - ) - - def test_dynamic_document_field_added(self): - self.account.verification_fields_needed = [ - "legal_entity.verification.document" - ] - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertIn("document", form.fields) - self.assertTrue( - isinstance(form.fields["document"], forms.FileField) - ) - - def test_multiple_dynamic_fields_added(self): - self.account.verification_fields_needed = [ - "legal_entity.verification.document", - "legal_entity.personal_id_number" - ] - form = AdditionalCustomAccountForm( - account=self.account - ) - self.assertIn("document", form.fields) - self.assertTrue( - isinstance(form.fields["document"], forms.FileField) - ) - self.assertIn("personal_id_number", form.fields) - self.assertTrue( - isinstance(form.fields["personal_id_number"], forms.CharField) - ) - - @override_settings( - PINAX_STRIPE_DOCUMENT_MAX_SIZE_KB=0 - ) - def test_clean_document_too_large(self): - self.account.verification_fields_needed = [ - "legal_entity.verification.document" - ] - form = AdditionalCustomAccountForm( - self.data, - account=self.account, - files={"document": get_image()} - ) - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors["document"], - [u"Document image is too large (> 0.0 MB)"] - ) - - def test_clean_document_wrong_type(self): - self.account.verification_fields_needed = [ - "legal_entity.verification.document" - ] - form = AdditionalCustomAccountForm( - self.data, - account=self.account, - files={"document": get_image(name="donkey.gif", _type="image/gif")} - ) - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors["document"], - [u"The type of image you supplied is not supported. Please upload a JPG or PNG file."] - ) - - def test_clean_dob_too_old(self): - data = copy(self.data) - data["dob"] = "1780-01-01" - form = AdditionalCustomAccountForm( - data, - account=self.account - ) - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors["dob"], - [u"This must be greater than 1900-01-01."] - ) - - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - @patch("stripe.Account.retrieve") - @patch("stripe.FileUpload.create") - def test_save(self, file_upload_mock, retrieve_mock, sync_mock): - self.account.verification_fields_needed = [ - "legal_entity.personal_id_number" - ] - form = AdditionalCustomAccountForm( - self.data, - account=self.account - ) - self.assertTrue(form.is_valid()) - form.save() - self.assertEqual( - retrieve_mock.return_value.legal_entity.first_name, - "Donkey" - ) - self.assertEqual( - retrieve_mock.return_value.legal_entity.personal_id_number, - "123123123" - ) - self.assertFalse(file_upload_mock.called) - - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - @patch("stripe.Account.retrieve") - @patch("stripe.FileUpload.create") - def test_save_with_document(self, file_upload_mock, retrieve_mock, sync_mock): - file_upload_mock.return_value = {"id": 5555} - self.account.verification_fields_needed = [ - "legal_entity.personal_id_number", - "legal_entity.verification.document" - ] - form = AdditionalCustomAccountForm( - self.data, - account=self.account, - files={"document": get_image()} - ) - self.assertTrue(form.is_valid()) - form.save() - self.assertEqual( - retrieve_mock.return_value.legal_entity.first_name, - "Donkey" - ) - self.assertEqual( - retrieve_mock.return_value.legal_entity.personal_id_number, - "123123123" - ) - self.assertTrue(file_upload_mock.called) - self.assertEqual( - retrieve_mock.return_value.legal_entity.verification.document, - file_upload_mock.return_value["id"] - ) - - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - @patch("stripe.Account.retrieve") - @patch("stripe.FileUpload.create") - def test_save_with_stripe_error(self, file_upload_mock, retrieve_mock, sync_mock): - retrieve_mock.return_value.save.side_effect = get_stripe_error() - self.account.verification_fields_needed = [ - "legal_entity.personal_id_number", - "legal_entity.verification.document" - ] - form = AdditionalCustomAccountForm( - self.data, - account=self.account, - files={"document": get_image()} - ) - self.assertTrue(form.is_valid()) - with self.assertRaises(InvalidRequestError): - form.save() - self.assertEqual( - form.errors["dob"], - [u"This value must be greater than 1900 (it currently is '1800')."] - ) diff --git a/pinax/stripe/tests/test_hooks.py b/pinax/stripe/tests/test_hooks.py deleted file mode 100644 index ea87714ee..000000000 --- a/pinax/stripe/tests/test_hooks.py +++ /dev/null @@ -1,81 +0,0 @@ -import decimal - -from django.contrib.auth import get_user_model -from django.core import mail -from django.test import TestCase - -from ..hooks import DefaultHookSet -from ..models import Charge, Customer - - -class HooksTestCase(TestCase): - - def setUp(self): - self.User = get_user_model() - self.user = self.User.objects.create_user( - username="patrick", - email="paltman@example.com" - ) - self.customer = Customer.objects.create( - user=self.user, - stripe_id="cus_xxxxxxxxxxxxxxx" - ) - self.hookset = DefaultHookSet() - - def test_adjust_subscription_quantity(self): - new_qty = self.hookset.adjust_subscription_quantity(customer=None, plan=None, quantity=3) - self.assertEqual(new_qty, 3) - - def test_adjust_subscription_quantity_none(self): - new_qty = self.hookset.adjust_subscription_quantity(customer=None, plan=None, quantity=None) - self.assertEqual(new_qty, 1) - - def test_trial_period(self): - period = self.hookset.trial_period(self.user, "some plan") - self.assertIsNone(period) - - def test_send_receipt(self): - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False, - receipt_sent=False - ) - self.hookset.send_receipt(charge) - self.assertTrue(Charge.objects.get(pk=charge.pk).receipt_sent) - - def test_send_receipt_with_email(self): - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False, - receipt_sent=False - ) - self.hookset.send_receipt(charge, email="goose@topgun.com") - self.assertTrue(Charge.objects.get(pk=charge.pk).receipt_sent) - self.assertEqual(mail.outbox[0].to, ["goose@topgun.com"]) - - def test_send_receipt_already_sent(self): - charge = Charge.objects.create( - stripe_id="ch_XXXXXX", - customer=self.customer, - source="card_01", - amount=decimal.Decimal("10.00"), - currency="usd", - paid=True, - refunded=False, - disputed=False, - receipt_sent=True - ) - self.hookset.send_receipt(charge) - self.assertTrue(Charge.objects.get(pk=charge.pk).receipt_sent) diff --git a/pinax/stripe/tests/test_managers.py b/pinax/stripe/tests/test_managers.py deleted file mode 100644 index 5f1b787e7..000000000 --- a/pinax/stripe/tests/test_managers.py +++ /dev/null @@ -1,195 +0,0 @@ -import datetime -import decimal - -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.utils import timezone - -from ..models import Charge, Customer, Plan, Subscription - - -class CustomerManagerTest(TestCase): - - def setUp(self): - User = get_user_model() - # create customers and current subscription records - period_start = datetime.datetime(2013, 4, 1, tzinfo=timezone.utc) - period_end = datetime.datetime(2013, 4, 30, tzinfo=timezone.utc) - start = datetime.datetime(2013, 1, 1, tzinfo=timezone.utc) - self.plan = Plan.objects.create( - stripe_id="p1", - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - self.plan2 = Plan.objects.create( - stripe_id="p2", - amount=5, - currency="usd", - interval="monthly", - interval_count=1, - name="Light" - ) - for i in range(10): - customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(i)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(i) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(i), - customer=customer, - plan=self.plan, - current_period_start=period_start, - current_period_end=period_end, - status="active", - start=start, - quantity=1 - ) - customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(11)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(11) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(11), - customer=customer, - plan=self.plan, - current_period_start=period_start, - current_period_end=period_end, - status="canceled", - canceled_at=period_end, - start=start, - quantity=1 - ) - customer = Customer.objects.create( - user=User.objects.create_user(username="patrick{0}".format(12)), - stripe_id="cus_xxxxxxxxxxxxxx{0}".format(12) - ) - Subscription.objects.create( - stripe_id="sub_{}".format(12), - customer=customer, - plan=self.plan2, - current_period_start=period_start, - current_period_end=period_end, - status="active", - start=start, - quantity=1 - ) - - def test_started_during_no_records(self): - self.assertEqual( - Customer.objects.started_during(2013, 4).count(), - 0 - ) - - def test_started_during_has_records(self): - self.assertEqual( - Customer.objects.started_during(2013, 1).count(), - 12 - ) - - def test_canceled_during(self): - self.assertEqual( - Customer.objects.canceled_during(2013, 4).count(), - 1 - ) - - def test_canceled_all(self): - self.assertEqual( - Customer.objects.canceled().count(), - 1 - ) - - def test_active_all(self): - self.assertEqual( - Customer.objects.active().count(), - 11 - ) - - def test_started_plan_summary(self): - for plan in Customer.objects.started_plan_summary_for(2013, 1): - if plan["subscription__plan"] == self.plan: - self.assertEqual(plan["count"], 11) - if plan["subscription__plan"] == self.plan2: - self.assertEqual(plan["count"], 1) - - def test_active_plan_summary(self): - for plan in Customer.objects.active_plan_summary(): - if plan["subscription__plan"] == self.plan: - self.assertEqual(plan["count"], 10) - if plan["subscription__plan"] == self.plan2: - self.assertEqual(plan["count"], 1) - - def test_canceled_plan_summary(self): - for plan in Customer.objects.canceled_plan_summary_for(2013, 1): - if plan["subscription__plan"] == self.plan: - self.assertEqual(plan["count"], 1) - if plan["subscription__plan"] == self.plan2: - self.assertEqual(plan["count"], 0) - - def test_churn(self): - self.assertEqual( - Customer.objects.churn(), - decimal.Decimal("1") / decimal.Decimal("11") - ) - - -class ChargeManagerTests(TestCase): - - def setUp(self): - customer = Customer.objects.create( - user=get_user_model().objects.create_user(username="patrick"), - stripe_id="cus_xxxxxxxxxxxxxx" - ) - Charge.objects.create( - stripe_id="ch_1", - customer=customer, - charge_created=datetime.datetime(2013, 1, 1, tzinfo=timezone.utc), - paid=True, - amount=decimal.Decimal("100"), - amount_refunded=decimal.Decimal("0") - ) - Charge.objects.create( - stripe_id="ch_2", - customer=customer, - charge_created=datetime.datetime(2013, 1, 1, tzinfo=timezone.utc), - paid=True, - amount=decimal.Decimal("100"), - amount_refunded=decimal.Decimal("10") - ) - Charge.objects.create( - stripe_id="ch_3", - customer=customer, - charge_created=datetime.datetime(2013, 1, 1, tzinfo=timezone.utc), - paid=False, - amount=decimal.Decimal("100"), - amount_refunded=decimal.Decimal("0") - ) - Charge.objects.create( - stripe_id="ch_4", - customer=customer, - charge_created=datetime.datetime(2013, 4, 1, tzinfo=timezone.utc), - paid=True, - amount=decimal.Decimal("500"), - amount_refunded=decimal.Decimal("15.42") - ) - - def test_charges_during(self): - charges = Charge.objects.during(2013, 1) - self.assertEqual(charges.count(), 3) - - def test_paid_totals_for_jan(self): - totals = Charge.objects.paid_totals_for(2013, 1) - self.assertEqual(totals["total_amount"], decimal.Decimal("200")) - self.assertEqual(totals["total_refunded"], decimal.Decimal("10")) - - def test_paid_totals_for_apr(self): - totals = Charge.objects.paid_totals_for(2013, 4) - self.assertEqual(totals["total_amount"], decimal.Decimal("500")) - self.assertEqual(totals["total_refunded"], decimal.Decimal("15.42")) - - def test_paid_totals_for_dec(self): - totals = Charge.objects.paid_totals_for(2013, 12) - self.assertEqual(totals["total_amount"], None) - self.assertEqual(totals["total_refunded"], None) diff --git a/pinax/stripe/tests/test_middleware.py b/pinax/stripe/tests/test_middleware.py deleted file mode 100644 index c92c74dbf..000000000 --- a/pinax/stripe/tests/test_middleware.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.contrib.auth import authenticate, get_user_model, login, logout -from django.test import TestCase -from django.utils import timezone - -from mock import Mock - -from ..conf import settings -from ..middleware import ActiveSubscriptionMiddleware -from ..models import Customer, Plan, Subscription - -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - - -class DummySession(dict): - - def cycle_key(self): - return - - def flush(self): - return - - -class ActiveSubscriptionMiddlewareTests(TestCase): - urls = "pinax.stripe.tests.urls" - - def setUp(self): - self.middleware = ActiveSubscriptionMiddleware() - self.request = Mock() - self.request.META = {} - self.request.session = DummySession() - - self.old_urls = settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS - settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS += ( - "signup", - "password_reset" - ) - - user = get_user_model().objects.create_user(username="patrick") - user.set_password("eldarion") - user.save() - user = authenticate(username="patrick", password="eldarion") - login(self.request, user) - - def tearDown(self): - settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS = self.old_urls - - def test_authed_user_with_no_customer_redirects_on_non_exempt_url(self): - self.request.path = "/the/app/" - response = self.middleware.process_request(self.request) - self.assertEqual(response.status_code, 302) - self.assertEqual( - response._headers["location"][1], - reverse(settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_REDIRECT) - ) - - def test_authed_user_with_no_customer_passes_with_exempt_url(self): - self.request.path = "/accounts/signup/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) - - def test_authed_user_with_no_customer_passes_with_exempt_url_containing_pattern(self): - self.request.path = "/password/reset/confirm/test-token/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) - - def test_authed_user_with_no_active_subscription_passes_with_exempt_url(self): - Customer.objects.create(stripe_id="cus_1", user=self.request.user) - self.request.path = "/accounts/signup/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) - - def test_authed_user_with_no_active_subscription_redirects_on_non_exempt_url(self): - Customer.objects.create(stripe_id="cus_1", user=self.request.user) - self.request.path = "/the/app/" - response = self.middleware.process_request(self.request) - self.assertEqual(response.status_code, 302) - self.assertEqual( - response._headers["location"][1], - reverse(settings.PINAX_STRIPE_SUBSCRIPTION_REQUIRED_REDIRECT) - ) - - def test_authed_user_with_active_subscription_redirects_on_non_exempt_url(self): - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.request.user - ) - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - Subscription.objects.create( - customer=customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active", - cancel_at_period_end=False - ) - self.request.path = "/the/app/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) - - def test_unauthed_user_passes(self): - logout(self.request) - self.request.path = "/the/app/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) - - def test_staff_user_passes(self): - self.request.user.is_staff = True - self.request.path = "/the/app/" - response = self.middleware.process_request(self.request) - self.assertIsNone(response) diff --git a/pinax/stripe/tests/test_models.py b/pinax/stripe/tests/test_models.py index ea9564eb9..128853b2e 100644 --- a/pinax/stripe/tests/test_models.py +++ b/pinax/stripe/tests/test_models.py @@ -14,20 +14,8 @@ from mock import call, patch from ..models import ( - Account, - BankAccount, - Card, - Charge, - Coupon, - Customer, Event, - EventProcessingException, - Invoice, - InvoiceItem, - Plan, - Subscription, - Transfer, - UserAccount + EventProcessingException ) try: @@ -40,46 +28,6 @@ class ModelTests(TestCase): - def test_plan_str_and_repr(self): - p = Plan(amount=decimal.Decimal("5"), name="My Plan", interval="monthly", interval_count=1) - self.assertTrue(p.name in _str(p)) - self.assertEqual(repr(p), "Plan(pk=None, name={p.name!r}, amount=Decimal('5'), currency={p.currency!r}, interval={p.interval!r}, interval_count=1, trial_period_days=None, stripe_id={p.stripe_id!r})".format(p=p)) - - def test_plan_repr_unicode(self): - p = Plan(amount=decimal.Decimal("5"), name=u"öre", interval="monthly", interval_count=1, stripe_id=u"öre") - if PY2: - self.assertEqual(repr(p), "Plan(pk=None, name=u'\\xf6re', amount=Decimal('5'), currency=u'', interval=u'monthly', interval_count=1, trial_period_days=None, stripe_id=u'\\xf6re')") - else: - self.assertEqual(repr(p), "Plan(pk=None, name='öre', amount=Decimal('5'), currency='', interval='monthly', interval_count=1, trial_period_days=None, stripe_id='öre')") - - def test_plan_str_usd(self): - p = Plan(amount=decimal.Decimal("5"), name="My Plan", currency="usd", interval="monthly", interval_count=1) - self.assertTrue(u"\u0024" in _str(p)) - - def test_plan_str_jpy(self): - p = Plan(amount=decimal.Decimal("5"), name="My Plan", currency="jpy", interval="monthly", interval_count=1) - self.assertTrue(u"\u00a5" in _str(p)) - - @patch("stripe.Plan.retrieve") - def test_plan_stripe_plan(self, RetrieveMock): - c = Plan(stripe_id="plan") - self.assertEqual(c.stripe_plan, RetrieveMock.return_value) - self.assertTrue(RetrieveMock.call_args_list, [ - call("plan", stripe_account=None)]) - - @patch("stripe.Plan.retrieve") - def test_plan_stripe_plan_with_account(self, RetrieveMock): - c = Plan(stripe_id="plan", stripe_account=Account(stripe_id="acct_A")) - self.assertEqual(c.stripe_plan, RetrieveMock.return_value) - self.assertTrue(RetrieveMock.call_args_list, [ - call("plan", stripe_account="acct_A")]) - - def test_plan_per_account(self): - Plan.objects.create(stripe_id="plan", amount=decimal.Decimal("100"), interval="monthly", interval_count=1) - account = Account.objects.create(stripe_id="acct_A") - Plan.objects.create(stripe_id="plan", stripe_account=account, amount=decimal.Decimal("100"), interval="monthly", interval_count=1) - self.assertEqual(Plan.objects.count(), 2) - def test_event_processing_exception_str(self): e = EventProcessingException(data="hello", message="hi there", traceback="fake") self.assertTrue("Event=" in str(e)) @@ -90,247 +38,17 @@ def test_event_str_and_repr(self): e = Event(kind="customer.deleted", webhook_message={}, created_at=created_at) self.assertTrue("customer.deleted" in str(e)) if PY2: - self.assertEqual(repr(e), "Event(pk=None, kind=u'customer.deleted', customer=None, valid=None, created_at={!s}, stripe_id=u'')".format( + self.assertEqual(repr(e), "Event(pk=None, kind=u'customer.deleted', customer=u'', valid=None, created_at={!s}, stripe_id=u'')".format( created_at_iso)) else: - self.assertEqual(repr(e), "Event(pk=None, kind='customer.deleted', customer=None, valid=None, created_at={!s}, stripe_id='')".format( + self.assertEqual(repr(e), "Event(pk=None, kind='customer.deleted', customer='', valid=None, created_at={!s}, stripe_id='')".format( created_at_iso)) e.stripe_id = "evt_X" - e.customer = Customer() + e.customer_id = "cus_YYY" if PY2: self.assertEqual(repr(e), "Event(pk=None, kind=u'customer.deleted', customer={!r}, valid=None, created_at={!s}, stripe_id=u'evt_X')".format( - e.customer, created_at_iso)) + e.customer_id, created_at_iso)) else: self.assertEqual(repr(e), "Event(pk=None, kind='customer.deleted', customer={!r}, valid=None, created_at={!s}, stripe_id='evt_X')".format( - e.customer, created_at_iso)) - - def test_customer_str_and_repr(self): - c = Customer() - self.assertEqual(str(c), "No User(s)") - if PY2: - self.assertEqual(repr(c), "Customer(pk=None, stripe_id=u'')") - else: - self.assertEqual(repr(c), "Customer(pk=None, stripe_id='')") - - def test_customer_with_user_str_and_repr(self): - User = get_user_model() - c = Customer(user=User()) - self.assertEqual(str(c), "") - if PY2: - self.assertEqual(repr(c), "Customer(pk=None, user=, stripe_id=u'')") - else: - self.assertEqual(repr(c), "Customer(pk=None, user=, stripe_id='')") - - def test_customer_saved_without_users_str(self): - c = Customer.objects.create() - self.assertEqual(str(c), "No User(s)") - c.stripe_id = "cu_XXX" - self.assertEqual(str(c), "No User(s) (cu_XXX)") - - def test_connected_customer_str_and_repr(self): - User = get_user_model() - user = User.objects.create() - account = Account.objects.create(stripe_id="acc_A") - customer = Customer.objects.create(stripe_id="cus_A", stripe_account=account) - UserAccount.objects.create(customer=customer, user=user, account=account) - self.assertEqual(str(customer), "") - if PY2: - self.assertEqual(repr(customer), "Customer(pk={c.pk}, users=, stripe_id=u'cus_A')".format(c=customer)) - else: - self.assertEqual(repr(customer), "Customer(pk={c.pk}, users=, stripe_id='cus_A')".format(c=customer)) - - def test_charge_repr(self): - charge = Charge() - if PY2: - self.assertEqual(repr(charge), "Charge(pk=None, customer=None, source=u'', amount=None, captured=None, paid=None, stripe_id=u'')") - else: - self.assertEqual(repr(charge), "Charge(pk=None, customer=None, source='', amount=None, captured=None, paid=None, stripe_id='')") - - def test_charge_str(self): - charge = Charge() - self.assertEqual(str(charge), "$0 (unpaid, uncaptured)") - charge.stripe_id = "ch_XXX" - charge.captured = True - charge.paid = True - charge.amount = decimal.Decimal(5) - self.assertEqual(str(charge), "$5") - charge.refunded = True - self.assertEqual(str(charge), "$5 (refunded)") - - def test_charge_total_amount(self): - charge = Charge() - self.assertEqual(charge.total_amount, 0) - charge.amount = decimal.Decimal(17) - self.assertEqual(charge.total_amount, 17) - charge.amount_refunded = decimal.Decimal(15.5) - self.assertEqual(charge.total_amount, 1.5) - - def test_plan_display_invoiceitem(self): - p = Plan(amount=decimal.Decimal("5"), name="My Plan", interval="monthly", interval_count=1) - p.save() - i = InvoiceItem(plan=p) - self.assertEqual(i.plan_display(), "My Plan") - - def test_coupon_percent(self): - c = Coupon(percent_off=25, duration="repeating", duration_in_months=3) - self.assertEqual(str(c), "Coupon for 25% off, repeating") - - def test_coupon_absolute(self): - c = Coupon(amount_off=decimal.Decimal(50.00), duration="once", currency="usd") - self.assertEqual(str(c), "Coupon for $50, once") - - def test_model_table_name(self): - self.assertEqual(Customer()._meta.db_table, "pinax_stripe_customer") - - def test_event_message(self): - event = Event(validated_message={"foo": 1}) - self.assertEqual(event.validated_message, event.message) - - def test_invoice_status(self): - self.assertEqual(Invoice(paid=True).status, "Paid") - - def test_invoice_status_not_paid(self): - self.assertEqual(Invoice(paid=False).status, "Open") - - def test_subscription_repr(self): - s = Subscription() - if PY2: - self.assertEqual(repr(s), "Subscription(pk=None, customer=None, plan=None, status=u'', stripe_id=u'')") - else: - self.assertEqual(repr(s), "Subscription(pk=None, customer=None, plan=None, status='', stripe_id='')") - s.customer = Customer() - s.plan = Plan() - s.status = "active" - s.stripe_id = "sub_X" - if PY2: - self.assertEqual( - repr(s), - "Subscription(pk=None, customer={o.customer!r}, plan={o.plan!r}, status=u'active', stripe_id=u'sub_X')".format(o=s)) - else: - self.assertEqual( - repr(s), - "Subscription(pk=None, customer={o.customer!r}, plan={o.plan!r}, status='active', stripe_id='sub_X')".format(o=s)) - - def test_subscription_total_amount(self): - sub = Subscription(plan=Plan(name="Pro Plan", amount=decimal.Decimal("100")), quantity=2) - self.assertEqual(sub.total_amount, decimal.Decimal("200")) - - def test_subscription_plan_display(self): - sub = Subscription(plan=Plan(name="Pro Plan")) - self.assertEqual(sub.plan_display(), "Pro Plan") - - def test_subscription_status_display(self): - sub = Subscription(status="overly_active") - self.assertEqual(sub.status_display(), "Overly Active") - - def test_subscription_delete(self): - plan = Plan.objects.create(stripe_id="pro2", amount=decimal.Decimal("100"), interval="monthly", interval_count=1) - customer = Customer.objects.create(stripe_id="foo") - sub = Subscription.objects.create(customer=customer, status="trialing", start=timezone.now(), plan=plan, quantity=1, cancel_at_period_end=True, current_period_end=(timezone.now() - datetime.timedelta(days=2))) - sub.delete() - self.assertIsNone(sub.status) - self.assertEqual(sub.quantity, 0) - self.assertEqual(sub.amount, 0) - - def test_account_str_and_repr(self): - a = Account() - self.assertEqual(str(a), " - ") - if PY2: - self.assertEqual(repr(a), "Account(pk=None, display_name=u'', type=None, authorized=True, stripe_id=u'')") - else: - self.assertEqual(repr(a), "Account(pk=None, display_name='', type=None, authorized=True, stripe_id='')") - a.stripe_id = "acct_X" - self.assertEqual(str(a), " - acct_X") - if PY2: - self.assertEqual(repr(a), "Account(pk=None, display_name=u'', type=None, authorized=True, stripe_id=u'acct_X')") - else: - self.assertEqual(repr(a), "Account(pk=None, display_name='', type=None, authorized=True, stripe_id='acct_X')") - a.display_name = "Display name" - a.authorized = False - self.assertEqual(str(a), "Display name - acct_X") - if PY2: - self.assertEqual(repr(a), "Account(pk=None, display_name=u'Display name', type=None, authorized=False, stripe_id=u'acct_X')") - else: - self.assertEqual(repr(a), "Account(pk=None, display_name='Display name', type=None, authorized=False, stripe_id='acct_X')") - - @patch("stripe.Subscription.retrieve") - def test_subscription_stripe_subscription_with_connnect(self, RetrieveMock): - a = Account(stripe_id="acc_X") - c = Customer(stripe_id="cus_X", stripe_account=a) - s = Subscription(stripe_id="sub_X", customer=c) - s.stripe_subscription - RetrieveMock.assert_called_once_with("sub_X", stripe_account="acc_X") - - def test_customer_required_fields(self): - c = Customer(stripe_id="cus_A") - c.full_clean() - - def test_user_account_validation(self): - User = get_user_model() - a = Account() - ua = UserAccount(user=User(), account=a, customer=Customer(stripe_account=Account())) - with self.assertRaises(ValidationError): - ua.clean() - - def test_user_account_repr(self): - User = get_user_model() - ua = UserAccount(user=User(), account=Account(), customer=Customer()) - self.assertEqual( - repr(ua), - "UserAccount(pk=None, user=, account={o.account!r}, customer={o.customer!r})".format( - o=ua)) - - def test_card_repr(self): - card = Card(exp_month=1, exp_year=2000) - self.assertEqual(repr(card), "Card(pk=None, customer=None)") - - card.customer = Customer.objects.create() - card.save() - self.assertEqual(repr(card), "Card(pk={c.pk}, customer={c.customer!r})".format(c=card)) - - def test_blank_with_null(self): - import inspect - import pinax.stripe.models - - clsmembers = inspect.getmembers(pinax.stripe.models, inspect.isclass) - classes = [x[1] for x in clsmembers - if issubclass(x[1], models.Model)] - - for klass in classes[0:1]: - for f in klass._meta.fields: - if f.null: - self.assertTrue(f.blank, msg="%s.%s should be blank=True" % (klass.__name__, f.name)) - - -class StripeObjectTests(TestCase): - - @patch("stripe.Charge.retrieve") - def test_stripe_charge(self, RetrieveMock): - Charge().stripe_charge - self.assertTrue(RetrieveMock.called) - - @patch("stripe.Customer.retrieve") - def test_stripe_customer(self, RetrieveMock): - Customer().stripe_customer - self.assertTrue(RetrieveMock.called) - - @patch("stripe.Invoice.retrieve") - def test_stripe_invoice(self, RetrieveMock): - Invoice().stripe_invoice - self.assertTrue(RetrieveMock.called) - - @patch("stripe.Subscription.retrieve") - def test_stripe_subscription(self, RetrieveMock): - Subscription(stripe_id="sub_X", customer=Customer(stripe_id="foo")).stripe_subscription - RetrieveMock.assert_called_once_with("sub_X", stripe_account=None) - - @patch("stripe.Transfer.retrieve") - def test_stripe_transfer(self, RetrieveMock): - Transfer(amount=10).stripe_transfer - self.assertTrue(RetrieveMock.called) - - @patch("stripe.Account.retrieve") - def test_stripe_bankaccount(self, RetrieveMock): - BankAccount(account=Account(stripe_id="foo")).stripe_bankaccount - self.assertTrue(RetrieveMock.return_value.external_accounts.retrieve.called) + e.customer_id, created_at_iso)) diff --git a/pinax/stripe/tests/test_views.py b/pinax/stripe/tests/test_views.py deleted file mode 100644 index b175aa699..000000000 --- a/pinax/stripe/tests/test_views.py +++ /dev/null @@ -1,484 +0,0 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.utils import timezone - -import stripe -from mock import patch - -from ..models import Card, Customer, Invoice, Plan, Subscription -from ..views import PaymentMethodCreateView - -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - - -class PaymentsContextMixinTests(TestCase): - - def test_payments_context_mixin_get_context_data(self): - data = PaymentMethodCreateView().get_context_data() - self.assertTrue("PINAX_STRIPE_PUBLIC_KEY" in data) - - -class InvoiceListViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - Invoice.objects.create( - stripe_id="inv_001", - customer=customer, - amount_due=100, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=100, - total=100, - date=timezone.now() - ) - Invoice.objects.create( - stripe_id="inv_002", - customer=customer, - amount_due=50, - period_end=timezone.now(), - period_start=timezone.now(), - subtotal=50, - total=50, - date=timezone.now() - ) - - def test_context(self): - self.client.login(username=self.user.username, password=self.password) - response = self.client.get( - reverse("pinax_stripe_invoice_list") - ) - self.assertTrue("invoice_list" in response.context_data) - self.assertEqual(response.context_data["invoice_list"].count(), 2) - self.assertEqual(response.context_data["invoice_list"][0].total, 100) - self.assertEqual(response.context_data["invoice_list"][1].total, 50) - - -class PaymentMethodListViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - Card.objects.create( - stripe_id="card_001", - customer=customer, - address_line_1_check="nothing", - address_zip_check="nothing", - country="US", - cvc_check="passed", - exp_month=1, - exp_year=2020, - funding="yes", - fingerprint="abc" - ) - - def test_context(self): - self.client.login(username=self.user.username, password=self.password) - response = self.client.get( - reverse("pinax_stripe_payment_method_list") - ) - self.assertTrue("payment_method_list" in response.context_data) - self.assertEqual(response.context_data["payment_method_list"].count(), 1) - self.assertEqual(response.context_data["payment_method_list"][0].stripe_id, "card_001") - - -class PaymentMethodCreateViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - - @patch("pinax.stripe.actions.sources.create_card") - def test_post(self, CreateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_create"), - {} - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_payment_method_list")) - - @patch("pinax.stripe.actions.sources.create_card") - def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_create"), - {} - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) - - -class PaymentMethodDeleteViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - self.card = Card.objects.create( - stripe_id="card_001", - customer=customer, - address_line_1_check="nothing", - address_zip_check="nothing", - country="US", - cvc_check="passed", - exp_month=1, - exp_year=2020, - funding="yes", - fingerprint="abc" - ) - - @patch("pinax.stripe.actions.sources.delete_card") - def test_post(self, CreateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_delete", args=[self.card.pk]), - {} - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_payment_method_list")) - - @patch("pinax.stripe.actions.sources.delete_card") - def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_delete", args=[self.card.pk]), - {} - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) - - -class PaymentMethodUpdateViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - self.card = Card.objects.create( - stripe_id="card_001", - customer=customer, - address_line_1_check="nothing", - address_zip_check="nothing", - country="US", - cvc_check="passed", - exp_month=1, - exp_year=2020, - funding="yes", - fingerprint="abc" - ) - - @patch("pinax.stripe.actions.sources.update_card") - def test_post(self, CreateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_update", args=[self.card.pk]), - { - "expMonth": 1, - "expYear": 2018 - } - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_payment_method_list")) - - @patch("pinax.stripe.actions.sources.update_card") - def test_post_invalid_form(self, CreateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_update", args=[self.card.pk]), - { - "expMonth": 13, - "expYear": 2014 - } - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context_data["form"].is_valid(), False) - - @patch("pinax.stripe.actions.sources.update_card") - def test_post_on_error(self, CreateMock): - CreateMock.side_effect = stripe.error.CardError("Bad card", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_payment_method_update", args=[self.card.pk]), - { - "expMonth": 1, - "expYear": 2018 - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) - - -class SubscriptionListViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - Subscription.objects.create( - stripe_id="sub_001", - customer=customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active" - ) - - def test_context(self): - self.client.login(username=self.user.username, password=self.password) - response = self.client.get( - reverse("pinax_stripe_subscription_list") - ) - self.assertTrue("subscription_list" in response.context_data) - self.assertEqual(response.context_data["subscription_list"].count(), 1) - self.assertEqual(response.context_data["subscription_list"][0].stripe_id, "sub_001") - - -class SubscriptionCreateViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - self.plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - - @patch("pinax.stripe.actions.subscriptions.create") - def test_post(self, CreateMock): - Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_create"), - { - "plan": self.plan.id - } - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_subscription_list")) - - @patch("pinax.stripe.actions.customers.create") - @patch("pinax.stripe.actions.subscriptions.create") - def test_post_no_prior_customer(self, CreateMock, CustomerCreateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_create"), - { - "plan": self.plan.id - } - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_subscription_list")) - self.assertTrue(CustomerCreateMock.called) - - @patch("pinax.stripe.actions.sources.create_card") - def test_post_on_error(self, CreateMock): - Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - CreateMock.side_effect = stripe.error.StripeError("Bad Mojo", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_create"), - { - "plan": self.plan.id - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) - - -class SubscriptionDeleteViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - self.subscription = Subscription.objects.create( - stripe_id="sub_001", - customer=customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active" - ) - - @patch("pinax.stripe.actions.subscriptions.cancel") - def test_post(self, CancelMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_delete", args=[self.subscription.pk]), - {} - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_subscription_list")) - - @patch("pinax.stripe.actions.subscriptions.cancel") - def test_post_on_error(self, CancelMock): - CancelMock.side_effect = stripe.error.StripeError("Bad Foo", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_delete", args=[self.subscription.pk]), - {} - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) - - -class SubscriptionUpdateViewTests(TestCase): - - def setUp(self): - self.password = "eldarion" - self.user = get_user_model().objects.create_user( - username="patrick", - password=self.password - ) - self.user.save() - customer = Customer.objects.create( - stripe_id="cus_1", - user=self.user - ) - plan = Plan.objects.create( - amount=10, - currency="usd", - interval="monthly", - interval_count=1, - name="Pro" - ) - self.subscription = Subscription.objects.create( - stripe_id="sub_001", - customer=customer, - plan=plan, - quantity=1, - start=timezone.now(), - status="active" - ) - - def test_get(self): - self.client.login(username=self.user.username, password=self.password) - response = self.client.get( - reverse("pinax_stripe_subscription_update", args=[self.subscription.pk]) - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("form" in response.context_data) - self.assertTrue(response.context_data["form"].initial["plan"], self.subscription.plan) - - @patch("pinax.stripe.actions.subscriptions.update") - def test_post(self, UpdateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_update", args=[self.subscription.pk]), - { - "plan": self.subscription.plan.id - } - ) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse("pinax_stripe_subscription_list")) - - @patch("pinax.stripe.actions.subscriptions.update") - def test_post_invalid(self, UpdateMock): - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_update", args=[self.subscription.pk]), - { - "plan": "not a real plan" - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context_data["form"].errors) > 0) - - @patch("pinax.stripe.actions.subscriptions.update") - def test_post_on_error(self, UpdateMock): - UpdateMock.side_effect = stripe.error.StripeError("Bad Foo", "Param", "CODE") - self.client.login(username=self.user.username, password=self.password) - response = self.client.post( - reverse("pinax_stripe_subscription_update", args=[self.subscription.pk]), - { - "plan": self.subscription.plan.id - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue("errors" in response.context_data) diff --git a/pinax/stripe/tests/test_webhooks.py b/pinax/stripe/tests/test_webhooks.py index 5c9bbd0d9..0deb5867c 100644 --- a/pinax/stripe/tests/test_webhooks.py +++ b/pinax/stripe/tests/test_webhooks.py @@ -1,7 +1,5 @@ -import decimal import json -from django.contrib.auth import get_user_model from django.dispatch import Signal from django.test import TestCase from django.test.client import Client @@ -10,32 +8,14 @@ import stripe from mock import patch -from . import ( - PLAN_CREATED_TEST_DATA, - TRANSFER_CREATED_TEST_DATA, - TRANSFER_PENDING_TEST_DATA -) from ..models import ( - Account, - Customer, Event, EventProcessingException, - Plan, - Transfer ) from ..webhooks import ( - AccountApplicationDeauthorizeWebhook, - AccountExternalAccountCreatedWebhook, - AccountUpdatedWebhook, - ChargeCapturedWebhook, - CustomerDeletedWebhook, - CustomerSourceCreatedWebhook, - CustomerSourceDeletedWebhook, - CustomerSubscriptionCreatedWebhook, - CustomerUpdatedWebhook, - InvoiceCreatedWebhook, Webhook, - registry + registry, + AccountExternalAccountCreatedWebhook ) try: @@ -125,13 +105,10 @@ def test_webhook_with_transfer_event(self, TransferMock, StripeEventMock): self.assertTrue(Event.objects.filter(kind="transfer.created").exists()) @patch("stripe.Event.retrieve") - @patch("stripe.Transfer.retrieve") - def test_webhook_associated_with_stripe_account(self, TransferMock, StripeEventMock): + def test_webhook_associated_with_stripe_account(self, StripeEventMock): connect_event_data = self.event_data.copy() - account = Account.objects.create(stripe_id="acc_XXX") - connect_event_data["account"] = account.stripe_id + connect_event_data["account"] = "acc_XXX" StripeEventMock.return_value.to_dict.return_value = connect_event_data - TransferMock.return_value = connect_event_data["data"]["object"] msg = json.dumps(connect_event_data) resp = Client().post( reverse("pinax_stripe_webhook"), @@ -141,12 +118,9 @@ def test_webhook_associated_with_stripe_account(self, TransferMock, StripeEventM self.assertEqual(resp.status_code, 200) self.assertTrue(Event.objects.filter(kind="transfer.created").exists()) self.assertEqual( - Event.objects.filter(kind="transfer.created").first().stripe_account, - account + Event.objects.filter(kind="transfer.created").first().account_id, + "acc_XXX" ) - self.assertEqual(TransferMock.call_args_list, [ - [("ach_XXXXXXXXXXXX",), {"stripe_account": "acc_XXX"}], - ]) def test_webhook_duplicate_event(self): data = {"id": 123} @@ -158,9 +132,7 @@ def test_webhook_duplicate_event(self): content_type="application/json" ) self.assertEqual(resp.status_code, 200) - dupe_event_exception = EventProcessingException.objects.get() - self.assertEqual(dupe_event_exception.message, "Duplicate event record") - self.assertEqual(str(dupe_event_exception.data), '{"id": 123}') + self.assertEqual(Event.objects.filter(stripe_id="123").count(), 1) def test_webhook_event_mismatch(self): event = Event(kind="account.updated") @@ -186,10 +158,9 @@ def signal_handler(sender, *args, **kwargs): webhook.name = "mismatch name" # Not sure how this ever happens due to the registry webhook.send_signal() - @patch("pinax.stripe.actions.customers.link_customer") @patch("pinax.stripe.webhooks.Webhook.validate") @patch("pinax.stripe.webhooks.Webhook.process_webhook") - def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock, LinkMock): + def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock): # note: we choose an event type for which we do no processing event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) ProcessWebhookMock.side_effect = stripe.error.StripeError("Message", "error") @@ -197,10 +168,9 @@ def test_process_exception_is_logged(self, ProcessWebhookMock, ValidateMock, Lin AccountExternalAccountCreatedWebhook(event).process() self.assertTrue(EventProcessingException.objects.filter(event=event).exists()) - @patch("pinax.stripe.actions.customers.link_customer") @patch("pinax.stripe.webhooks.Webhook.validate") @patch("pinax.stripe.webhooks.Webhook.process_webhook") - def test_process_exception_is_logged_non_stripeerror(self, ProcessWebhookMock, ValidateMock, LinkMock): + def test_process_exception_is_logged_non_stripeerror(self, ProcessWebhookMock, ValidateMock): # note: we choose an event type for which we do no processing event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) ProcessWebhookMock.side_effect = Exception("generic exception") @@ -208,428 +178,9 @@ def test_process_exception_is_logged_non_stripeerror(self, ProcessWebhookMock, V AccountExternalAccountCreatedWebhook(event).process() self.assertTrue(EventProcessingException.objects.filter(event=event).exists()) - @patch("pinax.stripe.actions.customers.link_customer") @patch("pinax.stripe.webhooks.Webhook.validate") - def test_process_return_none(self, ValidateMock, LinkMock): + def test_process_return_none(self, ValidateMock): # note: we choose an event type for which we do no processing event = Event.objects.create(kind="account.external_account.created", webhook_message={}, valid=True, processed=False) self.assertIsNone(AccountExternalAccountCreatedWebhook(event).process()) - -class ChargeWebhookTest(TestCase): - - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - def test_process_webhook(self, SyncMock, RetrieveMock): - event = Event.objects.create(kind=ChargeCapturedWebhook.name, webhook_message={}, valid=True, processed=False) - event.validated_message = dict(data=dict(object=dict(id=1))) - ChargeCapturedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - _, kwargs = RetrieveMock.call_args - self.assertEqual(kwargs["expand"], ["balance_transaction"]) - self.assertEqual(kwargs["stripe_account"], None) - - @patch("stripe.Charge.retrieve") - @patch("pinax.stripe.actions.charges.sync_charge_from_stripe_data") - def test_process_webhook_connect(self, SyncMock, RetrieveMock): - account = Account.objects.create(stripe_id="acc_A") - event = Event.objects.create(kind=ChargeCapturedWebhook.name, webhook_message={}, valid=True, processed=False, stripe_account=account) - event.validated_message = dict(data=dict(object=dict(id=1))) - ChargeCapturedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - _, kwargs = RetrieveMock.call_args - self.assertEqual(kwargs["expand"], ["balance_transaction"]) - self.assertEqual(kwargs["stripe_account"], "acc_A") - - -class CustomerDeletedWebhookTest(TestCase): - - def test_process_webhook_without_linked_customer(self): - event = Event.objects.create(kind=CustomerDeletedWebhook.name, webhook_message={}, valid=True, processed=False) - CustomerDeletedWebhook(event).process_webhook() - - def test_process_webhook_with_linked_customer(self): - User = get_user_model() - customer = Customer.objects.create(user=User.objects.create()) - self.assertIsNotNone(customer.user) - event = Event.objects.create(kind=CustomerDeletedWebhook.name, webhook_message={}, valid=True, processed=False, customer=customer) - CustomerDeletedWebhook(event).process_webhook() - self.assertIsNone(customer.user) - - -class CustomerUpdatedWebhookTest(TestCase): - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_process_webhook_without_customer(self, SyncMock): - event = Event.objects.create(kind=CustomerUpdatedWebhook.name, webhook_message={}, valid=True, processed=False) - CustomerUpdatedWebhook(event).process_webhook() - self.assertEqual(SyncMock.call_count, 0) - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_process_webhook_without_customer_with_data(self, SyncMock): - event = Event.objects.create(kind=CustomerUpdatedWebhook.name, webhook_message={}, valid=True, processed=False) - obj = object() - event.validated_message = dict(data=dict(object=obj)) - CustomerUpdatedWebhook(event).process_webhook() - self.assertEqual(SyncMock.call_count, 0) - - @patch("pinax.stripe.actions.customers.sync_customer") - def test_process_webhook_with_customer_with_data(self, SyncMock): - customer = Customer.objects.create() - event = Event.objects.create(kind=CustomerUpdatedWebhook.name, customer=customer, webhook_message={}, valid=True, processed=False) - obj = object() - event.validated_message = dict(data=dict(object=obj)) - CustomerUpdatedWebhook(event).process_webhook() - self.assertEqual(SyncMock.call_count, 1) - self.assertIs(SyncMock.call_args[0][0], customer) - self.assertIs(SyncMock.call_args[0][1], obj) - - -class CustomerSourceCreatedWebhookTest(TestCase): - - @patch("pinax.stripe.actions.sources.sync_payment_source_from_stripe_data") - def test_process_webhook(self, SyncMock): - event = Event.objects.create(kind=CustomerSourceCreatedWebhook.name, webhook_message={}, valid=True, processed=False) - event.validated_message = dict(data=dict(object=dict())) - CustomerSourceCreatedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - - -class CustomerSourceDeletedWebhookTest(TestCase): - - @patch("pinax.stripe.actions.sources.delete_card_object") - def test_process_webhook(self, SyncMock): - event = Event.objects.create(kind=CustomerSourceDeletedWebhook.name, webhook_message={}, valid=True, processed=False) - event.validated_message = dict(data=dict(object=dict(id=1))) - CustomerSourceDeletedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - - -class PlanCreatedWebhookTest(TestCase): - - @patch("stripe.Event.retrieve") - def test_plan_created(self, EventMock): - ev = EventMock() - ev.to_dict.return_value = PLAN_CREATED_TEST_DATA - event = Event.objects.create( - stripe_id=PLAN_CREATED_TEST_DATA["id"], - kind="plan.created", - livemode=True, - webhook_message=PLAN_CREATED_TEST_DATA, - validated_message=PLAN_CREATED_TEST_DATA, - valid=True - ) - registry.get(event.kind)(event).process() - self.assertEqual(Plan.objects.all().count(), 1) - - -class PlanUpdatedWebhookTest(TestCase): - - @patch("stripe.Event.retrieve") - def test_plan_created(self, EventMock): - Plan.objects.create( - stripe_id="gold1", - name="Gold Plan", - interval="month", - interval_count=1, - amount=decimal.Decimal("9.99") - ) - ev = EventMock() - ev.to_dict.return_value = PLAN_CREATED_TEST_DATA - event = Event.objects.create( - stripe_id=PLAN_CREATED_TEST_DATA["id"], - kind="plan.updated", - livemode=True, - webhook_message=PLAN_CREATED_TEST_DATA, - validated_message=PLAN_CREATED_TEST_DATA, - valid=True - ) - registry.get(event.kind)(event).process() - plan = Plan.objects.get(stripe_id="gold1") - self.assertEqual(plan.name, PLAN_CREATED_TEST_DATA["data"]["object"]["name"]) - - -class CustomerSubscriptionCreatedWebhookTest(TestCase): - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.customers.sync_customer") - def test_process_webhook(self, SyncMock, SubSyncMock): - event = Event.objects.create( - kind=CustomerSubscriptionCreatedWebhook.name, - customer=Customer.objects.create(), - validated_message={"data": {"object": {}}}, - valid=True, - processed=False) - CustomerSubscriptionCreatedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - self.assertTrue(SubSyncMock.called) - - @patch("pinax.stripe.actions.subscriptions.sync_subscription_from_stripe_data") - @patch("pinax.stripe.actions.customers.sync_customer") - def test_process_webhook_no_customer(self, SyncMock, SubSyncMock): - event = Event.objects.create( - kind=CustomerSubscriptionCreatedWebhook.name, - validated_message={"data": {"object": {}}}, - valid=True, - processed=False) - CustomerSubscriptionCreatedWebhook(event).process_webhook() - self.assertFalse(SyncMock.called) - self.assertTrue(SubSyncMock.called) - - -class CustomerSubscriptionUpdatedWebhookTest(TestCase): - - WEBHOOK_MESSAGE_DATA = { - "object": {"livemode": False} - } - - VALIDATED_MESSAGE_DATA = { - "previous_attributes": {"days_until_due": 30, "billing": "send_invoice"}, - "object": {"livemode": False} - } - - VALIDATED_MESSAGE_DATA_NOT_VALID = { - "previous_attributes": {"days_until_due": 30, "billing": "send_invoice"}, - "object": {"livemode": True} - } - - def test_is_event_valid_yes(self): - self.assertTrue(Webhook.is_event_valid(self.WEBHOOK_MESSAGE_DATA, self.VALIDATED_MESSAGE_DATA)) - - def test_is_event_valid_no(self): - self.assertFalse(Webhook.is_event_valid(self.WEBHOOK_MESSAGE_DATA, self.VALIDATED_MESSAGE_DATA_NOT_VALID)) - - -class InvoiceCreatedWebhookTest(TestCase): - - @patch("pinax.stripe.actions.invoices.sync_invoice_from_stripe_data") - def test_process_webhook(self, SyncMock): - event = Event.objects.create(kind=InvoiceCreatedWebhook.name, webhook_message={}, valid=True, processed=False) - event.validated_message = dict(data=dict(object=dict(id=1))) - InvoiceCreatedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - - -class TestTransferWebhooks(TestCase): - - @patch("stripe.Event.retrieve") - @patch("stripe.Transfer.retrieve") - def test_transfer_created(self, TransferMock, EventMock): - ev = EventMock() - ev.to_dict.return_value = TRANSFER_CREATED_TEST_DATA - TransferMock.return_value = TRANSFER_CREATED_TEST_DATA["data"]["object"] - event = Event.objects.create( - stripe_id=TRANSFER_CREATED_TEST_DATA["id"], - kind="transfer.created", - livemode=True, - webhook_message=TRANSFER_CREATED_TEST_DATA, - validated_message=TRANSFER_CREATED_TEST_DATA, - valid=True - ) - registry.get(event.kind)(event).process() - transfer = Transfer.objects.get(stripe_id="tr_XXXXXXXXXXXX") - self.assertEqual(transfer.amount, decimal.Decimal("4.55")) - self.assertEqual(transfer.status, "paid") - - @patch("stripe.Event.retrieve") - @patch("stripe.Transfer.retrieve") - def test_transfer_pending_create(self, TransferMock, EventMock): - ev = EventMock() - ev.to_dict.return_value = TRANSFER_PENDING_TEST_DATA - TransferMock.return_value = TRANSFER_PENDING_TEST_DATA["data"]["object"] - event = Event.objects.create( - stripe_id=TRANSFER_PENDING_TEST_DATA["id"], - kind="transfer.created", - livemode=True, - webhook_message=TRANSFER_PENDING_TEST_DATA, - validated_message=TRANSFER_PENDING_TEST_DATA, - valid=True - ) - registry.get(event.kind)(event).process() - transfer = Transfer.objects.get(stripe_id="tr_adlkj2l3kj23") - self.assertEqual(transfer.amount, decimal.Decimal("9.41")) - self.assertEqual(transfer.status, "pending") - - @patch("stripe.Event.retrieve") - @patch("stripe.Transfer.retrieve") - def test_transfer_paid_updates_existing_record(self, TransferMock, EventMock): - ev = EventMock() - ev.to_dict.return_value = TRANSFER_CREATED_TEST_DATA - TransferMock.return_value = TRANSFER_CREATED_TEST_DATA["data"]["object"] - event = Event.objects.create( - stripe_id=TRANSFER_CREATED_TEST_DATA["id"], - kind="transfer.created", - livemode=True, - webhook_message=TRANSFER_CREATED_TEST_DATA, - validated_message=TRANSFER_CREATED_TEST_DATA, - valid=True - ) - registry.get(event.kind)(event).process() - data = { - "created": 1364658818, - "data": { - "object": { - "account": { - "bank_name": "BANK OF AMERICA, N.A.", - "country": "US", - "last4": "9999", - "object": "bank_account" - }, - "amount": 455, - "currency": "usd", - "date": 1364601600, - "description": "STRIPE TRANSFER", - "fee": 0, - "fee_details": [], - "id": "tr_XXXXXXXXXXXX", - "livemode": True, - "object": "transfer", - "other_transfers": [], - "status": "paid", - "summary": { - "adjustment_count": 0, - "adjustment_fee_details": [], - "adjustment_fees": 0, - "adjustment_gross": 0, - "charge_count": 1, - "charge_fee_details": [{ - "amount": 45, - "application": None, - "currency": "usd", - "description": None, - "type": "stripe_fee" - }], - "charge_fees": 45, - "charge_gross": 500, - "collected_fee_count": 0, - "collected_fee_gross": 0, - "collected_fee_refund_count": 0, - "collected_fee_refund_gross": 0, - "currency": "usd", - "net": 455, - "refund_count": 0, - "refund_fee_details": [], - "refund_fees": 0, - "refund_gross": 0, - "validation_count": 0, - "validation_fees": 0 - }, - "transactions": { - "count": 1, - "data": [{ - "amount": 500, - "created": 1364064631, - "description": None, - "fee": 45, - "fee_details": [{ - "amount": 45, - "application": None, - "currency": "usd", - "description": "Stripe processing fees", - "type": "stripe_fee" - }], - "id": "ch_XXXXXXXXXX", - "net": 455, - "type": "charge" - }], - "object": "list", - "url": "/v1/transfers/XX/transactions" - } - } - }, - "id": "evt_YYYYYYYYYYYY", - "livemode": True, - "object": "event", - "pending_webhooks": 1, - "type": "transfer.paid" - } - paid_event = Event.objects.create( - stripe_id=data["id"], - kind="transfer.paid", - livemode=True, - webhook_message=data, - validated_message=data, - valid=True - ) - registry.get(paid_event.kind)(paid_event).process() - transfer = Transfer.objects.get(stripe_id="tr_XXXXXXXXXXXX") - self.assertEqual(transfer.status, "paid") - - -class AccountWebhookTest(TestCase): - - @classmethod - def setUpClass(cls): - super(AccountWebhookTest, cls).setUpClass() - cls.account = Account.objects.create(stripe_id="acc_aa") - - @patch("stripe.Account.retrieve") - @patch("pinax.stripe.actions.accounts.sync_account_from_stripe_data") - def test_process_webhook(self, SyncMock, RetrieveMock): - event = Event.objects.create( - kind=AccountUpdatedWebhook.name, - webhook_message={}, - valid=True, - processed=False - ) - event.validated_message = dict(data=dict(object=dict(id=1))) - AccountUpdatedWebhook(event).process_webhook() - self.assertTrue(SyncMock.called) - - @patch("stripe.Event.retrieve") - def test_process_deauthorize(self, RetrieveMock): - data = {"data": {"object": {"id": "evt_001"}}, - "account": self.account.stripe_id} - event = Event.objects.create( - kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message=data, - ) - RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************abcd' does not have access to account 'acc_aa' (or that account does not exist). Application access may have been revoked.") - AccountApplicationDeauthorizeWebhook(event).process() - self.assertTrue(event.valid) - self.assertTrue(event.processed) - self.account.refresh_from_db() - self.assertFalse(self.account.authorized) - - @patch("stripe.Event.retrieve") - def test_process_deauthorize_fake_response(self, RetrieveMock): - data = {"data": {"object": {"id": "evt_001"}}, - "account": self.account.stripe_id} - event = Event.objects.create( - kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message=data, - ) - RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************ABCD' does not have access to account 'acc_aa' (or that account does not exist). Application access may have been revoked.") - with self.assertRaises(stripe.error.PermissionError): - AccountApplicationDeauthorizeWebhook(event).process() - - @patch("stripe.Event.retrieve") - def test_process_deauthorize_with_delete_account(self, RetrieveMock): - data = {"data": {"object": {"id": "evt_002"}}, - "account": "acct_bb"} - event = Event.objects.create( - kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message=data, - ) - RetrieveMock.side_effect = stripe.error.PermissionError( - "The provided key 'sk_test_********************abcd' does not have access to account 'acct_bb' (or that account does not exist). Application access may have been revoked.") - AccountApplicationDeauthorizeWebhook(event).process() - self.assertTrue(event.valid) - self.assertTrue(event.processed) - self.assertIsNone(event.stripe_account) - - @patch("stripe.Event.retrieve") - def test_process_deauthorize_without_account(self, RetrieveMock): - data = {"data": {"object": {"id": "evt_001"}}} - event = Event.objects.create( - kind=AccountApplicationDeauthorizeWebhook.name, - webhook_message=data, - ) - RetrieveMock.return_value.to_dict.return_value = data - AccountApplicationDeauthorizeWebhook(event).process() - self.assertTrue(event.valid) - self.assertTrue(event.processed) - self.account.refresh_from_db() - self.assertTrue(self.account.authorized) diff --git a/pinax/stripe/urls.py b/pinax/stripe/urls.py index ec0db5f14..68c548870 100644 --- a/pinax/stripe/urls.py +++ b/pinax/stripe/urls.py @@ -1,30 +1,7 @@ from django.conf.urls import url -from .views import ( - InvoiceListView, - PaymentMethodCreateView, - PaymentMethodDeleteView, - PaymentMethodListView, - PaymentMethodUpdateView, - SubscriptionCreateView, - SubscriptionDeleteView, - SubscriptionListView, - SubscriptionUpdateView, - Webhook -) +from .views import Webhook urlpatterns = [ - url(r"^subscriptions/$", SubscriptionListView.as_view(), name="pinax_stripe_subscription_list"), - url(r"^subscriptions/create/$", SubscriptionCreateView.as_view(), name="pinax_stripe_subscription_create"), - url(r"^subscriptions/(?P\d+)/delete/$", SubscriptionDeleteView.as_view(), name="pinax_stripe_subscription_delete"), - url(r"^subscriptions/(?P\d+)/update/$", SubscriptionUpdateView.as_view(), name="pinax_stripe_subscription_update"), - - url(r"^payment-methods/$", PaymentMethodListView.as_view(), name="pinax_stripe_payment_method_list"), - url(r"^payment-methods/create/$", PaymentMethodCreateView.as_view(), name="pinax_stripe_payment_method_create"), - url(r"^payment-methods/(?P\d+)/delete/$", PaymentMethodDeleteView.as_view(), name="pinax_stripe_payment_method_delete"), - url(r"^payment-methods/(?P\d+)/update/$", PaymentMethodUpdateView.as_view(), name="pinax_stripe_payment_method_update"), - - url(r"^invoices/$", InvoiceListView.as_view(), name="pinax_stripe_invoice_list"), - url(r"^webhook/$", Webhook.as_view(), name="pinax_stripe_webhook"), ] diff --git a/pinax/stripe/views.py b/pinax/stripe/views.py index df6ba27b0..937a89ac1 100644 --- a/pinax/stripe/views.py +++ b/pinax/stripe/views.py @@ -1,212 +1,40 @@ import json from django.http import HttpResponse -from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.utils.encoding import smart_str from django.views.decorators.csrf import csrf_exempt -from django.views.generic import ( - DetailView, - FormView, - ListView, - TemplateView, - View -) -from django.views.generic.edit import FormMixin +from django.views.generic import View -import stripe - -from .actions import customers, events, exceptions, sources, subscriptions -from .conf import settings -from .forms import PaymentMethodForm, PlanForm -from .mixins import CustomerMixin, LoginRequiredMixin, PaymentsContextMixin -from .models import Card, Event, Invoice, Subscription - - -class InvoiceListView(LoginRequiredMixin, CustomerMixin, ListView): - model = Invoice - context_object_name = "invoice_list" - template_name = "pinax/stripe/invoice_list.html" - - def get_queryset(self): - return super(InvoiceListView, self).get_queryset().order_by("date") - - -class PaymentMethodListView(LoginRequiredMixin, CustomerMixin, ListView): - model = Card - context_object_name = "payment_method_list" - template_name = "pinax/stripe/paymentmethod_list.html" - - def get_queryset(self): - return super(PaymentMethodListView, self).get_queryset().order_by("created_at") - - -class PaymentMethodCreateView(LoginRequiredMixin, CustomerMixin, PaymentsContextMixin, TemplateView): - model = Card - template_name = "pinax/stripe/paymentmethod_create.html" - - def create_card(self, stripe_token): - sources.create_card(self.customer, token=stripe_token) - - def post(self, request, *args, **kwargs): - try: - self.create_card(request.POST.get("stripeToken")) - return redirect("pinax_stripe_payment_method_list") - except stripe.error.CardError as e: - return self.render_to_response(self.get_context_data(errors=smart_str(e))) - - -class PaymentMethodDeleteView(LoginRequiredMixin, CustomerMixin, DetailView): - model = Card - template_name = "pinax/stripe/paymentmethod_delete.html" - - def delete_card(self, stripe_id): - sources.delete_card(self.customer, stripe_id) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - try: - self.delete_card(self.object.stripe_id) - return redirect("pinax_stripe_payment_method_list") - except stripe.error.CardError as e: - return self.render_to_response(self.get_context_data(errors=smart_str(e))) - - -class PaymentMethodUpdateView(LoginRequiredMixin, CustomerMixin, PaymentsContextMixin, FormMixin, DetailView): - model = Card - form_class = PaymentMethodForm - template_name = "pinax/stripe/paymentmethod_update.html" - - def update_card(self, exp_month, exp_year): - sources.update_card(self.customer, self.object.stripe_id, exp_month=exp_month, exp_year=exp_year) - - def form_valid(self, form): - try: - self.update_card(form.cleaned_data["expMonth"], form.cleaned_data["expYear"]) - return redirect("pinax_stripe_payment_method_list") - except stripe.error.CardError as e: - return self.render_to_response(self.get_context_data(errors=smart_str(e))) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form(form_class=self.form_class) - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - - -class SubscriptionListView(LoginRequiredMixin, CustomerMixin, ListView): - model = Subscription - context_object_name = "subscription_list" - template_name = "pinax/stripe/subscription_list.html" - - def get_queryset(self): - return super(SubscriptionListView, self).get_queryset().order_by("created_at") - - -class SubscriptionCreateView(LoginRequiredMixin, PaymentsContextMixin, CustomerMixin, FormView): - template_name = "pinax/stripe/subscription_create.html" - form_class = PlanForm - - @property - def tax_percent(self): - return settings.PINAX_STRIPE_SUBSCRIPTION_TAX_PERCENT - - def set_customer(self): - if self.customer is None: - self._customer = customers.create(self.request.user) - - def subscribe(self, customer, plan, token): - subscriptions.create(customer, plan, token=token, tax_percent=self.tax_percent) - - def form_valid(self, form): - self.set_customer() - try: - self.subscribe(self.customer, plan=form.cleaned_data["plan"], token=self.request.POST.get("stripeToken")) - return redirect("pinax_stripe_subscription_list") - except stripe.error.StripeError as e: - return self.render_to_response(self.get_context_data(form=form, errors=smart_str(e))) - - -class SubscriptionDeleteView(LoginRequiredMixin, CustomerMixin, DetailView): - model = Subscription - template_name = "pinax/stripe/subscription_delete.html" - - def cancel(self): - subscriptions.cancel(self.object) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - try: - self.cancel() - return redirect("pinax_stripe_subscription_list") - except stripe.error.StripeError as e: - return self.render_to_response(self.get_context_data(errors=smart_str(e))) - - -class SubscriptionUpdateView(LoginRequiredMixin, CustomerMixin, FormMixin, DetailView): - model = Subscription - form_class = PlanForm - template_name = "pinax/stripe/subscription_update.html" - - @property - def current_plan(self): - if not hasattr(self, "_current_plan"): - self._current_plan = self.object.plan - return self._current_plan - - def get_context_data(self, **kwargs): - context = super(SubscriptionUpdateView, self).get_context_data(**kwargs) - context.update({ - "form": self.get_form(form_class=self.form_class) - }) - return context - - def update_subscription(self, plan_id): - subscriptions.update(self.object, plan_id) - - def get_initial(self): - initial = super(SubscriptionUpdateView, self).get_initial() - initial.update({ - "plan": self.current_plan - }) - return initial - - def form_valid(self, form): - try: - self.update_subscription(form.cleaned_data["plan"]) - return redirect("pinax_stripe_subscription_list") - except stripe.error.StripeError as e: - return self.render_to_response(self.get_context_data(form=form, errors=smart_str(e))) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form(form_class=self.form_class) - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) +from .models import Event +from .webhooks import registry class Webhook(View): + def add_event(self, data): + kind = data["type"] + event = Event.objects.create( + account_id=data.get("account", ""), + stripe_id=data["id"], + kind=kind, + livemode=data["livemode"], + webhook_message=data, + api_version=data["api_version"], + request=data.get("request", {}).get("id", ""), + pending_webhooks=data["pending_webhooks"] + ) + WebhookClass = registry.get(kind) + if WebhookClass is not None: + webhook = WebhookClass(event) + webhook.process() + @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): return super(Webhook, self).dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): - body = smart_str(self.request.body) - data = json.loads(body) - event = Event.objects.filter(stripe_id=data["id"]).first() - if event: - exceptions.log_exception(body, "Duplicate event record", event=event) - else: - events.add_event( - stripe_id=data["id"], - kind=data["type"], - livemode=data["livemode"], - api_version=data["api_version"], - message=data - ) + data = json.loads(smart_str(self.request.body)) + if not Event.objects.filter(stripe_id=data["id"]).exists(): + self.add_event(data) return HttpResponse() diff --git a/pinax/stripe/webhooks.py b/pinax/stripe/webhooks.py index d40fea2d9..f5846d21b 100644 --- a/pinax/stripe/webhooks.py +++ b/pinax/stripe/webhooks.py @@ -1,4 +1,6 @@ import json +import sys +import traceback from django.dispatch import Signal @@ -6,17 +8,6 @@ from six import with_metaclass from . import models -from .actions import ( - accounts, - charges, - customers, - exceptions, - invoices, - plans, - sources, - subscriptions, - transfers -) from .conf import settings from .utils import obfuscate_secret_key @@ -87,12 +78,10 @@ def validate(self): For Connect accounts we must fetch the event using the `stripe_account` parameter. """ - self.stripe_account = models.Account.objects.filter( - stripe_id=self.event.webhook_message.get("account")).first() - self.event.stripe_account = self.stripe_account + self.stripe_account = self.event.webhook_message.get("account", None) evt = stripe.Event.retrieve( self.event.stripe_id, - stripe_account=getattr(self.stripe_account, "stripe_id", None) + stripe_account=self.stripe_account ) self.event.validated_message = json.loads( json.dumps( @@ -116,6 +105,16 @@ def send_signal(self): if signal: return signal.send(sender=self.__class__, event=self.event) + def log_exception(self, data, exception): + info = sys.exc_info() + info_formatted = "".join(traceback.format_exception(*info)) if info[1] is not None else "" + models.EventProcessingException.objects.create( + event=self.event, + data=data or "", + message=str(exception), + traceback=info_formatted + ) + def process(self): if self.event.processed: return @@ -124,7 +123,6 @@ def process(self): return try: - customers.link_customer(self.event) self.process_webhook() self.send_signal() self.event.processed = True @@ -133,22 +131,14 @@ def process(self): data = None if isinstance(e, stripe.error.StripeError): data = e.http_body - exceptions.log_exception(data=data, exception=e, event=self.event) + self.log_exception(data=data, exception=e) raise e def process_webhook(self): return -class AccountWebhook(Webhook): - - def process_webhook(self): - accounts.sync_account_from_stripe_data( - stripe.Account.retrieve(self.event.message["data"]["object"]["id"]) - ) - - -class AccountUpdatedWebhook(AccountWebhook): +class AccountUpdatedWebhook(Webhook): name = "account.updated" description = "Occurs whenever an account status or property has changed." @@ -173,16 +163,13 @@ def validate(self): super(AccountApplicationDeauthorizeWebhook, self).validate() except stripe.error.PermissionError as exc: if self.stripe_account: - stripe_account_id = self.stripe_account.stripe_id - if not(stripe_account_id in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): + if not(self.stripe_account in str(exc) and obfuscate_secret_key(settings.PINAX_STRIPE_SECRET_KEY) in str(exc)): raise exc self.event.valid = True self.event.validated_message = self.event.webhook_message - def process_webhook(self): - if self.stripe_account is not None: - accounts.deauthorize(self.stripe_account) +# @@@ with signals not sure we need all these class AccountExternalAccountCreatedWebhook(Webhook): name = "account.external_account.created" @@ -239,61 +226,52 @@ class BitcoinReceiverTransactionCreatedWebhook(Webhook): description = "Occurs whenever bitcoin is pushed to a receiver." -class ChargeWebhook(Webhook): - - def process_webhook(self): - charges.sync_charge( - self.event.message["data"]["object"]["id"], - stripe_account=self.event.stripe_account_stripe_id, - ) - - -class ChargeCapturedWebhook(ChargeWebhook): +class ChargeCapturedWebhook(Webhook): name = "charge.captured" description = "Occurs whenever a previously uncaptured charge is captured." -class ChargeFailedWebhook(ChargeWebhook): +class ChargeFailedWebhook(Webhook): name = "charge.failed" description = "Occurs whenever a failed charge attempt occurs." -class ChargeRefundedWebhook(ChargeWebhook): +class ChargeRefundedWebhook(Webhook): name = "charge.refunded" description = "Occurs whenever a charge is refunded, including partial refunds." -class ChargeSucceededWebhook(ChargeWebhook): +class ChargeSucceededWebhook(Webhook): name = "charge.succeeded" description = "Occurs whenever a new charge is created and is successful." -class ChargeUpdatedWebhook(ChargeWebhook): +class ChargeUpdatedWebhook(Webhook): name = "charge.updated" description = "Occurs whenever a charge description or metadata is updated." -class ChargeDisputeClosedWebhook(ChargeWebhook): +class ChargeDisputeClosedWebhook(Webhook): name = "charge.dispute.closed" description = "Occurs when the dispute is resolved and the dispute status changes to won or lost." -class ChargeDisputeCreatedWebhook(ChargeWebhook): +class ChargeDisputeCreatedWebhook(Webhook): name = "charge.dispute.created" description = "Occurs whenever a customer disputes a charge with their bank (chargeback)." -class ChargeDisputeFundsReinstatedWebhook(ChargeWebhook): +class ChargeDisputeFundsReinstatedWebhook(Webhook): name = "charge.dispute.funds_reinstated" description = "Occurs when funds are reinstated to your account after a dispute is won." -class ChargeDisputeFundsWithdrawnWebhook(ChargeWebhook): +class ChargeDisputeFundsWithdrawnWebhook(Webhook): name = "charge.dispute.funds_withdrawn" description = "Occurs when funds are removed from your account due to a dispute." -class ChargeDisputeUpdatedWebhook(ChargeWebhook): +class ChargeDisputeUpdatedWebhook(Webhook): name = "charge.dispute.updated" description = "Occurs when the dispute is updated (usually with evidence)." @@ -322,20 +300,11 @@ class CustomerDeletedWebhook(Webhook): name = "customer.deleted" description = "Occurs whenever a customer is deleted." - def process_webhook(self): - if self.event.customer: - customers.purge_local(self.event.customer) - class CustomerUpdatedWebhook(Webhook): name = "customer.updated" description = "Occurs whenever any property of a customer changes." - def process_webhook(self): - if self.event.customer: - cu = self.event.message["data"]["object"] - customers.sync_customer(self.event.customer, cu) - class CustomerDiscountCreatedWebhook(Webhook): name = "customer.discount.created" @@ -352,91 +321,57 @@ class CustomerDiscountUpdatedWebhook(Webhook): description = "Occurs whenever a customer is switched from one coupon to another." -class CustomerSourceWebhook(Webhook): - - def process_webhook(self): - sources.sync_payment_source_from_stripe_data( - self.event.customer, - self.event.validated_message["data"]["object"] - ) - - -class CustomerSourceCreatedWebhook(CustomerSourceWebhook): +class CustomerSourceCreatedWebhook(Webhook): name = "customer.source.created" description = "Occurs whenever a new source is created for the customer." -class CustomerSourceDeletedWebhook(CustomerSourceWebhook): +class CustomerSourceDeletedWebhook(Webhook): name = "customer.source.deleted" description = "Occurs whenever a source is removed from a customer." - def process_webhook(self): - sources.delete_card_object(self.event.validated_message["data"]["object"]["id"]) - -class CustomerSourceUpdatedWebhook(CustomerSourceWebhook): +class CustomerSourceUpdatedWebhook(Webhook): name = "customer.source.updated" description = "Occurs whenever a source's details are changed." -class CustomerSubscriptionWebhook(Webhook): - - def process_webhook(self): - if self.event.validated_message: - subscriptions.sync_subscription_from_stripe_data( - self.event.customer, - self.event.validated_message["data"]["object"], - ) - - if self.event.customer: - customers.sync_customer(self.event.customer) - - -class CustomerSubscriptionCreatedWebhook(CustomerSubscriptionWebhook): +class CustomerSubscriptionCreatedWebhook(Webhook): name = "customer.subscription.created" description = "Occurs whenever a customer with no subscription is signed up for a plan." -class CustomerSubscriptionDeletedWebhook(CustomerSubscriptionWebhook): +class CustomerSubscriptionDeletedWebhook(Webhook): name = "customer.subscription.deleted" description = "Occurs whenever a customer ends their subscription." -class CustomerSubscriptionTrialWillEndWebhook(CustomerSubscriptionWebhook): +class CustomerSubscriptionTrialWillEndWebhook(Webhook): name = "customer.subscription.trial_will_end" description = "Occurs three days before the trial period of a subscription is scheduled to end." -class CustomerSubscriptionUpdatedWebhook(CustomerSubscriptionWebhook): +class CustomerSubscriptionUpdatedWebhook(Webhook): name = "customer.subscription.updated" description = "Occurs whenever a subscription changes. Examples would include switching from one plan to another, or switching status from trial to active." -class InvoiceWebhook(Webhook): - - def process_webhook(self): - invoices.sync_invoice_from_stripe_data( - self.event.validated_message["data"]["object"], - send_receipt=settings.PINAX_STRIPE_SEND_EMAIL_RECEIPTS - ) - - -class InvoiceCreatedWebhook(InvoiceWebhook): +class InvoiceCreatedWebhook(Webhook): name = "invoice.created" description = "Occurs whenever a new invoice is created. If you are using webhooks, Stripe will wait one hour after they have all succeeded to attempt to pay the invoice; the only exception here is on the first invoice, which gets created and paid immediately when you subscribe a customer to a plan. If your webhooks do not all respond successfully, Stripe will continue retrying the webhooks every hour and will not attempt to pay the invoice. After 3 days, Stripe will attempt to pay the invoice regardless of whether or not your webhooks have succeeded. See how to respond to a webhook." -class InvoicePaymentFailedWebhook(InvoiceWebhook): +class InvoicePaymentFailedWebhook(Webhook): name = "invoice.payment_failed" description = "Occurs whenever an invoice attempts to be paid, and the payment fails. This can occur either due to a declined payment, or because the customer has no active card. A particular case of note is that if a customer with no active card reaches the end of its free trial, an invoice.payment_failed notification will occur." -class InvoicePaymentSucceededWebhook(InvoiceWebhook): +class InvoicePaymentSucceededWebhook(Webhook): name = "invoice.payment_succeeded" description = "Occurs whenever an invoice attempts to be paid, and the payment succeeds." -class InvoiceUpdatedWebhook(InvoiceWebhook): +class InvoiceUpdatedWebhook(Webhook): name = "invoice.updated" description = "Occurs whenever an invoice changes (for example, the amount could change)." @@ -481,13 +416,7 @@ class PaymentCreatedWebhook(Webhook): description = "A payment has been received by a Connect account via Transfer from the platform account." -class PlanWebhook(Webhook): - - def process_webhook(self): - plans.sync_plan(self.event.message["data"]["object"], self.event) - - -class PlanCreatedWebhook(PlanWebhook): +class PlanCreatedWebhook(Webhook): name = "plan.created" description = "Occurs whenever a plan is created." @@ -497,7 +426,7 @@ class PlanDeletedWebhook(Webhook): description = "Occurs whenever a plan is deleted." -class PlanUpdatedWebhook(PlanWebhook): +class PlanUpdatedWebhook(Webhook): name = "plan.updated" description = "Occurs whenever a plan is updated." @@ -537,39 +466,27 @@ class SKUUpdatedWebhook(Webhook): description = "Occurs whenever a SKU is updated." -class TransferWebhook(Webhook): - - def process_webhook(self): - transfers.sync_transfer( - stripe.Transfer.retrieve( - self.event.message["data"]["object"]["id"], - stripe_account=self.event.stripe_account_stripe_id, - ), - self.event - ) - - -class TransferCreatedWebhook(TransferWebhook): +class TransferCreatedWebhook(Webhook): name = "transfer.created" description = "Occurs whenever a new transfer is created." -class TransferFailedWebhook(TransferWebhook): +class TransferFailedWebhook(Webhook): name = "transfer.failed" description = "Occurs whenever Stripe attempts to send a transfer and that transfer fails." -class TransferPaidWebhook(TransferWebhook): +class TransferPaidWebhook(Webhook): name = "transfer.paid" description = "Occurs whenever a sent transfer is expected to be available in the destination bank account. If the transfer failed, a transfer.failed webhook will additionally be sent at a later time. Note to Connect users: this event is only created for transfers from your connected Stripe accounts to their bank accounts, not for transfers to the connected accounts themselves." -class TransferReversedWebhook(TransferWebhook): +class TransferReversedWebhook(Webhook): name = "transfer.reversed" description = "Occurs whenever a transfer is reversed, including partial reversals." -class TransferUpdatedWebhook(TransferWebhook): +class TransferUpdatedWebhook(Webhook): name = "transfer.updated" description = "Occurs whenever the description or metadata of a transfer is updated."