> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe 迁移工具

> 保留现有 Stripe 调用，只需 3 步，将即将到期的订阅交接给 Waffo。

**它能做什么：** 在 Stripe 已付周期结束前，让客户先在 Waffo 完成卡输入和必要的 3DS 验证；到期前不扣款，到期时 Waffo 自动发起首笔扣款。等待期间可以查询迁移状态，也可以立即取消并阻止首次扣款。

**接入只需 3 步：** 换成 `WaffoStripe.client(...)`、给目标订阅补充路由标记与交接时间、接入 Waffo Webhook。现有 `com.stripe.*` 类型和调用模式继续保留。[快速接入](#快速接入)展示了完整的代码改造；上线前还需按接入指南持久化幂等键及订阅对应关系。

<Note>
  迁移工具目前提供 Java 版本：`com.waffo:waffo-java-stripe`。Node.js、Python、Go 版本即将发布。
</Note>

## 它不是 Waffo 原生 SDK

Waffo 提供两套完全不同的接入方式，先确认哪一套适合你。

|       | 原生 SDK                      | Stripe 迁移工具                   |
| ----- | --------------------------- | ----------------------------- |
| 制品    | `waffo-java`、`waffo-node` 等 | `waffo-java-stripe`           |
| 怎么来的  | 从 Waffo 的 `openapi.json` 生成 | 手写的兼容层，包住你自带的官方 `stripe-java` |
| 你写的代码 | Waffo 的接口和数据结构              | 保持 `com.stripe.*`，不变          |
| 能力范围  | Waffo 全部接口                  | 订阅 Checkout 的一个子集             |
| 适合谁   | 全新接入，或愿意按 Waffo 接口重写        | 已有 Stripe 集成，想低成本迁移订阅         |

**全新接入请直接用[原生 SDK](/docs/zh/developer-docs/sdk/java)。** 适配器的价值只在一种场景成立：你已经有一套跑在 Stripe 上的订阅代码，改动成本是主要顾虑。

## 迁移工具替你做了什么

<CardGroup cols={2}>
  <Card title="路由" icon="route">
    判断每个请求该发给 Waffo 还是原样发给 Stripe，你的调用代码不需要分支。
  </Card>

  <Card title="参数翻译" icon="languages">
    把 Stripe 的 `SessionCreateParams` 翻译成 Waffo 的订阅创建请求，包括金额、周期、币种、支付方式、收银台语言。
  </Card>

  <Card title="结果反向映射" icon="arrow-right-left">
    把 Waffo 的响应装回 Stripe 的 `Session`、`Subscription` 对象，你照常用 getter 取值。
  </Card>

  <Card title="通知翻译" icon="bell">
    把 Waffo 的订阅通知翻译成 Stripe 的 `Event`，你原有的 Webhook 分支代码继续可用。
  </Card>
</CardGroup>

## 工作原理

适配器返回的是一个标准的 `StripeClient`。它拦截每个请求，按三条规则决定去向：

| 规则   | 匹配条件                                         | 去向          |
| ---- | -------------------------------------------- | ----------- |
| 创建路由 | 订阅模式的 Checkout 创建，且带 `metadata.source=waffo` | Waffo       |
| 查询路由 | 对象 id 以 `wsub_` 或 `wcs_` 开头                  | Waffo       |
| 透传   | 其余全部请求                                       | Stripe，原样不动 |

也就是说，**没打标记的调用完全不受影响**——一次性支付、未标记的订阅、客户对象、价格对象，行为和迁移前一模一样。你可以只把一部分订阅切到 Waffo，两边并行运行。

一次完整的订阅支付会这样流转：

<Steps>
  <Step title="创建订阅 Checkout">
    你照常调用 `client.checkout().sessions().create(params)`，只是 `params` 里多了一行 `metadata.source=waffo`。适配器把它翻译成 Waffo 的订阅创建请求。
  </Step>

  <Step title="拿到收银台地址">
    返回的仍然是 Stripe 的 `Session` 对象，`session.getUrl()` 里装的是 Waffo 收银台地址。**你的跳转代码不用改**，它并不关心这个地址指向谁。
  </Step>

  <Step title="用户完成支付">
    用户在 Waffo 收银台完成支付。这一段完全由 Waffo 承接，你的应用不参与。
  </Step>

  <Step title="接收并翻译通知">
    Waffo 把订阅通知发到你配置的 `notifyUrl`。你的端点调用 `WaffoStripeWebhooks.handle(...)`，拿到 `WaffoStripeWebhookResult`，再通过 `result.getEvent()` 取得标准的 Stripe `Event`。
  </Step>

  <Step title="原有业务逻辑继续跑">
    你原来 `switch (event.getType())` 里处理 `customer.subscription.created`、`invoice.paid` 的代码不用改，直接接住翻译后的事件。
  </Step>
</Steps>

## 支持哪些场景

首期能力聚焦在**跳转式收银台的订阅**上。下面这张表用来判断你的现状能不能接住。

| 场景                               | 结果                        | 你要做什么                                                                   |
| -------------------------------- | ------------------------- | ----------------------------------------------------------------------- |
| 订阅模式、跳转式收银台、单个 line item、卡或可续费钱包 | 路由到 Waffo                 | 添加路由标记，并显式传入已持久化的幂等键                                                    |
| Stripe 订阅的已付周期即将结束               | 提前在 Waffo 完成卡验证，在交接时间自动首扣 | 把 Stripe 的周期结束时间作为 `billing_cycle_anchor`，并设置 `proration_behavior=none` |

完整的回落条件、原因码和取消能力边界见[接入指南的不支持用法](/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration#不支持的-stripe-用法)。

## 将 Stripe 的即将到期订阅无缝转移至 Waffo

假设 Stripe 订阅当前已付周期在时间 `T` 结束。你可以先按现有 Stripe 流程让旧订阅在 `T` 停止续费，再提前为同一客户创建从 `T` 开始的 Waffo 订阅：

<Steps>
  <Step title="读取交接时间">
    从 Stripe 订阅读取已付周期结束时间 `T`。
  </Step>

  <Step title="设置 Waffo 开始时间">
    在新的 Waffo 路由请求中，把 `subscription_data.billing_cycle_anchor` 设为 `T`，同时设置 `proration_behavior=none`。
  </Step>

  <Step title="提前完成验证">
    让客户在 `T` 之前打开 Waffo 收银台，完成卡输入和必要的 3DS 验证。此时不会收取首笔费用。
  </Step>

  <Step title="确认等待状态">
    在等待期间查询 `wsub_…`。`billing_cycle_anchor` 与 `current_period_end` 都等于 `T`，`metadata.waffo_current_period=0` 表示尚未开始首个账期。
  </Step>

  <Step title="自动完成首扣">
    到达 `T` 后，Waffo 自动发起首笔扣款，客户不需要再次操作。
  </Step>
</Steps>

如果客户在 `T` 之前取消迁移，调用 `Subscription.cancel("wsub_…")` 会立即取消 Waffo 订阅，并阻止预定的首次扣款。

<Note>
  Stripe 迁移工具只承接 Waffo 侧的申请、查询和取消，不会修改原 Stripe 订阅。你仍需通过现有 Stripe 代码或后台，把旧订阅配置为在同一个 `T` 结束。
</Note>

## 快速接入

下面分三步展示关键代码改造。除了代码调整，你还需要在发送请求前持久化幂等键，并在创建成功后保存 Waffo 订阅 id 与业务订单的关系。这些示例是放进现有类中的局部片段，沿用你项目已有的 imports、依赖字段和业务方法。

### 第一步：换客户端构造

<CodeGroup>
  ```java 改造后 theme={null}
  import com.stripe.Stripe;
  import com.stripe.StripeClient;
  import com.waffo.stripe.WaffoStripe;                             // ← 新增
  import com.waffo.stripe.config.WaffoConfig;                      // ← 新增

  // ← 新增：Waffo 凭据。环境变量名与 waffo-java 约定一致时，一行 fromEnv() 即可
  com.waffo.types.config.WaffoConfig waffoJavaConfig =
          com.waffo.types.config.WaffoConfig.fromEnv();

  // ← 新增：路由配置
  WaffoConfig routing = WaffoConfig.builder()
          .waffoConfig(waffoJavaConfig)
          .notifyUrl("https://your-app.example/webhooks/waffo")
          .build();

  Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY");              // ← 改这里：密钥改用全局设置
  StripeClient client = WaffoStripe.client(routing);                // ← 改这里：换掉构造方式
  ```

  ```java 改造前 theme={null}
  import com.stripe.StripeClient;

  StripeClient client = new StripeClient(System.getenv("STRIPE_SECRET_KEY"));
  ```
</CodeGroup>

拿到的 `client` 就是标准的 `StripeClient`，可以直接替换你原来那个，调用它的所有代码不用动。

### 第二步：添加路由标记与幂等键

<CodeGroup>
  ```java 改造后 theme={null}
  SessionCreateParams params = SessionCreateParams.builder()
          .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
          .setSuccessUrl("https://your-app.example/subscription/success")
          .setCancelUrl("https://your-app.example/subscription/cancel")
          .addLineItem(SessionCreateParams.LineItem.builder()
                  .setPrice("price_123")
                  .setQuantity(1L)
                  .build())
          .putMetadata("source", "waffo")                          // ← 新增：这一行决定走 Waffo
          .build();

  RequestOptions options = RequestOptions.builder()                // ← 新增：幂等键，调用前先持久化
          .setIdempotencyKey(persistedSubscriptionRequest)
          .build();

  Session session = client.checkout().sessions()
          .create(params, options);                                // ← 改这里：多传一个 options
  saveSubscriptionMapping(orderId, session.getSubscription());     // ← 新增：保存 wsub_ 与业务订单的关系
  redirect(session.getUrl());                                      // 不变：仍然从 getUrl() 取跳转地址
  ```

  ```java 改造前 theme={null}
  SessionCreateParams params = SessionCreateParams.builder()
          .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
          .setSuccessUrl("https://your-app.example/subscription/success")
          .setCancelUrl("https://your-app.example/subscription/cancel")
          .addLineItem(SessionCreateParams.LineItem.builder()
                  .setPrice("price_123")
                  .setQuantity(1L)
                  .build())
          .build();

  Session session = client.checkout().sessions().create(params);
  redirect(session.getUrl());
  ```
</CodeGroup>

示例刻意不设置 `uiMode`。Stripe 默认使用跳转式收银台，迁移工具也把未设置的值按跳转式收银台处理。这样可以同时兼容 `stripe-java` 24.11.x、32.x 和 33.x；不要在 32.x 或 33.x 中改用 `HOSTED_PAGE`，因为对应的 `hosted_page` 值会被判定为非跳转式收银台。

不要依赖 `stripe-java` 自动生成幂等键。请显式传入并持久化一个不超过 32 个字符的稳定键，在每次重试中复用。详细规则见[接入指南](/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration)。

### 第三步：接入 Waffo Webhook

你的应用仍需新增一个 HTTP 端点，原来的 Stripe Webhook 端点不用动。SDK 的 `handle(...)` 已经完成验签、解析、事件翻译和确认响应生成；端点只负责把翻译结果交给你的业务代码，并把 SDK 生成的确认响应交给 Web 框架返回。

<CodeGroup>
  ```java 改造后 theme={null}
  // Stripe 原端点保持不动，下面新增 Waffo 的 HTTP 路由
  @PostMapping(value = "/webhooks/waffo", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<String> onWaffo(@RequestBody String body,
                                        @RequestHeader("X-SIGNATURE") String signature) throws StripeException {

      WaffoStripeWebhookResult result = webhooks.handle(body, signature);   // SDK 完成验签、翻译和确认响应生成

      if (result.getPaymentNotification() != null) {
          subscriptionPaymentHandler.handle(                                // 订阅扣款单独记账
                  result.getPaymentNotification().getResult());
      }

      Event event = result.getEvent();
      if (event != null) {
          existingStripeWebhookDispatcher.dispatch(event);                  // 不变：复用原有分发逻辑
      }

      return ResponseEntity.ok()                                            // 将 SDK 结果映射为 Spring HTTP 响应
              .contentType(MediaType.APPLICATION_JSON)
              .body(result.getResponseBody());
  }
  ```

  ```java 改造前 theme={null}
  import com.stripe.net.Webhook;

  // 只有 Stripe 一个端点
  @PostMapping("/webhooks/stripe")
  public ResponseEntity<String> onStripe(@RequestBody String body,
                                         @RequestHeader("Stripe-Signature") String signature) {
      try {
          Event event = Webhook.constructEvent(body, signature, endpointSecret);
          existingStripeWebhookDispatcher.dispatch(event);
          return ResponseEntity.ok("ok");    // Stripe 只看状态码，响应体写什么都行
      } catch (SignatureVerificationException e) {
          return ResponseEntity.badRequest().build();
      }
  }
  ```
</CodeGroup>

`WaffoStripeWebhookResult` 已包含确认响应正文。示例最后几行只是把它映射为 Spring 的 `ResponseEntity`；使用其他 Web 框架时做等价映射即可。详见[接入指南](/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration)。

<Card title="接入指南" icon="book-open" href="/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration">
  配置参数全表、Webhook 处理方式、不支持的 Stripe 用法全表、接入检查表与 Sandbox 验证要求。
</Card>

### 用 AI 迁移技能自动完成

如果你使用 Claude Code、Codex 或 Cursor，可以让 AI 完成扫描与改造：

```bash theme={null}
npx @waffo/waffo-stripe-migrate
```

安装后在你的项目里对 AI 说「迁移 Stripe」。它会扫描你项目里所有 Stripe 调用、标出无法路由的地方、在你确认改动后写入代码，并引导完成 Sandbox 验收。

<Note>
  扫描结果只用于发现风险，不等于验收结论。无论用哪条路径接入，都必须通过你项目自己的接口跑通 Sandbox 全链路才算完成，参见[接入指南的 Sandbox 验证](/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration#sandbox-验证)。
</Note>

## 版本与前提

| 项目            | 说明                                                                                                  |
| ------------- | --------------------------------------------------------------------------------------------------- |
| 制品坐标          | `com.waffo:waffo-java-stripe`                                                                       |
| 当前版本          | 0.2.0（发布于 [Maven Central](https://central.sonatype.com/artifact/com.waffo/waffo-java-stripe/0.2.0)） |
| `stripe-java` | **由你提供**，适配器不锁定版本。下限 24.11.0，已验证到 33.x                                                              |
| `waffo-java`  | 3.0.0，由适配器传递引入                                                                                      |
| 构建目标          | Java 8，用 JDK 8 或 17 都能构建                                                                            |

`stripe-java` 是 provided 依赖，意味着适配器不会替你升级或降级它，你项目里现有的版本保持不动。请确保版本不低于 24.11.0。

接入前还需要确认：

* 你与 Waffo 已签订订阅业务合约，并拿到了 Sandbox 凭据
* 你要路由的币种在合约范围内，可用 `paymethodconfig/inquiry` 查询
* 你有一个可公网访问的 HTTPS 端点用于接收 Waffo 通知

## 相关资源

* [接入指南](/docs/zh/developer-docs/tools-and-references/references/stripe-adapter-integration)——完整接入步骤与技术细节
* [Waffo Java SDK](/docs/zh/developer-docs/sdk/java)——原生 SDK，全新接入的推荐方式
* [订阅与续费](/docs/zh/essentials/subscription-recurring)——Waffo 订阅能力总览
* [GitHub 仓库](https://github.com/waffo-com/waffo-stripe)——源码与更新日志
