fix: pass worker env to Stripe clients
This commit is contained in:
@@ -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` / “类似上面” 等占位措辞。
|
||||
@@ -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 文件。
|
||||
|
||||
- [ ] **步骤 2:request 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,
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:org 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
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:reward 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 文件。
|
||||
|
||||
- [ ] **步骤 2:discount utility 使用 context helper**
|
||||
|
||||
两个 discount utility 当前 offender 都是:
|
||||
|
||||
```ts
|
||||
createStripeCli({
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
legacyVersion: true,
|
||||
})
|
||||
```
|
||||
|
||||
改成:
|
||||
|
||||
```ts
|
||||
createStripeCliFromContext({
|
||||
ctx,
|
||||
legacyVersion: true,
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:billing action 有 ctx 时直接用 context helper**
|
||||
|
||||
对已经接收 `ctx` 的 billing action,把:
|
||||
|
||||
```ts
|
||||
createStripeCli({ org, env })
|
||||
```
|
||||
|
||||
改成:
|
||||
|
||||
```ts
|
||||
createStripeCliFromContext({ ctx })
|
||||
```
|
||||
|
||||
如果需要覆盖 env:
|
||||
|
||||
```ts
|
||||
createStripeCliFromContext({
|
||||
ctx,
|
||||
env,
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:workflow 参数补 `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 文件。
|
||||
|
||||
- [ ] **步骤 2:operation helper 补 `workerEnv` 参数**
|
||||
|
||||
对 operation helper 的参数对象增加:
|
||||
|
||||
```ts
|
||||
workerEnv?: Env;
|
||||
```
|
||||
|
||||
并把:
|
||||
|
||||
```ts
|
||||
createStripeCli({ org, env })
|
||||
```
|
||||
|
||||
改成:
|
||||
|
||||
```ts
|
||||
createStripeCli({
|
||||
org,
|
||||
env,
|
||||
workerEnv,
|
||||
})
|
||||
```
|
||||
|
||||
所有 request/webhook/billing 调用方必须继续向下传 `ctx.workerEnv` 或 webhook context 的 `workerEnv`。
|
||||
|
||||
- [ ] **步骤 3:webhook handler 使用 webhook context**
|
||||
|
||||
`handleCusDiscountDeleted.ts` 当前 legacy 调用保留版本:
|
||||
|
||||
```ts
|
||||
createStripeCli({
|
||||
org,
|
||||
env,
|
||||
legacyVersion: true,
|
||||
workerEnv: ctx.workerEnv,
|
||||
})
|
||||
```
|
||||
|
||||
如果该 handler 已经持有 `ctx.stripeCli` 且不需要 legacy version,优先复用 `ctx.stripeCli`。
|
||||
|
||||
- [ ] **步骤 4:stripeCusUtils 按函数逐个补参数**
|
||||
|
||||
`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,
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:resource 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。
|
||||
|
||||
- [ ] **步骤 4:marketplace 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 文件。
|
||||
|
||||
- [ ] **步骤 2:cron 使用 `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`。
|
||||
|
||||
- [ ] **步骤 3:migration 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。
|
||||
|
||||
- [ ] **步骤 3:Worker 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 兜底。
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -144,6 +144,7 @@ const resetCustomerEntitlementInDb = async ({
|
||||
db: ctx.db,
|
||||
cusEnt,
|
||||
nextResetAt,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(
|
||||
|
||||
11
server/src/external/connect/createStripeCli.ts
vendored
11
server/src/external/connect/createStripeCli.ts
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
12
server/src/external/stripe/stripeCusUtils.ts
vendored
12
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -115,6 +115,7 @@ export async function handleCusDiscountDeleted({
|
||||
org,
|
||||
env,
|
||||
legacyVersion: true,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
|
||||
await legacyStripe.customers.update(discount.customer, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -48,6 +48,7 @@ export const handleUpdateVercelBillingPlan = createRoute({
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env: ctx.env,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
|
||||
const stripeCustomer = await stripeCli.customers.retrieve(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
|
||||
@@ -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 }> =
|
||||
[];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -40,6 +40,7 @@ export const deleteCustomer = async ({
|
||||
org,
|
||||
env,
|
||||
stripeId: customer.processor.id,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export const processReset = async ({
|
||||
cusProduct,
|
||||
org,
|
||||
env,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
|
||||
// Compute rollover before resetting balance
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const handleCreateCheckout = async ({
|
||||
org,
|
||||
env: customer.env,
|
||||
legacyVersion: true,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
|
||||
const itemSets = await getStripeSubItems({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -75,6 +75,7 @@ export const handleAddCouponToCusV2 = createRoute({
|
||||
org,
|
||||
env,
|
||||
legacyVersion: true,
|
||||
workerEnv: ctx.workerEnv,
|
||||
});
|
||||
|
||||
await getOrCreateStripeCustomer({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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 }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
})(),
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user