> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Money トランザクションアクション

検証/消費/入手の各トランザクションアクションの仕様




## アクションの組み合わせと同時実行

すべてのサービスに共通する前提は [トランザクションアクションの組み合わせ]() にまとめています。先にそちらを読んでください。この節の残りは GS2-Money 固有の内容です。

GS2-Money のトランザクションアクションは 2 種類の対象を扱います。

- ウォレット : ネームスペース・ユーザー・スロットの組で決まります。更新のたびに全体が書き換わります。
- レシート : レシートから取り出した取引 ID で決まります。1 件の課金を記録します。1 つのトランザクションに、ユーザーあたり 1 件しか置けません。

操作 | 同じ行を重ねたとき | 入れ子越し | 別の対象になる境界
--- | --- | --- | ---
ウォレットへの加算<br>`DepositByUserId` | 同じ価格なら統合され、数が合算される。価格が違う加算は失敗する | 失敗する | ネームスペース・ユーザー・スロット
ウォレットからの減算<br>`WithdrawByUserId` | 統合され、数が合算される | 失敗する | ネームスペース・ユーザー・スロット
レシートの記録<br>`RecordReceipt` | 失敗する | 同じレシートなら失敗する | ネームスペース・ユーザー（行そのものはレシート単位）
レシートの記録の取り消し<br>`RevertRecordReceipt` | 失敗する | 失敗しない | ネームスペース・ユーザー（行そのものはレシート単位）

**同じユーザーへのレシートの記録どうし、取り消しどうしは、レシートが違っていても発行時にエラーになります。** レシートは取引 ID を経由して行にたどり着きますが、取引 ID の取り出し方はストアごとに違うため、見た目の違うレシートが同じ課金を指していることがあります。推測で畳まず、1 つのトランザクションに 1 件だけ置いてください。レシートを記録する陳列商品を数量 2 以上で購入した場合も、アクションが個数ぶん積まれるためこれに当たります。

最後の列は検査が線を引く位置であって、行の位置ではありません。レシートは今も 1 件ごとに別の行を持つので、内側から届いたものは行で判定されます。同じレシートの記録どうしは衝突し、違うレシートの記録どうしは衝突せず、取り消しどうしは 1 本に統合されます（同じ行を 2 回消すのは 1 回消すのと同じだからです）。

加算と減算は行が違い、境界が同じです。同じウォレットへの加算と減算を 1 つのトランザクションに入れることはできません。

加算は数だけでなく価格も持ち、価格が違う加算どうしは合算できません。そのため別々のアクションのまま残り、ウォレットの上で衝突します。トランザクションを分けるか、価格ごとに別のトランザクションで 1 回ずつ加算してください。GS2-Money2 にこの制限がないのは、あちらの入金が単一の金額ではなくリストを持っているからです。

残高は統合後の合計に対して判定されます。単独なら足りている減算でも、他の減算と合わさると弾かれることがあります。

**有償残高からのみ引く減算と通常の減算は、同じスロットに並べられません。** どちらの残高から引くかが食い違ったまま合算すると意図しない残高から引かれてしまうため、`paidOnly` が違う減算を同じウォレットに指定すると発行時にエラーになります。

ウォレットとレシートは別の対象です。スロットが違えば別のウォレットなので、1 つのトランザクションで複数のスロットに加算するのは問題ありません。

### 入れ子になったトランザクションに注意

ウォレットは更新のたびに全体が書き換わるため、内側と外側の両方から触るとトランザクションが失敗します。並べて書けば統合されたはずの減算どうしでも、片方が内側から届けば通りません。

通貨はコストとして使われることが多いので、この状況は起こりやすくなります。外側でウォレットから支払う購入と、内側でもウォレットから支払う景品が同居すると通りません。

### 制限を回避したい場合

ウォレットへの加算とレシートの記録の取り消しは入手アクション、減算とレシートの記録は消費アクションです。加算どうしの衝突は `acquireActionUseJobQueue` を有効にすれば解消できますが、加算と減算の同居は `enableAtomicCommit` を無効にしないと解消しません。通貨を扱う以上、この判断は慎重に行ってください。

レシートの記録にも、記録の取り消しにも抜け道はありません。検査はどちらの設定よりも前に走りますし、レシートは 1 件の課金であって同じ課金を二重に記録・取り消ししてはいけないので、有効にしても何も変わりません。トランザクションを分け、レシートを記録する陳列商品は 1 個ずつ購入してください。

### 同時実行とリトライ

ウォレットへの更新はリビジョンの照合を伴うため、同じウォレットを複数のリクエストが同時に更新すると、後から確定した側がコンフリクト (409) になります。リクエストの内容に問題があるわけではないので、リトライすれば成功します。リトライ時には最新の残高で判定し直されます。

---



## Consume Action

消費アクション

### Gs2Money:WithdrawByUserId

ユーザーIDを指定してウォレットから残高を消費<br>

指定されたユーザーのウォレットから指定量の通貨を消費します。<br>
paidOnly が false の場合、無償通貨が先に消費され、次に有償通貨が消費されます。

**数量指定可能なアクション：はい**

**反転可能なアクション：はい**

|  | 型 | 有効化条件 | 必須 | デフォルト | 値の制限 | 説明 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128文字 | ネームスペース名<br>ネームスペース固有の名前。英数字および -(ハイフン) _(アンダースコア) .(ピリオド)で指定します。 |
| userId | string |  | ✓|  |  ~ 128文字 | ユーザーID<br>`#{userId}` と設定することでログイン中のユーザーIDに置換されます。 |
| slot | int |  | ✓|  | 0 ~ 100000000 | スロット番号<br>プラットフォームやコンテキストごとにウォレット残高を分離するための識別子。<br>異なるスロットにより、別々の有償通貨プールを管理できます（例: iOS購入はスロット0、Androidはスロット1）。<br>無償通貨はネームスペースの shareFree 設定により、全スロット間で共有することもできます。 |
| count | int |  | ✓|  | 1 ~ 2147483646 | 消費する課金通貨の数量 |
| paidOnly | bool |  | | false |  | 有償通貨のみを対象とするか |
| timeOffsetToken | string |  | |  |  ~ 1024文字 | タイムオフセットトークン |




**JSON**
```json
{
    "action": "Gs2Money:WithdrawByUserId",
    "request": {
        "namespaceName": "[string]ネームスペース名",
        "userId": "[string]ユーザーID",
        "slot": "[int]スロット番号",
        "count": "[int]消費する課金通貨の数量",
        "paidOnly": "[bool]有償通貨のみを対象とするか",
        "timeOffsetToken": "[string]タイムオフセットトークン"
    }
}
```

**YAML**
```yaml

action: Gs2Money:WithdrawByUserId
request:
  namespaceName: "[string]ネームスペース名"
  userId: "[string]ユーザーID"
  slot: "[int]スロット番号"
  count: "[int]消費する課金通貨の数量"
  paidOnly: "[bool]有償通貨のみを対象とするか"
  timeOffsetToken: "[string]タイムオフセットトークン"
```

**GS2-Script**
```lua

transaction.service("money").consume.withdraw_by_user_id({
    namespaceName="[string]ネームスペース名",
    userId="[string]ユーザーID",
    slot="[int]スロット番号",
    count="[int]消費する課金通貨の数量",
    paidOnly="[bool]有償通貨のみを対象とするか",
    timeOffsetToken="[string]タイムオフセットトークン",
})
```


---

### Gs2Money:RecordReceipt

レシートを記録<br>

ストアプラットフォーム（Apple App Store / Google Play）からの購入レシートを記録・検証します。<br>
不正防止のためプラットフォームのサーバーに対してレシートが検証されます。リプレイ攻撃防止のため重複レシートは拒否されます。

**数量指定可能なアクション：いいえ**

**反転可能なアクション：いいえ**

|  | 型 | 有効化条件 | 必須 | デフォルト | 値の制限 | 説明 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128文字 | ネームスペース名<br>ネームスペース固有の名前。英数字および -(ハイフン) _(アンダースコア) .(ピリオド)で指定します。 |
| userId | string |  | ✓|  |  ~ 128文字 | ユーザーID<br>`#{userId}` と設定することでログイン中のユーザーIDに置換されます。 |
| contentsId | string |  | ✓|  |  ~ 1024文字 | ストアプラットフォームで販売されているコンテンツID |
| receipt | string |  | ✓|  |  ~ 524288文字 | レシート |
| timeOffsetToken | string |  | |  |  ~ 1024文字 | タイムオフセットトークン |




**JSON**
```json
{
    "action": "Gs2Money:RecordReceipt",
    "request": {
        "namespaceName": "[string]ネームスペース名",
        "userId": "[string]ユーザーID",
        "contentsId": "[string]ストアプラットフォームで販売されているコンテンツID",
        "receipt": "[string]レシート",
        "timeOffsetToken": "[string]タイムオフセットトークン"
    }
}
```

**YAML**
```yaml

action: Gs2Money:RecordReceipt
request:
  namespaceName: "[string]ネームスペース名"
  userId: "[string]ユーザーID"
  contentsId: "[string]ストアプラットフォームで販売されているコンテンツID"
  receipt: "[string]レシート"
  timeOffsetToken: "[string]タイムオフセットトークン"
```

**GS2-Script**
```lua

transaction.service("money").consume.record_receipt({
    namespaceName="[string]ネームスペース名",
    userId="[string]ユーザーID",
    contentsId="[string]ストアプラットフォームで販売されているコンテンツID",
    receipt="[string]レシート",
    timeOffsetToken="[string]タイムオフセットトークン",
})
```


---

## Acquire Action

入手アクション

### Gs2Money:DepositByUserId

ユーザーIDを指定してウォレットの残高に加算<br>

指定されたユーザーのウォレットに指定量の通貨を追加します。<br>
価格が 0 の場合は無償通貨として、それ以外の場合は有償通貨として扱われます。

**数量指定可能なアクション：はい**

**反転可能なアクション：はい**

|  | 型 | 有効化条件 | 必須 | デフォルト | 値の制限 | 説明 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128文字 | ネームスペース名<br>ネームスペース固有の名前。英数字および -(ハイフン) _(アンダースコア) .(ピリオド)で指定します。 |
| userId | string |  | ✓|  |  ~ 128文字 | ユーザーID<br>`#{userId}` と設定することでログイン中のユーザーIDに置換されます。 |
| slot | int |  | ✓|  | 0 ~ 100000000 | スロット番号<br>プラットフォームやコンテキストごとにウォレット残高を分離するための識別子。<br>異なるスロットにより、別々の有償通貨プールを管理できます（例: iOS購入はスロット0、Androidはスロット1）。<br>無償通貨はネームスペースの shareFree 設定により、全スロット間で共有することもできます。 |
| price | float |  | ✓|  | 0 ~ 100000.0 | 購入価格 |
| count | int |  | ✓|  | 1 ~ 2147483646 | 付与する課金通貨の数量 |
| timeOffsetToken | string |  | |  |  ~ 1024文字 | タイムオフセットトークン |




**JSON**
```json
{
    "action": "Gs2Money:DepositByUserId",
    "request": {
        "namespaceName": "[string]ネームスペース名",
        "userId": "[string]ユーザーID",
        "slot": "[int]スロット番号",
        "price": "[float]購入価格",
        "count": "[int]付与する課金通貨の数量",
        "timeOffsetToken": "[string]タイムオフセットトークン"
    }
}
```

**YAML**
```yaml

action: Gs2Money:DepositByUserId
request:
  namespaceName: "[string]ネームスペース名"
  userId: "[string]ユーザーID"
  slot: "[int]スロット番号"
  price: "[float]購入価格"
  count: "[int]付与する課金通貨の数量"
  timeOffsetToken: "[string]タイムオフセットトークン"
```

**GS2-Script**
```lua

transaction.service("money").acquire.deposit_by_user_id({
    namespaceName="[string]ネームスペース名",
    userId="[string]ユーザーID",
    slot="[int]スロット番号",
    price="[float]購入価格",
    count="[int]付与する課金通貨の数量",
    timeOffsetToken="[string]タイムオフセットトークン",
})
```


---

### Gs2Money:RevertRecordReceipt

ユーザーIDを指定してレシートの記録を削除<br>

トランザクションIDを抽出して対応するレコードを削除することで、以前に記録されたレシートを取り消します。<br>
ストアプラットフォームからの返金やチャージバック処理に使用します。

**数量指定可能なアクション：いいえ**

**反転可能なアクション：いいえ**

|  | 型 | 有効化条件 | 必須 | デフォルト | 値の制限 | 説明 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128文字 | ネームスペース名<br>ネームスペース固有の名前。英数字および -(ハイフン) _(アンダースコア) .(ピリオド)で指定します。 |
| userId | string |  | ✓|  |  ~ 128文字 | ユーザーID<br>`#{userId}` と設定することでログイン中のユーザーIDに置換されます。 |
| receipt | string |  | ✓|  |  ~ 524288文字 | レシート |
| timeOffsetToken | string |  | |  |  ~ 1024文字 | タイムオフセットトークン |




**JSON**
```json
{
    "action": "Gs2Money:RevertRecordReceipt",
    "request": {
        "namespaceName": "[string]ネームスペース名",
        "userId": "[string]ユーザーID",
        "receipt": "[string]レシート",
        "timeOffsetToken": "[string]タイムオフセットトークン"
    }
}
```

**YAML**
```yaml

action: Gs2Money:RevertRecordReceipt
request:
  namespaceName: "[string]ネームスペース名"
  userId: "[string]ユーザーID"
  receipt: "[string]レシート"
  timeOffsetToken: "[string]タイムオフセットトークン"
```

**GS2-Script**
```lua

transaction.service("money").acquire.revert_record_receipt({
    namespaceName="[string]ネームスペース名",
    userId="[string]ユーザーID",
    receipt="[string]レシート",
    timeOffsetToken="[string]タイムオフセットトークン",
})
```


---



