0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PAY.JP の SDK を Rust でバイブ実装してみる

Last updated at Posted at 2025-12-24

この記事は、PAY Advent Calendar 2025 の23日目として書いています。昨日は誕生日だったので、少々遅れてしまいました・・・

※今回実装したものは、テスト実装なので、くれぐれも使おうとはしないようにお願いします!

PAY.JP について

PAY.JP(ペイドットジェーピー)とは、開発者や事業者向けに、Webサイトやアプリにクレジットカード決済機能を簡単に導入できるオンライン決済代行サービスです。シンプルなAPIと豊富なライブラリが特徴で、PCI DSS準拠の高いセキュリティのもと、業界最低水準の決済手数料(Visa/Mastercard: 2.59%〜)で提供されており、サブスクリプションやプラットフォーム決済、Apple Payなど多様な決済に対応しています。

主な特徴

  • 簡単な導入: 開発者向けのAPIが用意されており、数行のコードでクレジットカード決済機能を実装できます
  • 高いセキュリティ: 国際標準のPCI DSS v4.0に準拠し、カード情報をトークン化することで安全に決済を処理します
  • リーズナブルな手数料: 業界最低水準の決済手数料で、コストを抑えて利用できます
  • 豊富な機能: クレジットカード決済のほか、定期課金、Apple Pay、プラットフォーム決済、通知機能などを統合して利用できます
  • 柔軟な料金プラン: ベンチャー企業や教育機関向けなど、業種や利用規模に応じたプランがあります
  • 資金調達サービス: 売上実績に基づいて資金を調達できる「PAY.JP YELL BANK」も提供しています

PAY.JP の SDK

オフィシャルとして、様々な言語で SDK が実装されています

Rust 用 SDK

最初は他のものを作っていたのですが、少々事情があり、そちらは一旦置いといて、こちらを出すことにしました。Rust 用の SDK が見当たらなかったというのも大きな理由です。

とりあえず、プロンプト

Go SDK を参照して作ったらしいですが、長いですね・・・

# PAY.JP Rust SDK 作成プロンプト

## 概要

日本の決済サービス PAY.JP (https://pay.jp) の Rust SDK を作成してください。
PAY.JP は REST ベースの決済 API を提供しており、都度の支払い、定期課金、顧客管理など多様な機能を持っています。

## 参考資料

- **API ドキュメント**: https://pay.jp/docs/api/
- **既存 SDK 実装**:
  - Go SDK: https://github.com/payjp/payjp-go (参考構造)
  - Node.js SDK: https://github.com/payjp/payjp-node
  - PHP SDK: https://github.com/payjp/payjp-php

## 基本仕様

### API エンドポイント
- Base URL: `https://api.pay.jp/v1`
- 認証: Basic 認証 (シークレットキーをユーザー名として使用、パスワードは空)
- リクエスト: GET, POST, DELETE
- レスポンス: JSON (UTF-8)

### 対応リソース

以下のリソースに対応した実装を行ってください:

1. **Charge (支払い)** - `ch_` prefix
   - create, retrieve, update, refund, capture, reauth, tds_finish, list

2. **Customer (顧客)** - `cus_` prefix
   - create, retrieve, update, delete, list

3. **Card (カード)** - `car_` prefix (Customer に紐づく)
   - create, retrieve, update, delete, list

4. **Token (トークン)** - `tok_` prefix
   - create, retrieve, tds_finish

5. **Plan (プラン)** - `pln_` prefix
   - create, retrieve, update, delete, list

6. **Subscription (定期課金)** - `sub_` prefix
   - create, retrieve, update, pause, resume, cancel, delete, list

7. **Transfer (入金)** - `tr_` prefix
   - retrieve, list, charges

8. **Event (イベント)** - `evnt_` prefix
   - retrieve, list

9. **Statement (取引明細)** - `st_` prefix
   - retrieve, statement_urls, list

10. **Balance (残高)** - `ba_` prefix
    - retrieve, statement_urls, list

11. **Term (集計区間)** - `tm_` prefix
    - retrieve, list

12. **ThreeDSecureRequest (3Dセキュアリクエスト)** - `tdsr_` prefix
    - create, retrieve, list

13. **Account (アカウント)**
    - retrieve

14. **Tenant (テナント)** - Platform API - `ten_` prefix
    - create, retrieve, update, delete, list, create_application_urls

15. **TenantTransfer (テナント入金)** - Platform API
    - retrieve, list, charges

## 推奨クレート

```toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
url = "2.5"

[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"
```

## 設計方針

### 1. クライアント構造

```rust
pub struct PayjpClient {
    api_key: String,
    http_client: reqwest::Client,
    base_url: String,
    // リトライ設定
    max_retry: u32,
    retry_initial_delay: Duration,
    retry_max_delay: Duration,
}

impl PayjpClient {
    pub fn new(api_key: impl Into<String>) -> Self;
    pub fn with_options(api_key: impl Into<String>, options: ClientOptions) -> Self;
    
    // リソースアクセサ
    pub fn charges(&self) -> ChargeService;
    pub fn customers(&self) -> CustomerService;
    pub fn plans(&self) -> PlanService;
    pub fn subscriptions(&self) -> SubscriptionService;
    pub fn tokens(&self) -> TokenService;
    pub fn transfers(&self) -> TransferService;
    pub fn events(&self) -> EventService;
    pub fn accounts(&self) -> AccountService;
    // ... 他のリソース
}
```

### 2. エラーハンドリング

PAY.JP API のエラーコードに対応した詳細なエラー型を定義:

```rust
#[derive(Debug, thiserror::Error)]
pub enum PayjpError {
    #[error("API error: {0}")]
    Api(ApiError),
    
    #[error("Card error: {0}")]
    Card(CardError),
    
    #[error("Authentication error: {0}")]
    Auth(String),
    
    #[error("Rate limit exceeded")]
    RateLimit,
    
    #[error("Network error: {0}")]
    Network(#[from] reqwest::Error),
    
    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
}

#[derive(Debug, Deserialize)]
pub struct ApiError {
    pub status: u16,
    pub r#type: String,
    pub message: String,
    pub code: Option<String>,
    pub param: Option<String>,
}
```

### 3. オブジェクト定義例

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Charge {
    pub id: String,
    pub object: String,
    pub livemode: bool,
    pub created: i64,
    pub amount: i64,
    pub currency: String,
    pub paid: bool,
    pub captured: bool,
    pub captured_at: Option<i64>,
    pub card: Option<Card>,
    pub customer: Option<String>,
    pub description: Option<String>,
    pub failure_code: Option<String>,
    pub failure_message: Option<String>,
    pub fee_rate: Option<String>,
    pub refunded: bool,
    pub amount_refunded: i64,
    pub refund_reason: Option<String>,
    pub subscription: Option<String>,
    pub metadata: Option<HashMap<String, String>>,
    pub expired_at: Option<i64>,
    pub three_d_secure_status: Option<ThreeDSecureStatus>,
    // Platform API
    pub tenant: Option<String>,
    pub platform_fee: Option<i64>,
    pub platform_fee_rate: Option<String>,
    pub total_platform_fee: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThreeDSecureStatus {
    Unverified,
    Verified,
    Attempted,
    Failed,
    Error,
}
```

### 4. リスト取得(ページネーション)

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResponse<T> {
    pub object: String,
    pub data: Vec<T>,
    pub has_more: bool,
    pub url: String,
    pub count: i64,
}

#[derive(Debug, Default, Clone, Serialize)]
pub struct ListParams {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offset: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub since: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub until: Option<i64>,
}
```

### 5. サービス実装例

```rust
pub struct ChargeService<'a> {
    client: &'a PayjpClient,
}

impl<'a> ChargeService<'a> {
    pub async fn create(&self, params: CreateChargeParams) -> Result<Charge, PayjpError>;
    pub async fn retrieve(&self, id: &str) -> Result<Charge, PayjpError>;
    pub async fn update(&self, id: &str, params: UpdateChargeParams) -> Result<Charge, PayjpError>;
    pub async fn capture(&self, id: &str, amount: Option<i64>) -> Result<Charge, PayjpError>;
    pub async fn refund(&self, id: &str, params: RefundParams) -> Result<Charge, PayjpError>;
    pub async fn reauth(&self, id: &str, expiry_days: Option<i64>) -> Result<Charge, PayjpError>;
    pub async fn tds_finish(&self, id: &str) -> Result<Charge, PayjpError>;
    pub async fn list(&self, params: ListChargeParams) -> Result<ListResponse<Charge>, PayjpError>;
}

#[derive(Debug, Default, Clone, Serialize)]
pub struct CreateChargeParams {
    pub amount: i64,
    pub currency: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub card: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub customer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub capture: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expiry_days: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub three_d_secure: Option<bool>,
    // Platform API
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tenant: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub platform_fee: Option<i64>,
}
```

### 6. レートリミット対応

PAY.JP には以下のレートリミットがあります:

| モード | ゾーン | 流速 (req/sec) |
|--------|--------|----------------|
| livemode | pk | 10 |
| livemode | payment | 14 |
| livemode | sk | 30 |
| testmode | pk | 2 |
| testmode | payment | 2 |
| testmode | sk | 2 |

HTTP 429 レスポンス時は Exponential Backoff with Jitter でリトライ:

```rust
impl PayjpClient {
    async fn request_with_retry<T: DeserializeOwned>(
        &self,
        method: Method,
        path: &str,
        body: Option<&impl Serialize>,
    ) -> Result<T, PayjpError> {
        let mut retry_count = 0;
        loop {
            match self.send_request(method.clone(), path, body).await {
                Ok(response) => return Ok(response),
                Err(PayjpError::RateLimit) if retry_count < self.max_retry => {
                    let delay = self.calculate_retry_delay(retry_count);
                    tokio::time::sleep(delay).await;
                    retry_count += 1;
                }
                Err(e) => return Err(e),
            }
        }
    }
    
    fn calculate_retry_delay(&self, retry_count: u32) -> Duration {
        let base = self.retry_initial_delay.as_millis() as u64 * 2u64.pow(retry_count);
        let max = self.retry_max_delay.as_millis() as u64;
        let capped = base.min(max);
        // Equal jitter: random between capped/2 and capped
        let jittered = capped / 2 + rand::thread_rng().gen_range(0..=capped / 2);
        Duration::from_millis(jittered)
    }
}
```

### 7. Metadata サポート

最大20キー、キー40文字、バリュー500文字まで:

```rust
pub type Metadata = HashMap<String, String>;

impl CreateChargeParams {
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.get_or_insert_with(HashMap::new)
            .insert(key.into(), value.into());
        self
    }
}
```

### 8. 3Dセキュア対応

```rust
pub struct ThreeDSecureRequestService<'a> {
    client: &'a PayjpClient,
}

impl<'a> ThreeDSecureRequestService<'a> {
    pub async fn create(&self, params: CreateThreeDSecureRequestParams) 
        -> Result<ThreeDSecureRequest, PayjpError>;
    pub async fn retrieve(&self, id: &str) -> Result<ThreeDSecureRequest, PayjpError>;
    pub async fn list(&self, params: ListParams) -> Result<ListResponse<ThreeDSecureRequest>, PayjpError>;
}
```

## プロジェクト構成

```
payjp-rust/
├── Cargo.toml
├── README.md
├── LICENSE
├── src/
│   ├── lib.rs
│   ├── client.rs           # PayjpClient 実装
│   ├── error.rs            # エラー型
│   ├── resources/
│   │   ├── mod.rs
│   │   ├── charge.rs
│   │   ├── customer.rs
│   │   ├── card.rs
│   │   ├── token.rs
│   │   ├── plan.rs
│   │   ├── subscription.rs
│   │   ├── transfer.rs
│   │   ├── event.rs
│   │   ├── statement.rs
│   │   ├── balance.rs
│   │   ├── term.rs
│   │   ├── three_d_secure.rs
│   │   ├── account.rs
│   │   └── platform/
│   │       ├── mod.rs
│   │       ├── tenant.rs
│   │       └── tenant_transfer.rs
│   ├── params/
│   │   ├── mod.rs
│   │   └── ... (各リソースのパラメータ型)
│   └── response/
│       ├── mod.rs
│       └── list.rs
├── tests/
│   ├── integration/
│   │   └── ... (統合テスト)
│   └── mock/
│       └── ... (モックテスト)
└── examples/
    ├── create_charge.rs
    ├── create_customer.rs
    ├── subscription.rs
    └── three_d_secure.rs
```

## 使用例

```rust
use payjp::{PayjpClient, CreateChargeParams};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = PayjpClient::new("sk_test_xxxxx");
    
    // 支払いを作成
    let charge = client.charges().create(CreateChargeParams {
        amount: 1000,
        currency: "jpy".to_string(),
        card: Some("tok_xxxxx".to_string()),
        description: Some("テスト支払い".to_string()),
        ..Default::default()
    }).await?;
    
    println!("Charge ID: {}", charge.id);
    
    // 顧客を作成
    let customer = client.customers().create(CreateCustomerParams {
        email: Some("test@example.com".to_string()),
        card: Some("tok_xxxxx".to_string()),
        ..Default::default()
    }).await?;
    
    // 定期課金を作成
    let subscription = client.subscriptions().create(CreateSubscriptionParams {
        customer: customer.id.clone(),
        plan: "pln_xxxxx".to_string(),
        ..Default::default()
    }).await?;
    
    Ok(())
}
```

## テスト方針

1. **単体テスト**: 各パラメータのシリアライズ/デシリアライズ
2. **モックテスト**: wiremock を使用した API レスポンスのモック
3. **統合テスト**: テスト環境 API キーを使用した実際の API 呼び出し(CI では skip)

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::{method, path, header};

    #[tokio::test]
    async fn test_create_charge() {
        let mock_server = MockServer::start().await;
        
        Mock::given(method("POST"))
            .and(path("/v1/charges"))
            .and(header("Authorization", "Basic c2tfdGVzdF94eHh4eDo="))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "id": "ch_test",
                "object": "charge",
                // ...
            })))
            .mount(&mock_server)
            .await;
        
        let client = PayjpClient::with_options(
            "sk_test_xxxxx",
            ClientOptions::default().base_url(&mock_server.uri())
        );
        
        let charge = client.charges().create(CreateChargeParams {
            amount: 1000,
            currency: "jpy".to_string(),
            card: Some("tok_test".to_string()),
            ..Default::default()
        }).await.unwrap();
        
        assert_eq!(charge.id, "ch_test");
    }
}
```

## 実装優先度

1. **Phase 1 (必須)**
   - Client, Error 基盤
   - Charge (支払い)
   - Customer + Card (顧客・カード)
   - Token (トークン)

2. **Phase 2 (重要)**
   - Plan (プラン)
   - Subscription (定期課金)
   - Transfer (入金)
   - Event (イベント)

3. **Phase 3 (追加)**
   - Statement, Balance, Term
   - ThreeDSecureRequest
   - Account

4. **Phase 4 (Platform)**
   - Tenant
   - TenantTransfer

## 注意事項

1. **カード情報非通過対応**: カード情報を直接受け取る API は使用不可。必ずトークン経由で処理
2. **金額制限**: 50円〜9,999,999円
3. **通貨**: 現状 "jpy" のみ
4. **Metadata**: 最大20キー、キー40文字、バリュー500文字
5. **返金期限**: 売上作成から180日以内
6. **与信枠確保期間**: 1〜60日(デフォルト7日)

## ドキュメンテーション

- `#[doc]` で各構造体・関数に詳細なドキュメントを記載
- examples/ に実用的なサンプルコードを用意
- README.md に Quick Start ガイドを記載

---

このプロンプトに従って、型安全で使いやすい PAY.JP Rust SDK を実装してください。

SDK作成

今回は、VibeCodingするので、ClaudeCodeにお任せします。
プロンプトをつっこんで、作成させると、しばらくしてコードが生成されます。

Cargoで実行できるテストコードも入っていたので、実行してみました。

In Cargo.toml around lines 13 to 27, update the rand dependency from "0.8" to a
current stable major (e.g., "0.9") and then run cargo update and the test suite
to catch any compatibility/API changes; also confirm that the pinned major
versions for tokio = "1" and chrono = "0.4" are intentional for your public API
and, if you rely on newer features, bump those to the appropriate compatible
majors and re-run tests.

早速エラーなので、直してもらいましょう。
修正はそこそこかかりました。
では、どうでしょう?

In src/resources/card.rs around lines 97 to 115, the enum ThreeDSecureStatus
conflicts with a different enum of the same name in three_d_secure.rs; rename
this enum to a distinct, context-specific name (e.g., CardThreeDSecureStatus)
and update all references/usages across the codebase to the new name, keep serde
attributes and variants unchanged, and run/adjust imports and any pattern
matches or serializations to reflect the rename.

ダメそうですね。
直してくれましたが、どうでしょう?

Check if this issue is valid — if so, understand the root cause and fix it. At src/client.rs, line 207:

<comment>Integer overflow risk in retry delay calculation. The multiplication 2u64.pow(retry_count) can panic when retry_count &gt;= 64, and the full calculation can overflow before being capped by .min(max). Use saturating_mul and saturating_pow to safely handle edge cases when users configure high max_retry values.</comment>

<file context> @@ -0,0 +1,316 @@ + + /// Calculate retry delay with exponential backoff and jitter. + fn calculate_retry_delay(&amp;self, retry_count: u32) -&gt; Duration { + let base = self.retry_initial_delay.as_millis() as u64 * 2u64.pow(retry_count); + let max = self.retry_max_delay.as_millis() as u64; + let capped = base.min(max); </file context>

ダメですね。
再度直してもらいます。

Check if this issue is valid — if so, understand the root cause and fix it. At examples/create_charge.rs, line 21:

<comment>Hardcoded expiration year 2025 is already expired or about to expire. Use a future year (e.g., 2030) to ensure this example remains functional.</comment>

<file context> @@ -0,0 +1,63 @@ + let card = CardDetails::new( + &quot;4242424242424242&quot;, // Test card number + 12, // Expiration month + 2025, // Expiration year + &quot;123&quot;, // CVC + ) </file context>

さきほどまでは、ネットワークっぽいエラーだったのが、変わってきましたね。
カード番号の話になってきました。
どうやら、期限の問題ぽいので、直してもらいます。

Check if this issue is valid — if so, understand the root cause and fix it. At src/resources/charge.rs, line 25:

<comment>Documentation error: JPY does not have cents. Japanese Yen is already the smallest currency unit with no subdivisions. Consider changing the example to a currency that has subdivisions (e.g., 'USD cents') or simply stating 'smallest currency unit (e.g., cents for USD, yen for JPY)'.</comment>

<file context> @@ -0,0 +1,558 @@ + /// Charge creation timestamp (Unix timestamp). + pub created: i64, + + /// Amount in the smallest currency unit (e.g., JPY cents). + pub amount: i64, + </file context>

さて、次はなんでしょう?
と思ったら、ここでProの上限がきてしまいました・・・
気を取り直して、翌日やりました。
JPY では cents 使えないよって話で、それは当たり前やんと思うので、直してもらいます。

Check if this issue is valid — if so, understand the root cause and fix it. At examples/three_d_secure.rs, line 16:

<comment>Card expiration year 2025 will expire within weeks (today is December 2025). Examples should use a year further in the future (e.g., 2030) to remain functional longer.</comment>

<file context> @@ -0,0 +1,65 @@ + let client = PayjpClient::new(api_key); + + // Create a token + let card = CardDetails::new(&quot;4242424242424242&quot;, 12, 2025, &quot;123&quot;) + .name(&quot;鈴木一郎&quot;) + .email(&quot;suzuki@example.com&quot;); </file context>

期限問題がまだ残っていたようなので、直してもらいます。

Check if this issue is valid — if so, understand the root cause and fix it. At src/resources/platform/tenant.rs, line 126:

<comment>Missing minimum_transfer_amount and bank_account builder methods in UpdateTenantParams. The struct defines these fields, but unlike CreateTenantParams, no builder methods are provided to set them. Users would have to manually assign these fields instead of using the fluent builder API.</comment>

<file context> @@ -0,0 +1,330 @@ + self + } + + /// Add metadata to the tenant. + pub fn metadata(mut self, key: impl Into&lt;String&gt;, value: impl Into&lt;String&gt;) -&gt; Self { + self.metadata </file context>

なんか、項目漏れっぽいので直してもらいます。

In src/client.rs around lines 130 to 144, the constructor uses .expect() when
building the reqwest client which can panic; change with_options to return
Result<Self, E> (e.g., Result<Self, reqwest::Error> or a crate-specific error
type) and replace .expect(...) with .build().map_err(|e| /* map to chosen error
*/)?; on success return Ok(Self { ... }); update any call sites to handle the
Result and propagate errors accordingly. Ensure the function signature and error
mapping are consistent with the crate's error conventions.

実装上の問題だったようなので、修正修正!

n src/client.rs around line 235, the User-Agent header is hardcoded to
"payjp-rust/0.1.0"; replace the fixed string with the package version from
Cargo.toml using the CARGO_PKG_VERSION compile-time env var (e.g. build the
header value from "payjp-rust/" + env!("CARGO_PKG_VERSION") or format! with
env!("CARGO_PKG_VERSION") and convert to a HeaderValue) so the header updates
automatically with the crate version and ensure you handle conversion to a
HeaderValue (or unwrap/expect with a clear message) when setting the header.

CARGOの話に戻ってきたようなので、直してもらいます。

In src/client.rs around lines 205 to 215, the code uses the nonexistent
rand::rng().random_range() API; replace that call with
rand::thread_rng().gen_range(0..=capped / 2) and ensure the Rng trait is in
scope (add use rand::Rng; at top if missing) so the jitter is produced via
thread_rng().gen_range(...), keeping the same inclusive range and
Duration::from_millis conversion.

関数置き換えろってエラーぽいですね。

Compiling payjp v0.1.0 (/Users/penpen/Git/payjp-rust-sdk)
warning: use of deprecated function rand::thread_rng: Renamed to rng
--> src/client.rs:222:43
|
222 | let jittered = capped / 2 + rand::thread_rng().gen_range(0..=capped / 2);
| ^^^^^^^^^^
|
= note: #[warn(deprecated)] on by default

warning: use of deprecated method rand::Rng::gen_range: Renamed to random_range
--> src/client.rs:222:56
|
222 | let jittered = capped / 2 + rand::thread_rng().gen_range(0..=capped / 2);
| ^^^^^^^^^

warning: payjp (lib) generated 2 warnings
Finished dev profile [unoptimized + debuginfo] target(s) in 22.97s
Running /Users/penpen/Git/payjp-rust-sdk/target/debug/examples/create_charge
Creating token...
Error: Network(reqwest::Error { kind: Builder, source: Custom("unsupported value") })

そしてネットワークエラー
この後は、CARGOでサンプルが動くまで、延々と3日くらい上限と戦いながらやってました。
そして、ついになんとか形になった感じです。

動いた?

テスト環境では動作するようになりました。
これで、実装がちゃんと完了してるとは言えないですが、サンプルは動きました。
ひたすら頑張った様は、Gitのログでもみてもらえればいいと思います。

あ、ブランチでテストではなく、PRを毎回のようにMainにマージしてたのは、微妙なのでやめましょう・・・

まとめ

今回結構面倒だったのは、パブリック鍵とパスワードでトークンを生成して、というのを毎回やらないといけないとか、生のカード番号飛ばすな(当たり前)とか、金融ならではのひっかりどころがあったような気がします。一応、サンプルで動作はしたのですが、本番で使うにはまだまだ怖いので、もっとブラッシュアップしていかないといけないですね。今後は、pay.jp のドキュメントを MCP で読んだりして、仕様把握をちゃんとAIにさせてやるとかしていきたいです。ただ、この短い時間で、なんとか形になるものができるのは、AIすごいなというとこですね。

  • プログラム経験はあまりなし(Rustいじるのは初めて)
  • 普段はSREやってる
  • クレカの業務知識は、一般に毛の生えた程度

くらいでも、なんとかなるので。
スマホアプリ生成のバイブコーディングとかもやっていましたが、こちらの方がまだいけそうな感じはします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?