saas-rs-sdk 0.6.0

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 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
    let req = RetrieveProduct::new(plan.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").map_err(|e| Error::internal(e.to_string()))?,
        )
        .await
        .map_err(|e| Error::internal(e.to_string()))?;
    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 service_id = service.id().map_err(|e| Error::internal(e.to_string()))?;
    let plan_id = plan.id().map_err(|e| Error::internal(e.to_string()))?;
    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(&plan_display_name)
        .id(plan_id.clone())
        .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();
    let plan_has_changed = {
        if let Some(product_id) = metadata::find_string(&plan, plan_metadata_field, PROVIDER_KEY, "productId")
            && product_id == product.id.as_str()
        {
            true
        } else {
            metadata::set(
                &mut plan,
                plan_metadata_field,
                PROVIDER_KEY,
                "productId",
                Bson::String(product.id.to_string()),
            );
            true
        }
    };

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

    if plan_has_changed {
        let new_plan = this
            .config_store
            .update(plans_bucket, plan.clone())
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        new_plan.clone_into(&mut plan);
        let plan_id = plan.id().map_err(|e| Error::internal(e.to_string()))?;
        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(),
            _ => service
                .get_str("name")
                .map_err(|e| Error::internal(e.to_string()))?
                .to_string(),
        },
        _ => service
            .get_str("name")
            .map_err(|e| Error::internal(e.to_string()))?
            .to_string(),
    })
}

fn get_plan_display_name(plan: &Model) -> Result<String, Error> {
    Ok(match plan.get_str("description") {
        Ok(description) => description.to_string(),
        _ => plan
            .get_str("name")
            .map_err(|e| Error::internal(e.to_string()))?
            .to_string(),
    })
}