diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..489ab2e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include zuora/*.wsdl diff --git a/pip-requirements.txt b/pip-requirements.txt deleted file mode 100644 index d61e8cd..0000000 --- a/pip-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Suds, patched version of 0.3.6 by KCal. (Suds 0.3.6 through 0.4 have a bug around suds/sax/document.py) -#-e git+https://github.com/kevincal/suds-patched@f9869d36fac3077da3b1351eb169518bbdd92ac3#egg=suds-dev diff --git a/setup.py b/setup.py index d4730d6..276bd79 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,16 @@ setupArgs = { 'name': 'zuora', - 'version': '1.0.0.8', + 'version': '1.0.16', 'author': 'MapMyFitness', 'author_email': 'brandon.fredericks@mapmyfitness.com', 'url': 'http://github.com/mapmyfitness/python-zuora', 'description': 'Zuora client library.', - 'packages': ['zuora'], + 'packages': [ + 'zuora', + 'zuora.rest_wrapper', + ], + 'package_data': {'zuora': ['./*.wsdl']}, } try: @@ -34,6 +38,7 @@ def run(self): setupArgs.update({ 'tests_require': ['pytest'], 'cmdclass': {'test': TestRunner}, + 'install_requires': ['httplib2', 'suds-jurko==0.6'], #'install_requires': ['suds >= 0.4', 'python-requests'], Worrying about getting dependency chain right later 'zip_safe': False, }) diff --git a/zuora/PCA-3G5.pem b/zuora/PCA-3G5.pem new file mode 100644 index 0000000..a9490be --- /dev/null +++ b/zuora/PCA-3G5.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- \ No newline at end of file diff --git a/zuora/client.py b/zuora/client.py index 8d753be..1a2eeec 100644 --- a/zuora/client.py +++ b/zuora/client.py @@ -16,14 +16,18 @@ z = zuora.Zuora(SETTINGS) account = z.get_account(23432) """ -from datetime import datetime, date +from datetime import datetime, date, timedelta from os import path import re +import httplib2 from suds import WebFault from suds.client import Client from suds.sax.element import Element from suds.xsd.doctor import Import, ImportDoctor +from suds.transport.http import HttpAuthenticated, HttpTransport +from suds.transport import Reply +from suds.sax.text import Text import logging log = logging.getLogger(__name__) @@ -33,21 +37,47 @@ log_suds.propagate = False SOAP_TIMESTAMP = '%Y-%m-%dT%H:%M:%S-06:00' +UTC_TIMESTAMP = '%Y-%m-%dT%H:%M:%S+00:00' from rest_client import RestClient +class HttpTransportWithKeepAlive(HttpAuthenticated, object): + + def __init__(self, use_cert=False): + super(HttpTransportWithKeepAlive, self).__init__() + if use_cert: + path_to_certs = path.abspath(path.dirname(__file__)) + cert_file = path_to_certs + "/PCA-3G5.pem" + self.http = httplib2.Http(timeout=20, ca_certs=cert_file) + else: + self.http = httplib2.Http(timeout=20, + disable_ssl_certificate_validation=True) + + def open(self, request): + return HttpTransport.open(self, request) + + def send(self, request): + headers, message = self.http.request(request.url, "POST", + body=request.message, + headers=request.headers) + response = Reply(200, headers, message) + return response + + class ZuoraException(Exception): """This is our base exception for the Zuora lib""" pass + class DoesNotExist(ZuoraException): """ Exception for when objects don't exist in Zuora """ pass + class MissingRequired(ZuoraException): """ Exception for when a required parameter is missing @@ -58,15 +88,6 @@ class MissingRequired(ZuoraException): # main class class Zuora: - #: Soap Service Client - client = None - - #: Currency - currency = 'USD' - - #: SessionID (TODO: put this into memcache) - session_id = None - def __init__(self, zuora_settings): """ Usage example: @@ -90,8 +111,9 @@ def __init__(self, zuora_settings): self.password = zuora_settings["password"] self.wsdl_file = zuora_settings["wsdl_file"] self.base_dir = path.dirname(__file__) - self.authorize_gateway = zuora_settings.get("gateway_name", None) + self.authorize_gateway = zuora_settings.get("gateway_name") self.create_test_users = zuora_settings.get("test_users", None) + self.use_cert = zuora_settings.get("SSL", False) # Build Client imp = Import('http://object.api.zuora.com/') @@ -102,8 +124,11 @@ def __init__(self, zuora_settings): wsdl_file = 'file://%s' % path.abspath( self.base_dir + "/" + self.wsdl_file) - self.client = Client(url=wsdl_file, doctor=schema_doctor, - cache=None) + self.client = Client( + url=wsdl_file, + doctor=schema_doctor, + cache=None, + transport=HttpTransportWithKeepAlive(self.use_cert)) # Force No Cache self.client.set_options(cache=None) @@ -111,6 +136,13 @@ def __init__(self, zuora_settings): # Create the rest client self.rest_client = RestClient(zuora_settings) + self.session_id = None + + def reset_transport(self): + self.client.options.transport = HttpTransportWithKeepAlive( + self.use_cert) + self.session_id = None + # Client Create def call(self, fn, *args, **kwargs): """ @@ -119,27 +151,36 @@ def call(self, fn, *args, **kwargs): :returns: the client response """ + last_error = None - try: - response = fn(*args, **kwargs) - except WebFault as err: - if err.fault.faultcode == "fns:INVALID_SESSION": + for i in range(0, 3): + if self.session_id is None or self.session_expiration <= datetime.now(): self.login() - try: - response = fn(*args, **kwargs) - except Exception as error: - log.error("Zuora: Unexpected Error. %s" % error) - raise ZuoraException("Zuora: Unexpected Error. %s"\ - % error) - else: - log.error("WebFault. Invalid Session. %s" % err.__dict__) - raise ZuoraException("WebFault. Invalid Session. %s"\ - % err.__dict__) - except Exception as error: - log.error("Zuora: Unexpected Error. %s" % error) - raise ZuoraException("Zuora: Unexpected Error. %s" % error) + try: + response = fn(*args, **kwargs) + log.info("Call sent: %s" % self.client.last_sent()) + log.info("Call received: %s" % self.client.last_received()) + # THIS OCCASIONALLY HAPPENS + # AND ITS BAD WE NEED TO RESET + if isinstance(response, Text): + log.error("Zuora: REALLY Unexpected Response!!!! %s, RESETTING TO RETRY", response) + self.reset_transport() + else: + log.debug("Zuora: Successful Response %s", response) + return response + except WebFault as err: + if err.fault.faultcode == "fns:INVALID_SESSION": + log.warn("Zuora: Invalid Session, LOGGING IN") + self.session_id = None + else: + log.error("WebFault. %s", err.__dict__) + raise ZuoraException("WebFault. %s" % err.__dict__) + except Exception as error: + log.error("Zuora: Unexpected Error. %s" % error) + last_error = error + self.reset_transport() - return response + raise ZuoraException("Zuora: Unexpected Error. %s" % last_error) # Client Create def create(self, z_object): @@ -186,8 +227,10 @@ def login(self): Creates the SOAP SessionHeader with the correct session_id from Zuora TODO: investigate methodology to persist session_id across sessions - - look at custom capabilities -- sqlalchemy caching - WEB-935 perhaps + we are currently keeping the session in memory for < 8 hours + which is the session expiration time of Zuora """ + self.session_expiration = datetime.now() + timedelta(hours=7, minutes=55) login_response = self.client.service.login(username=self.username, password=self.password) self.session_id = login_response.Session @@ -207,6 +250,26 @@ def login(self): SessionHeader.append(session) self.client.set_options(soapheaders=[SessionHeader]) + def query_all(self, query_string): + """ + Stitch together records from query(), query_more() results as needed. + https://knowledgecenter.zuora.com/BC_Developers/SOAP_API/M_Zuora_Object_Query_Language + + :param string query_string: ZQL query string + :returns: the API response + """ + current_response = self.query(query_string) + all_records = current_response.records + done = getattr(current_response, 'done') + while not done: + query_locator = getattr(current_response, 'queryLocator') + current_response = self.query_more(query_locator) + all_records += current_response.records + done = getattr(current_response, 'done') + response = current_response + response.records = all_records + return response + def query(self, query_string): """ Pass the zosql querystring into the query() SOAP method @@ -369,18 +432,58 @@ def cancel_subscription(self, subscription_key, effective_date=None): subscription_key, jsonParams={'cancellationEffectiveDate': effective_date}) return response + + def create_payment_method(self, baid=None, user_email=None): + payment_method = self.client.factory.create('ns2:PaymentMethod') + if baid: + payment_method.PaypalBaid = baid + # Paypal user e-mail required + payment_method.PaypalEmail = user_email + payment_method.PaypalType = 'ExpressCheckout' + payment_method.Type = 'PayPal' + + return payment_method + + def transfer_subscription(self, subscription_id, destination_account_id, name='MergeAccountsByGuid'): + + cur_sub = self.get_subscription(subscription_id) + + # Make Amendment + zAmendment = self.client.factory.create('ns2:Amendment') + effective_date = datetime.utcnow().strftime(UTC_TIMESTAMP) + zAmendment.EffectiveDate = effective_date + zAmendment.Name = "{} {}".format(name, effective_date) + + zAmendment.SubscriptionId = subscription_id + zAmendment.DestinationAccountId = destination_account_id + zAmendment.DestinationInvoiceOwnerId = destination_account_id + zAmendment.Type = "OwnerTransfer" + zAmendment.Description = "Transfer from account {c} to {d} based on GUID".format(c=cur_sub.AccountId, + d=destination_account_id) + zAmendment.Status = "Completed" + + # Create Amendment + response = self.create(zAmendment) + if not isinstance(response, list) or not response[0].Success: + raise ZuoraException( + "Unknown Error creating Amendment. %s" % response) + zAmendment.Id = response[0].Id + + return zAmendment def create_active_account(self, zAccount=None, zContact=None, payment_method_id=None, user=None, billing_address=None, shipping_address=None, - site_name=None, prepaid=False): + site_name=None, prepaid=False, + gateway_name=None): """ Create an Active Account for use in Subscribe() """ # Create Account if it doesn't exist if not zAccount: zAccount = self.make_account(user=user, site_name=site_name, - billing_address=billing_address) + billing_address=billing_address, + gateway_name=gateway_name) # Create Bill-To Contact on Account if not zContact: @@ -402,6 +505,17 @@ def create_active_account(self, zAccount=None, zContact=None, else: zPaymentMethod = None + self.activate_account(zAccount, zContact, + zShippingContact=zShippingContact, + payment_method_id=payment_method_id, + prepaid=prepaid) + + return {'account': zAccount, 'contact': zContact, + 'payment_method': zPaymentMethod, + 'shipping_contact': zShippingContact} + + def activate_account(self, zAccount, zContact, zShippingContact=None, + payment_method_id=None, prepaid=None): # Now Update the Draft Account to be Active zAccountUpdate = self.client.factory.create('ns2:Account') zAccountUpdate.Id = zAccount.Id @@ -415,6 +529,9 @@ def create_active_account(self, zAccount=None, zContact=None, if payment_method_id and not prepaid: zAccountUpdate.DefaultPaymentMethodId = payment_method_id zAccountUpdate.AutoPay = True + elif payment_method_id: + zAccountUpdate.DefaultPaymentMethodId = payment_method_id + zAccountUpdate.AutoPay = False else: zAccountUpdate.AutoPay = False response = self.update(zAccountUpdate) @@ -422,38 +539,144 @@ def create_active_account(self, zAccount=None, zContact=None, raise ZuoraException( "Unknown Error updating Account. %s" % response) - return {'account': zAccount, 'contact': zContact, - 'payment_method': zPaymentMethod, - 'shipping_contact': zShippingContact} + def get_account(self, user_guid=None, account_id=None, id_only=False, extra_fields=['GUID__c']): + """ + Checks to see if the loaded user has an account + """ + if id_only: + fields = 'Id' + else: + fields = """Id, AccountNumber, AutoPay, Balance, DefaultPaymentMethodId, + PaymentGateway, Name, Status, UpdatedDate""" + + if extra_fields: + fields += ', ' + fields += ', '.join(extra_fields) - def get_account(self, user_id): + # If no account id was specified + if not account_id: + qs = """ + SELECT + %s + FROM Account + WHERE GUID__c = '%s' + """ % (fields, user_guid) + else: + qs = """ + SELECT + %s + FROM Account + WHERE Id = '%s' + """ % (fields, account_id) + + response = self.query_all(qs) + if getattr(response, "records") and len(response.records) > 0: + zAccount = response.records[0] + return zAccount + else: + raise DoesNotExist("Unable to find Account for User GUID %s and AccountId %s"\ + % (user_guid, account_id)) + + def get_subscription(self, subscription_id=None): """ Checks to see if the loaded user has an account """ + + fields = """AccountId, AutoRenew, CancelledDate, ContractAcceptanceDate, ContractEffectiveDate, + CreatedById, CreatedDate, DeviceID__c, InitialTerm, IsInvoiceSeparate, Name, Notes, + OriginalCreatedDate, OriginalId, PreviousSubscriptionId, RenewalTerm, + ServiceActivationDate, Status, SubscriptionEndDate, SubscriptionStartDate, + TermEndDate, TermStartDate, TermType, UpdatedById, UpdatedDate, Version""" + qs = """ - SELECT Id FROM Account - WHERE AccountNumber = '%s' or AccountNumber = 'A-%s' - """ % (user_id, user_id) + SELECT + %s + FROM Subscription + WHERE Id = '%s' + """ % (fields, subscription_id) - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: - zAccount = response.records[0] + zSubscription = response.records[0] + return zSubscription + else: + raise DoesNotExist("Unable to find Subscription {s}".format(subscription_id)) + + def get_accounts(self, account_number_list=None, account_id_list=None, + account_id=None, status=None, created_start=None, created_end=None, + additional_fields_str='GUID__c, Notes'): + """ + Gets the Accounts matching criteria. + Note: If account_id_list provided, all other criteria are ignored. + """ + fields = """Id, AccountNumber, AutoPay, Balance, CreatedDate, DefaultPaymentMethodId, + PaymentGateway, Name, Status, UpdatedDate""" + if additional_fields_str: + fields += ', {}'.format(additional_fields_str) + + # Defaults + qs_filter = [] + + if account_number_list: + qs_filter.append("%s" % " OR ".join(["AccountNumber = '%s'" % number for number in account_number_list])) + elif account_id_list: + qs_filter.append("%s" % " OR ".join(["Id = '%s'" % account_id for account_id in account_id_list])) + else: + if account_id: + qs_filter.append("Id = '%s'" % account_id) + if status: + qs_filter.append("Status = '%s'" % status) + if created_start: + qs_filter.append("CreatedDate >= '%s'" % created_start) + if created_end: + qs_filter.append("CreatedDate <= '%s'" % created_end) + + if qs_filter: + qs = """ + SELECT + %s + FROM Account + WHERE %s + """ % (fields, " AND ".join(qs_filter)) + else: + qs = """ + SELECT + %s + FROM Account + """ % fields + + response = self.query_all(qs) + + if getattr(response, "records") and len(response.records) > 0: + zAccount = response.records return zAccount else: - raise DoesNotExist("Unable to find Account for User ID %s"\ - % user_id) + raise DoesNotExist("Unable to find Accounts") - def get_contact(self, email=None, account_id=None): + def get_contacts(self, email_list=None, account_id_list=None, email=None, account_id=None, + first_name=None, last_name=None, email_type='Personal'): """ - Checks to see if the loaded user has a contact + Checks to see if the loaded users have a contact """ qs_filter = [] - if account_id: - qs_filter.append("AccountId = '%s'" % account_id) + if email_type not in ['Personal', 'Work']: + SyntaxError('Only Work or Personal emails are supported') - if email: - qs_filter.append("PersonalEmail = '%s'" % email) + if email_list: + qs_filter.append("%s" % " OR ".join(["{type}Email = '{email}'".format(type=email_type, email=email) + for email in email_list])) + elif account_id_list: + qs_filter.append("%s" % " OR ".join(["AccountId = '%s'" % account_id for account_id in account_id_list])) + else: + if account_id: + qs_filter.append("AccountId = '%s'" % account_id) + if email: + qs_filter.append("{type}Email = '{email}'".format(type=email_type, email=email)) + if first_name: + qs_filter.append("FirstName = '%s'" % first_name) + if last_name: + qs_filter.append("LastName = '%s'" % last_name) qs = """ SELECT @@ -466,9 +689,9 @@ def get_contact(self, email=None, account_id=None): WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: - zContact = response.records[0] + zContact = response.records return zContact else: raise DoesNotExist("Unable to find Contact for Email %s"\ @@ -492,7 +715,7 @@ def get_invoice(self, invoice_id=None): WHERE Id = '%s' """ % invoice_id - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: zInvoice = response.records[0] return zInvoice @@ -514,7 +737,7 @@ def get_invoice_pdf(self, invoice_id=None): WHERE Id = '%s' """ % invoice_id - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: zInvoice = response.records[0] return zInvoice.Body @@ -522,7 +745,8 @@ def get_invoice_pdf(self, invoice_id=None): raise DoesNotExist("Unable to find Invoice for Id %s"\ % invoice_id) - def get_invoices(self, account_id=None): + def get_invoices(self, account_id=None, minimum_balance=None, + status=None, invoice_id_list=None, account_id_list=None): """ Gets the Invoices matching criteria. @@ -535,6 +759,17 @@ def get_invoices(self, account_id=None): if account_id: qs_filter.append("AccountId = '%s'" % account_id) + if minimum_balance: + qs_filter.append("Balance > '%s'" % minimum_balance) + + if status: + qs_filter.append("Status = '%s'" % status) + + if invoice_id_list: + qs_filter.append("%s" % " OR ".join(["Id = '%s'" % i for i in invoice_id_list])) + elif account_id_list: + qs_filter.append("%s" % " OR ".join(["AccountID = '%s'" % i for i in account_id_list])) + if qs_filter: qs = """ SELECT @@ -548,7 +783,7 @@ def get_invoices(self, account_id=None): WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zInvoices = response.records # Return the Match @@ -591,7 +826,7 @@ def get_invoice_items(self, invoice_id=None, subscription_id=None): WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zRecords = response.records # Return the Match @@ -633,7 +868,7 @@ def get_invoice_payment(self, invoice_payment_id=None): WHERE Id = '%s' """ % invoice_payment_id - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: zInvoicePayment = response.records[0] return zInvoicePayment @@ -664,7 +899,7 @@ def get_invoice_payments(self, invoice_id=None, payment_id=None): FROM InvoicePayment WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zInvoicePayments = response.records # Return the Match @@ -695,7 +930,7 @@ def get_payment(self, payment_id=None): WHERE Id = '%s' """ % payment_id - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: zPayment = response.records[0] return zPayment @@ -703,7 +938,7 @@ def get_payment(self, payment_id=None): raise DoesNotExist("Unable to find Payment for Id %s"\ % payment_id) - def get_payments(self, account_id=None): + def get_payments(self, account_id_list=None, account_id=None, payment_method_id_list=None, payment_method_id=None): """ Gets the Payments matching criteria. @@ -713,27 +948,33 @@ def get_payments(self, account_id=None): # Defaults qs_filter = [] - if account_id: - qs_filter.append("AccountId = '%s'" % account_id) + if account_id_list: + qs_filter.append("%s" % " OR ".join(["AccountId = '%s'" % i for i in account_id_list])) + elif payment_method_id_list: + qs_filter.append("%s" % " OR ".join(["PaymentMethodID = '%s'" % i for i in account_id_list])) + else: + if account_id: + qs_filter.append("AccountId = '%s'" % account_id) + if payment_method_id: + qs_filter.append("PaymentMethodID = '%s'" % payment_method_id) if qs_filter: qs = """ SELECT - AccountID, AccountingCode, Amount, - AppliedCreditBalanceAmount, AuthTransactionId, + AccountID, Amount, BankIdentificationNumber, CancelledOn, Comment, - CreatedById, CreatedDate, EffectiveDate, GatewayOrderId, - GatewayResponse, GatewayResponseCode, GatewayState, + CreatedById, CreatedDate, EffectiveDate, + Gateway, GatewayOrderId, GatewayResponse, GatewayResponseCode, GatewayState, + Id, MarkedForSubmissionOn, - PaymentMethodID, PaymentNumber, ReferenceId, RefundAmount, - SecondPaymentReferenceId, SettledOn, SoftDescriptor, - Status, SubmittedOn, TransferredToAccounting, + PaymentMethodID, ReferenceId, RefundAmount, + Status, SubmittedOn, Type, UpdatedById, UpdatedDate FROM Payment WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zPayments = response.records # Return the Match @@ -763,7 +1004,7 @@ def get_payment_method(self, payment_method_id): WHERE Id = '%s' """ % payment_method_id - response = self.query(qs) + response = self.query_all(qs) if getattr(response, "records") and len(response.records) > 0: zPaymentMethod = response.records[0] return zPaymentMethod @@ -771,7 +1012,7 @@ def get_payment_method(self, payment_method_id): raise DoesNotExist("Unable to find Payment Method for %s. %s"\ % (payment_method_id, response)) - def get_payment_methods(self, account_id=None, account_number=None, + def get_payment_methods(self, account_id_list=None, account_id=None, account_number=None, email=None, phone=None): """ Gets the Payment Methods matching criteria. @@ -786,88 +1027,89 @@ def get_payment_methods(self, account_id=None, account_number=None, # Defaults qs_filter = [] - # Account Number - if account_number: - qs = """ - SELECT - DefaultPaymentMethodId - FROM Account - WHERE AccountNumber = '%s' or AccountNumber = 'A-%s' - """ % (account_number, account_number) + if account_id_list: + qs_filter.append("%s" % " OR ".join(["AccountId = '%s'" % i for i in account_id_list])) + else: - response = self.query(qs) - if getattr(response, "records") and len(response.records) > 0: - zAccount = response.records[0] - # Check for a default payment method - try: - payment_method_id = zAccount.DefaultPaymentMethodId - except: - return [] + # Account Number + if account_number: + qs = """ + SELECT + DefaultPaymentMethodId + FROM Account + WHERE AccountNumber = '%s' or AccountNumber = 'A-%s' + """ % (account_number, account_number) - # Return as a List - return [self.get_payment_method(payment_method_id)] + response = self.query_all(qs) + if getattr(response, "records") and len(response.records) > 0: + zAccount = response.records[0] + # Check for a default payment method + try: + payment_method_id = zAccount.DefaultPaymentMethodId + except: + return [] - if account_id: - qs_filter.append("AccountId = '%s'" % account_id) + # Return as a List + return [self.get_payment_method(payment_method_id)] + + if account_id: + qs_filter.append("AccountId = '%s'" % account_id) - if email: - qs_filter.append("Email = '%s'" % email) + if email: + qs_filter.append("Email = '%s'" % email) - if phone: - qs_filter.append("Phone = '%s'" % phone) + if phone: + qs_filter.append("Phone = '%s'" % phone) if qs_filter: qs = """ SELECT AccountId, Active, CreatedById, CreatedDate, - CreditCardAddress1, CreditCardAddress2, - CreditCardCity, CreditCardCountry, CreditCardExpirationMonth, CreditCardExpirationYear, CreditCardHolderName, CreditCardMaskNumber, - CreditCardPostalCode, CreditCardState, CreditCardType, - Email, Name, PaypalBaid, PaypalEmail, - PaypalPreapprovalKey, PaypalType, Phone, Type + CreditCardAddress1, CreditCardAddress2, + CreditCardCity, CreditCardState, CreditCardPostalCode, CreditCardCountry, + CreditCardType, Id, + LastFailedSaleTransactionDate, LastTransactionDateTime, LastTransactionStatus, + MaxConsecutivePaymentFailures, Name, NumConsecutiveFailures, + PaymentMethodStatus, PaymentRetryWindow, + TotalNumberOfErrorPayments, TotalNumberOfProcessedPayments, + Type, UpdatedById, UpdatedDate FROM PaymentMethod WHERE %s """ % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zPaymentMethods = response.records # Return the Match return zPaymentMethods return [] - def get_products(self, product_id=None, shortcodes=None): + def get_products(self, product_id=None): """ Gets the Product. :param str product_id: ProductID - :param list shortcodes: List of shortcode strings """ qs_filter = None qs = """ SELECT Description, EffectiveEndDate, EffectiveStartDate, - Id, SKU, Name, ShortCode__c + Id, SKU, Name FROM Product """ # If we're looking for one specific product if product_id: qs_filter = "Id = '%s'" % product_id - # If we're pulling multiple products by their shortcodes - elif shortcodes: - qs_filter_list = ["ShortCode__c = '%s'" % code - for code in shortcodes] - qs_filter = " OR ".join(qs_filter_list) if qs_filter: qs += " WHERE %s" % qs_filter - response = self.query(qs) + response = self.query_all(qs) try: zProducts = response.records return zProducts @@ -905,13 +1147,16 @@ def get_rate_plan_charges(self, rate_plan_id=None, Segment, TCV, TriggerDate, TriggerEvent, UnusedUnitsCreditRates, UOM, UpdatedById, UpdatedDate, UpToPeriods, UsageRecordRatingOption, - UseDiscountSpecificAccountingCode, Version + UseDiscountSpecificAccountingCode, Version, + Alacarte__c, SKU__c, FeatureStatus__c, FeatureCode__c FROM RatePlanCharge """ % pricing_info where_id_string = "RatePlanId = '%s'" # If only querying with one rate plan id if rate_plan_id: qs_filter = where_id_string % rate_plan_id + elif product_rate_plan_charge_id: + qs_filter = "ProductRatePlanChargeId = '%s'" % product_rate_plan_charge_id # Otherwise we're querying with multiple rate plan id's else: qs_filter = None @@ -922,7 +1167,7 @@ def get_rate_plan_charges(self, rate_plan_id=None, qs_filter = " OR ".join(id_filter_list) qs += " WHERE %s" % qs_filter - response = self.query(qs) + response = self.query_all(qs) try: return response.records except: @@ -943,10 +1188,8 @@ def get_product_rate_plans(self, product_rate_plan_id=None, """ qs = """ SELECT - ActivityLevel__c, AgeGroup__c, Description, EffectiveEndDate, EffectiveStartDate, - Gender__c, Id, Name, - Priority__c, ProductId, Site__c, Term__c + Id, Name, ProductId FROM ProductRatePlan """ @@ -976,9 +1219,10 @@ def get_product_rate_plans(self, product_rate_plan_id=None, else: qs_filter = date_where - qs += " WHERE %s" % qs_filter + if qs_filter: + qs += " WHERE %s" % qs_filter - response = self.query(qs) + response = self.query_all(qs) try: zProductRatePlans = response.records return zProductRatePlans @@ -1000,14 +1244,13 @@ def get_product_rate_plan_charges(self, product_rate_plan_id=None, SELECT AccountingCode, BillCycleDay, BillCycleType, BillingPeriod, BillingPeriodAlignment, ChargeModel, ChargeType, - CustomImageURL__c, DefaultQuantity, Description, - ExclusiveOfferFlag__c, - HiddenBenefitText__c, Id, IncludedUnits, MaxQuantity, + DefaultQuantity, Description, + Id, IncludedUnits, MaxQuantity, MinQuantity, Name, NumberOfPeriod, OverageCalculationOption, OverageUnusedUnitsCreditOption, PriceIncreasePercentage, ProductRatePlanId, - RevRecCode, RevRecTriggerCondition, ShortCode__c, - SmoothingModel, SortOrder__c, SpecificBillingPeriod, + RevRecCode, RevRecTriggerCondition, + SmoothingModel, SpecificBillingPeriod, TriggerEvent, UOM, UpToPeriods, UseDiscountSpecificAccountingCode FROM ProductRatePlanCharge @@ -1028,9 +1271,10 @@ def get_product_rate_plan_charges(self, product_rate_plan_id=None, # Combine the product rate plan ids for the WHERE clause qs_filter = " OR ".join(id_filter_list) - qs += " WHERE %s" % qs_filter + if qs_filter: + qs += " WHERE %s" % qs_filter - response = self.query(qs) + response = self.query_all(qs) try: return response.records except: @@ -1072,7 +1316,7 @@ def get_product_rate_plan_charge_tiers( qs += " WHERE %s" % qs_filter - response = self.query(qs) + response = self.query_all(qs) try: zProductRatePlanChargeTiers = response.records return zProductRatePlanChargeTiers @@ -1334,7 +1578,7 @@ def get_product_rate_plan_charge_pricing(self, product_rate_plan_id): price = pricing_dict[charge_model][charge_type] for rpct in rpc["rate_charge_tiers"]: is_overage_price = rpct["is_overage_price"] - if is_overage_price == False: + if is_overage_price in [False, 'False']: price = price + float(rpct["price"]) pricing_dict[charge_model][charge_type] = price @@ -1342,7 +1586,8 @@ def get_product_rate_plan_charge_pricing(self, product_rate_plan_id): # Run Aggregates return pricing_dict - def get_rate_plans(self, product_rate_plan_id=None, subscription_id=None): + def get_rate_plans(self, product_rate_plan_id_list=None, subscription_id_list=None, + product_rate_plan_id=None, subscription_id=None): """ Gets the RatePlan matching criteria. @@ -1353,11 +1598,15 @@ def get_rate_plans(self, product_rate_plan_id=None, subscription_id=None): # Defaults qs_filter = [] - if product_rate_plan_id: - qs_filter.append("ProductRatePlanId = '%s'" % product_rate_plan_id) - - if subscription_id: - qs_filter.append("SubscriptionId = '%s'" % subscription_id) + if product_rate_plan_id_list: + qs_filter.append("%s" % " OR ".join(["ProductRatePlanId = '%s'" % i for i in product_rate_plan_id_list])) + elif subscription_id_list: + qs_filter.append("%s" % " OR ".join(["SubscriptionId = '%s'" % i for i in subscription_id_list])) + else: + if product_rate_plan_id: + qs_filter.append("ProductRatePlanId = '%s'" % product_rate_plan_id) + if subscription_id: + qs_filter.append("SubscriptionId = '%s'" % subscription_id) # Build Query qs = """ @@ -1372,13 +1621,14 @@ def get_rate_plans(self, product_rate_plan_id=None, subscription_id=None): if qs_filter: qs += "WHERE %s" % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zRecords = response.records # Return the Match return zRecords - def get_subscriptions(self, subscription_id=None, account_id=None, + def get_subscriptions(self, subscription_id_list=None, account_id_list=None, + subscription_id=None, account_id=None, auto_renew=None, status=None, term_type=None, term_end_date=None, term_start_date=None, subscription_number=None): @@ -1399,29 +1649,27 @@ def get_subscriptions(self, subscription_id=None, account_id=None, # Defaults qs_filter = [] - if subscription_id: - qs_filter.append("Id = '%s'" % subscription_id) - - if subscription_number: - qs_filter.append("Name = '%s'" % subscription_number) - - if account_id: - qs_filter.append("AccountId = '%s'" % account_id) - - if auto_renew: - qs_filter.append("AutoRenew = %s" % auto_renew.lower()) - - if status: - qs_filter.append("Status = '%s'" % status) - - if term_type: - qs_filter.append("TermType = '%s'" % term_type) - - if term_end_date: - qs_filter.append("TermEndDate = '%s'" % term_end_date) - - if term_start_date: - qs_filter.append("TermStartDate = '%s'" % term_start_date) + if subscription_id_list: + qs_filter.append("%s" % " OR ".join(["Id = '%s'" % i for i in subscription_id_list])) + elif account_id_list: + qs_filter.append("%s" % " OR ".join(["AccountId = '%s'" % i for i in account_id_list])) + else: + if subscription_id: + qs_filter.append("Id = '%s'" % subscription_id) + if subscription_number: + qs_filter.append("Name = '%s'" % subscription_number) + if account_id: + qs_filter.append("AccountId = '%s'" % account_id) + if auto_renew: + qs_filter.append("AutoRenew = %s" % auto_renew.lower()) + if status: + qs_filter.append("Status = '%s'" % status) + if term_type: + qs_filter.append("TermType = '%s'" % term_type) + if term_end_date: + qs_filter.append("TermEndDate = '%s'" % term_end_date) + if term_start_date: + qs_filter.append("TermStartDate = '%s'" % term_start_date) # Build Query qs = """ @@ -1429,27 +1677,153 @@ def get_subscriptions(self, subscription_id=None, account_id=None, AccountId, AutoRenew, CancelledDate, ContractAcceptanceDate, ContractEffectiveDate, - CreatedById, CreatedDate, InitialTerm, + CreatedById, CreatedDate, DeviceID__c, InitialTerm, IsInvoiceSeparate, Name, Notes, OriginalCreatedDate, OriginalId, PreviousSubscriptionId, RenewalTerm, ServiceActivationDate, Status, SubscriptionEndDate, SubscriptionStartDate, TermEndDate, TermStartDate, TermType, - UpdatedById, UpdatedDate, Version + UpdatedById, UpdatedDate, Version, + Petname__c, KitID__c, TrialDays__c, Type__c, SubscriptionGroup__c, OrderSource__c FROM Subscription """ if qs_filter: qs += "WHERE %s" % " AND ".join(qs_filter) - response = self.query(qs) + response = self.query_all(qs) zRecords = response.records # Return the Match return zRecords + + def setup_default_payment_method(self, account_id, payment_method_id=None, auto_pay=None): + # Update the default payment method on the account + account_dict = dict() + + if payment_method_id: + account_dict = {'DefaultPaymentMethodId': payment_method_id} + + if auto_pay is not None: + if auto_pay: + account_dict['AutoPay'] = True + elif auto_pay: + account_dict['AutoPay'] = False + + if account_dict: + self.update_account(account_id, account_dict) + + def gateway_confirm(self, account, user, gateway_name, + payment_method): + """Switches the gateway if the user is purchasing with a different + gateway. + + Returns True or False if the account already exists or not + """ + # Make sure the account exists already, otherwise the gateway will be + # specified on account creation + if not account or not getattr(account, 'PaymentGateway', None): + try: + zAccount = self.get_account(user.id) + except DoesNotExist: + logging.info("Gateway: Account DNE. user: %s" % (user.id)) + return False + logging.info("Gateway: Fetched Account. user: %s" % (user.id)) + else: + logging.info( + "Gateway: Account and Gateway existed. user: %s" % (user.id)) + zAccount = account + + # If the Payment Gateway still isn't specified, set it and change it + if not getattr(zAccount, 'PaymentGateway', None): + if gateway_name: + update_dict = {'PaymentGateway': gateway_name} + else: + update_dict = {'PaymentGateway': self.authorize_gateway} + logging.info("Gateway: switched user: %s update: %s" \ + % (user.id, update_dict)) + self.update_account(zAccount.Id, update_dict) + return True + + # If no gateway was specified, and the gateway is set + # to the default gateway + if not gateway_name \ + and zAccount.PaymentGateway == self.authorize_gateway: + # Do nothing + logging.info("Gateway: same default gateway user: %s gateway: %s" \ + % (user.id, zAccount.PaymentGateway)) + pass + # If there isn't a gateway specified, and they aren't set to the + # default gateway + elif not gateway_name \ + and zAccount.PaymentGateway != self.authorize_gateway: + # Update the account to the default gateway + self.update_account_payment(zAccount.Id, + self.authorize_gateway, + payment_method) + logging.info("Gateway: switched to default user: %s gateway: %s" \ + % (user.id, self.authorize_gateway)) + # If a gateway was specified, but their account is already + # set to that gateway + elif gateway_name and gateway_name == zAccount.PaymentGateway: + # Do nothing + logging.info( + "Gateway: same specified gateway user: %s gateway: %s" \ + % (user.id, gateway_name)) + pass + # If a gateway was specified, but their account is set to a + # different gateway + elif gateway_name and gateway_name != zAccount.PaymentGateway: + # Update the gateway to the specified gateway + self.update_account_payment(zAccount.Id, + gateway_name, + payment_method) + logging.info( + "Gateway: switched to specified gateway user: %s gateway: %s" \ + % (user.id, gateway_name)) + # We should never see this condition + else: + logging.error( + "Unexpected gateway conditions. gateway: %s acct_gateway: %s" \ + % (gateway_name, zAccount.PaymentGateway)) + + return True + + def update_account_payment(self, account_id, gateway, payment_method): + # These steps cannot be combined, and have to be executed in this order + # Update the account gateway + logging.info( + "Gateway: Updating Account Gateway. Account id: %s" % account_id) + gateway_dict = {'PaymentGateway': gateway} + self.update_account(account_id, gateway_dict) + # If the payment method hasn't been created yet + if payment_method and getattr(payment_method, 'Id', None) is None: + payment_method.AccountId = account_id + logging.info( + "Gateway: Creating Payment Method. Account: %s" % account_id) + response = self.create(payment_method) + if not isinstance(response, list) or not response[0].Success: + raise ZuoraException( + "Error creating Payment Method. Account id: %s resp: %s" \ + % (account_id, response)) + payment_method_id = response[0].Id + # No Payment Method specified + elif payment_method is None: + raise ZuoraException( + "Missing Payment Method. Account id: %s" % account_id) + # Payment Method exists already + else: + payment_method_id = payment_method.Id + logging.info( + "Gateway: Updating DefaultPayment Method. Account id: %s" \ + % account_id) + # Update the default payment method on the account + dpm_dict = {'DefaultPaymentMethodId': payment_method_id} + self.update_account(account_id, dpm_dict) def make_account(self, user=None, currency='USD', status="Draft", - lazy=False, site_name=None, billing_address=None): + lazy=False, site_name=None, billing_address=None, + gateway_name=None): """ The customer's account. Zuora uses the Account object to track all subscriptions, usage, and transactions for a single account to be @@ -1494,8 +1868,12 @@ def make_account(self, user=None, currency='USD', status="Draft", zAccount.PaymentTerm = 'Due Upon Receipt' zAccount.Status = status + # Specify what gateway to use for payments for the user + if gateway_name: + zAccount.PaymentGateway = gateway_name + # Determine which Payment Gateway to use, if specified - if self.authorize_gateway: + elif self.authorize_gateway: zAccount.PaymentGateway = self.authorize_gateway if self.create_test_users: @@ -1504,8 +1882,10 @@ def make_account(self, user=None, currency='USD', status="Draft", if site_name: zAccount.User_Site__c = site_name - - if lazy: + + # If specifying a gateway, the account will be created + # during the subscribe call + if lazy or gateway_name: return zAccount response = self.create(zAccount) @@ -1518,7 +1898,7 @@ def make_account(self, user=None, currency='USD', status="Draft", return zAccount def make_contact(self, user=None, billing_address=None, zAccount=None, - lazy=False): + lazy=False, gateway_name=None): """ This defines the contact (the end user) for the account. There are two types of contacts that need to be created as part of the customer @@ -1571,7 +1951,9 @@ def make_contact(self, user=None, billing_address=None, zAccount=None, if zAccount is not None and hasattr(zAccount, 'Id'): zContact.AccountId = zAccount.Id - if lazy: + # If specifying a gateway, the contact will be created + # during the subscribe call + if lazy or gateway_name: return zContact response = self.create(zContact) @@ -1583,6 +1965,42 @@ def make_contact(self, user=None, billing_address=None, zAccount=None, # Return return zContact + def make_payment(self, account_id, invoice_id, invoice_amount, + payment_method_id, payment_type='External', + payment_status='Processed', effective_date=None, + dry_run=False): + if not effective_date: + effective_date = date.today().strftime(SOAP_TIMESTAMP) + else: + effective_date = effective_date.strftime(SOAP_TIMESTAMP) + # Create the Payment + zPayment = self.client.factory.create('ns2:Payment') + zPayment.AccountId = account_id + zPayment.InvoiceId = invoice_id + zPayment.AppliedInvoiceAmount = invoice_amount + zPayment.PaymentMethodId = payment_method_id + zPayment.Type = payment_type + zPayment.Status = payment_status + zPayment.EffectiveDate = effective_date + + # If it's not a dry run, create the payment + if not dry_run: + response = self.create(zPayment) + # If the Payment creation failed + if not isinstance(response, list) or not response[0].Success: + logging.error("Error creating Payment, account: %s response: %s" \ + % (account_id, response)) + else: + zPayment.Id = response[0].Id + logging.info("Made Payment. Payment: %s Account: %s" % ( + zPayment.Id, account_id)) + # else, just output the dry run of the payment data + else: + logging.info("Dry run Payment. account: %s Payment: %s" % (account_id, zPayment)) + + # Return + return zPayment + def make_rate_plan_data(self, product_rate_plan_id): """ RatePlanData is used to pass complex data to the subscribe() call. @@ -1595,7 +2013,6 @@ def make_rate_plan_data(self, product_rate_plan_id): # Build Rate Plan zRatePlan = self.client.factory.create('ns0:RatePlan') - zRatePlan.AmendmentType = "NewProduct" zRatePlan.ProductRatePlanId = product_rate_plan_id # Build Rate Plan Data @@ -1698,6 +2115,42 @@ def remove_product_amendment(self, subscription_id, rate_plan_id): return response + def change_subscription_owner(self, subscription_id, destination_account_id): + """ + Use Amendment to make changes to a subscription. For example, if you + wish to change the terms and conditions of a subscription, you would + use an Amendment. + + :param str subscription_id: The identification number for the\ + subscription that is being amended. + :param str rate_plan_id: RatePlanID + + :returns: response + """ + effective_date = datetime.now().strftime(SOAP_TIMESTAMP) + + # Create the product amendment removal + zAmendment = self.create_product_amendment( + effective_date, + subscription_id, + name_prepend="Remove Product Amendment", + amendment_type='RemoveProduct') + + # Make Rate Plan + zRatePlan = self.client.factory.create('ns0:RatePlan') + zRatePlan.AmendmentType = "RemoveProduct" + zRatePlan.AmendmentId = zAmendment.Id + zRatePlan.AmendmentSubscriptionRatePlanId = rate_plan_id + response = self.create(zRatePlan) + if not isinstance(response, list) or not response[0].Success: + raise ZuoraException( + "Unknown Error creating RatePlan. %s" % response) + + # Update the product amendment + response = self.update_product_amendment(effective_date, zAmendment) + + return response + def subscribe(self, product_rate_plan_id, monthly_term, zAccount=None, zContact=None, zShippingContact=None, process_payments_flag=True, @@ -1708,7 +2161,7 @@ def subscribe(self, product_rate_plan_id, monthly_term, zAccount=None, user=None, billing_address=None, shipping_address=None, start_date=None, site_name=None, discount_product_rate_plan_id=None, - external_payment_method=None): + external_payment_method=None, gateway_name=None): """ The subscribe() call bundles the information required to create one or more new subscriptions. This is a combined call that you can use @@ -1729,25 +2182,42 @@ def subscribe(self, product_rate_plan_id, monthly_term, zAccount=None, :param str subscription_name: The name of the subscription. This is a\ unique identifier. If not specified, Zuora will auto-create a name. """ - # zAccount = self.client.factory.create('ns2:Account') - #Used to be called even if account existed, pulling it out for now + if user: + logging.info("Gateway: confirming gateway user: %s" % (user.id)) + # Get the payment method + if external_payment_method: + gateway_pm = external_payment_method + else: + gateway_pm = payment_method + # Account Gateway Check/Switch + existing_account = self.gateway_confirm( + zAccount, + user, + gateway_name, + gateway_pm) + else: + existing_account = False + # Get or Create Account if not zAccount: zAccount = self.make_account(user=user, site_name=site_name, - billing_address=billing_address) + billing_address=billing_address, + gateway_name=gateway_name) - if not zContact and not zAccount.Id: + if not zContact: # Create Contact zContact = self.make_contact(user=user, billing_address=billing_address, - zAccount=zAccount) + zAccount=zAccount, + gateway_name=gateway_name) # Add the shipping contact if it exists if not zShippingContact and shipping_address: zShippingContact = self.make_contact(user=user, billing_address=shipping_address, - zAccount=zAccount) - + zAccount=zAccount, + gateway_name=gateway_name) + # Get Rate Plan & Build Rate Plan Data zRatePlanData = self.make_rate_plan_data(product_rate_plan_id) @@ -1808,11 +2278,20 @@ def subscribe(self, product_rate_plan_id, monthly_term, zAccount=None, # Subscribe zSubscribeRequest = self.client.factory.create('ns0:SubscribeRequest') - zSubscribeRequest.Account = zAccount + # If the account already exists, just add the id to the + # subscribe request + if existing_account: + zSubscribeRequest.Account = self.get_account(user.id, id_only=True) + logging.info("Fetched just the account id, user: %s" % (user.id)) + else: + zSubscribeRequest.Account = zAccount zSubscribeRequest.BillToContact = zContact # Add the shipping contact if it exists if zShippingContact: zSubscribeRequest.SoldToContact = zShippingContact + # Otherwise default to the billing contact + else: + zSubscribeRequest.SoldToContact = zSubscribeRequest.BillToContact zSubscribeRequest.SubscriptionData = zSubscriptionData zSubscribeRequest.SubscribeOptions = zSubscriptionOptions @@ -1830,6 +2309,14 @@ def subscribe(self, product_rate_plan_id, monthly_term, zAccount=None, log.info("***Subscribe Request: %s" % zSubscribeRequest) response = self.call(fn, zSubscribeRequest) log.info("***Subscribe Response: %s" % response) + + # If the gateway is paypal, make sure AutoPay is set to True + if gateway_name and 'paypal' in gateway_name.lower(): + if isinstance(response, list): + SubscribeResponse = response[0] + else: + SubscribeResponse = response + self.update_account(SubscribeResponse.AccountId, {'AutoPay': True}) # return the response return response @@ -1909,6 +2396,6 @@ def name_underscore_fix(name_field): """ Make sure the name field has a value, otherwise return an underscore """ - if name_field and name_field != '': + if name_field and name_field.strip() != '': return name_field return '_' diff --git a/zuora/rest_client.py b/zuora/rest_client.py index bd17eb2..b741fd8 100644 --- a/zuora/rest_client.py +++ b/zuora/rest_client.py @@ -15,11 +15,12 @@ ## sampleSubsNumber: change to some Subscription Number in your Zuora tenant #baseUrl = 'https://apisandbox-api.zuora.com/rest/v1/' +baseUrl = 'https://api.zuora.com/rest/v1/' #username = 'rest.user@test.com' #password = 'Zuora001!' #Payment Id of Default Credit Card (specific per tenant) -hpmCreditCardPaymentMethodId = '2c92c0f93cf64d94013cfe2d20db61a7' +#hpmCreditCardPaymentMethodId = '2c92c0f93cf64d94013cfe2d20db61a7' class ZuoraConfig(object): @@ -30,6 +31,7 @@ def __init__(self, zuora_settings): self.headers = {'apiAccessKeyId': zuora_settings['username'], 'apiSecretAccessKey': zuora_settings['password'], 'Content-Type': 'application/json'} + self.base_url = baseUrl class RestClient(object): diff --git a/zuora/rest_wrapper/account_manager.py b/zuora/rest_wrapper/account_manager.py index d76f640..53cb50e 100644 --- a/zuora/rest_wrapper/account_manager.py +++ b/zuora/rest_wrapper/account_manager.py @@ -19,7 +19,8 @@ def create_account(self, **kwargs): return None response = requests.post(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -27,14 +28,16 @@ def get_account_summary(self, accountKey): fullUrl = self.zuora_config.base_url + 'accounts/' + accountKey + \ '/summary' - response = requests.get(fullUrl, headers=self.zuora_config.headers) + response = requests.get(fullUrl, headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect def get_account(self, accountKey): fullUrl = self.zuora_config.baseUrl + 'accounts/' + accountKey - response = requests.get(fullUrl, headers=self.zuora_config.headers) + response = requests.get(fullUrl, headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -47,5 +50,6 @@ def update_account(self, accountKey, **kwargs): data = None response = requests.put(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) diff --git a/zuora/rest_wrapper/catalog_manager.py b/zuora/rest_wrapper/catalog_manager.py index d89e5a9..f599248 100644 --- a/zuora/rest_wrapper/catalog_manager.py +++ b/zuora/rest_wrapper/catalog_manager.py @@ -14,5 +14,6 @@ def get_catalog(self, pageSize=10, page=1): 'page': page} response = requests.get(fullUrl, params=params, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) diff --git a/zuora/rest_wrapper/payment_method_manager.py b/zuora/rest_wrapper/payment_method_manager.py index bb596b1..5e8f512 100644 --- a/zuora/rest_wrapper/payment_method_manager.py +++ b/zuora/rest_wrapper/payment_method_manager.py @@ -18,7 +18,8 @@ def create_payment_method(self, **kwargs): data = json.dumps(kwargs) response = requests.post(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -28,7 +29,8 @@ def get_payment_methods(self, accountKey, pageSize=10): data = {'pageSize': pageSize} response = requests.get(fullUrl, params=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -44,7 +46,9 @@ def update_payment_method(self, paymentMethodId, **kwargs): return None response = requests.put(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False + ) return self.get_json(response) @rest_client_reconnect @@ -52,5 +56,6 @@ def delete_payment_method(self, paymentMethodId): fullUrl = self.zuora_config.base_url + 'payment-methods/' + \ paymentMethodId - response = requests.delete(fullUrl, headers=self.zuora_config.headers) + response = requests.delete(fullUrl, headers=self.zuora_config.headers, + verify=False) return self.get_json(response) diff --git a/zuora/rest_wrapper/request_base.py b/zuora/rest_wrapper/request_base.py index c942481..abc2aa6 100644 --- a/zuora/rest_wrapper/request_base.py +++ b/zuora/rest_wrapper/request_base.py @@ -29,7 +29,8 @@ def __init__(self, zuora_config): def login(self): fullUrl = self.zuora_config.base_url + 'connections' - response = requests.post(fullUrl, headers=self.zuora_config.headers) + response = requests.post(fullUrl, headers=self.zuora_config.headers, + verify=False) return self.get_json(response) def get_json(self, response): diff --git a/zuora/rest_wrapper/subscription_manager.py b/zuora/rest_wrapper/subscription_manager.py index 82a02b5..02c3928 100644 --- a/zuora/rest_wrapper/subscription_manager.py +++ b/zuora/rest_wrapper/subscription_manager.py @@ -16,13 +16,15 @@ def get_subscriptions_by_account(self, accountKey, pageSize=10): data = {'pageSize': pageSize} response = requests.get(fullUrl, params=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect def get_subscriptions_by_key(self, subsKey): fullUrl = self.zuora_config.base_url + 'subscriptions/' + subsKey - response = requests.get(fullUrl, headers=self.zuora_config.headers) + response = requests.get(fullUrl, headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -32,19 +34,25 @@ def renew_subscription(self, subsKey, '/renew' data = json.dumps(jsonParams) response = requests.put(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False + ) return self.get_json(response) @rest_client_reconnect def cancel_subscription(self, subsKey, jsonParams={}): - jsonParams.setdefault('cancellationPolicy', - self.zuora_config.default_cancellation_policy) + if 'cancellationEffectiveDate' in jsonParams: + jsonParams['cancellationPolicy'] = 'SpecificDate' + else: + jsonParams.setdefault('cancellationPolicy', + self.zuora_config.default_cancellation_policy) fullUrl = self.zuora_config.base_url + 'subscriptions/' + subsKey + \ '/cancel' data = json.dumps(jsonParams) log.info("Zuora REST: Canceling subscription: %s" % subsKey) response = requests.put(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -52,7 +60,8 @@ def preview_subscription(self, jsonParams): fullUrl = self.zuora_config.base_url + 'subscriptions/preview' data = json.dumps(jsonParams) response = requests.post(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -60,7 +69,8 @@ def create_subscription(self, jsonParams): fullUrl = self.zuora_config.base_url + 'subscriptions' data = json.dumps(jsonParams) response = requests.post(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -68,6 +78,7 @@ def update_subscription(self, subsKey, jsonParams): fullUrl = self.zuora_config.base_url + 'subscriptions/' + subsKey data = json.dumps(jsonParams) response = requests.put(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) \ No newline at end of file diff --git a/zuora/rest_wrapper/transaction_manager.py b/zuora/rest_wrapper/transaction_manager.py index a719b82..639e88f 100644 --- a/zuora/rest_wrapper/transaction_manager.py +++ b/zuora/rest_wrapper/transaction_manager.py @@ -13,7 +13,8 @@ def get_invoices(self, accountKey, pageSize=10): 'pageSize': pageSize } response = requests.get(fullUrl, params=params, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -24,7 +25,8 @@ def get_payments(self, accountKey, pageSize=10): 'pageSize': pageSize } response = requests.get(fullUrl, params=params, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) @rest_client_reconnect @@ -32,5 +34,6 @@ def invoice_and_collect(self, jsonParams): fullUrl = self.zuora_config.base_url + 'operations/invoice-collect' data = json.dumps(jsonParams) response = requests.post(fullUrl, data=data, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) diff --git a/zuora/rest_wrapper/usage_manager.py b/zuora/rest_wrapper/usage_manager.py index 58943a1..00c6cc7 100644 --- a/zuora/rest_wrapper/usage_manager.py +++ b/zuora/rest_wrapper/usage_manager.py @@ -10,5 +10,6 @@ def get_usage(self, accountKey, pageSize=10): accountKey params = {'pageSize': pageSize} response = requests.get(fullUrl, params=params, - headers=self.zuora_config.headers) + headers=self.zuora_config.headers, + verify=False) return self.get_json(response) diff --git a/zuora/tests.py b/zuora/tests.py index 19f435a..e16120a 100644 --- a/zuora/tests.py +++ b/zuora/tests.py @@ -94,7 +94,7 @@ def test_get_account_query_called(self): response = mock.Mock() response.records = [1] z.query.return_value = response - z.get_account(user_id=42) + z.get_account(user_guid=42) assert z.query.call_count == 1 def test_get_contact_query_called(self): diff --git a/zuora/zuora.a.68.0.wsdl b/zuora/zuora.a.68.0.wsdl new file mode 100644 index 0000000..0ab0c6c --- /dev/null +++ b/zuora/zuora.a.68.0.wsdl @@ -0,0 +1,1994 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gets the next batch of sObjects from a query + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +