GS2-Money2 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-Money2 adds to it.
The transaction actions of GS2-Money2 address one wallet, identified by the combination of namespace, user, and slot. A wallet holds the paid balance and the free balance together.
An update that moves only the free balance is written as an increment, so those combine. An update that moves the paid balance rewrites the wallet as a whole.
| Operation | Repeated in one transaction | Across a nested transaction | Boundary that separates targets |
|---|---|---|---|
Depositing and withdrawing free currency onlyDepositByUserId WithdrawByUserId | Any mix; combined and the amounts added up | Added up | namespace, user, slot |
Depositing or withdrawing where paid currency movesDepositByUserId WithdrawByUserId | Deposits are combined with their deposit transactions gathered into one list; withdrawals are combined and the amounts added up. A deposit together with a withdrawal fails | Fails | namespace, user, slot |
Verifying a receiptVerifyReceiptByUserId | Fails | Fails when they resolve to the same transaction id | namespace, user (the row itself is per transaction id) |
Because free-currency updates are increments, a deposit and a withdrawal of free currency may sit in one transaction and both apply. Where the paid balance moves, the wallet is rewritten as a whole and the two rows cannot be placed together.
A withdrawal that free currency alone cannot cover reaches into the paid balance, so it belongs to the second row even when you did not intend to spend paid currency.
These updates fall back to rewriting the wallet as a whole in several cases, and then behave like the second row: where the paid lots change, where the wallet was stored in the older format, where the free lots are not exactly one, or where the summary does not agree with the recorded lots. The last two only arise from data brought in by import.
Where sharedFreeCurrency is enabled, the free balance of every slot is held in one row belonging to slot 0. Operations on slot 0 take the increment path; free-currency operations issued against another slot are written as a whole instead.
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. Specify them separately.
VerifyReceiptByUserId is named like a verify action but its kind is consume. It leaves the wallet untouched, but it does record the receipt as an event row of its own, and it belongs in the consume list. It looks at the state as of the start of the transaction, so it cannot verify a receipt recorded by the same transaction.
Two receipt verifications 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 — on Google Play the transactionId you send is never looked at, and the orderId inside the payload decides the row. Two receipts that look different can therefore be the same charge, and two that look the same can be different charges. Rather than guess, GS2 asks you to keep one per transaction. Buying a display item that verifies a receipt with a quantity of 2 or more runs into this as well, because the actions are stacked once per unit.
Placing this verification in the same transaction as a deposit lets you deposit only after confirming that the receipt is genuine. Use that shape to prevent double counting.
Take care with nested transactions
Free-currency updates are safe across a nested transaction: they are applied together as one update whichever route they arrive by.
Where the paid balance moves, the wallet is rewritten as a whole, so touching it both from the inside and from the outside makes the transaction fail. 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.
Deposits of the same amount arriving from the inside and from the outside do not fail: they are gathered into one. Two deposits of 100 credit only 100, with no error. This applies to the second row, where the wallet is rewritten as a whole; differing amounts fail, so the silent mismatch happens only when the amounts agree.
If you want to avoid these restrictions
Depositing into a wallet is an acquire action; withdrawing and verifying 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.
Verifying a receipt has no such escape. The check runs before either setting is applied, and a receipt is one charge that must not be verified twice, so turning them on changes nothing here. Split it into separate transactions, and buy a display item that verifies a receipt one at a time.
Concurrency and retries
Free-currency updates do not conflict however many concurrent requests overlap, as long as the result stays within range. This is what the increment path buys: rewards, login bonuses, and mission payouts that all grant free currency no longer collide with one another.
Where the paid balance moves, the wallet is written with a revision check, so the request 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.
A different slot is a different wallet, so requests for the same user do not conflict when the slots differ. The exception is sharedFreeCurrency, where the free balance of every slot lives in one row.
Consume Action
Gs2Money2:WithdrawByUserId
Withdraw 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 Identifies the wallet slot. Different slots can be used to manage currency separately per platform (e.g., one slot for iOS, another for Android) when cross-platform balance sharing is not allowed. | ||
| withdrawCount | 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": "Gs2Money2:WithdrawByUserId",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"slot": "[int]Slot Number",
"withdrawCount": "[int]Quantity of premium currency to be consumed",
"paidOnly": "[bool]Whether to target only paid currency",
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money2:WithdrawByUserId
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
slot: "[int]Slot Number"
withdrawCount: "[int]Quantity of premium currency to be consumed"
paidOnly: "[bool]Whether to target only paid currency"
timeOffsetToken: "[string]Time offset token"transaction.service("money2").consume.withdraw_by_user_id({
namespaceName="[string]Namespace name",
userId="[string]User ID",
slot="[int]Slot Number",
withdrawCount="[int]Quantity of premium currency to be consumed",
paidOnly="[bool]Whether to target only paid currency",
timeOffsetToken="[string]Time offset token",
})Gs2Money2:VerifyReceiptByUserId
Mark a receipt as used by User ID
Verifies a purchase receipt for the specified user and records it as used. An event log is recorded and the verify receipt script configured in the Namespace is executed.
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. | ||
| contentName | string | ✓ | ~ 128 chars | Store Content Model name | ||
| receipt | Receipt | ✓ | Receipt | |||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money2:VerifyReceiptByUserId",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"contentName": "[string]Store Content Model name",
"receipt": {
"Store": "[string]Store",
"TransactionID": "[string]Transaction ID",
"Payload": "[string]Payload"
},
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money2:VerifyReceiptByUserId
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
contentName: "[string]Store Content Model name"
receipt:
Store: "[string]Store"
TransactionID: "[string]Transaction ID"
Payload: "[string]Payload"
timeOffsetToken: "[string]Time offset token"transaction.service("money2").consume.verify_receipt_by_user_id({
namespaceName="[string]Namespace name",
userId="[string]User ID",
contentName="[string]Store Content Model name",
receipt={
store="[string]Store",
transactionID="[string]Transaction ID",
payload="[string]Payload"
},
timeOffsetToken="[string]Time offset token",
})Acquire Action
Gs2Money2:DepositByUserId
Deposit balance to Wallet by User ID
Adds the specified deposit transactions to the wallet for the specified user. An event log is recorded for each deposit transaction.
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 Identifies the wallet slot. Different slots can be used to manage currency separately per platform (e.g., one slot for iOS, another for Android) when cross-platform balance sharing is not allowed. | ||
| depositTransactions | List<DepositTransaction> | ✓ | 1 ~ 1000 items | List of Deposit transactions | ||
| timeOffsetToken | string | ~ 1024 chars | Time offset token |
{
"action": "Gs2Money2:DepositByUserId",
"request": {
"namespaceName": "[string]Namespace name",
"userId": "[string]User ID",
"slot": "[int]Slot Number",
"depositTransactions": [
{
"price": "[double]Purchase Price",
"currency": "[string]Currency Code",
"count": "[int]Quantity of premium currency",
"depositedAt": "[long]Deposit Date"
}
],
"timeOffsetToken": "[string]Time offset token"
}
}action: Gs2Money2:DepositByUserId
request:
namespaceName: "[string]Namespace name"
userId: "[string]User ID"
slot: "[int]Slot Number"
depositTransactions:
- price: "[double]Purchase Price"
currency: "[string]Currency Code"
count: "[int]Quantity of premium currency"
depositedAt: "[long]Deposit Date"
timeOffsetToken: "[string]Time offset token"transaction.service("money2").acquire.deposit_by_user_id({
namespaceName="[string]Namespace name",
userId="[string]User ID",
slot="[int]Slot Number",
depositTransactions={
{
price="[double]Purchase Price",
currency="[string]Currency Code",
count="[int]Quantity of premium currency",
depositedAt="[long]Deposit Date"
}
},
timeOffsetToken="[string]Time offset token",
})