import json from internal.helpers import get_user_id, rds_query, get_email import datetime import logging from management.helpers import send_subscription_canceled_email from management.packages import get_package_price, find_price_by_id, sync_subscription, get_current_package from management.subscriptions import stripe_create_customer, stripe_invoices_list, stripe_customer_id, stripe_customer, \ stripe_payment_method, stripe_create_subscription, stripe_update_payment_billing, stripe_invoice, \ stripe_delete_subscription, stripe_subscription, stripe_update_subscription, stripe_cancel_subscription, \ create_setup_intent, confirm_setup_intent, confirm_payment_intent, stripe_update_payment_intent, \ stripe_update_billing, stripe_cancel_subscription_schedule, stripe_add_customer_vat, stripe_delete_customer_vat, \ stripe_schedule log = logging.getLogger().getChild("management.users") log.setLevel(logging.INFO) def get_current_user(endpoint, req, event): user_id = get_user_id(event) for user in rds_query(f"SELECT email, user_name, company_name FROM commons.user_company WHERE user_id = %(user_id)s", {'user_id': user_id}): try: name_parts = user['user_name'].split(' ') except Exception as e: log.exception('BROKEN NAME', exc_info=e) name_parts = ['', ''] return { 'id': user_id[:13], 'UID': user_id, 'email': user['email'], 'name': user['user_name'], 'firstName': ' '.join(name_parts[:-1]), 'lastName': name_parts[-1], 'companyName': user['company_name'] } else: error = RuntimeError('No user record') log.exception("get_current_user: DATABASE BROKEN") raise error def update_current_user(endpoint, req, event): # validate in_fields = {} if 'input' in req: inp = req['input'] for field in ['firstName', 'lastName', 'companyName']: if field in inp and inp[field]: in_fields[field] = inp[field] if len(in_fields) != 3: raise RuntimeError('Please make sure first, last and company name are filled in properly.') # Update sql tables update_fields = { 'user_name': f"{in_fields['firstName']} {in_fields['lastName']}", 'company_name': in_fields['companyName'] } update_query = f"UPDATE commons.user_company SET {','.join([f'{q} = %({q})s' for q in update_fields])} " \ f"WHERE user_id = %(user_id)s" user_id = get_user_id(event) update_fields['user_id'] = user_id rds_query(update_query, update_fields) return get_current_user(endpoint, req, event) def get_stripe_invoice(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) invoice_id = req['invoiceId'] return stripe_invoice(invoice_id) def get_stripe_invoices(endpoint, req, event): """ type StripeInvoice { created: AWSDate number: String pdf_url: String status: String paid: Boolean } :param endpoint: :param req: :param event: :return: """ user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) return [{'id': invoice['id'], 'created': str(datetime.date.fromtimestamp(invoice['created'])), 'number': invoice['number'], 'pdfUrl': invoice['invoice_pdf'], 'status': invoice['status'], 'paid': invoice['paid']} for invoice in stripe_invoices_list(customer_id)] def get_billing_info(endpoint, req, event): """ :param endpoint: :param req: :param event: :return: """ management_schema = req['management_schema'] for company in rds_query(f"SELECT billing_information FROM {management_schema}.company"): return company['billing_information'] if company['billing_information'] else { 'company': '', 'phone': '', 'vat': '', 'email': '', 'name': '', 'line1': '', 'line2': '', 'city': '', 'state': '', 'country': '', 'postalCode': '' } e = RuntimeError('Company record missing') log.exception('COMPANY RECORD MISSING', exc_info=e) return {} def update_billing_info(endpoint, req, event): management_schema = req['management_schema'] rds_query(f"UPDATE {management_schema}.company SET billing_information = %(billing_info)s", {'billing_info': json.dumps(req['billilingInformation'])}) req['billilingInformation']['__typename'] = 'BillingInformation' billing_info = get_billing_info(endpoint, req, event) user_id = get_user_id(event) customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) pm_id = customer['invoice_settings']['default_payment_method'] \ if 'default_payment_method' in customer['invoice_settings'] else None if pm_id: stripe_update_billing(customer_id, pm_id, billing_info) try: # check VAT tax_id = None for tax_obj in customer['tax_ids']['data']: if tax_obj['value'] == billing_info['vat']: tax_id = tax_obj else: stripe_delete_customer_vat(tax_obj['customer'], tax_obj['id']) if not tax_id and 'vat' in billing_info and billing_info['vat']: stripe_add_customer_vat(customer_id, billing_info['country'], billing_info['vat']) except Exception as e: log.exception("ERROR SETTING UP TAX ID", exc_info=e) return req['billilingInformation'] def update_payment_method(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) payment_id = req['stripePaymentId'] billing_info = get_billing_info(endpoint, req, event) stripe_update_payment_billing(customer_id, payment_id, billing_info) pm = stripe_payment_method(payment_id) return { 'id': payment_id, 'brand': pm['card']['brand'], 'country': pm['card']['country'], 'expMonth': pm['card']['exp_month'], 'expYear': pm['card']['exp_year'], 'lastFour': pm['card']['last4'], 'created': f"{datetime.datetime.fromtimestamp(pm['created']).isoformat(timespec='microseconds')}Z" } def setup_payment_intent(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) payment_id = req['stripePaymentId'] si = create_setup_intent(customer_id, payment_id) si = confirm_setup_intent(si['id']) return {'status': si['status'], 'clientSecret': si['client_secret'], 'lastError': si['last_setup_error']} def get_stripe_payment_method(endpoint, req, event): """ type StripePaymentMethod { id: ID brand: String country: String expMonth: String expYear: String lastFour: String created: AWSDateTime } :return: """ user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) try: pm_id = customer['invoice_settings']['default_payment_method'] if pm_id is not None: pm = stripe_payment_method(pm_id) return { 'id': pm_id, 'brand': pm['card']['brand'], 'country': pm['card']['country'], 'expMonth': pm['card']['exp_month'], 'expYear': pm['card']['exp_year'], 'lastFour': pm['card']['last4'], 'created': f"{datetime.datetime.fromtimestamp(pm['created']).isoformat(timespec='microseconds')}Z" } except Exception as e: log.exception('PAYMENT METHOD ERROR', exc_info=e) return None def make_subscription_result(sub, unconfirmed_setup_intent=None): if unconfirmed_setup_intent is not None: confirmed_setup_intent = confirm_setup_intent(unconfirmed_setup_intent['id']) return { 'subscriptionStatus': sub['status'], 'paymentIntentStatus': confirmed_setup_intent['status'], 'invoiceId': sub['latest_invoice']['id'], 'intentType': 'setup', 'clientSecret': confirmed_setup_intent['client_secret'], 'paymentIntentError': confirmed_setup_intent['last_setup_error']['message'] if confirmed_setup_intent['last_setup_error'] is not None and 'message' in confirmed_setup_intent['last_setup_error'] else confirmed_setup_intent['last_setup_error'], 'paymentMethodId': sub['latest_invoice']['payment_intent']['payment_method'] if sub['latest_invoice'] is not None and 'payment_intent' in sub['latest_invoice'] and sub['latest_invoice']['payment_intent'] is not None and 'payment_method' in sub['latest_invoice']['payment_intent'] else None } else: log.info(f'make_subscription_result: {sub}') return { 'subscriptionStatus': sub['status'], 'paymentIntentStatus': sub['latest_invoice']['payment_intent']['status'] if sub['latest_invoice'] and 'payment_intent' in sub['latest_invoice'] and sub['latest_invoice']['payment_intent'] and 'status' in sub['latest_invoice']['payment_intent'] else None, 'invoiceId': sub['latest_invoice']['id'], 'intentType': 'payment', 'clientSecret': sub['latest_invoice']['payment_intent']['client_secret'] if 'latest_invoice' in sub and sub['latest_invoice'] and 'payment_intent' in sub['latest_invoice'] and sub['latest_invoice']['payment_intent'] and 'client_secret' in sub['latest_invoice']['payment_intent'] and sub['latest_invoice']['payment_intent']['client_secret'] else None, 'paymentIntentError': sub['latest_invoice']['payment_intent']['last_payment_error']['message'] if 'latest_invoice' in sub and sub['latest_invoice'] and 'payment_intent' in sub['latest_invoice'] and sub['latest_invoice']['payment_intent'] and 'last_payment_error' in sub['latest_invoice']['payment_intent'] and sub['latest_invoice']['payment_intent']['last_payment_error'] and 'message' in sub['latest_invoice']['payment_intent']['last_payment_error'] else None, 'paymentMethodId': sub['latest_invoice']['payment_intent']['payment_method'] if sub['latest_invoice'] is not None and 'payment_intent' in sub['latest_invoice'] and sub['latest_invoice']['payment_intent'] is not None and 'payment_method' in sub['latest_invoice']['payment_intent'] else None } def cancel_stripe_subscription(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) for sub in customer['subscriptions']['data']: res = get_current_package(endpoint, req, event) stripe_cancel_subscription(sub['id']) send_subscription_canceled_email(get_email(event), res[0]['name'], datetime.date.fromisoformat(res[0]['nextPayment'])) return res else: raise RuntimeError('No subscription found') def cancel_stripe_subscription_schedule(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) for sub in customer['subscriptions']['data']: res = get_current_package(endpoint, req, event) if sub['schedule']: sched = stripe_schedule(sub['schedule']) packages = [find_price_by_id(phase['plans'][0]['price'])[0] for phase in sched['phases']] if len(packages) == 2 and packages[0]['campaign_name'] and packages[1]['name'] == packages[0]['resets_to_package']: raise RuntimeError("Can't cancel campaign end, please cancel whole subscription.") if stripe_cancel_subscription_schedule(sub['id']): return res[1:] else: raise RuntimeError('No schedule found') else: raise RuntimeError('No subscription found') def create_stripe_subscription(endpoint, req, event): """ createProductSubscription(stripePaymentId:String!, productPackageName:String!, billingCycle: String!) :param endpoint: :param req: :param event: :return: """ payment_id = req['stripePaymentId'] product_name = req['productPackageName'] billing_cycle = req['billingCycle'] user_id = get_user_id(event) management_schema = req['management_schema'] customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) billing_info = get_billing_info(endpoint, req, event) package, price = get_package_price(product_name, billing_cycle) price_id = price['stripe_price_id'] start_date = None new_payment_id = payment_id != customer['invoice_settings']['default_payment_method'] setup_intent = None campaign_code = package['campaign_name'] ## Special reset handling for dev for package in rds_query(f"SELECT package_name, prices FROM commons.packages " f"WHERE public = TRUE AND package_name = %(product_name)s", {'product_name': product_name}): if package['package_name'] == 'reset': for sub in customer['subscriptions']['data']: stripe_delete_subscription(sub['id']) rds_query(f"UPDATE {management_schema}.company " f"SET current_package_id = (SELECT id from commons.packages WHERE package_name = 'default'), " f"trial = FALSE, subscription_valid_until_date = NULL") return None if package['package_name'] == 'campaign': for sub in customer['subscriptions']['data']: stripe_delete_subscription(sub['id']) rds_query(f"UPDATE {management_schema}.company " f"SET current_package_id = (SELECT id from commons.packages WHERE package_name = 'default'), " f"trial = FALSE, subscription_valid_until_date = NULL, unused_campaign_code = 'EarlyAdopterCampaign'") return None for sub in customer['subscriptions']['data']: package, current_price = find_price_by_id(sub['plan']['id']) if sub['status'] == 'active': if current_price['stripe_price_id'] == price['stripe_price_id']: raise RuntimeError('Product already activated') if current_price['billingCycle'] == 'annual' and price['billingCycle'] == 'monthly': start_date = datetime.datetime.fromtimestamp(sub['current_period_end']) if new_payment_id: setup_intent = create_setup_intent(customer_id, payment_id) elif current_price['billingCycle'] == 'monthly' and price['billingCycle'] == 'annual': start_date = datetime.datetime.now() elif current_price['billingCycle'] == price['billingCycle']: if price['price'] > current_price['price']: start_date = datetime.datetime.now() else: start_date = datetime.datetime.fromtimestamp(sub['current_period_end']) if new_payment_id: setup_intent = create_setup_intent(customer_id, payment_id) stripe_update_payment_billing(customer_id, payment_id, billing_info) return make_subscription_result(sync_subscription(stripe_update_subscription(sub, price, start_date)), setup_intent) else: if current_price['stripe_price_id'] == price['stripe_price_id']: # retry current subscription latest_invoice = stripe_invoice(sub['latest_invoice']) if new_payment_id: stripe_update_payment_billing(customer_id, payment_id, billing_info) # also modify the payment of subscription payment intent stripe_update_payment_intent(latest_invoice['payment_intent']['id'], payment_method=payment_id) confirm_payment_intent(latest_invoice['payment_intent']['id']) return make_subscription_result(sync_subscription(stripe_subscription(sub['id']))) else: stripe_delete_subscription(sub['id']) # Actually create new dubscription if price_id and payment_id and billing_info and customer_id: # TODO: add company name to bill stripe_update_payment_billing(customer_id, payment_id, billing_info) sub = stripe_create_subscription(customer_id, payment_id, price_id, start_date) if campaign_code: #remove campaign code. rds_query(f"UPDATE {management_schema}.company SET unused_campaign_code = NULL") return make_subscription_result(sync_subscription(sub)) raise RuntimeError('Invalid parameters') def retry_stripe_subscription(endpoint, req, event): """ Retry works now by just doing the same thing again with create subscription and the retry condition will just be detected internally. :param endpoint: :param req: :param event: :return: """ raise RuntimeError('deprecated') def retry_stripe_invoice(endpoint, req, event): user_id = get_user_id(event) management_schema = req['management_schema'] invoice_id = req['invoiceId'] customer_id = stripe_customer_id(user_id, management_schema) customer = stripe_customer(customer_id) payment_id = req['stripePaymentId'] if 'stripePaymentId' in req and req['stripePaymentId'] is not None \ else customer['invoice_settings']['default_payment_method'] invoice = stripe_invoice(invoice_id) new_payment_id = payment_id != customer['invoice_settings']['default_payment_method'] try: # can go wrong, if in wrong state TODO: check state first? if new_payment_id: stripe_update_payment_billing(customer_id, payment_id, get_billing_info(endpoint, req, event)) stripe_update_payment_intent(invoice['payment_intent']['id'], payment_method=payment_id) confirm_payment_intent(invoice['payment_intent']['id']) except Exception as e: log.exception("Retry Invoice failed", exc_info=e) for sub in customer['subscriptions']['data']: return make_subscription_result(stripe_subscription(sub['id']))