saas-rs-sdk 0.6.3

The SaaS RS SDK
use super::PROVIDER_KEY;
use super::StripePaymentProcessor;
use super::sync_plan_costs;
use crate::models::metadata;
use crate::{payments::Error, pbbson::Model};
use change_case::pascal_case;
use pbbson::bson::Bson;
use std::collections::HashMap;
use std::str::FromStr;
use stripe_product::ProductType;
use stripe_product::product::{CreateProduct, Features, RetrieveProduct};

pub(super) async fn sync<B: Clone + FromStr + Send + Sync + ToString>(
    this: &StripePaymentProcessor<B>,
    plan: &Model,
    plan_metadata_field: &str,
    services_bucket: B,
    plans_bucket: B,
) -> Result<Model, Error> {
    // Find existing Stripe product for service plan
    if let Some(product_id) = metadata::find_string(plan, plan_metadata_field, PROVIDER_KEY, "productId") {
        let req = RetrieveProduct::new(product_id);
        match req.send(&this.client).await {
            Ok(_product) => return Ok(plan.clone()),
            Err(_e) => {
                // TODO rethrow everything but not found
            }
        }
    }

    // Create a Stripe product for service plan
    let service = this
        .config_store
        .find(services_bucket, plan.get_str("serviceId")?)
        .await?;
    let service_display_name = get_service_display_name(&service)?;
    let plan_display_name = get_plan_display_name(plan)?;
    let name = format!("{service_display_name} - {plan_display_name}",);
    let description = match plan.get_str("description") {
        Ok(description) => description,
        Err(_) => &plan_display_name,
    };
    let service_id = service.id()?;
    let plan_id = plan.id()?;
    let mut metadata = HashMap::from([
        ("serviceId".to_string(), service_id.clone()),
        ("planId".to_string(), plan_id.clone()),
    ]);
    if let Some(service_name) = service.get("name") {
        metadata.insert("serviceName".to_string(), service_name.to_string());
    }
    if let Some(plan_name) = plan.get("name") {
        metadata.insert("planName".to_string(), plan_name.to_string());
    }
    let req = CreateProduct::new(name)
        .active(true)
        .description(description)
        .marketing_features(get_marketing_features_from_bullets(plan))
        .metadata(metadata)
        .type_(ProductType::Service);
    let product = req.send(&this.client).await?;
    log::info!(plan_id, stripe_product_id = product.id.as_str(); "Created Stripe product");

    // Store Stripe product id in plan metadata
    let mut plan = plan.clone();
    metadata::set(
        &mut plan,
        plan_metadata_field,
        PROVIDER_KEY,
        "productId",
        Bson::String(product.id.to_string()),
    );

    // Sync costs
    sync_plan_costs::sync(this, &plan, plan_metadata_field).await?;

    // Store updated plan
    plan = this.config_store.update(plans_bucket, plan.clone()).await?;
    let plan_id = plan.id()?;
    log::info!(plan_id, stripe_product_id = product.id.as_str(); "Updated plan metadata to reference Stripe product");

    Ok(plan.clone())
}

fn get_marketing_features_from_bullets(plan: &Model) -> Vec<Features> {
    if let Some(Bson::Document(metadata)) = plan.get("metadata")
        && let Some(Bson::Array(bullets)) = metadata.get("bullets")
    {
        return bullets
            .iter()
            .map(|bullet| {
                Features::new(match bullet {
                    Bson::String(s) => s.clone(),
                    _ => bullet.to_string(),
                })
            })
            .collect();
    }
    vec![]
}

fn get_service_display_name(service: &Model) -> Result<String, Error> {
    Ok(match service.get("metadata") {
        Some(Bson::Document(metadata)) => match metadata.get_str("displayName") {
            Ok(display_name) => display_name.to_string(),
            _ => pascal_case(service.get_str("name")?),
        },
        _ => pascal_case(service.get_str("name")?),
    })
}

fn get_plan_display_name(plan: &Model) -> Result<String, Error> {
    Ok(pascal_case(plan.get_str("name")?))
}