# Cloud Codeを使用して購入を完了

> Cloud Codeモジュールをバックエンド ソリューションとして実装して、購入に対応します。

D2C支払いプロバイダーを使用する場合は、[Cloud Codeモジュール](/cloud-code/modules/overview.md)を使用して購入を完了できます。

Cloud Code は Webhook 購入イベントを検証し、成功の反応を返します。以下の実装するにはモジュールを記述する必要があります。

1. SKU(Stock Keeping単位)に基づいて、Cloud Saveなどのデータベースで製品資格を付与し、プレイヤーのインベントリを更新します。
2. orders API を呼び出して、注文を履行済みとしてマークします。

> **Important:**
>
> 注文を約定済みとしてマークすることは必須であり、任意ではありません。プレイヤーはどちらの場合でもエンタイトルメントを維持しますが、`paid`状態のままのオーダーでは、Unity Iapオーダーのトラッキングと分析は不完全のままになります。

orders APIの詳細については、[イベント タイプ](./implement-backend.md#event-types)のドキュメントを参照してください。

## モジュールのデプロイ##deploy-the-module

モジュールのデプロイ方法については、『Cloud Code [Get started](/cloud-code/modules/getting-started.md#deploy-the-module) guide』を参照してください。

## IAP ダッシュボードで Cloud Code モジュールを設定する##set-up-your-cloud-code-module-in-the-iap-dashboard

Unity DashboardでCloud Codeモジュールから購入情報を受け取るようにプロジェクトを設定します。

1. [Unity Dashboard](https://cloud.unity.com) で、**IAP**> **Payment Providers** を選択します。
2. **Entitlement 配信メソッド**で、**編集** を選択します。
3. **Cloud Code モジュール**オプションを選択し、ドロップダウンからモジュールとエンドポイントを選択します。
4. **Save configuration** (設定の保存) を選択します。

## ログの使用##use-logs

Cloud Code モジュールがトランザクションを処理する場合、[Cloud Code ダッシュボード](https://cloud.unity.com/home/organizations/default/projects/default/environments/default/cloud-code/logs)でログをビューできます。詳細については、Cloud Code [Logging](/cloud-code/logging/overview.md) のドキュメントを参照してください。

## Cloud Code サンプルモジュール##cloud-code-example-module

購入イベントの Webhook を処理する以下のモジュール例を参照してください。

```csharp
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudSave.Model;

名前空間の履行;

// Handles IAP purchase webhooks: grants product entitlements via Cloud Save and marks orders as fulfilled.
public class IapFulfillmentHandler
{
    private readonly ILogger<IapFulfillmentHandler> m＿Logger； .

    private static readonly HttpClient s_HttpClient = new() { Timeout = TimeSpan.FromSeconds(10) };
    private const string k_IapTransactionVerifierBaseUrl = "https://iap.services.api.unity.com/v1";

    // Accepts and stores the logger used for diagnostics.
    public IapFulfillmentHandler(ILogger<IapFulfillmentHandler> logger)
    {
        m_Logger = logger;
    }

    // Webhook entry point: grants entitlements from line items via Cloud Save, then marks the order as fulfilled and returns a response.
    [CloudCodeFunction("ProcessPurchaseFulfillment")]
    public async Task<WebhookResponse> ProcessPurchaseFulfillment(
        IExecutionContext コンテキスト
        IGameApiClient gameApiClient,
        GUID ID
        文字列バージョン、
        文字列 eventType、
        DateTime time、
        string projectId,
        string environmentId,
        文字列データ型、
        WebhookOrderData data)
    {
        try
        {
            m_Logger.LogInformation(
                "購入処理 - EventID: {EventID}, EventType: {EventType}, Time: {Time}, " +
                "ProjectID: {ProjectID}, EnvironmentId: {EnvironmentId}, PlayerId: {PlayerId}, " + "
                "PaymentProvider: {PaymentProvider}, PaymentProviderResourceId: {PaymentProviderResourceId}, " +
                "OrderId: {OrderId}, LineItems: {LineItems}, CustomReferenceId: {CustomReferenceId}",
                id.ToString(),
                eventType
                time.ToString(),
                projectId
                environmentId,
                data.PlayerId,
                data.PaymentProvider,
                data.PaymentProviderResourceId,
                データ.Id、
                JsonSerializer.Serialize(data.LineItems),
                data.CustomReferenceId);

            List<string> entitlements = await GrantProductEntitlements(
                context, gameApiClient, data.PlayerId, data.LineItems);
            m_Logger.LogInformation(
                " " +" の付いたオーダー {OrderId} の購入処理が正常に終了しました"
                "{Entitlements}",
                データ.Id、エンタイトルメント);

            try
            {
                m_Logger.LogInformation("Calling fulfillment API for order {OrderId}", data.Id);
                bool fulfillmentSuccess = await FulfillOrderAsync(
                    データ.Id、
                    projectId
                    environmentId,
                    context.ServiceToken ?? string.Empty);

                if (fulfillmentSuccess)
                {
                    m_Logger.LogInformation("Successfully marked order {OrderId} as fulfilled", data.Id);
                }
                else
                {
                    m_Logger.LogWarning(
                    "Failed to mark order {OrderId} as fulfilled, but entitlements was " (注文 {OrderId} を完了済みとしてマークできませんでしたが、エンタイトルメントは " +
                    "granted",
                    データ.Id);
                }
            }
            catch (例外)
            {
                m_Logger.LogError(ex, "Error calling fulfillment API for order {OrderId}: {Error}",
                    データ.Id, ex.Message);
            }

            return new WebhookResponse
            {
                Status = WebhookResponseCodes.WebhookStatusOK
            };
        }
        catch (例外)
        {
            m_Logger.LogError(
                "注文{OrderId}の履行中に予期しないエラーが発生しました: {Error}",
                データ.Id, ex.Message);

            return new WebhookResponse
            {
                Status = WebhookResponseCodes.WebhookStatusError,
                Code = WebhookResponseCodes.WebhookErrorDeclined,
                Description = ex.Message
            };
        }
    }

    // Loads the player inventory, applies each line item by SKU (gems, pass, storage), saves inventory, and returns the list of granted entitlement descriptions.
    private async Task<List<string>> GrantProductEntitlements(
        IExecutionContext コンテキスト
        IGameApiClient gameApiClient,
        文字列 playerId、
        リスト<WebhookLineItem>ラインアイテム)
    {
        if (lineItems == null || lineItems.Count == 0)
        {
            m_Logger.LogWarning("GrantProductEntitlements called with null or empty line items list");
            return new List<string>();
        }

        リスト<string>エンタイトルメント ＝ new リスト<string>（）；

        try
        {
            m_Logger.LogInformation("Getting player inventory for player {PlayerId}", playerId);
            PlayerInventory currentInventory = await GetPlayerInventory(
                context, gameApiClient, playerId);
            m_Logger.LogInformation("Retrieved inventory: {Inventory}", JsonSerializer.Serialize(currentInventory));

            Foreach (lineItems の WebhookLineItem 項目)
            {
                string sku = item.Sku;

                m_Logger.LogInformation("Processing line item:SKU={SKU}, ProductType={ProductType}",
                    sku, 項目.ProductType);

                switch (sku)
                {
                    case "com.unity.iap.test.adventure.pass.not":
                        int days = 30;
                        currentInventory.AdventurePass = DateTime.UtcNow.AddDays(days);
                        entitlements.Add($"{days} Days Adventure Pass");
                        break;

                    case "com.unity.iap.test.30.gems":
                        int gems30 = 30;
                        currentInventory.Gems += gems30;
                        entitlements.Add($"{gems30} Gems");
                        break;

                    case "com.unity.iap.test.premium.storage":
                        currentInventory.PremiumStorage = true;
                        entitlements.Add($"PremiumStorage = true");
                        break;

                    デフォルト
                        m_Logger.LogWarning("Unknown SKU {SKU} in fulfillment request", sku);
                        break;
                }
            }

            m_Logger.LogInformation("Updated inventory: inventory: {Inventory}",
                JsonSerializer.Serialize(currentInventory));

            m_Logger.LogInformation("Saving player inventory");
            await SavePlayerInventory(context, gameApiClient, playerId, currentInventory);
            m_Logger.LogInformation("Successfully saved player inventory");
        }
        catch (例外)
        {
            m_Logger.LogError("Error in GrantProductEntitlements: {Error}", ex.Message);
            throw;
        }

        return entitlements;
    }

    // Fetches the "player_inventory" key from Cloud Save for the player and deserializes it; returns a new empty inventory if missing or on error.
    private async Task<PlayerInventory> GetPlayerInventory( )
        IExecutionContext コンテキスト
        IGameApiClient gameApiClient,
        string playerId)
    {
        try
        {
            var result = await gameApiClient.CloudSaveData.GetItemsAsync(
                context
                context.ServiceToken!,
                context.ProjectId!,
                PlayerId
                new List<string> { "player_inventory" });

            if (result.Data.Results.Count <= 0)
            {
                m_Logger.LogInformation("No existing inventory found, creating new inventory");
                return new PlayerInventory();
            }

            var resultItem = result.Data.Results[0];
            if (resultItem.Value == null)
            {
                m_Logger.LogWarning("Inventory value is null, creating new inventory");
                return new PlayerInventory();
            }

            string inventoryJson;
            try
            {
                inventoryJson = resultItem.Value.ToString();
                if (string.IsNullOrWhiteSpace(inventoryJson))
                {
                    m_Logger.LogWarning("Inventory JSON is empty, creating new inventory");
                    return new PlayerInventory();
                }
            }
            catch (例外)
            {
                m_Logger.LogError("Failed to convert inventory value to string: {Error}", ex.Message);
                return new PlayerInventory();
            }

            try
            {
                PlayerInventory inventory = JsonSerializer.Deserialize<PlayerInventory>(
                    inventoryJson);
                return inventory ?? new PlayerInventory();
            }
            catch (例外)
            {
                m_Logger.LogError( インベントリのデシリアライズライズに失敗しました JSON: {Error}。JSON: {Json}", ex.Message, inventoryJson);
                return new PlayerInventory();
            }
        }
        catch (例外)
        {
            m_Logger.LogError("Unexpected error while getting inventory: {Error}", ex.Message);
            return new PlayerInventory();
        }
    }

    // Serializes the inventory to JSON and writes it to Cloud Save under the "player_inventory" key for the player.
    private async Task SavePlayerInventory(
        IExecutionContext コンテキスト
        IGameApiClient gameApiClient,
        文字列 playerId、
        PlayerInventory インベントリ)
    {
        try
        {
            if (inventory == null)
            {
                m_Logger.LogWarning("Attempted to save null inventory, skipping");
                return;
            }

            string inventoryJson;
            try
            {
                inventoryJson = JsonSerializer.Serialize(inventory);
            }
            catch (例外)
            {
                m_Logger.LogError("Failed to serialize inventory: {Error}", ex.Message);
                throw;
            }

            await gameApiClient.CloudSaveData.SetItemAsync(
                context
                context.ServiceToken!,
                context.ProjectId!,
                PlayerId
                new SetItemBody("player_inventory", inventoryJson));

            m_Logger.LogInformation("Successfully saved inventory for player {PlayerId}", playerId);
        }
        catch (例外)
        {
            m_Logger.LogError("Failed to save inventory: {Error}", ex.Message);
            throw;
        }
    }

    // Sends a PATCH request to the IAP orders API to set the order status to fulfilled; returns true on success.
    private async Task<bool> FulfillOrderAsync()
        文字列 orderId、
        string projectId,
        string environmentId,
        文字列 serviceToken)
    {
        if (string.IsNullOrEmpty(orderId))
        {
            m_Logger.LogWarning("Cannot fulfill order - order ID is null or empty");
            return false;
        }

        if (string.IsNullOrEmpty(projectId))
        {
            m_Logger.LogWarning("Cannot fulfill order - project ID is null or empty");
            return false;
        }

        if (string.IsNullOrEmpty(environmentId))
        {
            m_Logger.LogWarning("Cannot fulfill order - environment ID is null or empty");
            return false;
        }

        if (string.IsNullOrEmpty(serviceToken))
        {
            m_Logger.LogWarning("Cannot fulfill order - service token is null or empty");
            return false;
        }

        try
        {
            string url = $"{k_IapTransactionVerifierBaseUrl}/projects/{projectId}/environments/" +
                $"{environmentId}/orders/{orderId}";

            var requestBody = new { status = "fulfilled" };
            string jsonContent = JsonSerializer.Serialize(requestBody);
            StringContent content = new StringContent(
                jsonContent, Encoding.UTF8, "application/json");

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, url)
            {
                Content = content
            };
            request.Headers.Authorization = new AuthenticationHeaderValue(
                "Bearer", serviceToken);

            m_Logger.LogInformation("Sending fulfillment request to {Url} for order {OrderId}",
                URL, orderId);

            HttpResponseMessage response = await s_HttpClient.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                m_Logger.LogInformation("注文 {OrderId} の完了 API 呼び出しが成功しました。状態: {StatusCode}",
                    orderId, response.StatusCode);
                return true;
            }

            string responseBody = await response.Content.ReadAsStringAsync();
            m_Logger.LogWarning(
                "オーダー {OrderId} の完了 API 呼び出しが失敗しました。"状態: {StatusCode}, " +
                "反応: {ResponseBody}",
                orderId, response.StatusCode, responseBody);
            return false;
        }
        catch (例外)
        {
            m_Logger.LogError(ex, "Unexpected error while fulfilling order {OrderId}: {Error}", orderId, ex.Message);
            return false;
        }
    }
}

// Player inventory stored in Cloud Save (gems, adventure pass, premium storage, and similar).
public class PlayerInventory
{
    public int Gems { get; set; }
    public int XpBooster { get; set; }
    public DateTime?AdventurePass { get; set; }
    public bool PremiumStorage { get; set; }
}

// Order payload sent by the IAP webhook for a purchase event.
public class WebhookOrderData
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("playerId")]
    public string PlayerId { get; set; } = string.Empty;

    [JsonPropertyName("paymentProvider")]
    public string PaymentProvider { get; set; } = string.Empty;

    [JsonPropertyName("paymentProviderResourceId")]
    public string?PaymentProviderResourceId { get; set; }

    [JsonPropertyName("url")]
    public string Url { get; set; } = string.Empty;

    [JsonPropertyName("lineItems")]
    public List<WebhookLineItem> LineItems { get; set; } = new();

    [JsonPropertyName("currency")]
    public string Currency { get; set; } = string.Empty;

    [JsonPropertyName("amounts")]
    public WebhookAmounts?Amounts { get; set; }

    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;

    [JsonPropertyName("customReferenceId")]
    public string?CustomReferenceId { get; set; }

    [JsonPropertyName("metadata")]
    public Dictionary<string, string>?Metadata { get; set; }

    [JsonPropertyName("createdAt")]
    public DateTime CreatedAt { get; set; }

    [JsonPropertyName("updatedAt")]
    public DateTime UpdatedAt { get; set; }

    [JsonPropertyName("paidAt")]
    public DateTime?PaidAt { get; set; }

    [JsonPropertyName("fulfilledAt")]
    public DateTime?FulfilledAt { get; set; }
}

// A single purchasable item in an order (SKU, product type, and price).
public class WebhookLineItem
{
    [JsonPropertyName("sku")]
    public string Sku { get; set; } = string.Empty;

    [JsonPropertyName("productType")]
    public string ProductType { get; set; } = string.Empty;

    [JsonPropertyName("price")]
    public WebhookMoney Price { get; set; } = null!;
}

// Total and refunded amounts for an order, in micros.
public class WebhookAmounts
{
    [JsonPropertyName("totalMicros")]
    public long TotalMicros { get; set; }

    [JsonPropertyName("refundedMicros")]
    public long RefundedMicros { get; set; }
}

// A monetary amount and currency (amount in micros).
public class WebhookMoney
{
    [JsonPropertyName("amountMicros")]
    public long AmountMicros { get; set; }

    [JsonPropertyName("currency")]
    public string Currency { get; set; } = string.Empty;
}

// Status and error code constants for IAP webhook responses.
public static class WebhookResponseCodes
{
    public const string WebhookStatusOK = "ok";
    public const string WebhookStatusError = "error";
    public const string WebhookErrorDeclined = "declined";
}

// Response returned by the IAP fulfillment webhook to the IAP service.
public class WebhookResponse
{
    public string Status { get; set; } = string.Empty;
    public string?コード { get; set; }
    public string?Description { get; set; }
}

// Registers Cloud Code module dependencies (for example, the game API client).
public class ModuleConfig :ICloudCodeSetup
{
    // Registers the game API client as a singleton for re-use by Cloud Code functions.
    public void Setup(ICloudCodeConfig config)
    {
        config.Dependencies.AddSingleton(GameApiClient.Create());
    }
}
```

[Integrate D2C payment providers](./workflow.md#use-a-cloud-code-module-to-fulfill-purchases): Integrate D2C payment provider with IAP ワークフロー (D2C 支払いプロバイダーと IAP ワークフローの統合) ページに戻ります。
[Test your integration](./test-integration.md): ワークフローの次のステップに進み、D2C支払いプロバイダーを設定します。
