fix: pass worker env to Stripe clients

This commit is contained in:
2026-07-02 01:00:40 -07:00
parent cd681bebe1
commit fd95b2da5c
110 changed files with 1869 additions and 101 deletions

View File

@@ -0,0 +1,690 @@
# Cloudflare Stripe workerEnv 实现计划
> **给 agent 执行者:** 必选子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务逐项实现本计划。步骤统一使用 checkbox`- [ ]`)语法跟踪。
**目标:** 修复 Cloudflare Worker 上 Stripe webhook / checkout / billing 路径缺少 `workerEnv` 导致付款成功后 Autumn customer 不切换到 `pro` 的问题。
**方案概览:** 先用静态测试暴露裸 `createStripeCli({ org, env })` 调用,再增加上下文安全的 Stripe client helper并把 webhook、checkout、billing v2、product/price 初始化路径改成通过 `ctx.workerEnv` 创建 Stripe client。最后重放 Stripe `checkout.session.completed` 事件,验证测试 customer 从 `free` 切到 `pro`
**技术栈:** TypeScript、Hono、Cloudflare Workers、Wrangler、Stripe SDK、Bun test、Autumn billing v2。
---
## 文件结构
- 新建:`server/src/external/connect/createStripeCliFromContext.ts`
- 负责从 `AutumnContext` 安全创建 Stripe client统一传递 `ctx.workerEnv`
- 修改:`server/src/external/connect/createStripeCli.ts`
- 保留底层实现;在 secret-key flow 缺少 `workerEnv` 时抛明确错误。
- 修改:`server/src/external/stripe/webhookHandlers/**`
- webhook handler 内部优先使用 `ctx.stripeCli` 或上下文 helper。
- 修改:`server/src/internal/billing/v2/**`
- billing v2 setup / execute / invoice / subscription 路径使用上下文 helper。
- 修改:`server/src/internal/customers/**`
- attach、checkout、customer 创建、payment method 相关路径传递 `workerEnv`
- 修改:`server/src/internal/products/productUtils.ts`
- product / price 初始化路径使用传入的 `workerEnv`
- 修改:`shared/utils/utils.ts`
- 移除 `Bun.CryptoHasher`,改成 Worker 可用的同步 SHA-256 实现。
- 新建:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- 静态防回归测试,禁止运行时代码新增裸 `createStripeCli({ org, env })`
---
### 任务 1添加失败的静态防回归测试
**文件:**
- 新建:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- [ ] **步骤 1写失败测试**
创建文件:
```ts
import { describe, expect, test } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
const repoRoot = join(import.meta.dir, "../../..");
const serverSrc = join(repoRoot, "src");
const allowedFiles = new Set([
"external/connect/createStripeCli.ts",
"external/connect/createStripeCliFromContext.ts",
]);
const collectTsFiles = (dir: string): string[] => {
const files: string[] = [];
for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
const stat = statSync(path);
if (stat.isDirectory()) {
files.push(...collectTsFiles(path));
continue;
}
if (path.endsWith(".ts")) files.push(path);
}
return files;
};
const hasWorkerEnv = (callText: string) =>
callText.includes("workerEnv") || callText.includes("ctx.workerEnv");
describe("Cloudflare Stripe client creation", () => {
test("runtime code does not call createStripeCli without workerEnv", () => {
const offenders: string[] = [];
const files = collectTsFiles(serverSrc);
for (const file of files) {
const relativePath = relative(serverSrc, file);
if (allowedFiles.has(relativePath)) continue;
if (relativePath.includes("/tests/")) continue;
const source = readFileSync(file, "utf8");
const callRegex = /createStripeCli\s*\(\s*\{[\s\S]*?\}\s*\)/g;
for (const match of source.matchAll(callRegex)) {
const callText = match[0];
if (hasWorkerEnv(callText)) continue;
const line =
source.slice(0, match.index ?? 0).split("\n").length;
offenders.push(`${relativePath}:${line} ${callText.replace(/\s+/g, " ")}`);
}
}
expect(offenders).toEqual([]);
});
});
```
- [ ] **步骤 2运行测试确认先失败**
运行:
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL输出包含多个缺少 `workerEnv``createStripeCli(...)` 调用。
- [ ] **步骤 3提交失败测试**
```bash
git add server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
git commit -m "test: guard Cloudflare Stripe worker env usage"
```
---
### 任务 2新增上下文安全的 Stripe client helper
**文件:**
- 新建:`server/src/external/connect/createStripeCliFromContext.ts`
- 修改:`server/src/external/connect/createStripeCli.ts`
- 测试:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- [ ] **步骤 1新增 helper**
创建 `server/src/external/connect/createStripeCliFromContext.ts`
```ts
import type { AppEnv } from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { createStripeCli } from "./createStripeCli";
export const createStripeCliFromContext = ({
ctx,
env = ctx.env,
legacyVersion,
throughSecretKey,
skipInstrumentation,
}: {
ctx: AutumnContext;
env?: AppEnv;
legacyVersion?: boolean;
throughSecretKey?: boolean;
skipInstrumentation?: boolean;
}) =>
createStripeCli({
org: ctx.org,
env,
legacyVersion,
throughSecretKey,
skipInstrumentation,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 2强化底层保护**
`server/src/external/connect/createStripeCli.ts` 的 secret-key flow 中,`encrypted` 存在后立刻加入:
```ts
if (!workerEnv) {
throw new RecaseError({
message:
"Stripe secret key is encrypted, but Cloudflare workerEnv was not provided to createStripeCli",
code: ErrCode.StripeConfigNotFound,
statusCode: 500,
});
}
```
并把:
```ts
const decrypted = workerEnv ? decryptData(encrypted, workerEnv) : "";
```
改成:
```ts
const decrypted = decryptData(encrypted, workerEnv);
```
- [ ] **步骤 3运行类型检查**
```bash
cd server
bun run ts
```
预期PASS。
- [ ] **步骤 4运行静态测试确认仍失败**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL。此时 helper 已存在,但调用点还没改完。
- [ ] **步骤 5提交 helper**
```bash
git add server/src/external/connect/createStripeCli.ts server/src/external/connect/createStripeCliFromContext.ts
git commit -m "fix: require worker env for encrypted Stripe keys"
```
---
### 任务 3改造 checkout / attach 必经路径
**文件:**
- 修改:`server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts`
- 修改:`server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts`
- 修改:`server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts`
- 修改:`server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts`
- 修改:`server/src/internal/customers/attach/attachRouter.ts`
- 修改:`server/src/internal/customers/add-product/handleCreateCheckout.ts`
- 修改:`server/src/internal/products/productUtils.ts`
- 测试:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- [ ] **步骤 1把 attach 参数处理改成传 `workerEnv`**
在以下文件中,把 `createStripeCli({ org, env })` 改为:
```ts
createStripeCli({
org,
env,
workerEnv: ctx.workerEnv,
});
```
目标文件:
- `server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts`
- `server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts`
- `server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts`
- [ ] **步骤 2改造 `convertToParams.ts`**
`server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts` 中,把返回对象里的:
```ts
stripeCli: createStripeCli({ org, env }),
```
改为:
```ts
stripeCli: createStripeCli({ org, env, workerEnv: ctx.workerEnv }),
```
- [ ] **步骤 3改造 `checkStripeConnections`**
`server/src/internal/customers/attach/attachRouter.ts` 中,调用 `checkStripeProductExists` 时加入:
```ts
workerEnv: ctx.workerEnv,
```
完整调用形状:
```ts
checkStripeProductExists({
db,
org,
env,
product,
logger,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 4改造 legacy checkout 创建**
`server/src/internal/customers/add-product/handleCreateCheckout.ts` 中,把:
```ts
const stripeCli = createStripeCli({
org,
env: customer.env,
legacyVersion: true,
});
```
改为:
```ts
const stripeCli = createStripeCli({
org,
env: customer.env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 5运行静态测试**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:仍可能 FAIL但 offender 列表中不再包含上述 attach / checkout 文件。
- [ ] **步骤 6提交 checkout 路径改造**
```bash
git add \
server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts \
server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts \
server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts \
server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts \
server/src/internal/customers/attach/attachRouter.ts \
server/src/internal/customers/add-product/handleCreateCheckout.ts
git commit -m "fix: pass worker env through attach checkout Stripe paths"
```
---
### 任务 4改造 billing v2 Stripe 执行与 setup 路径
**文件:**
- 修改:`server/src/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/execute/executeStripeRefundAction.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/setup/fetchStripeCustomerForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/setup/fetchStripeSubscriptionForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/setup/fetchStripeSubscriptionScheduleForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/setup/fetchStripeTaxRateForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/setup/fetchStripeDiscountsForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/utils/invoices/stripeInvoiceOps.ts`
- 修改:`server/src/internal/billing/v2/execute/voidInvoicesOnImmediateCancel.ts`
- 修改:`server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts`
- [ ] **步骤 1统一替换 ctx 形态调用**
把以下形态:
```ts
createStripeCli({ org: ctx.org, env: ctx.env });
```
改为:
```ts
createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 2统一替换局部 `org/env` 但有 `ctx` 的调用**
把以下形态:
```ts
createStripeCli({ org, env });
```
在当前函数有 `ctx: AutumnContext` 时改为:
```ts
createStripeCli({ org, env, workerEnv: ctx.workerEnv });
```
- [ ] **步骤 3处理 `fullCustomer.env` 覆盖**
`executeStripeCheckoutSessionAction.ts` 中保持使用 customer env但加入 `workerEnv`
```ts
const stripeCli = createStripeCli({
org,
env: fullCustomer.env,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 4运行静态测试**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期billing v2 相关 offender 消失。
- [ ] **步骤 5提交 billing v2 改造**
```bash
git add server/src/internal/billing/v2 server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts
git commit -m "fix: pass worker env through billing Stripe paths"
```
---
### 任务 5改造 Stripe webhook handlers
**文件:**
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleLegacyCheckoutSessionMetadata.ts/handleSetupCheckout.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleLegacyCheckoutSessionMetadata.ts/handleCheckoutSessionCompletedLegacy.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleStandaloneSetupCheckout.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleSetupPaymentMetadata.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/setupStripeSubscriptionUpdatedContext.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleCancelOnPastDue.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceDiscounts.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceMetadata/handleInvoiceActionRequiredCompleted.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts`
- [ ] **步骤 1优先使用 `ctx.stripeCli`**
如果函数已经接收 `ctx: StripeWebhookContext` 且不需要 legacy API version直接使用
```ts
const stripeCli = ctx.stripeCli;
```
不要重新创建 Stripe client。
- [ ] **步骤 2需要 legacy API version 时显式传 `workerEnv`**
把:
```ts
createStripeCli({ org, env, legacyVersion: true });
```
改成:
```ts
createStripeCli({
org,
env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 3普通 webhook helper 显式传 `workerEnv`**
把:
```ts
createStripeCli({ org, env });
```
改成:
```ts
createStripeCli({ org, env, workerEnv: ctx.workerEnv });
```
- [ ] **步骤 4运行静态测试**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期webhook handler offender 消失。
- [ ] **步骤 5提交 webhook 改造**
```bash
git add server/src/external/stripe/webhookHandlers
git commit -m "fix: use worker env in Stripe webhook handlers"
```
---
### 任务 6移除 Worker 不支持的 `Bun.CryptoHasher`
**文件:**
- 修改:`shared/utils/utils.ts`
- [ ] **步骤 1替换 `hashString` 实现**
`hashString` 中的:
```ts
const hasher = new Bun.CryptoHasher("sha256");
hasher.update(str);
return hasher.digest("base64");
```
替换为同步、跨运行时实现。最小实现应满足:
```ts
hashString("abc") === "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
```
可以使用纯 TypeScript SHA-256 实现,或引入已声明依赖的跨运行时 hash 工具。不要使用 `node:crypto`,因为 `@autumn/shared` 会进入前端构建。
- [ ] **步骤 2添加最小验证命令**
运行:
```bash
bun -e 'import { hashString } from "./shared/utils/utils.ts"; if (hashString("abc") !== "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=") process.exit(1);'
```
预期exit code 0。
- [ ] **步骤 3运行类型检查**
```bash
cd server
bun run ts
```
预期PASS。
- [ ] **步骤 4提交 hash 修复**
```bash
git add shared/utils/utils.ts
git commit -m "fix: replace Bun-only hash in shared utils"
```
---
### 任务 7让静态测试全绿并处理例外
**文件:**
- 修改:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- 可能修改:剩余 offender 对应文件
- [ ] **步骤 1运行静态测试**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:如果 FAIL输出仅剩脚本、测试或真正不走加密 org key 的例外。
- [ ] **步骤 2处理剩余运行时代码 offender**
对运行时代码中的每个 offender
```ts
createStripeCli({ org, env });
```
改为以下二选一:
```ts
createStripeCli({ org, env, workerEnv: ctx.workerEnv });
```
或:
```ts
createStripeCliFromContext({ ctx });
```
- [ ] **步骤 3只对白名单路径放行**
如果某些路径是测试工具或脚本,例如 `server/src/utils/scriptUtils/**`,在测试里加入精确白名单。不要用宽泛目录白名单覆盖运行时代码。
- [ ] **步骤 4确认测试通过**
```bash
cd server
CI=1 bun test --isolate tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期PASS。
- [ ] **步骤 5提交静态测试收敛**
```bash
git add server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts server/src
git commit -m "test: enforce worker env for Stripe runtime clients"
```
---
### 任务 8部署并验证 checkout paid -> pro
**文件:**
- 不改代码;执行部署和线上验证。
- [ ] **步骤 1运行类型检查**
```bash
cd server
bun run ts
```
预期PASS。
- [ ] **步骤 2运行 dry-run**
```bash
cd server
bunx wrangler deploy --dry-run --outdir .wrangler/deploy-dry-run
```
预期PASS。允许出现 “Multiple environments are defined” warning。
- [ ] **步骤 3部署**
```bash
cd server
bunx wrangler deploy
```
预期:输出 `Current Version ID`。如果出现 `fetch failed`,重试一次;如果出现 `10021`,保存 startup profile 并暂停分析启动 CPU。
- [ ] **步骤 4确认测试 customer 当前状态**
运行:
```bash
curl -sS https://autumn-api.bowong.cc/v1/customers/test_user_001 \
-H 'Authorization: Bearer <AUTUMN_SECRET_KEY>' \
| jq '{subscriptions, balances}'
```
预期:如果还未 replay webhook可能仍是 `free`
- [ ] **步骤 5重放 Stripe checkout completed event**
先查最近事件:
```bash
curl -sS 'https://api.stripe.com/v1/events?limit=10&type=checkout.session.completed' \
-u '<STRIPE_SECRET_KEY>:' \
| jq '.data[] | {id, created, session: .data.object.id, payment_status: .data.object.payment_status, metadata: .data.object.metadata}'
```
选择带有 `autumn_metadata_id``payment_status == "paid"` 的 event重放
```bash
curl -sS -X POST 'https://api.stripe.com/v1/events/<EVENT_ID>/resend' \
-u '<STRIPE_SECRET_KEY>:' \
-d 'webhook_endpoint=<WEBHOOK_ENDPOINT_ID>'
```
预期Stripe API 返回重放成功,或该 event 被重新投递。
- [ ] **步骤 6确认 customer 切到 pro**
运行:
```bash
curl -sS https://autumn-api.bowong.cc/v1/customers/test_user_001 \
-H 'Authorization: Bearer <AUTUMN_SECRET_KEY>' \
| jq '{subscriptions, balances}'
```
预期:
```text
subscriptions includes plan_id == "pro"
balances.messages.granted == 100
```
- [ ] **步骤 7提交验证记录**
如果项目已有验证文档,追加一条;否则在最终回复里报告以下内容,不新建文件:
```text
typecheck: pass
static guard: pass
wrangler deploy: pass
stripe event replay: pass
customer pro state: pass
```
---
## 自检记录
- Spec 的“不把解密 key 存入 DB/业务对象”要求落在任务 2底层仍只在创建 Stripe client 时解密。
- Spec 的“webhook context 是第一等边界”要求落在任务 5。
- Spec 的“禁止裸调用回归”要求落在任务 1 和任务 7。
- Spec 的“replay 已支付 event 验证 pro 状态”要求落在任务 8。
- 没有使用 `TBD` / `TODO` / “类似上面” 等占位措辞。

View File

@@ -0,0 +1,820 @@
# Stripe workerEnv 剩余裸调用分批修复计划
> **给 agent 执行者:** 必选子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务逐项实现本计划。步骤统一使用 checkbox`- [ ]`)语法跟踪。
**目标:** 把当前剩余的 55 个运行时代码裸 `createStripeCli({ org, env })` 调用点改造成显式传递 Cloudflare `workerEnv`,让 encrypted Stripe secret-key flow 不再在线上请求路径随机 500。
**方案概览:** 以当前静态测试输出为准,先修 dashboard/API 用户请求路径,再修 billing v2 与 webhook/Stripe operations最后修 external integration 与 cron/migration 路径。每一批完成后运行静态 guard确认 offenders 数量下降;最后让 `no-naked-create-stripe-cli.test.ts` 归零,并跑 `bun run ts`
**技术栈:** TypeScript、Hono、Cloudflare Workers、Stripe SDK、Bun test、Wrangler。
---
## 当前基线
运行命令:
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
当前预期FAIL剩余 55 个 offenders。已知不再包含
- `getCusRewards.ts`
- `getCusPaymentMethodRes.ts`
- `handleGetCustomer.ts`
- `getApiCustomerExpand.ts`
- `getApiCustomerExpandV2.ts`
## 文件结构
- 已存在:`server/src/external/connect/createStripeCli.ts`
- 底层 Stripe client 工厂encrypted secret-key flow 缺少 `workerEnv` 时抛显式错误。
- 修改:`server/src/internal/customers/**`
- customer、billing portal、invoice、reward、referral 等 dashboard/API 请求路径。
- 修改:`server/src/internal/billing/**`
- billing v2 checkout、sync、subscription schedule、invoice、discount、workflow 路径。
- 修改:`server/src/external/stripe/**`
- webhook handler 与 Stripe operation helper。
- 修改:`server/src/external/vercel/**`
- Vercel marketplace/resource/installations 路径。
- 修改:`server/src/cron/**`
- scheduled cron 中通过 `CronContext.workerEnv` 创建 Stripe client。
- 修改:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- 仅在确认某个文件不是 Worker runtime 且不可能走 encrypted org Stripe key flow 时才加入 allowlist。
## 统一改造规则
保持底层工厂并显式传递 `workerEnv`
```ts
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
```
cron 路径使用:
```ts
const stripeCli = createStripeCli({
org,
env,
workerEnv: ctx.workerEnv,
});
```
禁止为了压过测试而写:
```ts
workerEnv: {} as Env
```
只有 `createStripeCli` 内部 Connect/master account fallback 保留已有兼容处理;运行时代码不新增空对象兜底。
---
### 任务 1确认当前 offenders 基线
**文件:**
- 测试:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- [ ] **步骤 1运行静态 guard**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL输出包含 55 个 offenders。
- [ ] **步骤 2保存 offenders 清单到执行笔记**
把测试输出中的文件列表复制到执行笔记,不提交临时文件。后续每完成一批,都用同一个命令确认数量下降。
---
### 任务 2修 customer/dashboard 请求路径
**文件:**
- 修改:`server/src/internal/customers/cusUtils/createNewCustomer.ts`
- 修改:`server/src/internal/customers/internalHandlers/handleGetCusReferrals.ts`
- 修改:`server/src/internal/customers/internalHandlers/handleGetInvoiceLineItems.ts`
- 修改:`server/src/internal/customers/cusProducts/cusProductUtils.ts`
- 修改:`server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts`
- 修改:`server/src/internal/customers/attach/attachRouter.ts`
- 修改:`server/src/internal/customers/actions/update/updateCustomer.ts`
- 修改:`server/src/internal/customers/actions/resetCustomerEntitlements/getResetAtUpdate.ts`
- 修改:`server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts`
- 修改:`server/src/internal/customers/handlers/handleAddCouponToCusV2.ts`
- 修改:`server/src/internal/customers/handlers/handleBillingPortal/createBillingPortalSession.ts`
- [ ] **步骤 1先运行测试确认本批仍为红**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含上述 customer 文件。
- [ ] **步骤 2改造有 `ctx` 的文件**
对已经持有 `ctx` 的文件,导入 `createStripeCliFromContext`,把:
```ts
createStripeCli({ org: ctx.org, env: ctx.env })
```
改成:
```ts
createStripeCliFromContext({ ctx })
```
如果原调用是:
```ts
createStripeCli({
org,
env,
legacyVersion: true,
})
```
且函数已经有 `ctx`,改成:
```ts
createStripeCliFromContext({
ctx,
legacyVersion: true,
})
```
- [ ] **步骤 3改造只有 `org/env` 的 helper**
如果 helper 没有 `ctx`,给参数对象增加 `workerEnv?: Env`,并在调用 `createStripeCli` 时传入:
```ts
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
```
所有调用该 helper 的 request handler 必须从 `ctx.workerEnv` 继续向下传:
```ts
await helper({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
```
- [ ] **步骤 4处理 `attachRouter.ts` 的 `attachParams`**
`attachRouter.ts` 当前 offender 是:
```ts
createStripeCli({
org: attachParams.org,
env: attachParams.customer.env,
})
```
在该 call site 所在作用域使用当前 request `ctx.workerEnv`
```ts
createStripeCli({
org: attachParams.org,
env: attachParams.customer.env,
workerEnv: ctx.workerEnv,
})
```
如果该作用域没有 `ctx`,把 `workerEnv?: Env` 加到 `attachParams` 的构建结果里,并从 router 的 `ctx.workerEnv` 填入。
- [ ] **步骤 5运行类型检查**
```bash
cd server
bun run ts
```
预期PASS。
- [ ] **步骤 6运行静态 guard**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL但不再包含任务 2 的 customer 文件。
- [ ] **步骤 7提交本批**
```bash
git add server/src/internal/customers
git commit -m "fix: pass worker env through customer Stripe clients"
```
---
### 任务 3修 org/product/invoice/reward 请求路径
**文件:**
- 修改:`server/src/internal/products/internalHandlers/handleGetStripeCoupons.ts`
- 修改:`server/src/internal/products/handlers/handleUpdatePlan/updateProductDetails.ts`
- 修改:`server/src/internal/invoices/handlers/handleGetStripeInvoice.ts`
- 修改:`server/src/internal/invoices/handlers/handleRedirectToInvoice.ts`
- 修改:`server/src/internal/rewards/actions/redeemPromoCode.ts`
- 修改:`server/src/internal/rewards/actions/triggerDiscount.ts`
- 修改:`server/src/internal/rewards/actions/triggerFreePaidProduct.ts`
- 修改:`server/src/internal/rewards/actions/triggerCheckoutReward.ts`
- 修改:`server/src/internal/orgs/orgUtils.ts`
- 修改:`server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts`
- 修改:`server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts`
- 修改:`server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts`
- 修改:`server/src/internal/balances/utils/paidAllocatedFeature/adjustAllowance.ts`
- [ ] **步骤 1确认本批 offenders 仍存在**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含任务 3 文件。
- [ ] **步骤 2request handler 改用 context helper**
对 handler 内已经有 `ctx` 的调用,把:
```ts
createStripeCli({ org, env })
```
改成:
```ts
createStripeCliFromContext({ ctx })
```
如果原调用使用不同 env
```ts
createStripeCli({ org, env: curProduct.env as AppEnv })
```
改成:
```ts
createStripeCliFromContext({
ctx,
env: curProduct.env as AppEnv,
})
```
- [ ] **步骤 3org utility 增加 `workerEnv` 参数**
`internal/orgs/orgUtils.ts` 是 shared utility不要假设全局 env。给相关函数参数增加
```ts
workerEnv?: Env;
```
并在创建 Stripe client 时传入:
```ts
createStripeCli({
org,
env,
throughSecretKey: true,
workerEnv,
});
```
调用方如果有 `ctx`,传:
```ts
workerEnv: ctx.workerEnv
```
调用方如果是 Cloudflare handler 的 `c.env`,传:
```ts
workerEnv: c.env
```
- [ ] **步骤 4reward action 保留 legacyVersion**
`triggerDiscount.ts` 原调用带 `legacyVersion: true`。改造后必须保留:
```ts
createStripeCliFromContext({
ctx,
legacyVersion: true,
})
```
- [ ] **步骤 5运行检查**
```bash
cd server
bun run ts
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:类型检查 PASS静态 guard 仍可能 FAIL但不再包含任务 3 文件。
- [ ] **步骤 6提交本批**
```bash
git add server/src/internal/products server/src/internal/invoices server/src/internal/rewards server/src/internal/orgs server/src/internal/balances
git commit -m "fix: pass worker env through product invoice reward Stripe clients"
```
---
### 任务 4修 billing v2 路径
**文件:**
- 修改:`server/src/internal/billing/v2/providers/stripe/utils/discounts/stripeCustomerToDiscounts.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/utils/discounts/subToDiscounts.ts`
- 修改:`server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts`
- 修改:`server/src/internal/billing/v2/workflows/grantCheckoutReward/grantCheckoutReward.ts`
- 修改:`server/src/internal/billing/v2/workflows/storeInvoiceLineItems/storeInvoiceLineItems.ts`
- 修改:`server/src/internal/billing/v2/actions/setupPayment/createSetupCheckoutSession.ts`
- 修改:`server/src/internal/billing/v2/actions/sync/syncProposals.ts`
- 修改:`server/src/internal/billing/v2/actions/sync/syncProposalsV2.ts`
- 修改:`server/src/internal/billing/v2/actions/sync/sync.ts`
- 修改:`server/src/internal/billing/handlers/handleSetupPayment.ts`
- [ ] **步骤 1确认本批 offenders**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含任务 4 文件。
- [ ] **步骤 2discount utility 使用 context helper**
两个 discount utility 当前 offender 都是:
```ts
createStripeCli({
org: ctx.org,
env: ctx.env,
legacyVersion: true,
})
```
改成:
```ts
createStripeCliFromContext({
ctx,
legacyVersion: true,
})
```
- [ ] **步骤 3billing action 有 ctx 时直接用 context helper**
对已经接收 `ctx` 的 billing action
```ts
createStripeCli({ org, env })
```
改成:
```ts
createStripeCliFromContext({ ctx })
```
如果需要覆盖 env
```ts
createStripeCliFromContext({
ctx,
env,
})
```
- [ ] **步骤 4workflow 参数补 `workerEnv`**
`grantCheckoutReward.ts``storeInvoiceLineItems.ts` 如果当前没有 `ctx`,从 workflow entry 参数或调用方补:
```ts
workerEnv?: Env;
```
并传入:
```ts
createStripeCli({
org,
env,
workerEnv,
});
```
调用方有 `ctx` 时传 `ctx.workerEnv`
- [ ] **步骤 5运行检查**
```bash
cd server
bun run ts
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:类型检查 PASS静态 guard 不再包含任务 4 文件。
- [ ] **步骤 6提交本批**
```bash
git add server/src/internal/billing
git commit -m "fix: pass worker env through billing Stripe clients"
```
---
### 任务 5修 external Stripe operations 与 webhook helper
**文件:**
- 修改:`server/src/external/stripe/invoices/operations/voidStripeInvoiceIfOpen.ts`
- 修改:`server/src/external/stripe/checkoutSessions/operations/getStripeCheckoutSession.ts`
- 修改:`server/src/external/stripe/subscriptions/operations/getExpandedStripeSubscription.ts`
- 修改:`server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts`
- 修改:`server/src/external/stripe/stripeCusUtils.ts`
- [ ] **步骤 1确认本批 offenders**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含任务 5 文件。
- [ ] **步骤 2operation helper 补 `workerEnv` 参数**
对 operation helper 的参数对象增加:
```ts
workerEnv?: Env;
```
并把:
```ts
createStripeCli({ org, env })
```
改成:
```ts
createStripeCli({
org,
env,
workerEnv,
})
```
所有 request/webhook/billing 调用方必须继续向下传 `ctx.workerEnv` 或 webhook context 的 `workerEnv`
- [ ] **步骤 3webhook handler 使用 webhook context**
`handleCusDiscountDeleted.ts` 当前 legacy 调用保留版本:
```ts
createStripeCli({
org,
env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
})
```
如果该 handler 已经持有 `ctx.stripeCli` 且不需要 legacy version优先复用 `ctx.stripeCli`
- [ ] **步骤 4stripeCusUtils 按函数逐个补参数**
`stripeCusUtils.ts` 内的四个 offender 不要用模块级 env。每个导出函数都按实际调用链补
```ts
workerEnv?: Env;
```
并在内部传:
```ts
createStripeCli({
org,
env,
workerEnv,
});
```
调用方如果已经有 `ctx`,传 `ctx.workerEnv`;如果调用方没有 Worker env 且只用于测试脚本,移动到测试 allowlist 前必须确认该函数不会在 production Worker encrypted-key path 被调用。
- [ ] **步骤 5运行检查**
```bash
cd server
bun run ts
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:类型检查 PASS静态 guard 不再包含任务 5 文件。
- [ ] **步骤 6提交本批**
```bash
git add server/src/external/stripe
git commit -m "fix: pass worker env through external Stripe helpers"
```
---
### 任务 6修 Vercel integration 路径
**文件:**
- 修改:`server/src/external/vercel/handlers/handleUpdateBillingPlan.ts`
- 修改:`server/src/external/vercel/handlers/resources/handleDeleteResource.ts`
- 修改:`server/src/external/vercel/handlers/resources/handleCreateResource.ts`
- 修改:`server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoiceNotPaid.ts`
- 修改:`server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoicePaid.ts`
- 修改:`server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoiceCreated.ts`
- [ ] **步骤 1确认本批 offenders**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含任务 6 文件。
- [ ] **步骤 2有 ctx 的 Vercel handler 用 context helper**
把:
```ts
createStripeCli({
org,
env: ctx.env,
})
```
改成:
```ts
createStripeCliFromContext({
ctx,
env: ctx.env,
})
```
- [ ] **步骤 3resource handler 显式传 Worker env**
如果 handler 只有 Hono `c`,使用:
```ts
createStripeCli({
org,
env: appEnv,
workerEnv: c.env,
})
```
或:
```ts
createStripeCli({
org,
env: env as AppEnv,
workerEnv: c.env,
})
```
不要从 process env 或全局变量取 Cloudflare binding。
- [ ] **步骤 4marketplace webhook handler 补 `workerEnv`**
如果 marketplace handler 参数里已有 context传对应 `ctx.workerEnv`。如果没有,把参数对象扩展为:
```ts
workerEnv?: Env;
```
并由上层 Worker handler 填入 `c.env``ctx.workerEnv`
- [ ] **步骤 5运行检查**
```bash
cd server
bun run ts
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:类型检查 PASS静态 guard 不再包含任务 6 文件。
- [ ] **步骤 6提交本批**
```bash
git add server/src/external/vercel
git commit -m "fix: pass worker env through Vercel Stripe clients"
```
---
### 任务 7修 cron 与 migration 路径
**文件:**
- 修改:`server/src/cron/invoiceCron/runInvoiceCron.ts`
- 修改:`server/src/cron/resetCron/getStripeSubscriptionAnchor.ts`
- 修改:`server/src/internal/migrations/migrationSteps/migrateRevenuecatCustomer.ts`
- [ ] **步骤 1确认本批 offenders**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期FAIL并包含任务 7 文件。
- [ ] **步骤 2cron 使用 `CronContext.workerEnv`**
`runInvoiceCron.ts` 当前调用:
```ts
createStripeCli({ org, env: customer.env })
```
改成:
```ts
createStripeCli({
org,
env: customer.env,
workerEnv: ctx.workerEnv,
})
```
`getStripeSubscriptionAnchor.ts` 如果已经接收 `ctx: CronContext`,直接使用:
```ts
createStripeCli({
org,
env,
workerEnv: ctx.workerEnv,
})
```
如果它当前只接收 `org/env`,把参数扩展为:
```ts
workerEnv?: Env;
```
并从 `runResetCron` 或上层 cron context 传入 `ctx.workerEnv`
- [ ] **步骤 3migration step 显式传递或隔离**
`migrateRevenuecatCustomer.ts` 如果在 production Worker runtime 内触发,参数链必须增加:
```ts
workerEnv?: Env;
```
并传:
```ts
createStripeCli({
org,
env,
workerEnv,
})
```
如果确认该 migration step 只在离线脚本中运行,不能直接忽略;需要把对应文件加入静态测试 allowlist并在 allowlist 旁写明:
```ts
// Offline migration script; it does not run inside the Cloudflare Worker request path.
```
- [ ] **步骤 4运行检查**
```bash
cd server
bun run ts
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期:类型检查 PASS静态 guard 不再包含任务 7 文件。
- [ ] **步骤 5提交本批**
```bash
git add server/src/cron server/src/internal/migrations server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
git commit -m "fix: pass worker env through cron Stripe clients"
```
---
### 任务 8最终归零、部署和业务验收
**文件:**
- 测试:`server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts`
- [ ] **步骤 1静态 guard 归零**
```bash
cd server
CI=1 bun test ./tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
```
预期PASS。
- [ ] **步骤 2类型检查**
```bash
cd server
bun run ts
```
预期PASS。
- [ ] **步骤 3Worker startup 检查**
```bash
cd server
bun run cf:startup
```
预期PASS。若生成或更新 `worker-startup.cpuprofile`,只有在用户明确要求保存 profiling artifact 时才提交。
- [ ] **步骤 4部署**
```bash
cd server
bun run cf:deploy
```
预期Wrangler 输出新的 Worker Version ID。
- [ ] **步骤 5复测已知 live curl**
运行用户提供的请求:
```bash
curl -sS -i 'https://autumn-api.bowong.cc/customers/test_user_001?expand=rewards' \
-H 'Accept: application/json, text/plain, */*' \
-H 'Origin: https://autumn.bowong.ai' \
-H 'Referer: https://autumn.bowong.ai/' \
-H 'app_env: sandbox' \
-H 'x-api-version: 1.2' \
-H 'x-client-type: dashboard' \
-b '__Secure-better-auth.session_token=<use-current-session-token>'
```
预期:`HTTP/1.1 200 OK`,响应包含:
```json
{
"customer": {
"rewards": {
"discounts": []
}
}
}
```
- [ ] **步骤 6提交最终验证记录**
如果前面各批已经分别提交,本步骤只提交计划或测试 allowlist 的最终调整:
```bash
git status --short
git add docs/superpowers/plans/2026-07-02-stripe-worker-env-offenders-batches.md server/tests/unit/cloudflare/no-naked-create-stripe-cli.test.ts
git commit -m "docs: plan Stripe worker env offender cleanup"
```
---
## 自检
- Spec 覆盖:本计划覆盖现有设计中的 request、webhook、billing、product、cron、external integration 路径。
- 占位扫描:计划中没有未完成占位项。
- 类型一致性:统一使用已有 `AutumnContext.workerEnv``CronContext.workerEnv` 和底层 `createStripeCli({ workerEnv })`
- 范围控制:不改变 Stripe Connect/master account 架构,不改变 secret 加密格式,不引入空对象 env 兜底。

View File

@@ -52,7 +52,11 @@ export const handleVoidInvoiceCron = async ({
});
if (!org || !customer) return;
const stripeCli = createStripeCli({ org, env: customer.env });
const stripeCli = createStripeCli({
org,
env: customer.env,
workerEnv: ctx.workerEnv,
});
if (!metadata.stripe_invoice_id) return;

View File

@@ -20,10 +20,12 @@ export const getStripeSubscriptionAnchor = async ({
db,
cusEnt,
nextResetAt,
workerEnv,
}: {
db: DrizzleCli;
cusEnt: ResetCusEnt;
nextResetAt: number;
workerEnv?: Env;
}) => {
const entInterval = cusEnt.entitlement?.interval;
if (entInterval && shortDurations.includes(entInterval)) return nextResetAt;
@@ -49,7 +51,7 @@ export const getStripeSubscriptionAnchor = async ({
const env = cusProduct.product.env as AppEnv;
const org = cusProduct.product.org as Organization;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
if (
!cusProduct.subscription_ids ||
cusProduct.subscription_ids.length === 0

View File

@@ -144,6 +144,7 @@ const resetCustomerEntitlementInDb = async ({
db: ctx.db,
cusEnt,
nextResetAt,
workerEnv: ctx.workerEnv,
});
} catch (error) {
console.log(

View File

@@ -44,6 +44,15 @@ export const createStripeCli = ({
});
}
if (!workerEnv) {
throw new RecaseError({
message:
"Stripe secret key is encrypted, but Cloudflare workerEnv was not provided to createStripeCli",
code: ErrCode.StripeConfigNotFound,
statusCode: 500,
});
}
const cacheKey = buildSecretKeyCacheKey({
orgId: org.id,
env,
@@ -54,7 +63,7 @@ export const createStripeCli = ({
return getOrCreateStripeClient({
cacheKey,
create: () => {
const decrypted = workerEnv ? decryptData(encrypted, workerEnv) : "";
const decrypted = decryptData(encrypted, workerEnv);
const client = new Stripe(decrypted, {
apiVersion: legacyVersion
? // biome-ignore lint/suspicious/noExplicitAny: Need to cast to any to avoid type error

View File

@@ -41,7 +41,7 @@ export const getStripeCheckoutSession = async <
expand: T;
}): Promise<ExpandedStripeCheckoutSession<T>> => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const checkoutSession = await stripeCli.checkout.sessions.retrieve(
checkoutSessionId,

View File

@@ -18,7 +18,7 @@ export const createStripeCustomer = async ({
};
}): Promise<ExpandedStripeCustomer> => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const idempotencyKey = buildStripeCustomerIdempotencyKey({
ctx,

View File

@@ -59,7 +59,7 @@ export async function getExpandedStripeCustomer({
expandTax?: boolean;
}): Promise<ExpandedStripeCustomer | undefined> {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const getExpandedStripeCustomerOptional = async () => {
if (!stripeCustomerId) return undefined;

View File

@@ -17,7 +17,7 @@ export const voidStripeInvoiceIfOpen = async ({
if (stripeInvoice.status !== "open") return;
const { org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const voidedInvoice = await stripeCli.invoices.voidInvoice(stripeInvoice.id);
await invoiceActions.updateFromStripe({

View File

@@ -35,12 +35,14 @@ export const deleteStripeCustomer = async ({
org,
env,
stripeId,
workerEnv,
}: {
org: Organization;
env: AppEnv;
stripeId: string;
workerEnv?: Env;
}) => {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
const stripeCustomer = await stripeCli.customers.del(stripeId);
@@ -172,7 +174,7 @@ export const attachPmToCus = async ({
};
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
try {
const token = willFail ? "tok_chargeCustomerFail" : "tok_visa";
@@ -231,7 +233,7 @@ export const attachAuthenticatePaymentMethod = async ({
customerId: string;
}) => {
const { org, env, db } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const autumnCustomer = await CusService.get({
db,
idOrInternalId: customerId,
@@ -258,11 +260,13 @@ export const attachAuthenticatePaymentMethod = async ({
const deleteAllStripeCustomers = async ({
org,
env,
workerEnv,
}: {
org: Organization;
env: AppEnv;
workerEnv?: Env;
}) => {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
const stripeCustomers = await stripeCli.customers.list({
limit: 100,

View File

@@ -20,7 +20,7 @@ export const getExpandedStripeSubscription = async ({
subscriptionId: string;
}): Promise<ExpandedStripeSubscription> => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const expandedStripeSubscription = await stripeCli.subscriptions.retrieve(
subscriptionId,

View File

@@ -115,6 +115,7 @@ export async function handleCusDiscountDeleted({
org,
env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
await legacyStripe.customers.update(discount.customer, {

View File

@@ -34,7 +34,7 @@ export const sendUsageAndReset = async ({
resetBalance?: boolean;
}) => {
const { org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const cusEnts = activeProduct.customer_entitlements;
const cusPrices = activeProduct.customer_prices;

View File

@@ -46,7 +46,7 @@ export const handleLegacyCheckoutSessionMetadata = async ({
if (metadata.type !== MetadataType.CheckoutSessionCompleted) return null;
// Get options
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const attachParams: AttachParams = metadata.data as AttachParams;
attachParams.req = ctx as AutumnContext;

View File

@@ -20,7 +20,7 @@ export const handleSetupCheckout = async ({
const { org, customer } = attachParams;
const paymentMethod = await getCusPaymentMethod({
stripeCli: createStripeCli({ org, env: customer.env }),
stripeCli: createStripeCli({ org, env: customer.env, workerEnv: ctx.workerEnv }),
stripeId: customer.processor?.id,
errorIfNone: false,
});
@@ -42,7 +42,7 @@ export const handleSetupCheckout = async ({
ctx,
attachParams: {
...attachParams,
stripeCli: createStripeCli({ org, env: customer.env }),
stripeCli: createStripeCli({ org, env: customer.env, workerEnv: ctx.workerEnv }),
},
branch: AttachBranch.New,
config: getDefaultAttachConfig(),

View File

@@ -39,7 +39,7 @@ export const handleSetupPaymentMetadata = async ({
}
// 1. Update customer's default payment method
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const paymentMethod = await updateDefaultPaymentMethod({
stripeCli,
stripeCustomerId,

View File

@@ -41,7 +41,7 @@ export const handleStandaloneSetupCheckout = async ({
return;
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const paymentMethod = await updateDefaultPaymentMethod({
stripeCli,
stripeCustomerId,

View File

@@ -25,7 +25,7 @@ export const handleStripeInvoiceDiscounts = async ({
// Handle coupon
const { org, env, logger } = ctx;
const { stripeInvoice, stripeSubscriptionId } = invoicePaidContext;
const stripeCli = createStripeCli({ org, env, legacyVersion: true });
const stripeCli = createStripeCli({ org, env, legacyVersion: true, workerEnv: ctx.workerEnv });
if (stripeInvoice.discounts.length === 0) return;
const stripeCus = await stripeCli.customers.retrieve(

View File

@@ -22,7 +22,7 @@ export const handleInvoiceActionRequiredCompleted = async ({
const { logger, org, env } = ctx;
logger.info(`invoice.paid, handling action required`);
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const paymentMethod = await getCusPaymentMethod({
stripeCli,

View File

@@ -27,7 +27,7 @@ export const setupStripeSubscriptionUpdatedContext = async ({
const previousAttributes = event.data.previous_attributes ?? {};
// Get current time (respecting test clocks)
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const nowMs = await stripeSubscriptionToNowMs({
stripeSubscription,
stripeCli,

View File

@@ -46,7 +46,7 @@ export const handleCancelOnPastDue = async ({
});
if (!isPastDueEvent) return;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// Get latest invoice
const latestInvoice = await stripeSubscriptionToLatestInvoice({

View File

@@ -55,7 +55,7 @@ export const releaseScheduleIfLastPhase = async ({
`[handleSchedulePhaseChanges] releasing schedule (last phase reached)`,
);
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
try {
await stripeCli.subscriptionSchedules.release(

View File

@@ -48,6 +48,7 @@ export const handleUpdateVercelBillingPlan = createRoute({
const stripeCli = createStripeCli({
org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
const stripeCustomer = await stripeCli.customers.retrieve(

View File

@@ -63,7 +63,7 @@ export const handleUpsertInstallation = createRoute({
});
// Create test clock for sandbox/development environments
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
let testClockId: string | undefined;
if (ctx.env === AppEnv.Sandbox) {
const testClock = await stripeCli.testHelpers.testClocks.create({

View File

@@ -24,7 +24,7 @@ export const handleMarketplaceInvoiceCreated = async ({
const { db, org, env, logger } = ctx;
const { installationId, externalInvoiceId } = payload;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// 1. Retrieve invoice
const invoice = await stripeCli.invoices.retrieve(externalInvoiceId, {

View File

@@ -40,7 +40,7 @@ export const handleMarketplaceInvoiceNotPaid = async ({
const { db, org, env, logger } = ctx;
const { installationId, externalInvoiceId } = payload;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const invoice = await stripeCli.invoices.retrieve(externalInvoiceId, {
expand: ["subscription"],

View File

@@ -34,7 +34,7 @@ export const handleMarketplaceInvoicePaid = async ({
const { db, org, env, logger } = ctx;
const { installationId, externalInvoiceId } = payload;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const invoice = await stripeCli.invoices.retrieve(externalInvoiceId, {
expand: ["subscription"],

View File

@@ -115,7 +115,11 @@ export const handleCreateResource = createRoute({
});
}
const stripeCli = createStripeCli({ org, env: appEnv });
const stripeCli = createStripeCli({
org,
env: appEnv,
workerEnv: ctx.workerEnv,
});
const stripeCustomer = await stripeCli.customers.retrieve(
customer.processor.id,
{

View File

@@ -72,7 +72,11 @@ export const handleDeleteResource = createRoute({
// Stripe connection — must be inside the guard, not above it.
let stripeCli: Stripe | null = null;
try {
stripeCli = createStripeCli({ org, env: env as AppEnv });
stripeCli = createStripeCli({
org,
env: env as AppEnv,
workerEnv: ctx.workerEnv,
});
} catch (error) {
logCaughtError({
logger,

View File

@@ -114,7 +114,7 @@ async function fetchMissingSubscriptionsFromStripe({
return [];
}
const stripe = createStripeCli({ org: ctx.org, env: ctx.env });
const stripe = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
const stripeSubs: Stripe.Subscription[] = [];
for (const subId of missingSubIds) {

View File

@@ -104,7 +104,7 @@ export const adjustAllowance = async ({
logger.info(`Updating arrear prorated usage: ${affectedFeature.name}`);
logger.info(`Customer: ${customer.name}, Org: ${org.slug}`);
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const sub = await cusProductToSub({
cusProduct,
stripeCli,

View File

@@ -34,7 +34,7 @@ export const handleSetupPayment = createRoute({
customer,
});
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// check if user already specified payment methods in their request
const hasUserSpecifiedPaymentMethods =

View File

@@ -83,6 +83,7 @@ const expireAndClear = async ({
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
await stripeCli.checkout.sessions.expire(checkoutSessionId);
} catch (error) {

View File

@@ -23,7 +23,7 @@ export const buildRestoreBillingContext = async ({
stripeCustomer?: Stripe.Customer;
stripeSubscriptionId: string;
}): Promise<BillingContext> => {
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
const stripeSubscription = await stripeCli.subscriptions.retrieve(
stripeSubscriptionId,

View File

@@ -57,7 +57,7 @@ export const createSetupCheckoutSession = async ({
params: SetupPaymentParamsV1;
}) => {
const { org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// 1. Insert metadata (if plan_id specified)
const metadata = params.plan_id

View File

@@ -27,7 +27,7 @@ const fetchStripeSubscription = async ({
stripeSubscriptionId?: string;
}): Promise<Stripe.Subscription | null> => {
if (!stripeSubscriptionId) return null;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
return stripeCli.subscriptions.retrieve(stripeSubscriptionId);
};
@@ -39,7 +39,7 @@ const fetchStripeSchedule = async ({
stripeScheduleId?: string;
}): Promise<Stripe.SubscriptionSchedule | null> => {
if (!stripeScheduleId) return null;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
return stripeCli.subscriptionSchedules.retrieve(stripeScheduleId);
};

View File

@@ -88,7 +88,7 @@ export const subscriptionToSyncParams = async ({
});
if (scheduleId) {
resolvedSchedule = await getStripeActiveSubscriptionSchedule({
stripeClient: createStripeCli({ org: ctx.org, env: ctx.env }),
stripeClient: createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv }),
subscriptionScheduleId: scheduleId,
expand: ["phases.items.price.product"],
});

View File

@@ -56,7 +56,7 @@ export const sync = async ({
});
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const results: Array<{ plan_id: string; success: boolean; error?: string }> =
[];

View File

@@ -73,7 +73,7 @@ export const syncProposals = async ({
}
// 2. Create Stripe client and list subscriptions
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const stripeSubscriptions = await stripeCli.subscriptions.list({
customer: stripeCustomerId,
limit: 100,

View File

@@ -157,7 +157,7 @@ export const syncProposalsV2 = async ({
});
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const [subscriptionList, scheduleList] = await Promise.all([
stripeCli.subscriptions.list({
customer: stripeCustomerId,

View File

@@ -30,7 +30,11 @@ export const voidInvoicesOnImmediateCancel = async ({
const { stripeSubscription, stripeCustomer, fullCustomer } = billingContext;
if (!stripeSubscription || !stripeCustomer) return;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
const { failed } = await voidOpenInvoicesForStripeSubscription({
ctx,
stripeCli,

View File

@@ -20,7 +20,11 @@ export const buildStripeRefundAction = async ({
if (!refundPlan) return undefined;
if (refundPlan.amount <= 0) return undefined;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
const stripeInvoice = await stripeCli.invoices.retrieve(
refundPlan.invoice.stripe_id,

View File

@@ -31,7 +31,11 @@ export const executeStripeCheckoutSessionAction = async ({
const { org, logger } = ctx;
const { fullCustomer } = billingContext;
const stripeCli = createStripeCli({ org, env: fullCustomer.env });
const stripeCli = createStripeCli({
org,
env: fullCustomer.env,
workerEnv: ctx.workerEnv,
});
const enablePlanImmediately = billingContext.enablePlanImmediately === true;
const metadataType = enablePlanImmediately

View File

@@ -12,7 +12,11 @@ export const executeStripeRefundAction = async ({
ctx: AutumnContext;
refundAction: StripeRefundAction;
}): Promise<Stripe.Refund | undefined> => {
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
ctx.logger.info(
`[executeStripeRefundAction] Refunding ${refundAction.amountInCents} cents from charge ${refundAction.chargeId}`,

View File

@@ -39,7 +39,11 @@ export const executeStripeSubscriptionAction = async ({
let { stripeSubscription, currentEpochMs } = billingContext;
const { logger } = ctx;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
// 2. Lock stripe subscription
if (stripeSubscription) {

View File

@@ -194,7 +194,7 @@ export const executeStripeSubscriptionScheduleAction = async ({
stripeSubscription?: Stripe.Subscription;
}): Promise<Stripe.SubscriptionSchedule | null> => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
logSubscriptionScheduleAction({
ctx,

View File

@@ -18,7 +18,7 @@ export const fetchStripeCustomerForBilling = async ({
createIfMissing?: boolean;
}) => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const expandTax = !!ctx.org.config.automatic_tax;
const stripeCus = createIfMissing

View File

@@ -98,7 +98,11 @@ export const fetchStripeDiscountsForBilling = async ({
stripeCustomer,
});
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
if (!paramDiscounts?.length) {
return existingDiscounts;

View File

@@ -41,7 +41,7 @@ export const fetchStripeSubscriptionForBilling = async ({
: undefined;
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const cusProductWithSub = getTargetSubscriptionCusProduct({
fullCus,

View File

@@ -21,7 +21,7 @@ export const fetchStripeSubscriptionScheduleForBilling = async ({
subscriptionScheduleId?: string;
}) => {
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const product: Product | undefined = products[0];

View File

@@ -16,6 +16,10 @@ export const fetchStripeTaxRateForBilling = async ({
taxRateId?: string;
}): Promise<Stripe.TaxRate | undefined> => {
if (!taxRateId) return undefined;
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
return await stripeCli.taxRates.retrieve(taxRateId);
};

View File

@@ -36,6 +36,7 @@ export const stripeCustomerToDiscounts = async ({
org: ctx.org,
env: ctx.env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
const legacyCustomer = await legacyStripeCli.customers.retrieve(

View File

@@ -41,6 +41,7 @@ export const subToDiscounts = async ({
org: ctx.org,
env: ctx.env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
const legacySub = await legacyStripeCli.subscriptions.retrieve(sub.id, {
expand: ["discounts.coupon"],

View File

@@ -57,7 +57,11 @@ export const createInvoiceForBilling = async ({
skipSubscriptionLink?: boolean;
};
}): Promise<PayInvoiceResult> => {
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
const { addLineParams } = stripeInvoiceAction;
const { invoiceMode } = billingContext;

View File

@@ -128,7 +128,11 @@ export const createStripeInvoiceItems = async ({
ctx,
invoiceItems,
}: CreateStripeInvoiceItemsParams): Promise<Stripe.InvoiceItem[]> => {
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
return Promise.all(
invoiceItems.map((item) => stripeCli.invoiceItems.create(item)),

View File

@@ -17,7 +17,7 @@ export const executeStripeSubscriptionOperation = async ({
subscriptionAction: StripeSubscriptionAction;
}) => {
const { org, env } = ctx;
const stripeClient = createStripeCli({ org, env });
const stripeClient = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const { paymentMethod } = billingContext;
const invoiceModeParams = billingContext.invoiceMode

View File

@@ -94,7 +94,7 @@ export const computeStripeTaxPreviewForNetSubtotal = async ({
if (!billingContext.stripeCustomer?.id) return undefined;
const currency = orgToCurrency({ org: ctx.org });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
if (netSubtotal === 0) {
return {

View File

@@ -61,7 +61,7 @@ export const grantCheckoutReward = async ({
return;
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const redemptions = await redemptionRepo.getByCustomer({
db,

View File

@@ -35,7 +35,7 @@ export const storeInvoiceLineItems = async ({
payload;
try {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// 1. Fetch invoice line items from Stripe
const stripeLineItems = await getStripeInvoiceLineItems({

View File

@@ -40,6 +40,7 @@ export const deleteCustomer = async ({
org,
env,
stripeId: customer.processor.id,
workerEnv: ctx.workerEnv,
});
}
} catch (error) {

View File

@@ -24,6 +24,7 @@ export const getResetAtUpdate = async ({
cusProduct,
org,
env,
workerEnv,
}: {
curResetAt: number;
interval: EntInterval;
@@ -31,6 +32,7 @@ export const getResetAtUpdate = async ({
cusProduct: FullCusProduct | null;
org: Organization;
env: AppEnv;
workerEnv?: Env;
}): Promise<number> => {
const nextResetAt = getNextResetAt({
curReset: new UTCDate(curResetAt),
@@ -59,7 +61,7 @@ export const getResetAtUpdate = async ({
}
try {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
const subId = cusProduct.subscription_ids[0];
const sub = await stripeCli.subscriptions.retrieve(subId);

View File

@@ -77,6 +77,7 @@ export const processReset = async ({
cusProduct,
org,
env,
workerEnv: ctx.workerEnv,
});
// Compute rollover before resetting balance

View File

@@ -78,7 +78,7 @@ export const updateCustomer = async ({
const newStripeId = newCusData.stripe_id;
if (notNullish(newStripeId) && stripeId !== newStripeId) {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
await stripeCli.customers.retrieve(newStripeId);
stripeId = newCusData.stripe_id;
@@ -129,7 +129,7 @@ export const updateCustomer = async ({
};
if (Object.keys(stripeUpdate).length > 0 && stripeId) {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
await stripeCli.customers.update(stripeId, stripeUpdate);
}

View File

@@ -39,6 +39,7 @@ export const handleCreateCheckout = async ({
org,
env: customer.env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
const itemSets = await getStripeSubItems({

View File

@@ -169,6 +169,7 @@ export const checkStripeConnections = async ({
env,
product,
logger,
workerEnv: ctx.workerEnv,
}),
);
}
@@ -213,14 +214,17 @@ const createStripePrices = async ({
const customerHasPm = async ({
attachParams,
workerEnv,
}: {
attachParams: AttachParams;
workerEnv?: Env;
}) => {
// SCENARIO 3: No payment method, checkout required
const paymentMethod = await getCusPaymentMethod({
stripeCli: createStripeCli({
org: attachParams.org,
env: attachParams.customer.env,
workerEnv,
}),
stripeId: attachParams.customer.processor?.id,
});

View File

@@ -19,7 +19,7 @@ export const getStripeCusData = async ({
}
const { org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const stripeCus = await getOrCreateStripeCustomer({
ctx,

View File

@@ -16,7 +16,7 @@ export const checkToAttachParams = async ({
}) => {
const { org, env, db } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const stripeCusData = await getStripeCusData({
ctx,
customer,

View File

@@ -192,7 +192,7 @@ export const rewardProgramToAttachParams = ({
cusProducts: customer.customer_products,
entities: [],
features,
stripeCli: createStripeCli({ org, env }),
stripeCli: createStripeCli({ org, env, workerEnv: ctx.workerEnv }),
paymentMethod: null,
replaceables: [],
} satisfies AttachParams;

View File

@@ -74,7 +74,7 @@ export const processAttachBody = async ({
// 1. Get customer and products
const { org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const { customer, products } = await getCustomerAndProducts({
ctx,

View File

@@ -122,6 +122,7 @@ const updateCusEntInStripe = async ({
cusPrices,
org,
env,
workerEnv,
customer,
amountUsed,
eventId,
@@ -130,6 +131,7 @@ const updateCusEntInStripe = async ({
cusPrices: FullCustomerPrice[];
org: Organization;
env: AppEnv;
workerEnv?: Env;
customer: Customer;
amountUsed: number;
eventId: string;
@@ -144,6 +146,7 @@ const updateCusEntInStripe = async ({
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
await stripeCli.billing.meterEvents.create({

View File

@@ -71,7 +71,7 @@ export const activateDefaultProduct = async ({
return false;
}
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const defaultIsFree = isFreeProduct(defaultProd.prices);
// Initialize Stripe customer and products if needed (for paid non-trial products)

View File

@@ -62,6 +62,7 @@ export const getApiCustomerExpand = async ({
getCusRewards({
org,
env,
workerEnv: ctx.workerEnv,
fullCus,
subIds: fullCus.customer_products.flatMap(
(cp: FullCusProduct) => cp.subscription_ids || [],
@@ -76,6 +77,7 @@ export const getApiCustomerExpand = async ({
getCusPaymentMethodRes({
org,
env,
workerEnv: ctx.workerEnv,
fullCus,
expand: cusExpand,
}),

View File

@@ -88,6 +88,7 @@ export const getApiCustomerExpandV2 = async ({
getCusRewards({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
fullCus,
subIds,
expand: cusExpand,
@@ -100,6 +101,7 @@ export const getApiCustomerExpandV2 = async ({
getCusPaymentMethodRes({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
fullCus,
expand: cusExpand,
}),

View File

@@ -114,7 +114,7 @@ export const createNewCustomer = async ({
// Check if stripeCli exists
if (nonFreeProds.length > 0) {
createStripeCli({ org, env });
createStripeCli({ org, env, workerEnv: ctx.workerEnv });
}
const newCustomer = await CusService.insert({
@@ -149,7 +149,11 @@ export const createNewCustomer = async ({
);
if (!isFreeProduct(defaultProd.prices)) {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({
org,
env,
workerEnv: ctx.workerEnv,
});
await initStripeCusAndProducts({
ctx,
customer: newCustomer,

View File

@@ -10,11 +10,13 @@ import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
export const getCusPaymentMethodRes = async ({
org,
env,
workerEnv,
fullCus,
expand,
}: {
org: Organization;
env: AppEnv;
workerEnv?: Env;
fullCus: FullCustomer;
expand: CustomerExpand[];
}) => {
@@ -25,6 +27,7 @@ export const getCusPaymentMethodRes = async ({
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
const paymentMethod = await getCusPaymentMethod({

View File

@@ -15,12 +15,14 @@ import { getOriginalCouponId } from "../../../rewards/rewardUtils";
export const getCusRewards = async ({
org,
env,
workerEnv,
fullCus,
subIds,
expand,
}: {
org: Organization;
env: AppEnv;
workerEnv?: Env;
fullCus: FullCustomer;
subIds?: string[];
expand?: CustomerExpand[];
@@ -37,6 +39,7 @@ export const getCusRewards = async ({
org,
env,
legacyVersion: true,
workerEnv,
});
const [stripeCus, stripeSubs] = await Promise.all([

View File

@@ -75,6 +75,7 @@ export const handleAddCouponToCusV2 = createRoute({
org,
env,
legacyVersion: true,
workerEnv: ctx.workerEnv,
});
await getOrCreateStripeCustomer({

View File

@@ -17,7 +17,7 @@ export const createBillingPortalSession = async ({
configurationId?: string;
}) => {
const { db, org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
// Determine the Stripe customer ID to use
const stripeCustomer = await getOrCreateStripeCustomer({

View File

@@ -41,7 +41,7 @@ export const handleGetBillingPortal = createRoute({
});
}
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
const stripeCustomer = await getOrCreateStripeCustomer({
ctx,

View File

@@ -27,7 +27,7 @@ export const handleRefundInvoice = createRoute({
const { stripe_invoice_id } = c.req.param();
const { mode, amount } = c.req.valid("json");
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env, workerEnv: ctx.workerEnv });
// 1. Retrieve the Stripe invoice with payments expanded
let stripeInvoice: Stripe.Invoice;

View File

@@ -29,7 +29,7 @@ export const handleDecreaseAndTransfer = async ({
}) => {
// 1. Create new cus product for entity...
const { org, env, db, logger, features } = ctx;
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const product = cusProductToProduct({ cusProduct });
// Decrease quantity of cus product...

View File

@@ -10,7 +10,7 @@ export const handleGetCusReferrals = createRoute({
scopes: [Scopes.Customers.Read],
params: z.object({ customer_id: z.string() }),
handler: async (c) => {
const { env, db, org } = c.get("ctx");
const { env, db, org, workerEnv } = c.get("ctx");
const { customer_id } = c.req.param();
const internalCustomer = await CusService.get({
@@ -38,7 +38,7 @@ export const handleGetCusReferrals = createRoute({
}),
(async () => {
if (isStripeConnected({ org, env }) && internalCustomer.processor?.id) {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
const stripeCus: any = await stripeCli.customers.retrieve(
internalCustomer.processor.id,
);

View File

@@ -62,6 +62,7 @@ export const handleGetCustomer = createRoute({
getCusRewards({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
fullCus,
subIds: fullCus.customer_products.flatMap(
(cp: FullCusProduct) => cp.subscription_ids || [],

View File

@@ -31,6 +31,7 @@ export const handleGetInvoiceLineItems = createRoute({
const stripe = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
const autumnInvoices = (
await InvoiceService.getMany({

View File

@@ -9,12 +9,13 @@ import { createRoute } from "@/honoMiddlewares/routeHandler.js";
export const handleGetStripeInvoice = createRoute({
scopes: [Scopes.Billing.Read],
handler: async (c) => {
const { org, env } = c.get("ctx");
const { org, env, workerEnv } = c.get("ctx");
const { stripe_invoice_id } = c.req.param();
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
const stripeInvoice = await stripeCli.invoices.retrieve(stripe_invoice_id);

View File

@@ -12,7 +12,7 @@ import { InvoiceService } from "../InvoiceService.js";
export const handleRedirectToInvoice = createRoute({
scopes: [Scopes.Public],
handler: async (c) => {
const { db } = c.get("ctx");
const { db, workerEnv } = c.get("ctx");
const { invoiceId } = c.req.param();
const invoice = await InvoiceService.get({
@@ -45,6 +45,7 @@ export const handleRedirectToInvoice = createRoute({
const stripeCli = createStripeCli({
org,
env,
workerEnv,
});
const stripeInvoice = await stripeCli.invoices.retrieve(

View File

@@ -75,7 +75,11 @@ export const insertMetadataFromBillingPlan = async ({
// If stripeInvoice, update stripeInvoice with metadata id
if (stripeInvoice) {
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
const stripeCli = createStripeCli({
org: ctx.org,
env: ctx.env,
workerEnv: ctx.workerEnv,
});
await stripeCli.invoices.update(stripeInvoice.id, {
metadata: {
autumn_metadata_id: metadata.id,

View File

@@ -74,7 +74,7 @@ export const migrateRevenueCatCustomer = async ({
entitlements: toProduct.entitlements,
entities: fullCus.entities || [],
org,
stripeCli: createStripeCli({ org, env }),
stripeCli: createStripeCli({ org, env, workerEnv: ctx.workerEnv }),
paymentMethod: null,
freeTrial: null,
optionsList: cusProduct.options || [],

View File

@@ -38,7 +38,7 @@ export const deleteOrg = async ({
await Promise.all([
deleteOrgSvixApps({ org, logger, workerEnv }),
deleteOrgStripeWebhooks({ org, logger }),
deleteOrgStripeWebhooks({ org, logger, workerEnv }),
deleteOrgStripeAccounts({ org, logger, workerEnv }),
]);

View File

@@ -5,20 +5,29 @@ import { deleteStripeWebhook } from "../orgUtils.js";
export const deleteOrgStripeWebhooks = async ({
org,
logger,
workerEnv,
}: {
org: Organization;
logger: Logger;
workerEnv?: Env;
}) => {
if (!workerEnv) {
logger.error(`Cannot delete stripe webhooks for ${org.id}: missing Worker env`);
return;
}
const { error } = await tryCatch(
(async () => {
await deleteStripeWebhook({
org,
env: AppEnv.Sandbox,
workerEnv,
});
await deleteStripeWebhook({
org,
env: AppEnv.Live,
workerEnv,
});
})(),
);

View File

@@ -53,7 +53,7 @@ export const deletePlatformSubOrg = async ({
await deleteSvixWebhooks({ org, logger, workerEnv });
logger.info("2. Deleting stripe webhooks");
await deleteStripeWebhooks({ org, logger });
await deleteStripeWebhooks({ org, logger, workerEnv });
logger.info("3. Deleting stripe accounts");
await deleteStripeAccounts({ org, logger, workerEnv });

View File

@@ -50,19 +50,28 @@ const deleteSvixWebhooks = async ({
const deleteStripeWebhooks = async ({
org,
logger,
workerEnv,
}: {
org: Organization;
logger: any;
workerEnv?: Env;
}) => {
if (!workerEnv) {
logger.error(`Cannot delete stripe webhooks for ${org.id}: missing Worker env`);
return;
}
try {
await deleteStripeWebhook({
org: org,
env: AppEnv.Sandbox,
workerEnv,
});
await deleteStripeWebhook({
org: org,
env: AppEnv.Live,
workerEnv,
});
} catch (error: any) {
logger.error(

View File

@@ -26,7 +26,12 @@ const disconnectStripe = async ({
workerEnv: Env;
}) => {
if (isStripeConnected({ org, env, throughSecretKey: true })) {
const stripeCli = createStripeCli({ org, env, throughSecretKey: true });
const stripeCli = createStripeCli({
org,
env,
throughSecretKey: true,
workerEnv,
});
const webhooks = await stripeCli.webhookEndpoints.list();
for (const webhook of webhooks.data) {
if (webhook.url.includes(org.id) && webhook.url.includes(env)) {

View File

@@ -14,7 +14,7 @@ export const handleGetStripeAccount = createRoute({
}
try {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv: ctx.workerEnv });
const accountDetails = await stripeCli.accounts.retrieve();
return c.json(accountDetails);
} catch (error) {

View File

@@ -123,7 +123,11 @@ export const handleOAuthCallback = async (c: Context<HonoEnv>) => {
}
// Standard flow returns detailed error
const master = createStripeCli({ org: existingOrg, env });
const master = createStripeCli({
org: existingOrg,
env,
workerEnv: c.env,
});
const account = await master.accounts.retrieve(accountId);
redirectUrl.searchParams.set("error", "account_already_connected");
redirectUrl.searchParams.set("account_id", accountId);

View File

@@ -33,16 +33,18 @@ export const shouldReconnectStripe = async ({
env,
logger,
stripeKey,
workerEnv,
}: {
org: Organization;
env: AppEnv;
logger: any;
stripeKey: string;
workerEnv: Env;
}) => {
if (!isStripeConnected({ org, env })) return true;
try {
const stripeCli = createStripeCli({ org, env });
const stripeCli = createStripeCli({ org, env, workerEnv });
const newKey = new Stripe(stripeKey);
const oldAccount = await stripeCli.accounts.retrieve();
@@ -133,13 +135,20 @@ const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
export const deleteStripeWebhook = async ({
org,
env,
workerEnv,
}: {
org: Organization;
env: AppEnv;
workerEnv: Env;
}) => {
if (!isStripeConnected({ org, env, throughSecretKey: true })) return;
const stripeCli = createStripeCli({ org, env, throughSecretKey: true });
const stripeCli = createStripeCli({
org,
env,
throughSecretKey: true,
workerEnv,
});
const webhookEndpoints = await stripeCli.webhookEndpoints.list({
limit: 100,
});

View File

@@ -48,19 +48,28 @@ export const deleteSvixWebhooks = async ({
export const deleteStripeWebhooks = async ({
org,
logger,
workerEnv,
}: {
org: Organization;
logger: Logger;
workerEnv?: Env;
}) => {
if (!workerEnv) {
logger.error(`Cannot delete stripe webhooks for ${org.id}: missing Worker env`);
return;
}
try {
await deleteStripeWebhook({
org: org,
env: AppEnv.Sandbox,
workerEnv,
});
await deleteStripeWebhook({
org: org,
env: AppEnv.Live,
workerEnv,
});
} catch (error: any) {
logger.error(

View File

@@ -147,6 +147,7 @@ export const handleLegacyPlatformExchange = createRoute({
env: AppEnv.Sandbox,
stripeKey: stripe_test_key,
logger,
workerEnv: c.env,
});
if (reconnectStripe) {
@@ -188,6 +189,7 @@ export const handleLegacyPlatformExchange = createRoute({
env: AppEnv.Live,
stripeKey: stripe_live_key,
logger,
workerEnv: c.env,
});
if (reconnectStripe) {

View File

@@ -116,6 +116,7 @@ export const updateProduct = async ({
org,
rewardPrograms,
logger: ctx.logger,
workerEnv: ctx.workerEnv,
});
const itemsExist = notNullish(updates.items);

Some files were not shown because too many files have changed in this diff Show More