diff --git a/data/database.py b/data/database.py index bc7367108..08bd54003 100644 --- a/data/database.py +++ b/data/database.py @@ -288,6 +288,7 @@ class User(BaseModel): last_invalid_login = DateTimeField(default=datetime.utcnow) removed_tag_expiration_s = IntegerField(default=1209600) # Two weeks enabled = BooleanField(default=True) + invoice_email_address = CharField(null=True, index=True) def delete_instance(self, recursive=False, delete_nullable=False): # If we are deleting a robot account, only execute the subset of queries necessary. diff --git a/data/migrations/versions/471caec2cb66_add_invoice_email_address_to_user.py b/data/migrations/versions/471caec2cb66_add_invoice_email_address_to_user.py new file mode 100644 index 000000000..173f23624 --- /dev/null +++ b/data/migrations/versions/471caec2cb66_add_invoice_email_address_to_user.py @@ -0,0 +1,27 @@ +"""Add invoice email address to user + +Revision ID: 471caec2cb66 +Revises: 88e0f440a2f +Create Date: 2015-12-28 13:57:17.761334 + +""" + +# revision identifiers, used by Alembic. +revision = '471caec2cb66' +down_revision = '88e0f440a2f' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('invoice_email_address', sa.String(length=255), nullable=True)) + op.create_index('user_invoice_email_address', 'user', ['invoice_email_address'], unique=False) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'invoice_email_address') + ### end Alembic commands ### diff --git a/data/model/user.py b/data/model/user.py index d05ea1693..85f08d220 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -125,7 +125,13 @@ def change_username(user_id, new_username): return user -def change_invoice_email(user, invoice_email): +def change_invoice_email_address(user, invoice_email_address): + # Note: We null out the address if it is an empty string. + user.invoice_email_address = invoice_email_address or None + user.save() + + +def change_send_invoice_email(user, invoice_email): user.invoice_email = invoice_email user.save() diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 818851caf..db5e1a67f 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -44,6 +44,7 @@ def org_view(o, teams): if is_admin: view['invoice_email'] = o.invoice_email + view['invoice_email_address'] = o.invoice_email_address return view @@ -119,6 +120,10 @@ class Organization(ApiResource): 'type': 'boolean', 'description': 'Whether the organization desires to receive emails for invoices', }, + 'invoice_email_address': { + 'type': 'string', + 'description': 'The email address at which to receive invoices', + }, 'tag_expiration': { 'type': 'integer', 'maximum': 2592000, @@ -159,7 +164,13 @@ class Organization(ApiResource): org_data = request.get_json() if 'invoice_email' in org_data: logger.debug('Changing invoice_email for organization: %s', org.username) - model.user.change_invoice_email(org, org_data['invoice_email']) + model.user.change_send_invoice_email(org, org_data['invoice_email']) + + if ('invoice_email_address' in org_data and + org_data['invoice_email_address'] != org.invoice_email_address): + new_email = org_data['invoice_email_address'] + logger.debug('Changing invoice email address for organization: %s', org.username) + model.user.change_invoice_email_address(org, new_email) if 'email' in org_data and org_data['email'] != org.email: new_email = org_data['email'] diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 1801598f7..1319fd2d6 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -109,6 +109,7 @@ def user_view(user): 'email': user.email, 'logins': [login_view(login) for login in logins], 'invoice_email': user.invoice_email, + 'invoice_email_address': user.invoice_email_address, 'preferred_namespace': not (user.stripe_id is None), 'tag_expiration': user.removed_tag_expiration_s, }) @@ -195,6 +196,10 @@ class User(ApiResource): 'type': 'string', 'description': 'The user\'s username', }, + 'invoice_email_address': { + 'type': 'string', + 'description': 'Custom email address for receiving invoices', + } }, }, 'UserView': { @@ -283,12 +288,16 @@ class User(ApiResource): if 'invoice_email' in user_data: logger.debug('Changing invoice_email for user: %s', user.username) - model.user.change_invoice_email(user, user_data['invoice_email']) + model.user.change_send_invoice_email(user, user_data['invoice_email']) if 'tag_expiration' in user_data: logger.debug('Changing user tag expiration to: %ss', user_data['tag_expiration']) model.user.change_user_tag_expiration(user, user_data['tag_expiration']) + if ('invoice_email_address' in user_data and + user_data['invoice_email_address'] != user.invoice_email_address): + model.user.change_invoice_email_address(user, user_data['invoice_email_address']) + if 'email' in user_data and user_data['email'] != user.email: new_email = user_data['email'] if model.user.find_user_by_email(new_email): diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 1b3d23f23..cb13f9fd9 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -37,7 +37,7 @@ def stripe_webhook(): invoice = stripe.Invoice.retrieve(invoice_id) if invoice: invoice_html = renderInvoiceToHtml(invoice, user) - send_invoice_email(user.email, invoice_html) + send_invoice_email(user.invoice_email_address or user.email, invoice_html) elif event_type.startswith('customer.subscription.'): cust_email = user.email if user is not None else 'unknown@domain.com' diff --git a/static/css/directives/ui/billing-options.css b/static/css/directives/ui/billing-options.css new file mode 100644 index 000000000..3e0f4ee06 --- /dev/null +++ b/static/css/directives/ui/billing-options.css @@ -0,0 +1,15 @@ +.billing-options .settings-option { + padding: 4px; + font-size: 18px; + margin-bottom: 10px; +} + +.billing-options .settings-option label { + margin-left: 6px; +} + +.billing-options .settings-option .settings-description { + font-size: 16px; + color: #888; + padding-left: 26px; +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index b58bac9fd..904c45aeb 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -506,21 +506,6 @@ i.toggle-icon:hover { vertical-align: middle; } -.settings-option { - padding: 4px; - font-size: 18px; - margin-bottom: 10px; -} - -.settings-option label { - margin-left: 6px; -} - -.settings-option .settings-description { - font-size: 12px; - color: #aaa; -} - .organization-header-element { padding: 20px; margin-bottom: 20px; diff --git a/static/directives/billing-options.html b/static/directives/billing-options.html index aa9089f03..9428e4258 100644 --- a/static/directives/billing-options.html +++ b/static/directives/billing-options.html @@ -31,7 +31,7 @@
- Billing Options + Billing Receipts
@@ -39,7 +39,7 @@
- If checked, a receipt email will be sent to {{ obj.email }} on every successful charge + If checked, a receipt email will be sent to {{ obj.invoice_email_address || obj.email }} on every successful charge
diff --git a/static/js/directives/ui/billing-options.js b/static/js/directives/ui/billing-options.js index 4400d527b..d082310c1 100644 --- a/static/js/directives/ui/billing-options.js +++ b/static/js/directives/ui/billing-options.js @@ -34,6 +34,23 @@ angular.module('quay').directive('billingOptions', function () { return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */); }; + $scope.changeInvoiceEmailAddress = function() { + bootbox.prompt('Enter the email address for receiving receipts', function(email) { + // Copied from Angular. + var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; + if (!email || !EMAIL_REGEXP.test(email)) { + return; + } + + $scope.obj['invoice_email_address'] = email; + + var errorHandler = ApiService.errorDisplay('Could not change user details'); + ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) { + $scope.working = false; + }, errorHandler); + }); + }; + $scope.changeCard = function() { var previousCard = $scope.currentCard; $scope.changingCard = true; diff --git a/test/data/test.db b/test/data/test.db index 89268f9eb..1738e57c4 100644 Binary files a/test/data/test.db and b/test/data/test.db differ diff --git a/tools/emailinvoice.py b/tools/emailinvoice.py index 9c0096f59..436fdc92f 100644 --- a/tools/emailinvoice.py +++ b/tools/emailinvoice.py @@ -26,8 +26,8 @@ def sendInvoice(invoice_id): with app.app_context(): invoice_html = renderInvoiceToHtml(invoice, user) - send_invoice_email(user.email, invoice_html) - print 'Invoice sent to %s' % (user.email) + send_invoice_email(user.invoice_email_address or user.email, invoice_html) + print 'Invoice sent to %s' % (user.invoice_email_address or user.email) parser = argparse.ArgumentParser(description='Email an invoice') parser.add_argument('invoice_id', help='The invoice ID')