Skip to content

feat(stripe): Telegram sales notifications for subscription lifecycle events#766

Merged
sweetmantech merged 3 commits into
mainfrom
feat/stripe-sales-telegram-lifecycle
Jul 8, 2026
Merged

feat(stripe): Telegram sales notifications for subscription lifecycle events#766
sweetmantech merged 3 commits into
mainfrom
feat/stripe-sales-telegram-lifecycle

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Part of recoupable/chat#1858 (PR 1 of 2 — see the PR matrix there).

What

Extends the existing Stripe webhook receiver (app/api/webhooks/stripe) to send admin-Telegram sales notifications for the subscription lifecycle:

Event Message
customer.subscription.created 🆕 New subscription — flags trials as "new card on file" with the trial-end date
customer.subscription.trial_will_end ⏳ Trial ending soon (3 days out) — the proactive-outreach alert
customer.subscription.updated ⚠️ Churn scheduled on the cancel_at_period_end false→true flip / 💚 Cancellation withdrawn on true→false; all other updates stay silent
customer.subscription.deleted ❌ Churn final, with cancellation_details feedback/reason

Every message carries lifetime customer value (succeeded charges net of refunds, via paginated charges.list).

New units (one exported function per file): getCustomerLifetimeValue, getCustomerEmail (deleted-customer tolerant, never throws), formatUsd, formatStripeTimestamp, buildSubscriptionSalesContext, sendSalesNotification (skips isTestEmail accounts, swallows Telegram failures so a notification can never turn a webhook delivery into a non-200), and four event processors wired into stripeWebhookHandler.

Why

On 2026-07-05 a Pro-trial customer scheduled cancellation one day before their first charge and nothing anywhere told us they had ever signed up. Full context, live-data evidence, and architecture decisions: recoupable/chat#1858.

Notes for review

  • The cancel_at_period_end flip (not deleted) is the primary churn signal — deletion fires at period end, potentially weeks after the decision.
  • Credit-granting paths are untouched.
  • The Stripe webhook endpoint config (event types, .dev URL) is already live — these events currently no-op with a 200 until this merges.
  • Payment notifications (invoice.paid, top-ups) are PR 2, stacked on this branch.

Test plan

  • TDD red→green per unit; 186 tests pass across lib/stripe, lib/telegram, app/api/webhooks with zero env vars set.
  • tsc --noEmit: no errors in touched files (pre-existing unrelated test-file errors on main remain).
  • Preview + live verification results will be posted as PR comments.

🤖 Generated with Claude Code


Summary by cubic

Adds admin Telegram sales notifications for Stripe subscription lifecycle events to surface trials, scheduled churn, and saved customers without blocking webhook delivery.

  • New Features

    • Handle customer.subscription.created, trial_will_end, updated (only cancel_at_period_end flips), and deleted in stripeWebhookHandler.
    • Send Telegram messages with customer email (when known), plan, and lifetime value; include trial end, cancel date, and cancellation feedback; stay silent on non-signal updates.
    • Add buildSubscriptionSalesContext, getCustomerEmail, getCustomerLifetimeValue (succeeded charges net of refunds, paginated), formatUsd, formatStripeTimestamp; sendSalesNotification skips isTestEmail accounts and never throws so webhooks stay 200.
  • Refactors

    • Prettier/ESLint cleanups; no behavior changes.

Written for commit 9d52d00. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added richer Stripe subscription event handling, including creation, update, trial ending, and cancellation notifications.
    • Added formatted sales notifications with customer, plan, lifetime value, and trial/cancellation details.
    • Added utilities for formatting dates and USD amounts in notifications.
  • Bug Fixes
    • Improved handling of missing customer email or plan data by falling back to safe defaults.
    • Notification delivery now skips internal/test accounts and avoids failing the overall flow on send errors.

… events

Handle customer.subscription.created / trial_will_end / updated
(cancel_at_period_end flips only) / deleted in the Stripe webhook and
send admin Telegram messages with customer email, plan, cancellation
feedback, and lifetime value (succeeded charges net of refunds).

Part of recoupable/chat#1858 (PR 1 of 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 8, 2026 2:33am

Request Review

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • lib/stripe/__tests__/processSubscriptionCreated.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/processSubscriptionUpdated.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/stripeWebhookHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc07aa7f-c7b6-492c-92ef-31914f27bb61

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds new Stripe helper modules (formatUsd, formatStripeTimestamp, getCustomerEmail, getCustomerLifetimeValue, buildSubscriptionSalesContext), four subscription lifecycle webhook processors (created, updated, deleted, trial_will_end), a Telegram sendSalesNotification helper, and wires these processors into the existing Stripe webhook dispatch handler.

Changes

Subscription Sales Notification Pipeline

Layer / File(s) Summary
Formatting and data-fetch helpers
lib/stripe/formatUsd.ts, lib/stripe/formatStripeTimestamp.ts, lib/stripe/getCustomerEmail.ts, lib/stripe/getCustomerLifetimeValue.ts
Adds cents-to-dollar formatting, unix timestamp to ISO date formatting, Stripe customer email lookup, and paginated lifetime value computation from succeeded charges net of refunds.
Sales context builder
lib/stripe/buildSubscriptionSalesContext.ts
Defines SubscriptionSalesContext and buildSubscriptionSalesContext, concurrently fetching email/lifetime value and composing customer, plan, and lifetime message lines from a Stripe subscription.
Telegram sales notification sender
lib/telegram/sendSalesNotification.ts
Adds sendSalesNotification, skipping test emails via isTestEmail and otherwise sending a message via Telegram, logging (not throwing) on failure.
Subscription lifecycle processors
lib/stripe/processSubscriptionCreated.ts, lib/stripe/processSubscriptionTrialWillEnd.ts, lib/stripe/processSubscriptionUpdated.ts, lib/stripe/processSubscriptionDeleted.ts
Builds sales context and composes/sends messages for subscription created, trial-will-end, updated (cancel flag toggled), and deleted events, including conditional trial/cancellation details.
Webhook handler dispatch wiring
lib/stripe/stripeWebhookHandler.ts
Imports and dispatches the four new processors for customer.subscription.created/trial_will_end/updated/deleted events.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Stripe
  participant stripeWebhookHandler
  participant Processor as SubscriptionProcessor
  participant buildSubscriptionSalesContext
  participant sendSalesNotification
  participant Telegram

  Stripe->>stripeWebhookHandler: subscription lifecycle event
  stripeWebhookHandler->>Processor: dispatch(subscription, previous_attributes?)
  Processor->>buildSubscriptionSalesContext: subscription
  buildSubscriptionSalesContext-->>Processor: customerLine, planLine, lifetimeLine, email
  Processor->>Processor: compose message text
  Processor->>sendSalesNotification: email, text
  alt email is test account
    sendSalesNotification-->>Processor: skip
  else
    sendSalesNotification->>Telegram: sendMessage(text)
    Telegram-->>sendSalesNotification: success or error
  end
Loading

Possibly related issues

Poem

A rabbit hops through webhooks deep,
Tracking trials, sales it keeps,
Lifetime value, plan and mail,
Telegram whispers a churn-y tale,
🐇 Ping! goes the sale, the bunny cheers,
No crash, just logs, through all the years.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning FAIL: getCustomerLifetimeValue still has no error handling, and a few new helpers (e.g. buildSubscriptionSalesContext, processSubscriptionUpdated) exceed the 20-line guideline. Wrap lifetime lookup in try/catch and return 0 on failure; extract shared notification-line building into smaller helpers so each function stays under 20 lines.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stripe-sales-telegram-lifecycle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Verification 2026-07-07:

Check Documented Actual
TDD red before green, every unit 7 new-module tests failed on missing modules + handler dispatch failed before impl; all green after
Unit suites lib/stripe + lib/telegram + app/api/webhooks 186 tests passed, zero env vars set
tsc --noEmit no errors in touched files 0 (pre-existing unrelated test-file errors on main unchanged)
Preview (built from 74f38fd) route deployed, signature check intact https://api-rgt8q3i7i-recoup.vercel.app/api/webhooks/stripe → unsigned POST = 400 {"error":"Missing stripe-signature header"}

Full signed-webhook e2e isn't possible pre-merge (requires the prod STRIPE_WEBHOOK_SECRET); the post-merge live test is subscribe-with-own-card → cancel-schedule → cancel, expecting 3 Telegram messages (chat#1858 'Test 3').

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/stripe/getCustomerLifetimeValue.ts`:
- Around line 11-34: `getCustomerLifetimeValue` currently throws when
`stripeClient.charges.list` fails, which can bubble up through
`buildSubscriptionSalesContext` and break `stripeWebhookHandler`. Update this
helper to be non-throwing by wrapping the charges lookup loop in error handling,
logging the failure with enough context for `customerId`, and returning 0 on any
Stripe/listing error. Keep the existing accumulation logic in
`getCustomerLifetimeValue` unchanged for successful pages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6481813a-0554-4fc4-b0da-e7bb49bc9668

📥 Commits

Reviewing files that changed from the base of the PR and between 9a6cd0e and 74f38fd.

⛔ Files ignored due to path filters (12)
  • app/api/webhooks/stripe/__tests__/routeTestMocks.ts is excluded by !**/__tests__/** and included by app/**
  • lib/stripe/__tests__/buildSubscriptionSalesContext.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/formatStripeTimestamp.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/formatUsd.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/getCustomerEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/getCustomerLifetimeValue.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/processSubscriptionCreated.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/processSubscriptionDeleted.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/processSubscriptionTrialWillEnd.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/processSubscriptionUpdated.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/stripeWebhookHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/telegram/__tests__/sendSalesNotification.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (11)
  • lib/stripe/buildSubscriptionSalesContext.ts
  • lib/stripe/formatStripeTimestamp.ts
  • lib/stripe/formatUsd.ts
  • lib/stripe/getCustomerEmail.ts
  • lib/stripe/getCustomerLifetimeValue.ts
  • lib/stripe/processSubscriptionCreated.ts
  • lib/stripe/processSubscriptionDeleted.ts
  • lib/stripe/processSubscriptionTrialWillEnd.ts
  • lib/stripe/processSubscriptionUpdated.ts
  • lib/stripe/stripeWebhookHandler.ts
  • lib/telegram/sendSalesNotification.ts

Comment on lines +11 to +34
export const getCustomerLifetimeValue = async (customerId: string): Promise<number> => {
let totalCents = 0;
let startingAfter: string | undefined;
let hasMore = true;

while (hasMore) {
const listParams: Stripe.ChargeListParams = { customer: customerId, limit: PAGE_LIMIT };
if (startingAfter) listParams.starting_after = startingAfter;

const page = await stripeClient.charges.list(listParams);

for (const charge of page.data) {
if (charge.status !== "succeeded") continue;
totalCents += charge.amount - charge.amount_refunded;
}

hasMore = page.has_more;
const lastId = page.data.at(-1)?.id;
if (!lastId) break;
startingAfter = lastId;
}

return totalCents;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if stripeWebhookHandler wraps notification processing in try/catch
rg -n -A10 'processSubscription|sendSalesNotification|buildSubscriptionSalesContext' lib/stripe/stripeWebhookHandler.ts

Repository: recoupable/api

Length of output: 1994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the notification pipeline and the helper functions mentioned in the review.
rg -n -A40 -B10 'getCustomerEmail|getCustomerLifetimeValue|buildSubscriptionSalesContext|sendSalesNotification|processSubscriptionCreated|processSubscriptionUpdated|processSubscriptionTrialWillEnd|processSubscriptionDeleted|Promise\.all' lib/stripe

Repository: recoupable/api

Length of output: 50370


Make getCustomerLifetimeValue non-throwing
getCustomerEmail already degrades safely; this lookup should do the same. A transient charges.list failure bubbles out of buildSubscriptionSalesContext and makes stripeWebhookHandler return 500 for the event. Log the error and return 0 here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/stripe/getCustomerLifetimeValue.ts` around lines 11 - 34,
`getCustomerLifetimeValue` currently throws when `stripeClient.charges.list`
fails, which can bubble up through `buildSubscriptionSalesContext` and break
`stripeWebhookHandler`. Update this helper to be non-throwing by wrapping the
charges lookup loop in error handling, logging the failure with enough context
for `customerId`, and returning 0 on any Stripe/listing error. Keep the existing
accumulation logic in `getCustomerLifetimeValue` unchanged for successful pages.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 23 files

Confidence score: 3/5

  • In lib/stripe/buildSubscriptionSalesContext.ts, a failure in the lifetime-value API currently bubbles up and can return HTTP 500 to Stripe, which risks dropped/retried webhook deliveries and missed sales notifications — wrap the LTV call with a safe fallback (e.g., 0 cents) before merging.
  • In lib/telegram/__tests__/sendSalesNotification.test.ts, vi.clearAllMocks() can leave a prior mockRejectedValue implementation active, making later tests order-dependent and potentially masking real regressions — reset mock implementations in beforeEach to keep the suite deterministic.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/stripe/buildSubscriptionSalesContext.ts">

<violation number="1" location="lib/stripe/buildSubscriptionSalesContext.ts:26">
P2: Lifetime-value API failure drops the notification and returns 500 to Stripe. Wrap the LTV call so a transient Stripe error doesn't break webhook delivery — return 0 cents as fallback, consistent with the pattern `getCustomerEmail` already uses.</violation>
</file>

<file name="lib/telegram/__tests__/sendSalesNotification.test.ts">

<violation number="1" location="lib/telegram/__tests__/sendSalesNotification.test.ts:15">
P3: This suite can become order-dependent after the Telegram-failure case because `vi.clearAllMocks()` leaves `sendMessageMock.mockRejectedValue(...)` in place. Resetting mock implementations in `beforeEach` would keep later tests from inheriting the rejected `sendMessage` behavior.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Stripe as Stripe Webhook
    participant API as Next.js API Route
    participant Handler as stripeWebhookHandler
    participant Created as processSubscriptionCreated
    participant Updated as processSubscriptionUpdated
    participant Ctx as buildSubscriptionSalesContext
    participant Email as getCustomerEmail
    participant LTV as getCustomerLifetimeValue
    participant Notify as sendSalesNotification
    participant Test as isTestEmail
    participant TG as Telegram Bot

    Note over Stripe,TG: NEW: Subscription lifecycle notifications

    Stripe->>API: POST /api/webhooks/stripe (signed event)
    API->>Handler: dispatch event
    Handler->>Handler: verifyStripeWebhookEvent (existing)

    alt event.type == "customer.subscription.created"
        Handler->>Created: processSubscriptionCreated(subscription)
        Created->>Ctx: buildSubscriptionSalesContext(subscription)
        Ctx->>+Email: getCustomerEmail(customerId)
        Ctx->>+LTV: getCustomerLifetimeValue(customerId)
        Email->>Stripe: customers.retrieve(customerId)
        Stripe-->>Email: customer (or deleted/null)
        alt customer deleted or no email
            Email-->>Ctx: null
        else
            Email-->>Ctx: "fan@example.com"
        end
        LTV->>Stripe: charges.list(customer, paginated)
        Stripe-->>LTV: succeeded charges net refunds
        LTV-->>Ctx: total cents
        Ctx-->>Created: {email, customerLine, planLine, lifetimeLine}
        Note over Created: Formats message with trial info if applicable
        Created->>Notify: sendSalesNotification({email, text})
        Notify->>Test: isTestEmail(email)
        alt email present and test account
            Test-->>Notify: true → skip
        else
            Test-->>Notify: false → proceed
            Notify->>TG: sendMessage(text)
            alt Telegram fails
                TG--xNotify: error
                Notify->>Notify: console.error, swallow
            else success
                TG-->>Notify: OK
            end
        end
        Notify-->>Created: void
    else event.type == "customer.subscription.trial_will_end"
        Handler->>Created: processSubscriptionTrialWillEnd(subscription)
        Note over Created,Notify: Same pattern → proactive outreach alert
    else event.type == "customer.subscription.updated"
        Handler->>Updated: processSubscriptionUpdated(sub, previous_attributes)
        alt cancel_at_period_end flipped false→true
            Updated->>Updated: churn scheduled message
        else flipped true→false
            Updated->>Updated: cancellation withdrawn message
        else no flip or no previous
            Updated->>Updated: return early, no notification
        end
        Updated->>Ctx: buildSubscriptionSalesContext (only if flip detected)
        Ctx->>Email: getCustomerEmail
        Ctx->>LTV: getCustomerLifetimeValue
        Ctx-->>Updated: context
        Updated->>Notify: sendSalesNotification(...)
        Notify->>Test: isTestEmail
        Notify->>TG: sendMessage (or skip)
    else event.type == "customer.subscription.deleted"
        Handler->>Updated: processSubscriptionDeleted(subscription)
        Note over Updated,Notify: Final churn message with cancellation_details
    end

    Handler-->>API: 200 {received: true}
    API-->>Stripe: HTTP 200
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


const [email, lifetimeCents] = await Promise.all([
getCustomerEmail(customerId),
getCustomerLifetimeValue(customerId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Lifetime-value API failure drops the notification and returns 500 to Stripe. Wrap the LTV call so a transient Stripe error doesn't break webhook delivery — return 0 cents as fallback, consistent with the pattern getCustomerEmail already uses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/stripe/buildSubscriptionSalesContext.ts, line 26:

<comment>Lifetime-value API failure drops the notification and returns 500 to Stripe. Wrap the LTV call so a transient Stripe error doesn't break webhook delivery — return 0 cents as fallback, consistent with the pattern `getCustomerEmail` already uses.</comment>

<file context>
@@ -0,0 +1,41 @@
+
+  const [email, lifetimeCents] = await Promise.all([
+    getCustomerEmail(customerId),
+    getCustomerLifetimeValue(customerId),
+  ]);
+
</file context>


describe("sendSalesNotification", () => {
beforeEach(() => {
vi.clearAllMocks();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This suite can become order-dependent after the Telegram-failure case because vi.clearAllMocks() leaves sendMessageMock.mockRejectedValue(...) in place. Resetting mock implementations in beforeEach would keep later tests from inheriting the rejected sendMessage behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/telegram/__tests__/sendSalesNotification.test.ts, line 15:

<comment>This suite can become order-dependent after the Telegram-failure case because `vi.clearAllMocks()` leaves `sendMessageMock.mockRejectedValue(...)` in place. Resetting mock implementations in `beforeEach` would keep later tests from inheriting the rejected `sendMessage` behavior.</comment>

<file context>
@@ -0,0 +1,45 @@
+
+describe("sendSalesNotification", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    isTestEmailMock.mockReturnValue(false);
+    vi.spyOn(console, "error").mockImplementation(() => undefined);
</file context>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sweetmantech

sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Live end-to-end verification 2026-07-08, against preview api-nbcqk8zvm (built from 9d52d00, code identical to CI-green head):

Method: temporary second Stripe webhook endpoint (we_1Tqkud…) pointed at the preview, its signing secret set as a branch-scoped preview env var, then a real trial subscription ($99 Pro price, no card, throwaway customer agent@recoupable.dev) walked through the full lifecycle in live mode. All deliveries were made and signed by Stripe itself.

Event (real, Stripe-delivered) Expected Result
customer.subscription.created (trialing) 🆕 trial message w/ trial-end + LTV $0.00 ✅ delivered (pending_webhooks: 0), Telegram received
customer.subscription.updated cancel flip false→true, feedback too_expensive ⚠️ churn scheduled w/ feedback ✅ Telegram received
customer.subscription.trial_will_end ⏳ reach-out alert ✅ Telegram received
customer.subscription.updated cancel flip true→false 💚 customer saved ✅ Telegram received
customer.subscription.updated metadata-only silence ✅ no message, event 200'd
customer.subscription.deleted ❌ churn final ✅ Telegram received

All 5 expected Telegram messages confirmed received in the admin chat by @sweetmantech; the silent case stayed silent. All 6 events show pending_webhooks: 0.

Cleanup done: temp webhook endpoint deleted, branch-scoped env var removed, test customer deleted (subscription was trial-only — $0 charged).

One finding for the fleet: the preview environment's STRIPE_WEBHOOK_SECRET is empty by default, so previews can't verify webhooks without the branch-var dance above — worth knowing for future webhook testing (documented in chat#1858).

Verbatim Telegram transcript (admin chat, 2026-07-07 21:34 local, posted by @sweetmantech):

🆕 New subscription (trial) — new card on file
Customer: agent@recoupable.dev (cus_UqS1B12gqsZpe9)
Plan: $99.00/month
Trial ends: 2026-07-10
Lifetime value: $0.00

⏳ Trial ending soon — reach out now
Customer: agent@recoupable.dev (cus_UqS1B12gqsZpe9)
Plan: $99.00/month
Trial ends: 2026-07-10
Lifetime value: $0.00

⚠️ Churn scheduled — cancels at period end
Customer: agent@recoupable.dev (cus_UqS1B12gqsZpe9)
Plan: $99.00/month
Cancels: 2026-07-10
Feedback: too_expensive
Reason: cancellation_requested
Lifetime value: $0.00

💚 Cancellation withdrawn — customer saved
Customer: agent@recoupable.dev (cus_UqS1B12gqsZpe9)
Plan: $99.00/month
Lifetime value: $0.00

❌ Subscription canceled — churn final
Customer: agent@recoupable.dev (cus_UqS1B12gqsZpe9)
Plan: $99.00/month
Reason: cancellation_requested
Lifetime value: $0.00

Every field correct: customer email + id, plan, trial-end date, cancellation feedback/reason, and lifetime value. The metadata-only update produced no message (silent-guard verified).

@sweetmantech sweetmantech merged commit d6d44e9 into main Jul 8, 2026
6 checks passed
@sweetmantech sweetmantech deleted the feat/stripe-sales-telegram-lifecycle branch July 8, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant