Discord BOTでのレジ及び売上管理について（GTA RP/FiveM）

# 導入の背景について
FiveMで猫カフェのアルバイトをやっており、最初はスプレッドシートで管理していたが、猫カフェのオーナーになったことを機にDiscordでのレジを導入しました。

目的としては、計算のしやすさ、各人の売上の見える化、お店へのバック分の集計、人気商品の情報取得などを行えるようにしました。

## 実際のDiscord画面
【レジ画面】
![スクリーンショット 2024-09-16 14.39.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/6c1b872e-1fb3-6f33-6205-bc13d67185d6.png)

![スクリーンショット 2024-09-16 14.47.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/eb448895-ac12-3aeb-d2b1-7f27cb458b64.png)

【販売報告チャンネル】
![スクリーンショット 2024-09-16 14.48.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/9ed5c26d-654a-3d32-1037-d2db8f858299.png)


【キャッシュバック画面】
![スクリーンショット 2024-09-16 14.45.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/d5e666ff-fae6-e966-c45f-9e0c83ac1b67.png)

【オーナー用メニュー】
![スクリーンショット 2024-09-16 14.42.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/53ca4dda-9c48-c45e-7376-8258bfacdc67.png)


## DiscordBotの概念図
構成のイメージ図は以下の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/c1820b1c-98bf-84ca-0d72-ead1ae91c877.png)

[簡易版]魔法少女カフェBOT
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/1273ba36-0f14-9f46-2ddf-8dc79e2010d2.png)

## 実際のソースコード
猫カフェ版のすべてのソースコードを載せると長いので、簡易版で作成した魔法少女カフェ向けのコードを記載。
処理については、バック分の計算や売上個数管理がなく、ただのレジとしての機能になる。

```python:35-magic-cafe-bot.py
import discord
from discord.ext import commands
import time
from discord.ui import View, Button, Modal, TextInput

#DiscordBOTのトークン　自身で作成したものを貼り付ける
TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxx"

# 必要なインテントを設定
intents = discord.Intents.default()
intents.message_content = True

# Botの設定
bot = commands.Bot(command_prefix='!', intents=intents)

# 商品と単価の辞書
items = {
    '魔法のサンドイッチ': 5000,
    '魔法のケーキ': 5000,
    '星のクッキー': 5000,
    '魔法のエリクサー': 5000,
    'プリズムソーダ': 5000,
    'ミスティックラテ': 5000
}

# ユーザーごとの購入履歴を保持する辞書
user_purchases = {}

# ボタンのビューを作成
class ButtonView(View):
    def __init__(self):
        super().__init__(timeout=None)  # タイムアウトを無効に設定

class QuantityModal(discord.ui.Modal):
    def __init__(self, item_name, item_price):
        super().__init__(title=f'{item_name}の数量入力')
        self.item_name = item_name
        self.item_price = item_price

        self.quantity = discord.ui.TextInput(
            label='数量を入力してください',
            placeholder='例: 3',
            min_length=1,
            max_length=3,
            required=True
        )
        self.add_item(self.quantity)

    async def on_submit(self, interaction: discord.Interaction):
        quantity = int(self.quantity.value)
        total_cost = self.item_price * quantity
        user_id = interaction.user.id
        
        if user_id not in user_purchases:
            user_purchases[user_id] = {}
        
        if self.item_name in user_purchases[user_id]:
            user_purchases[user_id][self.item_name]['quantity'] += quantity
            user_purchases[user_id][self.item_name]['total_cost'] += total_cost
        else:
            user_purchases[user_id][self.item_name] = {'quantity': quantity, 'total_cost': total_cost}

        # 変更を反映するための埋め込みメッセージを更新
        await update_embed(interaction)

        # 応答を遅延させることで、ユーザーにはメッセージを表示しない
        await interaction.response.defer()

class ItemButton(discord.ui.Button):
    def __init__(self, label, price):
        super().__init__(label=label, style=discord.ButtonStyle.primary)
        self.price = price

    async def callback(self, interaction: discord.Interaction):
        await interaction.response.send_modal(QuantityModal(self.label, self.price))

class CheckoutButton(discord.ui.Button):
    def __init__(self, report_channel_id: int):
        super().__init__(label='精算', style=discord.ButtonStyle.success)
        self.report_channel_id = report_channel_id 

    async def callback(self, interaction: discord.Interaction):
        user_id = interaction.user.id
        user_name = interaction.user.name
        total = sum(item['total_cost'] for item in user_purchases.get(user_id, {}).values())

        await interaction.response.send_message(f'合計金額は{total:,}円です。', ephemeral=True)

        # 報告先チャンネルにメッセージを送信
        report_channel = interaction.guild.get_channel(self.report_channel_id)
        if report_channel:
            await report_channel.send(f'{user_name} さんの売り上げ金額は {total:,} 円です。')
        else:
            print(f"チャンネルID {self.report_channel_id} が見つかりません。")

        # 報告後初期化
        time.sleep(5)
        user_purchases[interaction.user.id] = {}
        await update_embed(interaction)

class ResetButton(discord.ui.Button):
    def __init__(self):
        super().__init__(label='リセット', style=discord.ButtonStyle.danger)

    async def callback(self, interaction: discord.Interaction):
        user_purchases[interaction.user.id] = {}
        await interaction.response.send_message('合計金額がリセットされました。', ephemeral=True)
        await update_embed(interaction)

class ItemView(discord.ui.View):
    def __init__(self, items, report_channel_id: int):
        super().__init__(timeout=None)  # タイムアウトを無効に設定
        for item_name, item_price in items.items():
            self.add_item(ItemButton(label=item_name, price=item_price))
        self.add_item(CheckoutButton(report_channel_id=report_channel_id))
        self.add_item(ResetButton())

async def update_embed(interaction: discord.Interaction):
    user_id = interaction.user.id
    embed = discord.Embed(
        title='35マジカル レジ',
        description='商品の合計金額を計算してくれます！'
    )
    for item_name, item_price in items.items():
        quantity = user_purchases.get(user_id, {}).get(item_name, {}).get('quantity', 0)
        total_cost = user_purchases.get(user_id, {}).get(item_name, {}).get('total_cost', 0)
        embed.add_field(name=item_name, value=f'個数: {quantity}   合計金額: {total_cost:,}円', inline=False)

    # 合計金額を埋め込みの最後に追加
    total_amount = sum(item['total_cost'] for item in user_purchases.get(user_id, {}).values())
    embed.add_field(name='総合計金額', value=f'{total_amount:,}円', inline=False)
    
    await interaction.message.edit(embed=embed)

@bot.event
async def on_ready():
    print(f'{bot.user}としてログインしました')

@bot.command(name='start', help='35マジカルレジを開始します')
async def start(ctx):
    report_channel_id = 123456789012345  # 実際のチャンネルIDに置き換える 

    embed = discord.Embed(
        title='35マジカル レジ',
        description='商品の合計金額を計算してくれます！'
    )
    total_amount = 0  # 合計金額を計算するための変数
    for item_name, item_price in items.items():
        embed.add_field(name=item_name, value=f'個数: 0   合計金額: 0円', inline=False)

    # 合計金額を埋め込みの最後に追加
    embed.add_field(name='総合計金額', value=f'{total_amount:,}円', inline=False)

    await ctx.send(embed=embed, view=ItemView(items, report_channel_id))

# Botの起動
bot.run(TOKEN)
```

