mcpField
P
pos-integration

pos-integration

github.com/finsilabsその他60102 回インストール6 日前に更新
リポジトリ

pos-integration について

実店舗の POS とオンラインストアを連携するためのスキルガイドです。Shopify、WooCommerce、BigCommerce、Square の連携方法を扱い、在庫同期、チャネル横断返品、カスタム連携を説明します。

主な機能

  1. 01プラットフォーム別 POS 連携経路の整理
  2. 02Shopify POS と店舗拠点の設定
  3. 03Square との双方向在庫同期
  4. 04Webhook によるオンライン在庫更新
  5. 05チャネル横断返品への対応

活用シーン

  1. 01オンラインストアへの実店舗追加
  2. 02チャネル間の在庫差異の削減
  3. 03店頭でのオンライン注文返品処理
  4. 04複数店舗の在庫・注文管理の統合

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill pos-integration

インストール後、Claude Code で/pos-integrationを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/cross-channel-in-store-returns/
criteria.json
task.md
evals/square-catalog-sync/
criteria.json
task.md
evals/square-webhook-inventory-sync/
criteria.json
task.md

詳細

POS 統合

概要

POS 統合により、実店舗の小売業務をオンラインストアと接続し、チャネルをまたいだ在庫の一元的な可視化、注文の一元管理、一貫した顧客プロフィールを実現できます。顧客が店舗で商品を購入すると、オンラインストアの在庫にも直ちに反映する必要があります。また、顧客がオンライン注文を店舗で返品した場合は、返金が決済ゲートウェイに連携される必要があります。このスキルでは、Square と Shopify POS をコマースプラットフォームに接続し、在庫をリアルタイムで同期し、チャネルをまたいだ返品を処理する方法を説明します。

このスキルを使用する場面

  • 既存のオンラインストアに加えて実店舗を開設するとき
  • 店舗とオンラインの在庫差異により、在庫数を超える注文が発生しているとき
  • 顧客がオンライン購入品を店舗で返品できることを期待しているとき(オムニチャネル返品)
  • フランチャイズや複数店舗の小売業務で、在庫とレポートを一元管理する必要があるとき
  • カスタムのキオスクやタブレットベースの POS アプリケーションを構築するとき

基本手順

ステップ 1:プラットフォームと POS 統合の方法を決定する
プラットフォーム最も簡単な POS 統合一元化される内容
ShopifyShopify POS(組み込み、Pro は月額 $89) — Shopify のオンラインストアとネイティブに連携するよう設計されていますPOS とオンラインストア間で在庫が即時同期され、注文は統合された管理画面に表示され、顧客プロフィールとロイヤルティポイントが一元化されます
WooCommerceSquare + WooCommerce Square プラグイン(無料)在庫を双方向に同期します。Square POS の販売が WooCommerce の在庫数を更新し、注文を WooCommerce にインポートできます
BigCommerceSquare + BigCommerce Square 統合WooCommerce と同様ですが、BigCommerce のチャネル設定で構成します。BigCommerce は Stripe Terminal にも直接対応しています
カスタム / ヘッドレスSquare API または Stripe Terminalすべてを自由に制御できます。Webhook で在庫を同期し、API で POS 注文をインポートして、統合された注文ダッシュボードを構築できます
ステップ 2:プラットフォームごとの POS 設定

Shopify + Shopify POS

Shopify POS は在庫を単一のシステムで管理するため、Shopify ストアに最もシームレスな選択肢です。

  1. Shopify POS Pro に登録します(月額 $89、または Shopify Plus に含まれます)。

    • Shopify 管理画面で Point of Sale → Devices に移動します
    • iPad または iPhone に Shopify POS アプリをインストールします
    • カードリーダーを接続します(Shopify 独自の Tap & Chip リーダーを提供しています)
  2. 各店舗のロケーションを設定します。

    • Settings → Locations に移動し、各実店舗の Add location をクリックします
    • ロケーションごとに在庫数を割り当てます。Shopify は各ロケーションの在庫を個別に追跡します
  3. 在庫は自動的に同期されます。

    • POS アプリで販売すると、そのロケーションの在庫数が直ちに減少します
    • オンラインストアで複数のロケーションから販売するよう設定できます(設定するには Settings → Shipping and delivery → Local pickup に移動します)
  4. クリック&コレクト(オンラインで購入、店舗で受け取り)を設定します。

    • Settings → Shipping and delivery → Local pickup に移動し、各ロケーションで有効にします
    • 顧客はチェックアウト時に受け取り場所を選択します。注文は該当店舗の POS アプリの Pickups に表示されます

WooCommerce + Square

Square を WooCommerce に接続する:

  1. wordpress.org から WooCommerce Square プラグイン(無料、Square が開発)をインストールします
  2. WooCommerce → Settings → Integrations → Square に移動し、Connect with Square をクリックします
  3. Square アカウントにログインし、Square のビジネス拠点を選択します
  4. Square プラグインの設定で、Sync inventory(双方向)を有効にします
  5. 初回同期を実行します:WooCommerce → System Status → Tools → Sync Products with Square に移動します

在庫同期の仕組み:

  • Square POS で商品が販売されると、WooCommerce の在庫数が自動的に減ります(Webhook 経由で数分以内)
  • WooCommerce がオンライン注文を受け取ると、Square の在庫数が減ります
  • 同期履歴の確認や不整合のトラブルシューティングを行うには、WooCommerce → Square → Sync Log に移動します

オンライン注文の店頭返品に対応する:

  • WooCommerce 管理画面で開始された返品は、元の支払いに Square が使われていれば自動的に Square に同期されます
  • Stripe で支払われたオンライン注文が店頭で返品された場合は、Square で返金処理を行い、WooCommerce で手動照合するか、カスタム返金エンドポイントを構築します(下記の「カスタム」セクションを参照)

BigCommerce + Square
  1. BigCommerce 管理画面の Channel Manager に移動し、+ Create New Channel をクリックします
  2. チャネル一覧から Square を選択します
  3. BigCommerce に Square アカウントへの接続を許可します
  4. 在庫同期の設定を行います。BigCommerce と Square の間で在庫数が双方向に同期されます
  5. レポートを一元化するため、Square POS の注文を BigCommerce にインポートできます

カスタム/ヘッドレス + Square API

Square クライアントを初期化する:

// lib/pos/square-client.ts
import { Client, Environment } from 'square';

export const squareClient = new Client({
  accessToken: process.env.SQUARE_ACCESS_TOKEN!,
  environment: process.env.NODE_ENV === 'production' ? Environment.Production : Environment.Sandbox,
});

export const { catalogApi, inventoryApi, ordersApi, paymentsApi, locationsApi } = squareClient;

Square の在庫 Webhook を処理する(店頭販売 → オンラインストアを更新):

// POST /api/webhooks/square
import { createHmac } from 'node:crypto';

export async function POST(req: NextRequest) {
  const rawBody = Buffer.from(await req.arrayBuffer());
  const signature = req.headers.get('x-square-hmacsha256-signature') ?? '';

  // Square HMAC includes the full notification URL in the signature
  const notificationUrl = `${process.env.APP_URL}/api/webhooks/square`;
  const expected = createHmac('sha256', process.env.SQUARE_WEBHOOK_SECRET!)
    .update(notificationUrl + rawBody.toString('utf8'))
    .digest('base64');

  if (signature !== expected) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(rawBody.toString('utf8'));

  if (event.type === 'inventory.count.updated') {
    const counts = event.data.object.inventory_counts ?? [];
    for (const count of counts) {
      const variant = await db.variants.findBySquareCatalogId(count.catalog_object_id);
      if (!variant) continue;

      await db.inventory.updateLocationQuantity(variant.sku, count.location_id, parseInt(count.quantity));
      const total = await db.inventory.getTotalAvailable(variant.sku);
      await redis.setex(`inventory:${variant.productId}`, 3600, String(total));
    }
  }

  return NextResponse.json({ accepted: true });
}

チャネル間の返品を処理する(オンライン注文を店頭で返品):

// lib/pos/returns.ts
export async function processInStoreReturn(params: {
  orderId: string;
  lineItems: Array<{ lineItemId: string; quantity: number }>;
  locationId: string;
}) {
  const { orderId, lineItems, locationId } = params;
  const order = await db.orders.findById(orderId);
  if (!order) throw new Error(`Order ${orderId} not found`);

  // Calculate refund amount
  const refundAmount = lineItems.reduce((sum, item) => {
    const line = order.lineItems.find(l => l.id === item.lineItemId)!;
    return sum + (line.unitPriceCents * item.quantity);
  }, 0);

  // Issue refund via original payment gateway
  if (order.paymentGateway === 'stripe') {
    await stripe.refunds.create({
      payment_intent: order.stripePaymentIntentId,
      amount: refundAmount,
      metadata: { orderId, locationId, channel: 'in_store_return' },
    });
  } else if (order.paymentGateway === 'square') {
    await paymentsApi.createPaymentRefund({
      idempotencyKey: `return_${orderId}_${Date.now()}`,
      paymentId: order.squarePaymentId!,
      amountMoney: { amount: BigInt(refundAmount), currency: 'USD' },
    });
  }

  // Restock returned items at the return location
  for (const item of lineItems) {
    const line = order.lineItems.find(l => l.id === item.lineItemId)!;
    await db.inventory.incrementLocationQuantity(line.sku, locationId, item.quantity);
  }

  await db.orders.addReturn({ orderId, lineItems, processedAtLocation: locationId, processedAt: new Date() });
}

商品カタログを Square に同期する(Square で在庫を追跡する前に必要です):

export async function syncProductToSquare(product: Product) {
  const { result } = await catalogApi.batchUpsertCatalogObjects({
    idempotencyKey: `sync_${product.sku}_${Date.now()}`,
    batches: [{
      objects: [{
        type: 'ITEM',
        id: `#item_${product.sku}`,
        itemData: {
          name: product.name,
          variations: product.variants.map(variant => ({
            type: 'ITEM_VARIATION' as const,
            id: `#var_${variant.sku}`,
            itemVariationData: {
              itemId: `#item_${product.sku}`,
              name: variant.name,
              sku: variant.sku,
              pricingType: 'FIXED_PRICING',
              priceMoney: { amount: BigInt(Math.round(variant.price * 100)), currency: 'USD' },
              trackInventory: true,
            },
          })),
        },
      }],
    }],
  });

  // Store Square-assigned IDs for future inventory updates
  for (const mapping of result.idMappings ?? []) {
    if (mapping.clientObjectId?.startsWith('#var_')) {
      const sku = mapping.clientObjectId.replace('#var_', '');
      await db.variants.update(sku, { squareCatalogVariationId: mapping.objectId });
    }
  }
}

ベストプラクティス

  • Shopify のマーチャントは Shopify POS を使用してください。オンラインストアと単一の在庫システムを共有するため、同期の遅延や重複レコードがなく、レポートも最初から一元化されています
  • オンラインストア用に安全在庫を確保してください。実在庫の 100% をオンラインで販売可能にせず、在庫同期の反映よりも早く発生する店頭販売に備えて、10~20% をバッファーとして確保します
  • ロケーション対応の在庫管理を導入してください。全体の合計だけでなく、実店舗ごとに在庫を追跡します。これにより、クリック&コレクトの在庫状況を正確に表示できます
  • 同期の遅延を監視してください。POS での販売からオンライン上に在庫数の更新が反映されるまでの時間を測定し、遅延が 5 分を超えたらアラートを出します
  • 手動照合ツールを構築してください。在庫の不整合は発生します。店舗管理者が POS の在庫数とシステム上の在庫を比較し、手動同期を実行できる UI が必要です

よくある落とし穴

問題解決策
WooCommerce Square の同期が停止するWooCommerce → Settings → Integrations → Square で Square 接続を再認証します。OAuth トークンは期限切れになるため、通常、プラグインから再認証を求められます
同期の遅延による在庫の過剰販売マーケットプレイス/POS の設定で安全在庫のバッファーを設定します。需要の高い商品については、チェックアウト時に Square の在庫 API を使って最終的な在庫確認を行います
Square のカタログ ID が SKU と一致しなくなる内部 SKU と Square のカタログバリエーション ID の対応表を管理します。同期がずれた場合は、Square のカタログから対応表を再構築します
チャネル間の返品によって在庫が二重計上される返品が開始された時点ではなく、商品が実際に店舗で受領された時点(status: received)でのみ在庫を戻します
Shopify POS に受け取り用のオンライン注文が表示されない顧客がチェックアウト時に Pick up in store を選択したこと、また、Settings → Locations で POS デバイスに正しいロケーションが割り当てられていることを確認します

関連スキル

  • @marketplace-connectors
  • @webhook-architecture
  • @flash-sale-scaling
  • @monitoring-alerting-commerce

コメント