jcloude/press/api/saas.py
2025-12-23 21:34:08 +08:00

381 lines
10 KiB
Python

import json
from typing import TYPE_CHECKING
import jingrow
from jingrow.core.utils import find
from jcloude.api.account import get_account_request_from_key
from jcloude.jcloude.pagetype.site.erpnext_site import get_erpnext_domain
from jcloude.jcloude.pagetype.site.saas_pool import get as get_pooled_saas_site
from jcloude.jcloude.pagetype.site.saas_site import (
SaasSite,
get_default_team_for_app,
get_saas_domain,
get_saas_site_plan,
set_site_in_subscription_docs,
)
from jcloude.jcloude.pagetype.team.team import Team
from jcloude.utils import log_error
from jcloude.utils.telemetry import capture, identify
if TYPE_CHECKING:
from jcloude.jcloude.pagetype.site.site import Site
# ----------------------------- SIGNUP APIs ---------------------------------
@jingrow.whitelist(allow_guest=True)
def account_request(
subdomain,
email,
first_name,
last_name,
country,
app,
url_args=None,
):
"""
return: Stripe setup intent and AR key if stripe flow, else None
"""
from jingrow.utils.html_utils import clean_html
email = email.strip().lower()
jingrow.utils.validate_email_address(email, True)
if not check_subdomain_availability(subdomain, app):
jingrow.throw(f"Subdomain {subdomain} is already taken")
all_countries = jingrow.db.get_all("Country", pluck="name")
country = find(all_countries, lambda x: x.lower() == country.lower())
if not country:
jingrow.throw("Country field should be a valid country name")
team = jingrow.db.get_value("Team", {"user": email})
if team and jingrow.db.exists("Invoice", {"team": team, "status": "Unpaid", "type": "Subscription"}):
jingrow.throw(f"Account {email} already exists with unpaid invoices")
current_user = jingrow.session.user
try:
jingrow.set_user("Administrator")
account_request = jingrow.get_pg(
{
"pagetype": "Account Request",
"saas": True,
"saas_app": app,
"erpnext": False,
"subdomain": subdomain,
"email": email,
"role": "Jcloude Admin",
"first_name": clean_html(first_name),
"last_name": clean_html(last_name),
"country": country,
"url_args": url_args or json.dumps({}),
"send_email": True,
}
)
site_name = account_request.get_site_name()
identify(
site_name,
app=account_request.saas_app,
source=json.loads(url_args).get("source") if url_args else "fc",
)
account_request.insert(ignore_permissions=True)
capture("completed_server_account_request", "fc_product_trial", site_name)
except Exception as e:
log_error("Account Request Creation Failed", data=e)
raise
finally:
jingrow.set_user(current_user)
create_or_rename_saas_site(app, account_request)
def create_or_rename_saas_site(app, account_request):
"""
Creates site for Saas App. These are differentiated by `standby_for` field in site pg
"""
current_user = jingrow.session.user
current_session_data = jingrow.session.data
jingrow.set_user("Administrator")
try:
enable_hybrid_pools = jingrow.db.get_value("Saas Settings", app, "enable_hybrid_pools")
hybrid_saas_pool = get_hybrid_saas_pool(account_request) if enable_hybrid_pools else ""
pooled_site = get_pooled_saas_site(app, hybrid_saas_pool)
if pooled_site:
SaasSite(site=pooled_site, app=app).rename_pooled_site(account_request)
else:
saas_site = SaasSite(
account_request=account_request, app=app, hybrid_saas_pool=hybrid_saas_pool
).insert(ignore_permissions=True)
set_site_in_subscription_docs(saas_site.subscription_docs, saas_site.name)
capture("completed_server_site_created", "fc_product_trial", account_request.get_site_name())
except Exception as e:
log_error("Saas Site Creation or Rename failed", data=e)
finally:
jingrow.set_user(current_user)
jingrow.session.data = current_session_data
@jingrow.whitelist()
def new_saas_site(subdomain, app):
jingrow.only_for("System Manager")
pooled_site = get_pooled_saas_site(app)
if pooled_site:
site = SaasSite(site=pooled_site, app=app).rename_pooled_site(subdomain=subdomain)
else:
site = SaasSite(app=app, subdomain=subdomain).insert(ignore_permissions=True)
site.create_subscription(get_saas_site_plan(app))
site.reload()
site.team = get_default_team_for_app(app)
site.save(ignore_permissions=True)
jingrow.db.commit()
return site
@jingrow.whitelist()
def get_saas_site_status(site):
if jingrow.db.exists("Site", site):
return {"site": site, "status": jingrow.db.get_value("Site", site, "status")}
return {"site": site, "status": "Pending"}
def get_hybrid_saas_pool(account_request):
"""
1. Get all hybrid pools and their rules
2. Filter based on rules and return Hybrid pool
3. Returns the first rule match
return: The hybrid pool name that site belongs to based on the Account Request
conditions
"""
hybrid_pool = ""
all_pools = jingrow.get_all("Hybrid Saas Pool", {"app": account_request.saas_app}, pluck="name")
ar_rules = jingrow.get_all(
"Account Request Rules",
{"parent": ("in", all_pools)},
["parent", "field", "condition", "value"],
group_by="parent",
)
for rule in ar_rules:
eval_locals = eval_locals = dict(
account_request=account_request,
)
if jingrow.safe_eval(
f"account_request.{rule.field} {rule.condition} '{rule.value}'", None, eval_locals
):
hybrid_pool = rule.parent
return hybrid_pool # noqa: RET504
return hybrid_pool
@jingrow.whitelist(allow_guest=True)
def check_subdomain_availability(subdomain, app):
"""
Checks if subdomain is available to create a new site
"""
# Only for ERPNext domains
if len(subdomain) <= 4:
return False
banned_domains = jingrow.get_all("Blocked Domain", {"block_for_all": 1}, pluck="name")
if banned_domains and subdomain in banned_domains:
return False
exists = bool(
jingrow.db.exists("Blocked Domain", {"name": subdomain, "root_domain": get_erpnext_domain()})
or jingrow.db.exists(
"Site",
{
"subdomain": subdomain,
"domain": get_saas_domain(app),
"status": ("!=", "Archived"),
},
)
)
if exists:
return False
return True
@jingrow.whitelist(allow_guest=True)
def setup_account(key, business_data=None):
"""
Includes the data collection step in setup-account.html
"""
account_request = get_account_request_from_key(key)
if not account_request:
jingrow.throw("Invalid or Expired Key")
capture(
"init_server_setup_account",
"fc_product_trial",
account_request.get_site_name(),
)
jingrow.set_user("Administrator")
if business_data:
business_data = jingrow.parse_json(business_data)
if isinstance(business_data, dict):
business_data = {
key: business_data.get(key)
for key in [
"company",
"no_of_employees",
"industry",
"no_of_users",
"designation",
"phone_number",
"referral_source",
"agreed_to_partner_consent",
]
}
account_request.update(business_data)
account_request.save(ignore_permissions=True)
create_marketplace_subscription(account_request)
capture(
"completed_server_setup_account",
"fc_product_trial",
account_request.get_site_name(),
)
@jingrow.whitelist(allow_guest=True)
def headless_setup_account(key):
"""
Ignores the data collection step in setup-account.html
"""
account_request = get_account_request_from_key(key)
if not account_request:
jingrow.throw("Invalid or Expired Key")
capture(
"init_server_setup_account",
"fc_product_trial",
account_request.get_site_name(),
)
jingrow.set_user("Administrator")
create_marketplace_subscription(account_request)
# create team and enable the subscriptions for site
capture(
"completed_server_setup_account",
"fc_product_trial",
account_request.get_site_name(),
)
jingrow.local.response["type"] = "redirect"
jingrow.local.response["location"] = f"/prepare-site?key={key}&app={account_request.saas_app}"
def create_marketplace_subscription(account_request):
"""
Create team, subscription for site and Saas Subscription
"""
team_pg = create_team(account_request)
site_name = jingrow.db.get_value("Site", {"account_request": account_request.name})
if site_name:
jingrow.db.set_value("Site", site_name, "team", team_pg.name)
subscription = jingrow.db.exists("Subscription", {"document_name": site_name})
if subscription:
jingrow.db.set_value("Subscription", subscription, "team", team_pg.name)
marketplace_subscriptions = jingrow.get_all(
"Subscription",
{"document_type": "Marketplace App", "site": site_name, "enabled": 0},
pluck="name",
)
for subscription in marketplace_subscriptions:
jingrow.db.set_value(
"Subscription",
subscription,
{"enabled": 1, "team": team_pg.name},
)
jingrow.set_user(team_pg.user)
jingrow.local.login_manager.login_as(team_pg.user)
return site_name
def create_team(account_request, get_stripe_id=False):
"""
Create team and return pg
"""
email = account_request.email
if not jingrow.db.exists("Team", {"user": email}):
team_pg = Team.create_new(
account_request,
account_request.first_name,
account_request.last_name,
country=account_request.country,
is_us_eu=account_request.is_us_eu,
via_erpnext=True,
user_exists=jingrow.db.exists("User", email),
)
else:
team_pg = jingrow.get_pg("Team", {"user": email})
if get_stripe_id:
return team_pg.stripe_customer_id
return team_pg
@jingrow.whitelist(allow_guest=True)
def get_site_status(key, app=None):
"""
return: Site status
"""
account_request = get_account_request_from_key(key)
if not account_request:
jingrow.throw("Invalid or Expired Key")
domain = get_saas_domain(app) if app else get_erpnext_domain()
site = jingrow.db.get_value(
"Site",
{"subdomain": account_request.subdomain, "domain": domain, "status": "Active"},
["status", "subdomain", "name"],
as_dict=1,
)
if site:
capture("completed_site_allocation", "fc_product_trial", site.name)
return site
return {"status": "Pending"}
@jingrow.whitelist(allow_guest=True)
def get_site_url_and_sid(key, app=None):
"""
return: Site url and session id for login-redirect
"""
account_request = get_account_request_from_key(key)
if not account_request:
jingrow.throw("Invalid or Expired Key")
domain = get_saas_domain(app) if app else get_erpnext_domain()
name = jingrow.db.get_value("Site", {"subdomain": account_request.subdomain, "domain": domain})
site: "Site" = jingrow.get_pg("Site", name)
if site.additional_system_user_created:
return site.login_as_team()
return site.login_as_admin()