import stripe from internal.commons import STRIPE_SECRET_NAME from internal.helpers import get_secret, rds_query from management.helpers import tax_type_for_country import logging log = logging.getLogger().getChild("management.subscriptions") log.setLevel(logging.INFO) try: stripe_secret = get_secret(STRIPE_SECRET_NAME) stripe.api_key = stripe_secret['secret_key'] sign_secret = stripe_secret['sign_secret'] except Exception as e: log.exception("failed to set stripe api key", exc_info=e) def stripe_create_customer(user_id, email, name): """ Creates stripe customer for particular email. Or retrieves existing account. :param user_id: :param email: :param name: :return: stripe_customer_id """ # just in case check, that we don't already have it created and lost or smth. for existing in stripe.Customer.list(email=email).auto_paging_iter(): # got existing customer return existing['id'] return stripe.Customer.create(email=email, name=name, metadata={'user_id': user_id})['id'] def stripe_delete_customer(customer_id): try: return stripe.Customer.delete(sid=customer_id)['deleted'] except Exception as e: log.exception("Deleting Stripe customer failed", exc_info=e) return None def stripe_update_customer(customer_id, **kw): """ Can modify existing customer, including changing email and whatevs. :param stripe_customer_id: :param kw: :return: """ try: return stripe.Customer.modify(sid=customer_id, **kw) except Exception as e: log.exception("Updating Stripe customer failed", exc_info=e) return None def stripe_update_billing(customer_id, payment_method_id, billing_info): if isinstance(billing_info, dict) and any(billing_info.values()): billing_details = { "address": { "city": billing_info['city'] if 'city' in billing_info and billing_info['city'] is not None else '', "country": billing_info['country'] if 'country' in billing_info and billing_info['country'] is not None else '', "line1": billing_info['line1'] if 'line1' in billing_info and billing_info['line1'] is not None else '', "line2": billing_info['line2'] if 'line2' in billing_info and billing_info['line2'] is not None else '', "postal_code": billing_info['postalCode'] if 'postalCode' in billing_info and billing_info['postalCode'] is not None else '', "state": billing_info['state'] if 'state' in billing_info and billing_info['state'] is not None else '' }, "email": billing_info['email'] if 'email' in billing_info and billing_info['email'] is not None else '', "name": billing_info['name'] if 'name' in billing_info and billing_info['name'] else billing_info['company'] if 'company' in billing_info and billing_info['company'] else '', } # don't set params, if they are empty if 'phone' in billing_info and billing_info['phone']: billing_details['phone'] = billing_info['phone'] stripe.PaymentMethod.modify(sid=payment_method_id, billing_details=billing_details) stripe.Customer.modify(sid=customer_id, name=f"""{billing_info['company']}, {billing_info['name']}""" if 'name' in billing_info and 'company' in billing_info and billing_info['company'] and billing_info['name'] else billing_info['name'] if 'name' in billing_info and billing_info['name'] else billing_info['company'] if 'company' in billing_info and billing_info['company'] else '') def stripe_update_payment_billing(customer_id, payment_method_id, billing_info=None): """ input BillingInformationInput { phone: String email: String name: String company: String vat: String line1: String line2: String city: String state: String country: String postalCode: String } :param customer_id: :param payment_method_id: :param billing_info: :return: """ if payment_method_id: stripe.PaymentMethod.attach(sid=payment_method_id, customer=customer_id) stripe.Customer.modify(sid=customer_id, invoice_settings={'default_payment_method': payment_method_id}) stripe_update_billing(customer_id, payment_method_id, billing_info) def stripe_create_subscription(customer_id, payment_method_id, stripe_price_id, start_date): sub = stripe.Subscription.create(customer=customer_id, items=[{'price': stripe_price_id}], expand=['latest_invoice.payment_intent'] ) log.info(f'stripe_create_subscription: {sub}') return sub def stripe_subscription(sub_id, expand=True): if expand: return stripe.Subscription.retrieve(sub_id, expand=['latest_invoice.payment_intent'] ) else: return stripe.Subscription.retrieve(sub_id) def stripe_schedule(sched_id): return stripe.SubscriptionSchedule.retrieve(sched_id) def stripe_update_subscription(sub, price, start_date): if sub['schedule']: schedule = stripe_schedule(sub['schedule']) else: if sub['cancel_at_period_end']: stripe.Subscription.modify(sid=sub['id'], cancel_at_period_end=False) schedule = stripe.SubscriptionSchedule.create(from_subscription=sub['id']) # keep phases, that are ongoing before starting new phase. newphases = [schedule['phases'][0]] #newphases = [phase for phase in schedule['phases'] if phase['start_date'] < start_date.timestamp()] # actually means we have to modify the subscription directly? if newphases[-1]['end_date'] > start_date.timestamp(): log.info(f"MODIFY SUBSCRIPTION DIRECTLY") #release schedule stripe.SubscriptionSchedule.release(schedule['id']) if sub['cancel_at_period_end']: stripe.Subscription.modify(sid=sub['id'], cancel_at_period_end=False) stripe.Subscription.modify(sub['id'], payment_behavior='pending_if_incomplete', proration_behavior='always_invoice', items=[{ 'id': sub['items']['data'][0]['id'], 'price': price['stripe_price_id'], }], proration_date=start_date) return stripe_subscription(sub['id']) #newphases[-1]['end_date'] = start_date # add new phase newphases[-1]['end_date'] = start_date # there can't be a gap. newphases.append({'start_date': start_date, 'plans': [ {'plan': price['stripe_price_id'], 'price': price['stripe_price_id']}]}) # modify subscription if sub['cancel_at_period_end']: stripe.SubscriptionSchedule.release(schedule['id']) stripe.Subscription.modify(sid=sub['id'], cancel_at_period_end=False) schedule = stripe.SubscriptionSchedule.create(from_subscription=sub['id']) log.info(f"MODIFY SCHEDULE {newphases}") schedule.modify(sid=schedule['id'], phases=newphases) return stripe_subscription(sub['id']) def stripe_delete_subscription(sub_id): return stripe.Subscription.delete(sub_id) def stripe_cancel_subscription_schedule(sub_id): sub = stripe_subscription(sub_id) res = False if sub['schedule']: stripe.SubscriptionSchedule.release(sub['schedule']) res = True if sub['pending_update']: stripe.Invoice.void_invoice(sub['latest_invoice']) res = True return res def stripe_cancel_subscription(sub_id): stripe_cancel_subscription_schedule(sub_id) return stripe.Subscription.modify(sid=sub_id, cancel_at_period_end=True) def stripe_invoices_list(customer_id): return stripe.Invoice.list(customer=customer_id).auto_paging_iter() def stripe_customer_id(user_id, management_schema): """ Retrieves and/or creates stripe customer id. :param user_id: :param management_schema: :return: """ for company in rds_query(f"SELECT stripe_customer_id FROM {management_schema}.company"): if company['stripe_customer_id'] is not None: return company['stripe_customer_id'] else: for user in rds_query(f"SELECT user_id, email, user_name \"name\" FROM commons.user_company " f"WHERE user_id = %(user_id)s", {'user_id': user_id}): customer_id = stripe_create_customer(**user) # store the id try: rds_query([f"INSERT INTO commons.stripe_customer (stripe_id, company_id) VALUES (%(sid)s, %(company_id)s ) " f"ON CONFLICT ON CONSTRAINT stripe_customer_unique_stripe_id " f"DO UPDATE SET company_id = excluded.company_id ", f"UPDATE {management_schema}.company SET stripe_customer_id = %(sid)s"], {'sid': customer_id, 'company_id': management_schema}) return customer_id except Exception as e: log.exception('sstripe_customer_id creation race condition', exc_info=e) return stripe_customer_id(user_id, management_schema) raise RuntimeError("Failed to retrieve stripe id") def stripe_payment_method(pid): return stripe.PaymentMethod.retrieve(id=pid) def stripe_customer(cid): return stripe.Customer.retrieve(id=cid) def stripe_invoice(iid, expand=True): if expand: return stripe.Invoice.retrieve(id=iid, expand=['payment_intent']) else: return stripe.Invoice.retrieve(id=iid) def construct_stripe_event(payload, sig_header): return stripe.Webhook.construct_event(payload, sig_header, sign_secret) def create_setup_intent(customer_id, payment_id): return stripe.SetupIntent.create(customer=customer_id, payment_method=payment_id) def confirm_setup_intent(sid): return stripe.SetupIntent.confirm(sid=sid) def confirm_payment_intent(sid): return stripe.PaymentIntent.confirm(sid=sid) def stripe_update_payment_intent(payment_intent_id, **kw): return stripe.PaymentIntent.modify(payment_intent_id, **kw) def stripe_add_customer_vat(customer_id, country, vat): try: tax_type = tax_type_for_country(country) if tax_type: return stripe.Customer.create_tax_id( customer_id, type=tax_type, value=vat, ) else: return None except Exception as e: log.exception("NO TAX ID TYPE FOR COUNTRY") return None def stripe_delete_customer_vat(customer_id, vat_id): return stripe.Customer.delete_tax_id(customer_id, vat_id)