上記のコードでDiscord Botのトークン及び、報告用のチャンネルIDを変更することで動作させることが可能です。


## DiscordBotを動かすための事前準備（Discord側）
DisocrdBotの作成については、別記事にまとめているので、以下のリンクから参照してください。

https://qiita.com/KumaRider/items/5b8c50af953e61cf09be

### チャンネルIDの確認方法
チャンネルIDは、投稿したいチャンネルを右クリックすると一番下に出てきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/d069c92f-b7bb-695f-c7ae-b3a08446ac2f.png)

:::note warn
チャンネルIDが出てこない場合は、Discordのユーザー設定を開き、開発で検索して、開発者モードを有効にしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/984cd4cd-fa38-fa49-55df-499abd6c8006.png)
:::

## DiscordBotを動かすための事前準備（Python側）
Pythonのインストールについては、省略します。（他の記事を参照にインストールしてください）

必要モジュールのインストール方法については、「discord.py」の「はじめに」に載っているので、以下のサイトを参照してインストールをしてください。（以下のサイトには載っていないですが、discord-componentsのインストールも必要です）

https://discordpy.readthedocs.io/ja/latest/intro.html


## DiscordBotの起動方法

レジを起動させたいDsicordのチャンネルで以下のコマンドを入力して起動します。

!start

起動画面のイメージとしては、以下の通り
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/025bc03f-94d7-e33f-ede1-2a8169fc658b.png)


## 動作環境
作者の動作環境は、Windows10でpython3.11で動作している状況です。（基本的にPCが24時間稼働であり、常時Pythonを起動させれるため、PC上で動作させています。）
PCをちゃんとシャットダウンされている方は、外部サービスやFiveMサーバでの動作を検討が必要です。

今回のBot（Python）については、常時起動しておく必要があるため、FiveMサーバで稼働させることをおすすめします。


## 最後に
今後は、fivem server（Ubuntu 22.04）に移行を検討中です。

また、今回、公開したコードは、「35VILLAGE RP」というFiveMサーバの魔法少女カフェで実際に利用してもらっています。
また、猫カフェ版についても、「35VILLAGE RP」の猫カフェで利用しており、自身がオーナーということで紹介した機能以外にも、自動販売機（自作FievMスクリプト）の補充報告で利用したり、日々更新をしていっている状況です。
ぜひ、遊びに来てみてください。