Ai knowledge and logicHelpers

Recharge subscription helpers

Look up, cancel and reschedule Recharge subscriptions from Python, returning the raw Recharge subscription.

Plain helpers for the common Recharge subscription tasks. They read the business's Recharge connection with get_integration_credentials, call the Recharge API (2021-11) and return subscriptions as Recharge returns them; see the Recharge API reference for every field. They raise ValueError if Recharge isn't connected and requests.HTTPError if Recharge rejects the request.

HelperWhat it does
recharge_get_subscription(context, subscription_id)The subscription, or None.
recharge_find_subscriptions(context, email=None, phone=None)The customer's subscriptions. Phone numbers are matched through Octocom's Recharge sync, because Recharge can't search customers by phone.
recharge_cancel_subscription(context, subscription_id, reason="Octocom Dashboard")Cancels the subscription.
recharge_next_charge_date(subscription)The next charge as a UTC datetime, or None.
recharge_set_next_charge_date(context, subscription_id, when)Moves the next charge to when.
recharge_change_frequency(context, subscription_id, interval_months)Charges and ships every interval_months months.
recharge_apply_next_charge_discount(context, subscription_id, percentage)Creates a single-use percentage discount and puts it on the subscription's address, so it applies to the next charge.

Businesses moved off the built-in subscription actions get default actions with the built-in names (getSubscriptionById, getSubscriptionByEmail, cancelSubscription, skipSubscriptionMonths, ...) that call these helpers. They are ordinary Python actions you can read and change.

The full source is below. Copy it into your own action when you need something it doesn't do.


recharge_subscription_widget(context, cancel_action="cancel_subscription") returns a ready-to-render subscription panel with confirmation buttons. Pair it with recharge_cancel_widget_subscription(context) in a widget-owned action. Setup, business scoping and shared source.

Source

import secrets
from datetime import datetime, timezone

import requests

RECHARGE_API = "https://api.rechargeapps.com"


def _recharge_request(context, method, path, **kwargs):
    business = context.get("business") or {}
    slug = business.get("slug") or (context.get("conversation") or {}).get("businessSlug")
    if not slug:
        raise ValueError("The business is not available in the context")
    recharge = (get_integration_credentials(context).get(slug) or {}).get("recharge")
    if not recharge:
        raise ValueError("Recharge is not connected for this business")
    return requests.request(
        method,
        RECHARGE_API + path,
        headers={
            "X-Recharge-Version": "2021-11",
            "X-Recharge-Access-Token": recharge["apiToken"],
            "Accept": "application/json",
            "Content-Type": "application/json",
        },
        timeout=20,
        **kwargs,
    )


def recharge_get_subscription(context, subscription_id):
    """The Recharge subscription with this ID, or None."""
    subscription_id = str(subscription_id).strip().lstrip("#")
    if not subscription_id.isdigit():
        return None
    response = _recharge_request(context, "GET", f"/subscriptions/{subscription_id}")
    if response.status_code == 404:
        return None
    response.raise_for_status()
    return response.json().get("subscription")


def _recharge_customer_id_by_phone(context, phone):
    # Recharge can't search customers by phone; Octocom's Recharge sync indexes them.
    business = context.get("business") or {}
    slug = business.get("slug") or (context.get("conversation") or {}).get("businessSlug")
    response = requests.get(
        f"{OCTOCOM_API_BASE_URL}/rest/private/integrations/recharge/customer-id",
        params={"businessSlug": slug, "phone": phone},
        headers={"X-API-Key": OCTOCOM_ORG_API_KEY},
        timeout=20,
    )
    response.raise_for_status()
    return response.json().get("customerId")


def recharge_find_subscriptions(context, email=None, phone=None):
    """The customer's Recharge subscriptions, found by email or phone."""
    if email:
        response = _recharge_request(context, "GET", "/customers", params={"email": email.strip()})
        response.raise_for_status()
        customers = response.json().get("customers") or []
        customer_ids = [c["id"] for c in customers]
    else:
        customer_id = _recharge_customer_id_by_phone(context, phone)
        customer_ids = [customer_id] if customer_id else []
    subscriptions = []
    for customer_id in customer_ids:
        response = _recharge_request(
            context, "GET", "/subscriptions", params={"customer_id": customer_id, "limit": 250}
        )
        response.raise_for_status()
        subscriptions += response.json().get("subscriptions") or []
    return subscriptions


def recharge_cancel_subscription(context, subscription_id, reason="Octocom Dashboard"):
    response = _recharge_request(
        context, "POST", f"/subscriptions/{subscription_id}/cancel", json={"cancellation_reason": reason}
    )
    response.raise_for_status()


def recharge_subscription_widget(context, cancel_action="cancel_subscription"):
    """Sidebar data for the current customer's Recharge subscriptions, with cancel buttons."""
    _, email = _subscription_widget_identity(context)
    items = []
    for sub in recharge_find_subscriptions(context, email=email) if email else []:
        date = recharge_next_charge_date(sub)
        currency = sub.get("presentment_currency") or ""
        items.append(_subscription_widget_item(
            context, sub["id"], sub.get("product_title") or "Subscription", sub.get("status"), {
                "Variant": sub.get("variant_title"),
                "Quantity": sub.get("quantity"),
                "Unit price": _subscription_widget_value(sub.get("price"), currency),
                "Next charge": date.strftime("%Y-%m-%d UTC") if date else None,
                "Billing": _subscription_widget_value(sub.get("charge_interval_frequency"), sub.get("order_interval_unit")),
                "Delivery": _subscription_widget_value(sub.get("order_interval_frequency"), sub.get("order_interval_unit")),
            }, [], cancel_action,
        ))
    return {"title": "Subscriptions", "items": items}


def recharge_cancel_widget_subscription(context):
    """Action for recharge_subscription_widget; preserves the Octocom Dashboard cancellation reason."""
    return _subscription_widget_cancel(context, recharge_find_subscriptions, recharge_cancel_subscription)


def recharge_next_charge_date(subscription):
    """When the subscription next charges (UTC), or None."""
    value = subscription.get("next_charge_scheduled_at")
    return datetime.fromisoformat(value[:10]).replace(tzinfo=timezone.utc) if value else None


def recharge_set_next_charge_date(context, subscription_id, when):
    response = _recharge_request(
        context,
        "POST",
        f"/subscriptions/{subscription_id}/set_next_charge_date",
        json={"date": when.strftime("%Y-%m-%d")},
    )
    response.raise_for_status()


def recharge_change_frequency(context, subscription_id, interval_months):
    response = _recharge_request(
        context,
        "PUT",
        f"/subscriptions/{subscription_id}",
        json={
            "charge_interval_frequency": interval_months,
            "order_interval_frequency": interval_months,
            "order_interval_unit": "month",
        },
    )
    response.raise_for_status()


def recharge_apply_next_charge_discount(context, subscription_id, percentage):
    """Discount the subscription's next charge: a single-use code on its address."""
    response = _recharge_request(
        context,
        "POST",
        "/discounts",
        json={
            "applies_to": {"purchase_item_type": "ALL"},
            "code": "octocom-" + secrets.token_hex(5),
            "status": "enabled",
            "usage_limits": {"one_application_per_customer": True, "max_subsequent_redemptions": 0},
            "value": str(percentage),
            "value_type": "percentage",
        },
    )
    response.raise_for_status()
    discount_id = response.json()["discount"]["id"]
    subscription = recharge_get_subscription(context, subscription_id)
    if subscription is None:
        raise ValueError("The subscription was not found")
    response = _recharge_request(
        context, "PUT", f"/addresses/{subscription['address_id']}", json={"discounts": [{"id": discount_id}]}
    )
    response.raise_for_status()

On this page