{"id":50171,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:16.890Z","digest":"a80aeaaa9ae66467a7b069dfef73cc083d92604665d4b12c25533729f3bf4f99","against":null,"payload":{"description":"Composes shared or multi-market recovery of unfinished Hive Axyl consumables in Unity: discover, hand to the game server as a recovery and close after its delivery confirmation. Use after crashes or incomplete delivery/close when the market is unspecified or several markets are involved. Does not start checkout.","included_files":[{"relative_path":"references/apple.md","size_in_bytes":2712},{"relative_path":"references/google.md","size_in_bytes":5196},{"relative_path":"references/pg.md","size_in_bytes":2365},{"relative_path":"references/steam.md","size_in_bytes":1964}],"name":"axyl-purchase-recovery","skill_md_contents":"---\nname: axyl-purchase-recovery\ndescription: \"Composes shared or multi-market recovery of unfinished Hive Axyl consumables in Unity: discover, hand to the game server as a recovery and close after its delivery confirmation. Use after crashes or incomplete delivery/close when the market is unspecified or several markets are involved. Does not start checkout.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Recover unfinished consumable purchases\n\nFinds consumables that were paid but not delivered or closed, hands them to the game\nserver as a recovery and closes them once it confirms delivery; no checkout starts. The\nInstalled SDK, SDK results, Payment state, New consumable purchase and Recover\nundelivered consumables sections below apply to every step.\n\n**Inputs:** the market, the originating player's session, country, language, the\naccount UUID and the original order data from the app's ledger.\n**App owns:** recovery timing, pending-state storage and the game server's\nverification, delivery ledger and confirmation.\n\nFor one named market, prefer its installed `axyl-purchase-market-*` skill.\nOtherwise read only that market's procedure:\n\n| Market | Read | Discovery and close |\n|---|---|---|\n| Apple | [Apple StoreKit](references/apple.md) | Unfinished transactions, record again, post then finish |\n| Google | [Google Play Billing](references/google.md) | In-app purchase query, record again, post then consume |\n| Steam | [Steam microtransactions](references/steam.md) | Server restore of recorded entries, finalize |\n| PG | [PG hosted payment](references/pg.md) | Server restore, order correlation, finalize |\n\nSubscription entitlement recovery is a different contract that the recipes do not\ndefine.\n\n## Verify\n\n- [ ] Discovery uses the market's source: a failed Google query is not zero\n  purchases, pending Play entries are skipped and Google never calls\n  `RestorePurchasesAsync`.\n- [ ] Apple and Google record again (tolerating only `VerifyError`), Steam and PG\n  never record twice, and every entry goes to the game server as a recovery; the\n  client calls no verify API.\n- [ ] Prices come from the original order, not zero placeholders or today's catalog.\n- [ ] The ledger decides what remains, a close retry never grants again, and two\n  purchases of one product stay separate.\n- [ ] A cancellation or partial failure keeps the other receipts and is not\n  reported as fully recovered.\n\n<!-- axyl-shared: core/installed-sdk.md -->\n\n## Installed SDK\n\n- Read `Packages/manifest.json`, the lock file and each resolved `package.json`.\n  All `com.com2usplatform.hiveaxyl.*` packages must be the same release (UPM does\n  not enforce this); `com.hive.axyl.*` packages are a release before 1.0.0-rc.9.\n- The installed SDK wins for APIs, types and contracts. If its release differs\n  from this skill's `sdk-version`, apply a step only after its fields, result\n  variants, platform support and failure behaviour match; otherwise stop at that\n  step and report what is missing. Never change the installed version.\n- The app owns UI, storage, the device key and orchestration. Sample and recipe\n  classes are not SDK APIs.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/results.md -->\n\n## SDK results\n\nMatch each call's concrete result and read `Success.Data` only after checking the\nfields the next step needs. The markers live in `Hive.Axyl.Contracts.Result`.\n\n| Result | Handling |\n|---|---|\n| `IUserCanceledOutcome` | The user dismissed OS or provider UI: stop this attempt quietly |\n| `IUnknownOutcome` | Keep `Code` and `RawJson`; never treat it as a known refusal or a success |\n| `IUntypedProblem` | Technical failure in `Problem` (`HiveError`): network, timeout or caller `Cancelled` |\n| Other typed variant | Only that call's documented meaning |\n\nPass `new ApiCallContext { Token = ct }` and check cancellation before each\nfollow-up call; a request cancelled in flight may still have taken effect. Never\nbranch on HTTP status, exception text or raw bodies, and never log tokens or\ncredentials.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/payment-state.md -->\n\n## Payment state\n\nResolve `Hive.Axyl.Payments.IPaymentsService`, registered once at startup with\n`AddPayments()` (`https://commerce-api.hiveaxyl.com`), `AddPayments(sandbox: true)`\n(`https://sandbox-commerce-api.hiveaxyl.com`) or `AddPayments(baseUrl)` for a\nprovisioned host, as the app's environment selects. Require the originating\nplayer's authenticated session and the selected market's addon; never initialize\nper purchase. Never log receipts.\n\n### Retained state\n\n`PurchaseOrder`, `PendingPurchase`, `PurchaseDeliveryConfirmation` and the recipe\nsources are sample types: carry the same values in app-owned types through every\nstage.\n\n| State | Values |\n|---|---|\n| Order | Market, originating player, product ID, decimal price, currency, country, language, server ID, app version, IAP payload, account UUID; Steam store player ID |\n| Pending | Record receipt, verification token, finish token, order ID, store transaction ID, the order, any local store verification objection |\n| Confirmed | The game server's delivery confirmation for that pending purchase, with any updated closing receipt or verified subscription product ID |\n\nA missing addon or input, or a pending purchase from another market, stops before\nthe next mutation. Keep receipts and IDs after a failure or cancellation: payment\nor delivery may already have happened. Persist recoverable state under the app's\nstorage policy, and coordinate purchase, recovery and account changes so that no\ntransaction is delivered twice at once or moved to another player.\n\n`AccountUuid` is optional on `PrePurchase`, `PurchaseRequest` and\n`SubscriptionPrePurchaseRequest`; an unset one is left off, so the server cannot\ncompare the account. Set it: derive it as UUIDv5 from the game's fixed namespace UUID\n(from game configuration, the same on every platform) and the invariant decimal\nplayer ID string, in standard network byte order (mind C# `Guid`'s layout). Send the\nsame value as Apple `AppAccountToken`, Google `ObfuscatedAccountId` and in Axyl\nrequests; the SDK does not generate it.\n\n### Catalog\n\nApple/Google: use configured product IDs, or call\n`ListStoreProductIdsAsync(new StoreRequest { ProviderId, Country, Language,\nAppVersion }, ctx)` and select the intended entry of\n`PaymentsListStoreProductIdsResult.Success.Data.Stores`: `Products` for consumables,\n`ProductSubscriptions` for subscriptions (the request has no product type). An\nabsent or empty list calls for configured IDs or a catalog diagnosis; never reuse\nconsumable IDs as subscriptions or invent one. Then query the native store and send\nthe mapped details with the market's `Fetch*ProductsAsync`. Steam and PG fetch their\nserver catalog directly.\n\nEach fetch returns its own result over `ProductResponseData`. Choose the\n`Data.Products` entry by `ProductId` and `ProductType`, and keep its decimal `Price`,\n`Currency` and offers with the response's country and language as the purchase\ninputs. Never parse `DisplayPrice`, rescale a decimal into micros or turn a missing\nprice into zero (a configured zero-price phase is real). A catalog failure, an empty\ncatalog and a failed checkout are different results. Recovery keeps the original\ntransaction's price and uses the catalog only for missing data.\n\n### Receipt identities\n\n| Market | Record receipt | Verification token for the game server | Store finish |\n|---|---|---|---|\n| Apple | Original transaction `JwsRepresentation` | Same JWS | `AppleTransaction.Id` as decimal `ulong` |\n| Google | JSON envelope `{\"purchase_data\": OriginalJson, \"signature\": Signature}` | Consumable: `PurchaseToken` with `ProductId`; subscription: the envelope | Same `PurchaseToken` |\n| Steam | Server `AxylReceipt` from init or restore | Same sealed value | None on the device |\n| PG | Server `AxylReceipt` from restore | Same sealed value | None on the device |\n\nBuild the Google envelope with a JSON serializer around the untouched\n`OriginalJson` string; never parse and reserialize it. A payment URL is never a\nreceipt. Keep the order ID and store transaction ID as separate values: a restore\nentry's `StoreTransactionId` can differ from its order key.\n\nProvider enums are request-specific (`PrePurchaseProviderId`,\n`PurchaseRequestProviderId`, `PurchaseFinalizeRequestProviderId`, ...): use the named\n`Apple`, `Google`, `Steam` or `Pg` member where it exists; never cast between enums\nor default an unknown market.\n\n### Game-server verification, delivery, close\n\nReceipt verification is server to server: the Payments contract marks\n`/payment/v1/purchase/verify` and `/payment/v1/subscription/verify` server-only, so\nthe client SDK has no verify call. After the purchase is recorded, send the pending\npurchase to the game's own authenticated server: market, record receipt,\nverification token, order ID, store transaction ID, order metadata and account, and\nwhether it is a new purchase or a recovery (the server maps that to verify request\ntype 1 or 2).\n\nThe game server verifies with the payment backend, checks account, product and price\n(for a subscription also current refund and expiry eligibility) and grants once\nthrough its durable ledger. It answers with a delivery confirmation for that pending\npurchase, also when the same transaction was already granted, carrying any updated\nclosing receipt (for a subscription, the verified product ID). The endpoint, request\nand response are the app's design. The confirmation is an integration contract, not\nproof of payment: the client never manufactures one, and a rejection, failure,\ntimeout or missing confirmation keeps the pending purchase and closes nothing.\n\nClose only with a confirmation that matches the pending purchase, using the market's\nprocedure; `RequestPurchaseAsync` and `FinalizePurchaseAsync` run after delivery. A\nclose failure leaves a delivered, unfinished transaction: retry the close with the\nsame confirmation, never grant again. Remove the pending record only after the close\nsucceeds.\n\n`ItemResultAsync(ItemResultBody, ctx)` reports actual delivery (`AxylTransactionId`\nfrom the game server's verification; `Status` 1 success, 2 cancelled without\nclawback, 3 cancelled with clawback; `Assets` granted). The recipes do not use it. If\nthe integration requires it, agree its owner with the backend and report the ledger\nresult, never status 1 merely because a purchase was recorded.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: payments/consumable.md -->\n\n## New consumable purchase\n\nUse only the selected market's acquisition and close. Inputs: the catalog product\nwith its price and currency, country, language, the account UUID and the market's\nprerequisites.\n\n1. Apple, Google, PG: `CreatePrePurchaseAsync(new PrePurchase { ProviderId,\n   ProductId, Price, Currency, Country, Language, ServerId,\n   RequestDate = DateTimeOffset.UtcNow, IapPayload, AccountUuid }, ctx)`; require\n   success. Steam starts with its init instead.\n2. Acquire with the market. Apple and Google must return a purchased transaction,\n   not approval-pending or an opened sheet; Steam must be approved; PG returns a URL\n   and waits for external payment, and after an explicit resume its receipt comes\n   from server restore.\n3. Keep the pending state from the receipt identities table.\n4. `RecordStorePurchaseAsync(new PurchaseRequest { AxylReceipt = recordReceipt,\n   ProviderId, ProductId, Country, Language, Currency, Price, OrderId,\n   StoreTransactionId, ServerId, IapPayload, AccountUuid }, ctx)`. A refusal, an\n   unknown result or a failure stops before the game server.\n5. Send the pending purchase to the game server as a **new purchase** and wait for\n   its delivery confirmation; the client calls no verify API.\n6. Close with that confirmation.\n\nCheck cancellation between stages and keep any receipt already obtained. Never\nrepeat checkout because a record, the game server or a close failed.\n\n### Close after delivery\n\n| Market | Close |\n|---|---|\n| Apple | `RequestPurchaseAsync(new PurchasePostRequest { ProviderId = PurchasePostRequestProviderId.Apple, AxylReceipt = originalJws, StoreTransactionId = originalTransactionId }, ctx)`; only `Success` or this call's `VerifyDuplicated` permits `FinishTransactionAsync(new FinishTransactionRequest { TransactionId = parsedUlongId }, ct)` |\n| Google | `RequestPurchaseAsync(new PurchasePostRequest { ProviderId = PurchasePostRequestProviderId.Google, AxylReceipt = recordedEnvelope, ProductId = productId, StoreTransactionId = playOrderId }, ctx)`; only `Success` or this call's `VerifyDuplicated` permits `ConsumeAsync(originalPurchaseToken, ct)`. Never consume after a failed or unknown post; after a confirmed post, consume `PurchaseNotFound` means already closed |\n| Steam, PG | `FinalizePurchaseAsync(new PurchaseFinalizeRequest { ProviderId, AxylReceipt }, ctx)` with the confirmation's closing receipt when it has one, else the original server receipt; require success |\n\nApple posts the original JWS and finishes the original ID; Google posts the recorded\nenvelope and consumes the original token. The confirmation's closing receipt is only\nfor the server-side finalize. Completion is confirmed delivery plus a successful close; while\nthe close is pending, keep that state and every recovery input. A subscription\ncloses differently.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: payments/recovery.md -->\n\n## Recover undelivered consumables\n\nBuys nothing new: discover each open transaction at its source, hand it to the game\nserver as a recovery, and close it once the server confirms delivery. Requires country, language,\nthe originating account UUID and the optional server ID and app version.\n\n| Market | Discovery |\n|---|---|\n| Apple | `IAppleStoreKitPlugin.GetUnfinishedTransactionsAsync(new GetUnfinishedTransactionsRequest(), ct)`; keep each transaction's product, original JWS, ID and verification objection. Current entitlements are not a substitute |\n| Google | After `StartConnectionAsync(ct)` (`Success` or `AlreadyConnected`), `QueryPurchasesAsync(ProductType.Inapp, ct)` must return `Success`; keep only `PurchaseState.Purchased` entries with products and map each like a fresh purchase (envelope, `PurchaseToken`, `StoreTransactionId = OrderId`). A failed query is not an empty result |\n| Steam, PG | `RestorePurchasesAsync(new PurchaseRestoreRequest { ProviderId, Country, Language, ServerId, AppVersion }, ctx)`; keep each `Data.Restores` entry's `AxylReceipt`, `OrderId`, `StoreTransactionId`, product, price, currency and payload, and confirm its market. An empty success does not prove an in-flight payment was cancelled |\n\n1. Build pending state per transaction. Apple and Google entries carry no price or\n   currency: take the original values from the app's order ledger or backend, and\n   the catalog only for missing data, never zero placeholders.\n2. Apple and Google record again: `RecordStorePurchaseAsync(new PurchaseRequest {\n   AxylReceipt = recordReceipt, ... })` with the new-purchase fields. Continue on\n   `Success` or `PaymentsRecordStorePurchaseResult.VerifyError` only; the game\n   server's verification still decides. Steam/PG entries are already recorded: never\n   record them again, and only check the restored data locally (a receipt is present,\n   the market matches).\n3. Send each pending purchase to the game server as a **recovery** (the server\n   verifies with request type 2; Google's verification token is the `PurchaseToken`\n   with `ProductId`) and wait for its delivery confirmation. The server grants only\n   what its ledger lacks.\n4. Run the market's new-purchase close with that confirmation.\n\nTrack progress per entry: a cancellation or failure keeps the other receipts, and a\npartial batch is not \"all recovered\". A close failure after delivery retries the\nclose only. Never match two purchases by product ID alone; the PG payment page\nresponse carries no order ID. The recipe names a Google entry by its first product,\nso a multi-product purchase needs one entry per product.\n\n<!-- /axyl-shared -->\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}