V2.1: Plafond negatif, reserve vault, carte bancaire visuelle, panel rewrite
- db: added client_negative_floor column to banks - bank_manager: vault check on depot, negative floor on retrait/virement - banque: new /banque set-plafond and /banque set-reserve commands - bank_card: new Pillow-based bank card image generator - panel: full rewrite (no more JSON), guild name in title, new banking flow
This commit is contained in:
parent
c9ad155bf6
commit
48ec3a3d89
5 changed files with 868 additions and 1637 deletions
|
|
@ -240,6 +240,56 @@ class Banque(commands.Cog):
|
|||
except ValueError as e:
|
||||
await interaction.response.send_message(f"❌ {e}", ephemeral=True)
|
||||
|
||||
@banque_group.command(name="set-plafond", description="Définir le plafond négatif autorisé pour les comptes clients")
|
||||
@app_commands.describe(plafond="Plafond négatif (ex: 1000 = les clients peuvent descendre à -1000€)")
|
||||
async def set_plafond(self, interaction: discord.Interaction, plafond: float):
|
||||
bank = await self._get_user_bank(interaction)
|
||||
if not bank:
|
||||
await interaction.response.send_message(
|
||||
"❌ Vous n'avez pas de rôle de banquier sur ce serveur.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if plafond < 0 or plafond > 10000:
|
||||
await interaction.response.send_message(
|
||||
"❌ Le plafond doit être entre 0 et 10000€.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
ok = await bank_manager.set_negative_floor(interaction.guild.id, bank["id"], plafond)
|
||||
if ok:
|
||||
await interaction.response.send_message(
|
||||
f"✅ Plafond négatif de **{bank['name']}** défini à **-{plafond:.2f}€**.",
|
||||
ephemeral=True,
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message("❌ Erreur.", ephemeral=True)
|
||||
|
||||
@banque_group.command(name="set-reserve", description="Définir le montant de la réserve bancaire (vault)")
|
||||
@app_commands.describe(montant="Montant de la réserve")
|
||||
async def set_reserve(self, interaction: discord.Interaction, montant: float):
|
||||
bank = await self._get_user_bank(interaction)
|
||||
if not bank:
|
||||
await interaction.response.send_message(
|
||||
"❌ Vous n'avez pas de rôle de banquier sur ce serveur.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if montant < 0:
|
||||
await interaction.response.send_message(
|
||||
"❌ La réserve ne peut pas être négative.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
ok = await bank_manager.set_vault_balance(interaction.guild.id, bank["id"], montant)
|
||||
if ok:
|
||||
await interaction.response.send_message(
|
||||
f"✅ Réserve de **{bank['name']}** définie à **{montant:.2f}€**.",
|
||||
ephemeral=True,
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message("❌ Erreur.", ephemeral=True)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Banque(bot))
|
||||
|
|
|
|||
2215
commandes/panel.py
2215
commandes/panel.py
File diff suppressed because it is too large
Load diff
158
utils/bank_card.py
Normal file
158
utils/bank_card.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import os
|
||||
import math
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
class BankCardGenerator:
|
||||
WIDTH = 856
|
||||
HEIGHT = 540
|
||||
RADIUS = 30
|
||||
|
||||
COLORS = {
|
||||
"dark": (25, 25, 35),
|
||||
"mid": (35, 40, 60),
|
||||
"light": (50, 55, 80),
|
||||
"accent": (0, 180, 160),
|
||||
"accent_dim": (0, 120, 105),
|
||||
"gold": (212, 175, 55),
|
||||
"white": (255, 255, 255),
|
||||
"gray": (160, 165, 180),
|
||||
"dim": (90, 95, 110),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.font_title = ImageFont.load_default()
|
||||
self.font_label = ImageFont.load_default()
|
||||
self.font_value = ImageFont.load_default()
|
||||
self.font_rib = ImageFont.load_default()
|
||||
self.font_large = ImageFont.load_default()
|
||||
try:
|
||||
base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts")
|
||||
if os.path.isdir(base):
|
||||
for name in os.listdir(base):
|
||||
path = os.path.join(base, name)
|
||||
lower = name.lower()
|
||||
if "bold" in lower:
|
||||
self.font_title = ImageFont.truetype(path, 26)
|
||||
self.font_large = ImageFont.truetype(path, 36)
|
||||
elif "mono" in lower or "regular" in lower:
|
||||
self.font_label = ImageFont.truetype(path, 16)
|
||||
self.font_value = ImageFont.truetype(path, 22)
|
||||
self.font_rib = ImageFont.truetype(path, 20)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _rounded_rect(self, draw, xy, radius, fill, outline=None, width=0):
|
||||
x0, y0, x1, y1 = xy
|
||||
r = radius
|
||||
draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill, outline=outline, width=width)
|
||||
draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill)
|
||||
draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill)
|
||||
if outline and width:
|
||||
draw.arc([x0, y0, x0 + 2 * r, y0 + 2 * r], 180, 270, fill=outline, width=width)
|
||||
draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 0, fill=outline, width=width)
|
||||
draw.arc([x0, y1 - 2 * r, x0 + 2 * r, y1], 90, 180, fill=outline, width=width)
|
||||
draw.arc([x1 - 2 * r, y1 - 2 * r, x1, y1], 0, 90, fill=outline, width=width)
|
||||
draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width)
|
||||
draw.line([x0 + r, y1, x1 - r, y1], fill=outline, width=width)
|
||||
draw.line([x0, y0 + r, x0, y1 - r], fill=outline, width=width)
|
||||
draw.line([x1, y0 + r, x1, y1 - r], fill=outline, width=width)
|
||||
|
||||
def _draw_gradient_rect(self, draw, xy, color_start, color_end, vertical=True):
|
||||
x0, y0, x1, y1 = xy
|
||||
if vertical:
|
||||
for y in range(y0, y1):
|
||||
t = (y - y0) / max(1, y1 - y0)
|
||||
r = int(color_start[0] + (color_end[0] - color_start[0]) * t)
|
||||
g = int(color_start[1] + (color_end[1] - color_start[1]) * t)
|
||||
b = int(color_start[2] + (color_end[2] - color_start[2]) * t)
|
||||
draw.line([(x0, y), (x1, y)], fill=(r, g, b))
|
||||
else:
|
||||
for x in range(x0, x1):
|
||||
t = (x - x0) / max(1, x1 - x0)
|
||||
r = int(color_start[0] + (color_end[0] - color_start[0]) * t)
|
||||
g = int(color_start[1] + (color_end[1] - color_start[1]) * t)
|
||||
b = int(color_start[2] + (color_end[2] - color_start[2]) * t)
|
||||
draw.line([(x, y0), (x, y1)], fill=(r, g, b))
|
||||
|
||||
def _draw_chip(self, draw, x, y):
|
||||
w, h = 55, 40
|
||||
self._rounded_rect(draw, (x, y, x + w, y + h), 6, fill=(200, 175, 50), outline=(180, 155, 40), width=2)
|
||||
draw.line([(x + 8, y + h // 3), (x + w - 8, y + h // 3)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + 8, y + 2 * h // 3), (x + w - 8, y + 2 * h // 3)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + w // 3, y + 4), (x + w // 3, y + h - 4)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + 2 * w // 3, y + 4), (x + 2 * w // 3, y + h - 4)], fill=(160, 135, 30), width=1)
|
||||
|
||||
def _draw_wave(self, draw, y_base, color, amplitude=15, segments=8):
|
||||
points = []
|
||||
w = self.WIDTH
|
||||
for i in range(segments + 1):
|
||||
x = int(w * i / segments)
|
||||
y = y_base + int(amplitude * math.sin(i * 1.2))
|
||||
points.append((x, y))
|
||||
for i in range(len(points) - 1):
|
||||
draw.line([points[i], points[i + 1]], fill=color, width=2)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
bank_name: str,
|
||||
holder_name: str,
|
||||
rib: str,
|
||||
solde: float,
|
||||
account_type: str = "particulier",
|
||||
tva_rate: float = 0.0,
|
||||
) -> BytesIO:
|
||||
img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.COLORS["dark"])
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
self._draw_gradient_rect(draw, (0, 0, self.WIDTH, self.HEIGHT), self.COLORS["dark"], self.COLORS["mid"])
|
||||
|
||||
self._draw_wave(draw, 120, (*self.COLORS["accent"][:3],), amplitude=20, segments=10)
|
||||
self._draw_wave(draw, 140, (*self.COLORS["accent_dim"][:3],), amplitude=15, segments=12)
|
||||
self._draw_wave(draw, 360, (*self.COLORS["light"][:3],), amplitude=12, segments=8)
|
||||
|
||||
self._rounded_rect(draw, (20, 20, self.WIDTH - 20, self.HEIGHT - 20), self.RADIUS, fill=None, outline=self.COLORS["accent"], width=2)
|
||||
|
||||
self._draw_chip(draw, 60, 130)
|
||||
|
||||
draw.text((75, 185), "VICTORIA'S VICE", fill=self.COLORS["accent"], font=self.font_title)
|
||||
draw.text((75, 218), bank_name.upper(), fill=self.COLORS["gray"], font=self.font_label)
|
||||
|
||||
type_label = "COMPTE ENTREPRISE" if account_type == "entreprise" else "COMPTE PARTICULIER"
|
||||
draw.text((self.WIDTH - 200, 45), type_label, fill=self.COLORS["dim"], font=self.font_label)
|
||||
|
||||
y_rib = 280
|
||||
draw.text((60, y_rib), "RIB", fill=self.COLORS["dim"], font=self.font_label)
|
||||
|
||||
rib_parts = []
|
||||
clean_rib = rib.replace(" ", "")
|
||||
for i in range(0, len(clean_rib), 4):
|
||||
rib_parts.append(clean_rib[i:i + 4])
|
||||
rib_display = " ".join(rib_parts)
|
||||
draw.text((60, y_rib + 22), rib_display, fill=self.COLORS["white"], font=self.font_rib)
|
||||
|
||||
y_solde = 350
|
||||
draw.text((60, y_solde), "SOLDE", fill=self.COLORS["dim"], font=self.font_label)
|
||||
solde_str = f"{solde:,.2f} EUR".replace(",", " ")
|
||||
draw.text((60, y_solde + 22), solde_str, fill=self.COLORS["gold"], font=self.font_large)
|
||||
|
||||
y_bottom = self.HEIGHT - 100
|
||||
draw.text((60, y_bottom), "TITULAIRE", fill=self.COLORS["dim"], font=self.font_label)
|
||||
draw.text((60, y_bottom + 20), holder_name.upper(), fill=self.COLORS["white"], font=self.font_value)
|
||||
|
||||
if account_type == "entreprise" and tva_rate > 0:
|
||||
draw.text((self.WIDTH - 200, y_bottom), "TVA", fill=self.COLORS["dim"], font=self.font_label)
|
||||
draw.text((self.WIDTH - 200, y_bottom + 20), f"{tva_rate}%", fill=self.COLORS["accent"], font=self.font_value)
|
||||
|
||||
for i in range(5):
|
||||
x = self.WIDTH - 60 - i * 35
|
||||
draw.ellipse([x, self.HEIGHT - 50, x + 25, self.HEIGHT - 25], fill=self.COLORS["accent"] if i < 3 else self.COLORS["dim"])
|
||||
|
||||
buf = BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
buf.seek(0)
|
||||
return buf
|
||||
|
|
@ -212,7 +212,17 @@ class BankManager:
|
|||
raise ValueError("Compte destination introuvable.")
|
||||
|
||||
if source["solde"] < montant:
|
||||
raise ValueError(f"Solde insuffisant. Disponible : {source['solde']:.2f}€")
|
||||
cursor = await db.execute(
|
||||
"SELECT client_negative_floor FROM banks WHERE id = ? AND guild_id = ?",
|
||||
(source["bank_id"], guild_id),
|
||||
)
|
||||
bank_row = await cursor.fetchone()
|
||||
floor = bank_row[0] if bank_row else 0.0
|
||||
nouveau_solde = source["solde"] - montant
|
||||
if nouveau_solde < -floor:
|
||||
raise ValueError(
|
||||
f"Solde insuffisant. Disponible : {source['solde']:.2f}€, plafond négatif : {-floor:.2f}€"
|
||||
)
|
||||
|
||||
tva_montant = 0.0
|
||||
montant_net = montant
|
||||
|
|
@ -287,13 +297,26 @@ class BankManager:
|
|||
if not compte:
|
||||
raise ValueError("Compte introuvable.")
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT vault_balance FROM banks WHERE id = ? AND guild_id = ?",
|
||||
(compte["bank_id"], guild_id),
|
||||
)
|
||||
bank_row = await cursor.fetchone()
|
||||
if not bank_row:
|
||||
raise ValueError("Banque introuvable.")
|
||||
vault = bank_row[0]
|
||||
if vault < montant:
|
||||
raise ValueError(
|
||||
f"Réserve bancaire insuffisante. Disponible : {vault:.2f}€"
|
||||
)
|
||||
|
||||
cursor = await db.execute(
|
||||
"UPDATE accounts SET solde = solde + ? WHERE rib = ? AND guild_id = ?",
|
||||
(montant, rib, guild_id),
|
||||
)
|
||||
cursor = await db.execute(
|
||||
"UPDATE banks SET clients_total_balance = clients_total_balance + ? WHERE id = ? AND guild_id = ?",
|
||||
(montant, compte["bank_id"], guild_id),
|
||||
"UPDATE banks SET clients_total_balance = clients_total_balance + ?, vault_balance = vault_balance - ? WHERE id = ? AND guild_id = ?",
|
||||
(montant, montant, compte["bank_id"], guild_id),
|
||||
)
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
|
@ -318,16 +341,26 @@ class BankManager:
|
|||
compte = await self._get_account(db, guild_id, rib)
|
||||
if not compte:
|
||||
raise ValueError("Compte introuvable.")
|
||||
if compte["solde"] < montant:
|
||||
raise ValueError(f"Solde insuffisant. Disponible : {compte['solde']:.2f}€")
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT client_negative_floor FROM banks WHERE id = ? AND guild_id = ?",
|
||||
(compte["bank_id"], guild_id),
|
||||
)
|
||||
bank_row = await cursor.fetchone()
|
||||
floor = bank_row[0] if bank_row else 0.0
|
||||
nouveau_solde = compte["solde"] - montant
|
||||
if nouveau_solde < -floor:
|
||||
raise ValueError(
|
||||
f"Plafond négatif atteint ({-floor:.2f}€). Solde actuel : {compte['solde']:.2f}€"
|
||||
)
|
||||
|
||||
cursor = await db.execute(
|
||||
"UPDATE accounts SET solde = solde - ? WHERE rib = ? AND guild_id = ?",
|
||||
(montant, rib, guild_id),
|
||||
)
|
||||
cursor = await db.execute(
|
||||
"UPDATE banks SET clients_total_balance = clients_total_balance - ? WHERE id = ? AND guild_id = ?",
|
||||
(montant, compte["bank_id"], guild_id),
|
||||
"UPDATE banks SET clients_total_balance = clients_total_balance - ?, vault_balance = vault_balance + ? WHERE id = ? AND guild_id = ?",
|
||||
(montant, montant, compte["bank_id"], guild_id),
|
||||
)
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
|
@ -337,7 +370,7 @@ class BankManager:
|
|||
(guild_id, rib, compte["user_id"], montant, "retrait_banquier", now, json.dumps({"banquier_id": banquier_id})),
|
||||
)
|
||||
await db.commit()
|
||||
return {"nouveau_solde": compte["solde"] - montant}
|
||||
return {"nouveau_solde": nouveau_solde}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
|
@ -498,3 +531,27 @@ class BankManager:
|
|||
)
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def set_negative_floor(self, guild_id: int, bank_id: int, floor: float) -> bool:
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"UPDATE banks SET client_negative_floor = ? WHERE id = ? AND guild_id = ?",
|
||||
(floor, bank_id, guild_id),
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
async def set_vault_balance(self, guild_id: int, bank_id: int, montant: float) -> bool:
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"UPDATE banks SET vault_balance = ? WHERE id = ? AND guild_id = ?",
|
||||
(montant, bank_id, guild_id),
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
await db.close()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS banks (
|
|||
establishment_code INTEGER NOT NULL,
|
||||
vault_balance REAL DEFAULT 0.0,
|
||||
clients_total_balance REAL DEFAULT 0.0,
|
||||
client_negative_floor REAL DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(guild_id, name),
|
||||
UNIQUE(guild_id, establishment_code)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue