GS2-Money Transaction Actions
Combining actions, and concurrency
The background common to every service is collected in Combining Transaction Actions. Read that first; the rest of this section is what GS2-Money adds to it.
The transaction actions of GS2-Money address two kinds of target.
- Wallet: identified by the combination of namespace, user, and slot. It is rewritten as a whole on every update.
- Receipt: identified by the transaction id taken out of the receipt. It records one charge. Only one of them fits in a transaction, per user.
| Operation | Repeated in one transaction | Across a nested transaction | Boundary that separates targets |
|---|---|---|---|
Depositing into a walletDepositByUserId | Combined at the same price, and the counts added up. Deposits at differing prices fail | Fails | namespace, user, slot |
Withdrawing from a walletWithdrawByUserId | Combined, and the counts are added up | Fails | namespace, user, slot |
Recording a receiptRecordReceipt | Fails | Fails on the same receipt | namespace, user (the row itself is per receipt) |
Reverting a receipt recordRevertRecordReceipt | Fails | Does not fail | namespace, user (the row itself is per receipt) |
Two receipt records, or two reverts, against one user are rejected when the transaction is issued, even when they carry different receipts. A receipt reaches its row through a transaction id, and how that id is read out of the receipt differs by store, so two receipts that look different can still be the same charge. Rather than guess, GS2 asks you to keep one per transaction. Buying a display item that records a receipt with a quantity of 2 or more runs into this as well, because the actions are stacked once per unit.
The last column is where the check draws the line, not where the row is. A receipt still lives in a row of its own, so what comes from inside a nested transaction is judged against the row: two records of the same receipt collide, two records of different receipts do not, and two reverts merge into one because deleting the same row twice is the same as deleting it once.
Depositing and withdrawing are separate rows sharing a boundary, so a deposit and a withdrawal against the same wallet cannot go in one transaction.
A deposit carries a price as well as a count, and deposits at differing prices cannot be added up. They stay as separate actions and collide on the wallet. Split the transaction, or deposit once per price in separate transactions. GS2-Money2 does not have this restriction, because a deposit there carries a list rather than a single amount.
The balance is judged against the total after combining. A withdrawal that fits on its own can still be rejected when combined with another one.
A paid-only withdrawal and an ordinary one cannot be placed against the same slot. Adding them up while they disagree on which balance to draw from would take the money out of a balance you did not intend, so withdrawals with differing paidOnly against one wallet are rejected when the transaction is issued.
A wallet and a receipt are separate targets, and a different slot is a different wallet, so depositing into several slots in one transaction is fine.
Take care with nested transactions
A wallet is rewritten as a whole on every update, so touching it both from the inside and from the outside makes the transaction fail. Even two withdrawals, which would have been combined if written side by side, do not go through when one of them arrives from the inside.
Currency is a common cost, so this is easy to hit: a purchase that charges the wallet on the outside and a prize that also charges it on the inside will not go through together.
If you want to avoid these restrictions
Depositing into a wallet and reverting a receipt record are acquire actions; withdrawing and recording a receipt are consume actions. Turning acquireActionUseJobQueue on clears collisions between deposits, but a deposit together with a withdrawal needs enableAtomicCommit turned off. Since currency is involved, weigh that decision carefully.
Neither recording a receipt nor reverting a record has such an escape. The check runs before either setting is applied, and a receipt is one charge that must not be recorded or reverted twice, so turning them on changes nothing here. Split it into separate transactions, and buy a display item that records a receipt one at a time.
Concurrency and retries
Updates to a wallet are written with a revision check, so when several requests update the same wallet at the same time, the one confirmed later returns a conflict (409). Nothing is wrong with the request, so retrying will succeed, and the balance is re-evaluated on the retry.
Consume Action
Gs2Money:WithdrawByUserId
Consume balance from Wallet by User ID
Withdraws the specified amount of currency from the wallet for the specified user. If paidOnly is false, free currency is consumed first, then paid currency.
Quantity specification supported: YES
Reversible action: YES
| Type | Condition | Required | Default | Value Limits | Description | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128 chars | Namespace name Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.). | ||
| userId | string | ✓ | ~ 128 chars | User ID Specify #{userId} to substitute the currently logged-in user’s ID. | ||
| slot | int | ✓ | 0 ~ 100000000 | Slot Number An identifier for separating wallet balances by platform or context. Different slots allow managing separate paid currency pools (e.g., iOS purchases in slot 0, Android in slot 1). Free currency can optionally be shared across all slots via the Namespace’s shareFree setting. | ||
| count | int | ✓ | 1 ~ 2147483646 | Quantity of premium currency to be consumed | ||
| paidOnly | bool | false | Whether to target only paid currency | |||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money:WithdrawByUserId",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"slot": "[int]Slot Number",
"count": "[int]Quantity of premium currency to be consumed",
"paidOnly": "[bool]Whether to target only paid currency",
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money:WithdrawByUserId
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
slot: "[int]Slot Number"
count: "[int]Quantity of premium currency to be consumed"
paidOnly: "[bool]Whether to target only paid currency"
timeOffsetToken: "[string]Time offset token"transaction.service("money").consume.withdraw_by_user_id({
namespaceName="[string]Namespace name",
userId="[string]User ID",
slot="[int]Slot Number",
count="[int]Quantity of premium currency to be consumed",
paidOnly="[bool]Whether to target only paid currency",
timeOffsetToken="[string]Time offset token",
})Gs2Money:RecordReceipt
Record receipt
Records and validates a purchase receipt from a store platform (Apple App Store / Google Play). The receipt is verified against the platform’s servers to prevent fraud. Duplicate receipts are rejected to prevent replay attacks.
Quantity specification supported: NO
Reversible action: NO
| Type | Condition | Required | Default | Value Limits | Description | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128 chars | Namespace name Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.). | ||
| userId | string | ✓ | ~ 128 chars | User ID Specify #{userId} to substitute the currently logged-in user’s ID. | ||
| contentsId | string | ✓ | ~ 1024 chars | Content IDs sold on the store platform | ||
| receipt | string | ✓ | ~ 524288 chars | Receipt | ||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money:RecordReceipt",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"contentsId": "[string]Content IDs sold on the store platform",
"receipt": "[string]Receipt",
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money:RecordReceipt
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
contentsId: "[string]Content IDs sold on the store platform"
receipt: "[string]Receipt"
timeOffsetToken: "[string]Time offset token"transaction.service("money").consume.record_receipt({
namespaceName="[string]Namespace name",
userId="[string]User ID",
contentsId="[string]Content IDs sold on the store platform",
receipt="[string]Receipt",
timeOffsetToken="[string]Time offset token",
})Acquire Action
Gs2Money:DepositByUserId
Deposit balance to Wallet by User ID
Adds the specified amount of currency to the wallet for the specified user. If the price is 0, it is treated as free currency; otherwise, it is treated as paid currency.
Quantity specification supported: YES
Reversible action: YES
| Type | Condition | Required | Default | Value Limits | Description | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128 chars | Namespace name Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.). | ||
| userId | string | ✓ | ~ 128 chars | User ID Specify #{userId} to substitute the currently logged-in user’s ID. | ||
| slot | int | ✓ | 0 ~ 100000000 | Slot Number An identifier for separating wallet balances by platform or context. Different slots allow managing separate paid currency pools (e.g., iOS purchases in slot 0, Android in slot 1). Free currency can optionally be shared across all slots via the Namespace’s shareFree setting. | ||
| price | float | ✓ | 0 ~ 100000.0 | Purchase Price | ||
| count | int | ✓ | 1 ~ 2147483646 | Quantity of premium currency to be granted | ||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money:DepositByUserId",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"slot": "[int]Slot Number",
"price": "[float]Purchase Price",
"count": "[int]Quantity of premium currency to be granted",
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money:DepositByUserId
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
slot: "[int]Slot Number"
price: "[float]Purchase Price"
count: "[int]Quantity of premium currency to be granted"
timeOffsetToken: "[string]Time offset token"transaction.service("money").acquire.deposit_by_user_id({
namespaceName="[string]Namespace name",
userId="[string]User ID",
slot="[int]Slot Number",
price="[float]Purchase Price",
count="[int]Quantity of premium currency to be granted",
timeOffsetToken="[string]Time offset token",
})Gs2Money:RevertRecordReceipt
Delete receipt record by User ID
Reverts a previously recorded receipt by extracting the transaction ID and deleting the corresponding record. Used for handling refunds or chargebacks from the store platform.
Quantity specification supported: NO
Reversible action: NO
| Type | Condition | Required | Default | Value Limits | Description | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128 chars | Namespace name Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.). | ||
| userId | string | ✓ | ~ 128 chars | User ID Specify #{userId} to substitute the currently logged-in user’s ID. | ||
| receipt | string | ✓ | ~ 524288 chars | Receipt | ||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money:RevertRecordReceipt",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"receipt": "[string]Receipt",
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money:RevertRecordReceipt
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
receipt: "[string]Receipt"
timeOffsetToken: "[string]Time offset token"transaction.service("money").acquire.revert_record_receipt({
namespaceName="[string]Namespace name",
userId="[string]User ID",
receipt="[string]Receipt",
timeOffsetToken="[string]Time offset token",
})