diff --git a/.env.example b/.env.example
index add48a5..d25e432 100644
--- a/.env.example
+++ b/.env.example
@@ -7,7 +7,7 @@
 # any secret values.
 
 BOT_TOKEN=
-BOT_VER=0.8.2
+BOT_VER=0.8.4
 BOT_AUTHOR=Vylpes
 BOT_OWNERID=147392775707426816
 BOT_CLIENTID=682942374040961060
@@ -34,8 +34,6 @@ DB_LOGGING=
 DB_DATA_LOCATION=./.temp/database
 DB_ROOT_HOST=0.0.0.0
 
-DB_CARD_FILE=:memory:
-
 EXPRESS_PORT=3302
 
-GDRIVESYNC_AUTO=true
+GDRIVESYNC_AUTO=false
diff --git a/.forgejo/workflows/production.yml b/.forgejo/workflows/production.yml
index d7b014b..cb4ed46 100644
--- a/.forgejo/workflows/production.yml
+++ b/.forgejo/workflows/production.yml
@@ -23,7 +23,7 @@ jobs:
     - run: yarn lint
 
     - name: "Copy files over to location"
-      run: cp -r . ${{ secrets.PROD_REPO_PATH }}
+      run: rsync -rvzP . ${{ secrets.PROD_REPO_PATH }}
 
   deploy:
     environment: prod
diff --git a/.forgejo/workflows/stage.yml b/.forgejo/workflows/stage.yml
index 60712ca..b6c27da 100644
--- a/.forgejo/workflows/stage.yml
+++ b/.forgejo/workflows/stage.yml
@@ -23,7 +23,7 @@ jobs:
     - run: yarn lint
 
     - name: "Copy files over to location"
-      run: cp -r . ${{ secrets.STAGE_REPO_PATH }}
+      run: rsync -rvzP . ${{ secrets.STAGE_REPO_PATH }}
 
   deploy:
     environment: prod
diff --git a/package.json b/package.json
index 0dc2630..137ca15 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "card-drop",
-  "version": "0.8.2",
+  "version": "0.8.4",
   "main": "./dist/bot.js",
   "typings": "./dist",
   "scripts": {
@@ -30,6 +30,7 @@
     "@types/express": "^5.0.0",
     "@types/jest": "^29.5.14",
     "@types/uuid": "^10.0.0",
+    "axios": "^1.8.4",
     "body-parser": "^1.20.2",
     "canvas": "^2.11.2",
     "clone-deep": "^4.0.1",
@@ -42,6 +43,7 @@
     "jest": "^29.0.0",
     "jest-mock-extended": "^3.0.0",
     "jimp": "^1.6.0",
+    "minimatch": "9.0.5",
     "mysql": "^2.18.1",
     "ts-jest": "^29.0.0",
     "typeorm": "0.3.20",
diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts
index 522ab40..d139f42 100644
--- a/src/buttonEvents/Claim.ts
+++ b/src/buttonEvents/Claim.ts
@@ -1,12 +1,12 @@
 import { ButtonInteraction } from "discord.js";
 import { ButtonEvent } from "../type/buttonEvent";
 import Inventory from "../database/entities/app/Inventory";
-import { CoreClient } from "../client/client";
 import { default as eClaim } from "../database/entities/app/Claim";
 import AppLogger from "../client/appLogger";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import User from "../database/entities/app/User";
 import CardConstants from "../constants/CardConstants";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
+import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper";
 
 export default class Claim extends ButtonEvent {
     public override async execute(interaction: ButtonInteraction) {
@@ -22,10 +22,10 @@ export default class Claim extends ButtonEvent {
         const userId = interaction.user.id;
 
         const whenDropped = interaction.message.createdAt;
-        const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago
+        const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago
 
         if (whenDropped < lastClaimableDate) {
-            await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 5 minutes of it being dropped!`);
+            await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 2 minutes of it being dropped!`);
             return;
         }
 
@@ -35,11 +35,6 @@ export default class Claim extends ButtonEvent {
 
         AppLogger.LogSilly("Button/Claim", `${user.Id} has ${user.Currency} currency`);
 
-        if (!user.RemoveCurrency(CardConstants.ClaimCost)) {
-            await interaction.channel.send(`${interaction.user}, Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`);
-            return;
-        }
-
         const claimed = await eClaim.FetchOneByClaimId(claimId);
 
         if (claimed) {
@@ -47,13 +42,6 @@ export default class Claim extends ButtonEvent {
             return;
         }
 
-        if (claimId == CoreClient.ClaimId && userId != droppedBy) {
-            await interaction.channel.send(`${interaction.user}, The latest dropped card can only be claimed by the user who dropped it!`);
-            return;
-        }
-
-        await user.Save(User, user);
-
         let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
 
         if (!inventory) {
@@ -69,16 +57,18 @@ export default class Claim extends ButtonEvent {
 
         await claim.Save(eClaim, claim);
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
+        const card = GetCardsHelper.GetCardByCardNumber(cardNumber);
 
         if (!card) {
+            AppLogger.LogError("Button/Claim", `Unable to find card, ${cardNumber}`);
+
             return;
         }
 
         const imageFileName = card.card.path.split("/").pop()!;
 
-        const embed = CardDropHelperMetadata.GenerateDropEmbed(card, inventory.Quantity, imageFileName, interaction.user.username, user.Currency);
-        const row = CardDropHelperMetadata.GenerateDropButtons(card, claimId, interaction.user.id, true);
+        const embed = DropEmbedHelper.GenerateDropEmbed(card, inventory.Quantity, imageFileName, interaction.user.username, user.Currency);
+        const row = DropEmbedHelper.GenerateDropButtons(card, claimId, interaction.user.id, true);
 
         await interaction.editReply({
             embeds: [ embed ],
diff --git a/src/buttonEvents/Effects.ts b/src/buttonEvents/Effects.ts
new file mode 100644
index 0000000..cd1a765
--- /dev/null
+++ b/src/buttonEvents/Effects.ts
@@ -0,0 +1,26 @@
+import { ButtonInteraction } from "discord.js";
+import { ButtonEvent } from "../type/buttonEvent";
+import List from "./Effects/List";
+import Use from "./Effects/Use";
+import AppLogger from "../client/appLogger";
+import Buy from "./Effects/Buy";
+
+export default class Effects extends ButtonEvent {
+    public override async execute(interaction: ButtonInteraction) {
+        const action = interaction.customId.split(" ")[1];
+
+        switch (action) {
+            case "list":
+                await List(interaction);
+                break;
+            case "use":
+                await Use.Execute(interaction);
+                break;
+            case "buy":
+                await Buy.Execute(interaction);
+                break;
+            default:
+                AppLogger.LogError("Buttons/Effects", `Unknown action, ${action}`);
+        }
+    }
+}
diff --git a/src/buttonEvents/Effects/Buy.ts b/src/buttonEvents/Effects/Buy.ts
new file mode 100644
index 0000000..49cc75f
--- /dev/null
+++ b/src/buttonEvents/Effects/Buy.ts
@@ -0,0 +1,120 @@
+import {ButtonInteraction} from "discord.js";
+import AppLogger from "../../client/appLogger";
+import EffectHelper from "../../helpers/EffectHelper";
+import EmbedColours from "../../constants/EmbedColours";
+import User from "../../database/entities/app/User";
+import {EffectDetails} from "../../constants/EffectDetails";
+
+export default class Buy {
+    public static async Execute(interaction: ButtonInteraction) {
+        const subaction = interaction.customId.split(" ")[2];
+
+        switch (subaction) {
+            case "confirm":
+                await this.Confirm(interaction);
+                break;
+            case "cancel":
+                await this.Cancel(interaction);
+                break;
+            default:
+                AppLogger.LogError("Buy", `Unknown subaction, effects ${subaction}`);
+        }
+    }
+
+    private static async Confirm(interaction: ButtonInteraction) {
+        const id = interaction.customId.split(" ")[3];
+        const quantity = interaction.customId.split(" ")[4];
+
+        if (!id || !quantity) {
+            AppLogger.LogError("Buy Confirm", "Not enough parameters");
+            return;
+        }
+        
+        const effectDetail = EffectDetails.get(id);
+
+        if (!effectDetail) {
+            AppLogger.LogError("Buy Confirm", "Effect detail not found!");
+            return;
+        }
+
+        const quantityNumber = Number(quantity);
+        
+        if (!quantityNumber || quantityNumber < 1) {
+            AppLogger.LogError("Buy Confirm", "Invalid number");
+            return;
+        }
+
+        const totalCost = effectDetail.cost * quantityNumber;
+
+        const user = await User.FetchOneById(User, interaction.user.id);
+
+        if (!user) {
+            AppLogger.LogError("Buy Confirm", "Unable to find user");
+            return;
+        }
+
+        if (user.Currency < totalCost) {
+            interaction.reply(`You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`);
+            return;
+        }
+
+        user.RemoveCurrency(totalCost);
+        await user.Save(User, user);
+
+        await EffectHelper.AddEffectToUserInventory(interaction.user.id, id, quantityNumber);
+
+        const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true);
+
+        if (typeof generatedEmbed == "string") {
+            await interaction.reply(generatedEmbed);
+            return;
+        }
+
+        generatedEmbed.embed.setColor(EmbedColours.Success);
+        generatedEmbed.embed.setFooter({ text: "Purchased" });
+
+        await interaction.update({
+            embeds: [ generatedEmbed.embed ],
+            components: [ generatedEmbed.row ],
+        });
+    }
+
+    private static async Cancel(interaction: ButtonInteraction) {
+        const id = interaction.customId.split(" ")[3];
+        const quantity = interaction.customId.split(" ")[4];
+
+        if (!id || !quantity) {
+            AppLogger.LogError("Buy Cancel", "Not enough parameters");
+            return;
+        }
+        
+        const effectDetail = EffectDetails.get(id);
+
+        if (!effectDetail) {
+            AppLogger.LogError("Buy Cancel", "Effect detail not found!");
+            return;
+        }
+
+        const quantityNumber = Number(quantity);
+        
+        if (!quantityNumber || quantityNumber < 1) {
+            AppLogger.LogError("Buy Cancel", "Invalid number");
+            return;
+        }
+
+        const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true);
+
+        if (typeof generatedEmbed == "string") {
+            await interaction.reply(generatedEmbed);
+            return;
+        }
+
+        generatedEmbed.embed.setColor(EmbedColours.Error);
+        generatedEmbed.embed.setFooter({ text: "Cancelled" });
+
+        await interaction.update({
+            embeds: [ generatedEmbed.embed ],
+            components: [ generatedEmbed.row ],
+        });
+    }
+}
diff --git a/src/buttonEvents/Effects/List.ts b/src/buttonEvents/Effects/List.ts
new file mode 100644
index 0000000..d86dfce
--- /dev/null
+++ b/src/buttonEvents/Effects/List.ts
@@ -0,0 +1,20 @@
+import { ButtonInteraction } from "discord.js";
+import EffectHelper from "../../helpers/EffectHelper";
+
+export default async function List(interaction: ButtonInteraction) {
+    const pageOption = interaction.customId.split(" ")[2];
+
+    const page = Number(pageOption);
+
+    if (!page) {
+        await interaction.reply("Page option is not a valid number");
+        return;
+    }
+
+    const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page);
+
+    await interaction.update({
+        embeds: [ result.embed ],
+        components: [ result.row ],
+    });
+}
\ No newline at end of file
diff --git a/src/buttonEvents/Effects/Use.ts b/src/buttonEvents/Effects/Use.ts
new file mode 100644
index 0000000..5bfe2fd
--- /dev/null
+++ b/src/buttonEvents/Effects/Use.ts
@@ -0,0 +1,132 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js";
+import { EffectDetails } from "../../constants/EffectDetails";
+import EffectHelper from "../../helpers/EffectHelper";
+import EmbedColours from "../../constants/EmbedColours";
+import TimeLengthInput from "../../helpers/TimeLengthInput";
+import AppLogger from "../../client/appLogger";
+
+export default class Use {
+    public static async Execute(interaction: ButtonInteraction) {
+        const subaction = interaction.customId.split(" ")[2];
+
+        switch (subaction) {
+            case "confirm":
+                await this.UseConfirm(interaction);
+                break;
+            case "cancel":
+                await this.UseCancel(interaction);
+                break;
+        }
+    }
+
+    private static async UseConfirm(interaction: ButtonInteraction) {
+        const id = interaction.customId.split(" ")[3];
+
+        const effectDetail = EffectDetails.get(id);
+
+        if (!effectDetail) {
+            AppLogger.LogError("Button/Effects/Use", `Effect not found, ${id}`);
+
+            await interaction.reply("Effect not found in system!");
+            return;
+        }
+
+        const now = new Date();
+
+        const whenExpires = new Date(now.getTime() + effectDetail.duration);
+
+        const result = await EffectHelper.UseEffect(interaction.user.id, id, whenExpires);
+
+        if (!result) {
+            await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
+            return;
+        }
+
+        const embed = new EmbedBuilder()
+            .setTitle("Effect Used")
+            .setDescription("You now have an active effect!")
+            .setColor(EmbedColours.Green)
+            .addFields([
+                {
+                    name: "Effect",
+                    value: effectDetail.friendlyName,
+                    inline: true,
+                },
+                {
+                    name: "Expires",
+                    value: `<t:${Math.round(whenExpires.getTime() / 1000)}:f>`,
+                    inline: true,
+                },
+            ]);
+
+        const row = new ActionRowBuilder<ButtonBuilder>()
+            .addComponents([
+                new ButtonBuilder()
+                    .setLabel("Confirm")
+                    .setCustomId(`effects use confirm ${effectDetail.id}`)
+                    .setStyle(ButtonStyle.Primary)
+                    .setDisabled(true),
+                new ButtonBuilder()
+                    .setLabel("Cancel")
+                    .setCustomId(`effects use cancel ${effectDetail.id}`)
+                    .setStyle(ButtonStyle.Danger)
+                    .setDisabled(true),
+            ]);
+
+        await interaction.update({
+            embeds: [ embed ],
+            components: [ row ],
+        });
+    }
+
+    private static async UseCancel(interaction: ButtonInteraction) {
+        const id = interaction.customId.split(" ")[3];
+
+        const effectDetail = EffectDetails.get(id);
+
+        if (!effectDetail) {
+            AppLogger.LogError("Button/Effects/Cancel", `Effect not found, ${id}`);
+
+            await interaction.reply("Effect not found in system!");
+            return;
+        }
+
+        const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
+
+        const embed = new EmbedBuilder()
+            .setTitle("Effect Use Cancelled")
+            .setDescription("The effect from your inventory has not been used")
+            .setColor(EmbedColours.Grey)
+            .addFields([
+                {
+                    name: "Effect",
+                    value: effectDetail.friendlyName,
+                    inline: true,
+                },
+                {
+                    name: "Expires",
+                    value: timeLengthInput.GetLengthShort(),
+                    inline: true,
+                },
+            ]);
+
+        const row = new ActionRowBuilder<ButtonBuilder>()
+            .addComponents([
+                new ButtonBuilder()
+                    .setLabel("Confirm")
+                    .setCustomId(`effects use confirm ${effectDetail.id}`)
+                    .setStyle(ButtonStyle.Primary)
+                    .setDisabled(true),
+                new ButtonBuilder()
+                    .setLabel("Cancel")
+                    .setCustomId(`effects use cancel ${effectDetail.id}`)
+                    .setStyle(ButtonStyle.Danger)
+                    .setDisabled(true),
+            ]);
+
+        await interaction.update({
+            embeds: [ embed ],
+            components: [ row ],
+        });
+    }
+}
diff --git a/src/buttonEvents/Multidrop.ts b/src/buttonEvents/Multidrop.ts
index 604f02c..e6ea7c2 100644
--- a/src/buttonEvents/Multidrop.ts
+++ b/src/buttonEvents/Multidrop.ts
@@ -1,7 +1,6 @@
 import { AttachmentBuilder, ButtonInteraction, EmbedBuilder } from "discord.js";
 import { ButtonEvent } from "../type/buttonEvent";
 import AppLogger from "../client/appLogger";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import Inventory from "../database/entities/app/Inventory";
 import EmbedColours from "../constants/EmbedColours";
 import { readFileSync } from "fs";
@@ -9,6 +8,8 @@ import path from "path";
 import ErrorMessages from "../constants/ErrorMessages";
 import User from "../database/entities/app/User";
 import { GetSacrificeAmount } from "../constants/CardRarity";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
+import MultidropEmbedHelper from "../helpers/DropHelpers/MultidropEmbedHelper";
 
 export default class Multidrop extends ButtonEvent {
     public override async execute(interaction: ButtonInteraction) {
@@ -37,7 +38,7 @@ export default class Multidrop extends ButtonEvent {
             return;
         }
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
+        const card = GetCardsHelper.GetCardByCardNumber(cardNumber);
 
         if (!card) {
             await interaction.reply("Unable to find card.");
@@ -85,7 +86,7 @@ export default class Multidrop extends ButtonEvent {
         }
 
         // Drop next card
-        const randomCard = CardDropHelperMetadata.GetRandomCard();
+        const randomCard = GetCardsHelper.GetRandomCard();
         cardsRemaining -= 1;
 
         if (!randomCard) {
@@ -105,9 +106,9 @@ export default class Multidrop extends ButtonEvent {
             const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
             const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-            const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
+            const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
 
-            const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
+            const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
 
             await interaction.editReply({
                 embeds: [ embed ],
@@ -131,7 +132,7 @@ export default class Multidrop extends ButtonEvent {
             return;
         }
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
+        const card = GetCardsHelper.GetCardByCardNumber(cardNumber);
 
         if (!card) {
             await interaction.reply("Unable to find card.");
@@ -175,7 +176,7 @@ export default class Multidrop extends ButtonEvent {
         }
 
         // Drop next card
-        const randomCard = CardDropHelperMetadata.GetRandomCard();
+        const randomCard = GetCardsHelper.GetRandomCard();
         cardsRemaining -= 1;
 
         if (!randomCard) {
@@ -195,9 +196,9 @@ export default class Multidrop extends ButtonEvent {
             const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
             const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-            const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
+            const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
 
-            const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
+            const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
 
             await interaction.editReply({
                 embeds: [ embed ],
diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts
index 12578db..6d97024 100644
--- a/src/buttonEvents/Reroll.ts
+++ b/src/buttonEvents/Reroll.ts
@@ -5,11 +5,12 @@ import { v4 } from "uuid";
 import { CoreClient } from "../client/client";
 import Inventory from "../database/entities/app/Inventory";
 import Config from "../database/entities/app/Config";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import path from "path";
 import AppLogger from "../client/appLogger";
 import User from "../database/entities/app/User";
 import CardConstants from "../constants/CardConstants";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
+import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper";
 
 export default class Reroll extends ButtonEvent {
     public override async execute(interaction: ButtonInteraction) {
@@ -34,12 +35,14 @@ export default class Reroll extends ButtonEvent {
             AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`);
         }
 
-        if (user.Currency < CardConstants.ClaimCost) {
+        if (!user.RemoveCurrency(CardConstants.ClaimCost)) {
             await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`);
             return;
         }
 
-        const randomCard = CardDropHelperMetadata.GetRandomCard();
+        await user.Save(User, user);
+
+        const randomCard = await GetCardsHelper.FetchCard(interaction.user.id);
 
         if (!randomCard) {
             await interaction.reply("Unable to fetch card, please try again.");
@@ -51,27 +54,32 @@ export default class Reroll extends ButtonEvent {
         try {
             AppLogger.LogVerbose("Button/Reroll", `Sending next drop: ${randomCard.card.id} (${randomCard.card.name})`);
 
-            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
-            const imageFileName = randomCard.card.path.split("/").pop()!;
+            const files = [];
+            let imageFileName = "";
 
-            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+            if (!(randomCard.card.path.startsWith("http://") || randomCard.card.path.startsWith("https://"))) {
+                const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
+                imageFileName = randomCard.card.path.split("/").pop()!;
+
+                const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+                files.push(attachment);
+            }
 
             const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
             const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-            const embed = CardDropHelperMetadata.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency);
+            const embed = DropEmbedHelper.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency);
 
             const claimId = v4();
 
-            const row = CardDropHelperMetadata.GenerateDropButtons(randomCard, claimId, interaction.user.id);
+            const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id);
 
             await interaction.editReply({
                 embeds: [ embed ],
-                files: [ attachment ],
+                files: files,
                 components: [ row ],
             });
-
-            CoreClient.ClaimId = claimId;
         } catch (e) {
             AppLogger.LogError("Button/Reroll", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
 
diff --git a/src/buttonEvents/Sacrifice.ts b/src/buttonEvents/Sacrifice.ts
index 463ca1f..de0bb77 100644
--- a/src/buttonEvents/Sacrifice.ts
+++ b/src/buttonEvents/Sacrifice.ts
@@ -1,10 +1,10 @@
 import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js";
 import { ButtonEvent } from "../type/buttonEvent";
 import Inventory from "../database/entities/app/Inventory";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import { CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity";
 import EmbedColours from "../constants/EmbedColours";
 import User from "../database/entities/app/User";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
 
 export default class Sacrifice extends ButtonEvent {
     public override async execute(interaction: ButtonInteraction) {
@@ -42,7 +42,7 @@ export default class Sacrifice extends ButtonEvent {
             return;
         }
 
-        const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
+        const cardData = GetCardsHelper.GetCardByCardNumber(cardNumber);
 
         if (!cardData) {
             await interaction.reply("Unable to find card in the database.");
@@ -124,7 +124,7 @@ export default class Sacrifice extends ButtonEvent {
             return;
         }
 
-        const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
+        const cardData = GetCardsHelper.GetCardByCardNumber(cardNumber);
 
         if (!cardData) {
             await interaction.reply("Unable to find card in the database.");
diff --git a/src/buttonEvents/View.ts b/src/buttonEvents/View.ts
index f79a1af..007a911 100644
--- a/src/buttonEvents/View.ts
+++ b/src/buttonEvents/View.ts
@@ -19,7 +19,7 @@ export default class View extends ButtonEvent {
         await interaction.editReply({
             embeds: [ searchResult.embed ],
             components: [ searchResult.row ],
-            files: [ searchResult.attachment ],
+            files: searchResult.attachments,
         });
     }
 }
diff --git a/src/client/appLogger.ts b/src/client/appLogger.ts
index f1ba1dc..6c33609 100644
--- a/src/client/appLogger.ts
+++ b/src/client/appLogger.ts
@@ -86,4 +86,12 @@ export default class AppLogger {
     public static LogSilly(label: string, message: string) {
         AppLogger.Logger.silly({ label, message });
     }
+
+    public static CatchError(label: string, error: unknown) {
+        if (error instanceof Error) {
+            AppLogger.Logger.error({ label, message: error.message });
+        } else {
+            AppLogger.Logger.error({ label, message: error });
+        }
+    }
 }
diff --git a/src/client/client.ts b/src/client/client.ts
index 87b496e..57551fb 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -31,7 +31,6 @@ export class CoreClient extends Client {
     private _webhooks: Webhooks;
     private _timerHelper: TimerHelper;
 
-    public static ClaimId: string;
     public static Environment: Environment;
     public static AllowDrops: boolean;
     public static Cards: SeriesMetadata[];
diff --git a/src/commands/drop.ts b/src/commands/drop.ts
index 6799a2c..ac8008c 100644
--- a/src/commands/drop.ts
+++ b/src/commands/drop.ts
@@ -5,12 +5,13 @@ import { CoreClient } from "../client/client";
 import { v4 } from "uuid";
 import Inventory from "../database/entities/app/Inventory";
 import Config from "../database/entities/app/Config";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import path from "path";
 import AppLogger from "../client/appLogger";
 import User from "../database/entities/app/User";
 import CardConstants from "../constants/CardConstants";
 import ErrorMessages from "../constants/ErrorMessages";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
+import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper";
 
 export default class Drop extends Command {
     constructor() {
@@ -42,12 +43,14 @@ export default class Drop extends Command {
             AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`);
         }
 
-        if (user.Currency < CardConstants.ClaimCost) {
+        if (!user.RemoveCurrency(CardConstants.ClaimCost)) {
             await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency));
             return;
         }
 
-        const randomCard = CardDropHelperMetadata.GetRandomCard();
+        await user.Save(User, user);
+
+        const randomCard = await GetCardsHelper.FetchCard(interaction.user.id);
 
         if (!randomCard) {
             AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard);
@@ -58,28 +61,33 @@ export default class Drop extends Command {
         await interaction.deferReply();
 
         try {
-            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
-            const imageFileName = randomCard.card.path.split("/").pop()!;
+            const files = [];
+            let imageFileName = "";
 
-            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+            if (!(randomCard.card.path.startsWith("http://") || randomCard.card.path.startsWith("https://"))) {
+                const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
+                imageFileName = randomCard.card.path.split("/").pop()!;
+
+                const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+                files.push(attachment);
+            }
 
             const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
             const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-            const embed = CardDropHelperMetadata.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency);
+            const embed = DropEmbedHelper.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency);
 
             const claimId = v4();
 
-            const row = CardDropHelperMetadata.GenerateDropButtons(randomCard, claimId, interaction.user.id);
+            const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id);
 
             await interaction.editReply({
                 embeds: [ embed ],
-                files: [ attachment ],
+                files: files,
                 components: [ row ],
             });
 
-            CoreClient.ClaimId = claimId;
-
         } catch (e) {
             AppLogger.LogError("Commands/Drop", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
 
diff --git a/src/commands/effects.ts b/src/commands/effects.ts
new file mode 100644
index 0000000..cd6d1d4
--- /dev/null
+++ b/src/commands/effects.ts
@@ -0,0 +1,64 @@
+import { CommandInteraction, SlashCommandBuilder } from "discord.js";
+import { Command } from "../type/command";
+import { EffectChoices } from "../constants/EffectDetails";
+import AppLogger from "../client/appLogger";
+import List from "./effects/List";
+import Use from "./effects/Use";
+import Buy from "./effects/Buy";
+
+export default class Effects extends Command {
+    constructor() {
+        super();
+
+        this.CommandBuilder = new SlashCommandBuilder()
+            .setName("effects")
+            .setDescription("Effects")
+            .addSubcommand(x => x
+                .setName("list")
+                .setDescription("List all effects I have")
+                .addNumberOption(x => x
+                    .setName("page")
+                    .setDescription("The page number")
+                    .setMinValue(1)))
+            .addSubcommand(x => x
+                .setName("use")
+                .setDescription("Use an effect in your inventory")
+                .addStringOption(y => y
+                    .setName("id")
+                    .setDescription("The effect id to use")
+                    .setRequired(true)
+                    .setChoices(EffectChoices)))
+            .addSubcommand(x => x
+                .setName("buy")
+                .setDescription("Buy more effects")
+                .addStringOption(y => y
+                    .setName("id")
+                    .setDescription("The effect id to buy")
+                    .setRequired(true)
+                    .setChoices(EffectChoices))
+                .addNumberOption(y => y
+                    .setName("quantity")
+                    .setDescription("The amount to buy")
+                    .setMinValue(1)));
+    }
+
+    public override async execute(interaction: CommandInteraction) {
+        if (!interaction.isChatInputCommand()) return;
+
+        const subcommand = interaction.options.getSubcommand();
+
+        switch (subcommand) {
+            case "list":
+                await List(interaction);
+                break;
+            case "use":
+                await Use(interaction);
+                break;
+            case "buy":
+                await Buy(interaction);
+                break;
+            default:
+                AppLogger.LogError("Commands/Effects", `Invalid subcommand: ${subcommand}`);
+        }
+    }
+}
diff --git a/src/commands/effects/Buy.ts b/src/commands/effects/Buy.ts
new file mode 100644
index 0000000..3ebf587
--- /dev/null
+++ b/src/commands/effects/Buy.ts
@@ -0,0 +1,22 @@
+import { CommandInteraction } from "discord.js";
+import EffectHelper from "../../helpers/EffectHelper";
+
+export default async function Buy(interaction: CommandInteraction) {
+    const id = interaction.options.get("id", true).value!;
+    const quantity = interaction.options.get("quantity")?.value ?? 1;
+
+    const idValue = id.toString();
+    const quantityValue = Number(quantity);
+
+    const result = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, idValue, quantityValue, false);
+
+    if (typeof result == "string") {
+        await interaction.reply(result);
+        return;
+    }
+
+    await interaction.reply({
+        embeds: [ result.embed ],
+        components: [ result.row ],
+    });
+}
\ No newline at end of file
diff --git a/src/commands/effects/List.ts b/src/commands/effects/List.ts
new file mode 100644
index 0000000..14e6085
--- /dev/null
+++ b/src/commands/effects/List.ts
@@ -0,0 +1,15 @@
+import { CommandInteraction } from "discord.js";
+import EffectHelper from "../../helpers/EffectHelper";
+
+export default async function List(interaction: CommandInteraction) {
+    const pageOption = interaction.options.get("page");
+
+    const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1;
+
+    const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page);
+
+    await interaction.reply({
+        embeds: [ result.embed ],
+        components: [ result.row ],
+    });
+}
\ No newline at end of file
diff --git a/src/commands/effects/Use.ts b/src/commands/effects/Use.ts
new file mode 100644
index 0000000..9f72ae0
--- /dev/null
+++ b/src/commands/effects/Use.ts
@@ -0,0 +1,62 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder } from "discord.js";
+import { EffectDetails } from "../../constants/EffectDetails";
+import AppLogger from "../../client/appLogger";
+import EffectHelper from "../../helpers/EffectHelper";
+import TimeLengthInput from "../../helpers/TimeLengthInput";
+import EmbedColours from "../../constants/EmbedColours";
+
+export default async function Use(interaction: CommandInteraction) {
+    const id = interaction.options.get("id", true).value!.toString();
+
+    const effectDetail = EffectDetails.get(id);
+
+    if (!effectDetail) {
+        AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`);
+
+        await interaction.reply("Unable to find effect!");
+        return;
+    }
+
+    const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id);
+
+    if (!canUseEffect) {
+        await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
+        return;
+    }
+
+    const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
+
+    const embed = new EmbedBuilder()
+        .setTitle("Effect Confirmation")
+        .setDescription("Would you like to use this effect?")
+        .setColor(EmbedColours.Ok)
+        .addFields([
+            {
+                name: "Effect",
+                value: effectDetail.friendlyName,
+                inline: true,
+            },
+            {
+                name: "Length",
+                value: timeLengthInput.GetLengthShort(),
+                inline: true,
+            },
+        ]);
+
+    const row = new ActionRowBuilder<ButtonBuilder>()
+        .addComponents([
+            new ButtonBuilder()
+                .setLabel("Confirm")
+                .setCustomId(`effects use confirm ${effectDetail.id}`)
+                .setStyle(ButtonStyle.Primary),
+            new ButtonBuilder()
+                .setLabel("Cancel")
+                .setCustomId(`effects use cancel ${effectDetail.id}`)
+                .setStyle(ButtonStyle.Danger),
+        ]);
+
+    await interaction.reply({
+        embeds: [ embed ],
+        components: [ row ],
+    });
+}
\ No newline at end of file
diff --git a/src/commands/give.ts b/src/commands/give.ts
index 3ffbe8f..35dfa04 100644
--- a/src/commands/give.ts
+++ b/src/commands/give.ts
@@ -2,10 +2,10 @@ import { CacheType, CommandInteraction, PermissionsBitField, SlashCommandBuilder
 import { Command } from "../type/command";
 import { CoreClient } from "../client/client";
 import Config from "../database/entities/app/Config";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import Inventory from "../database/entities/app/Inventory";
 import AppLogger from "../client/appLogger";
 import User from "../database/entities/app/User";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
 
 export default class Give extends Command {
     constructor() {
@@ -81,7 +81,7 @@ export default class Give extends Command {
 
         AppLogger.LogSilly("Commands/Give/GiveCard", `Parameters: cardNumber=${cardNumber.value}, user=${user.id}`);
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber.value!.toString());
+        const card = GetCardsHelper.GetCardByCardNumber(cardNumber.value!.toString());
 
         if (!card) {
             await interaction.reply("Unable to fetch card, please try again.");
diff --git a/src/commands/id.ts b/src/commands/id.ts
index ae924a6..0f11aaa 100644
--- a/src/commands/id.ts
+++ b/src/commands/id.ts
@@ -4,8 +4,8 @@ import { CoreClient } from "../client/client";
 import { readFileSync } from "fs";
 import path from "path";
 import Inventory from "../database/entities/app/Inventory";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import AppLogger from "../client/appLogger";
+import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper";
 
 export default class Id extends Command {
     constructor() {
@@ -43,31 +43,29 @@ export default class Id extends Command {
         const series = CoreClient.Cards
             .find(x => x.cards.includes(card))!;
 
-        let image: Buffer;
-        const imageFileName = card.path.split("/").pop()!;
+        const files = [];
+        let imageFileName = "";
 
-        try {
-            image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
-        } catch {
-            AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
+        if (!(card.path.startsWith("http://") || card.path.startsWith("https://"))) {
+            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
+            imageFileName = card.path.split("/").pop()!;
 
-            await interaction.reply(`Unable to fetch image for card ${card.id}.`);
-            return;
+            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+            files.push(attachment);
         }
 
         await interaction.deferReply();
 
-        const attachment = new AttachmentBuilder(image, { name: imageFileName });
-
         const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
         const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-        const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
+        const embed = DropEmbedHelper.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
 
         try {
             await interaction.editReply({
                 embeds: [ embed ],
-                files: [ attachment ],
+                files: files,
             });
         } catch (e) {
             AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`);
diff --git a/src/commands/multidrop.ts b/src/commands/multidrop.ts
index f35b921..aa42686 100644
--- a/src/commands/multidrop.ts
+++ b/src/commands/multidrop.ts
@@ -6,10 +6,11 @@ import Config from "../database/entities/app/Config";
 import AppLogger from "../client/appLogger";
 import User from "../database/entities/app/User";
 import CardConstants from "../constants/CardConstants";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import { readFileSync } from "fs";
 import path from "path";
 import Inventory from "../database/entities/app/Inventory";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
+import MultidropEmbedHelper from "../helpers/DropHelpers/MultidropEmbedHelper";
 
 export default class Multidrop extends Command {
     constructor() {
@@ -49,7 +50,7 @@ export default class Multidrop extends Command {
         user.RemoveCurrency(CardConstants.MultidropCost);
         await user.Save(User, user);
 
-        const randomCard = CardDropHelperMetadata.GetRandomCard();
+        const randomCard = GetCardsHelper.GetRandomCard();
         const cardsRemaining = CardConstants.MultidropQuantity - 1;
 
         if (!randomCard) {
@@ -69,9 +70,9 @@ export default class Multidrop extends Command {
             const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
             const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-            const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
+            const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
 
-            const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id);
+            const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id);
 
             await interaction.editReply({
                 embeds: [ embed ],
diff --git a/src/commands/sacrifice.ts b/src/commands/sacrifice.ts
index 9683714..a44a69e 100644
--- a/src/commands/sacrifice.ts
+++ b/src/commands/sacrifice.ts
@@ -2,8 +2,8 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, CommandInterac
 import { Command } from "../type/command";
 import Inventory from "../database/entities/app/Inventory";
 import { CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity";
-import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
 import EmbedColours from "../constants/EmbedColours";
+import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper";
 
 export default class Sacrifice extends Command {
     constructor() {
@@ -41,7 +41,7 @@ export default class Sacrifice extends Command {
             return;
         }
 
-        const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string);
+        const cardData = GetCardsHelper.GetCardByCardNumber(cardnumber.value! as string);
 
         if (!cardData) {
             await interaction.reply("Unable to find card in the database.");
diff --git a/src/commands/series.ts b/src/commands/series.ts
index e268db9..16ae0db 100644
--- a/src/commands/series.ts
+++ b/src/commands/series.ts
@@ -60,13 +60,18 @@ export default class Series extends Command {
             return;
         }
 
-        const embed = await SeriesHelper.GenerateSeriesViewPage(series.id, 0, interaction.user.id);
+        try {
+            const embed = await SeriesHelper.GenerateSeriesViewPage(series.id, 0, interaction.user.id);
 
-        await interaction.followUp({
-            embeds: [ embed!.embed ],
-            components: [ embed!.row ],
-            files: [ embed!.image ],
-        });
+            await interaction.followUp({
+                embeds: [ embed!.embed ],
+                components: [ embed!.row ],
+                files: [ embed!.image ],
+            });
+        } catch (e) {
+            await interaction.followUp("An error has occured generating the series grid.");
+            AppLogger.CatchError("Series", e);
+        }
     }
 
     private async ListSeries(interaction: CommandInteraction) {
diff --git a/src/commands/stage/dropnumber.ts b/src/commands/stage/dropnumber.ts
index 0642327..c61ed43 100644
--- a/src/commands/stage/dropnumber.ts
+++ b/src/commands/stage/dropnumber.ts
@@ -1,11 +1,12 @@
-import { AttachmentBuilder, CacheType, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
+import { AttachmentBuilder, CacheType, CommandInteraction, SlashCommandBuilder } from "discord.js";
 import { Command } from "../../type/command";
 import { readFileSync } from "fs";
 import Inventory from "../../database/entities/app/Inventory";
 import { v4 } from "uuid";
 import { CoreClient } from "../../client/client";
 import path from "path";
-import CardDropHelperMetadata from "../../helpers/CardDropHelperMetadata";
+import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper";
+import AppLogger from "../../client/appLogger";
 
 export default class Dropnumber extends Command {
     constructor() {
@@ -40,48 +41,40 @@ export default class Dropnumber extends Command {
             return;
         }
 
-        const series = CoreClient.Cards
-            .find(x => x.cards.includes(card))!;
-
-        let image: Buffer;
-        const imageFileName = card.path.split("/").pop()!;
-
-        try {
-            image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
-        } catch {
-            await interaction.reply(`Unable to fetch image for card ${card.id}`);
-            return;
-        }
-
+        const claimId = v4();
         await interaction.deferReply();
 
-        const attachment = new AttachmentBuilder(image, { name: imageFileName });
+    try {
+        const files = [];
+        let imageFileName = "";
+
+        if (!(card.path.startsWith("http://") || card.path.startsWith("https://"))) {
+            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
+            imageFileName = card.path.split("/").pop()!;
+
+            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+            files.push(attachment);
+        }
+
+        const series = CoreClient.Cards
+            .find(x => x.cards.includes(card))!;
 
         const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
         const quantityClaimed = inventory ? inventory.Quantity : 0;
 
-        const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
+        const embed = DropEmbedHelper.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
 
-        const claimId = v4();
+        const row = DropEmbedHelper.GenerateDropButtons({ card, series }, claimId, interaction.user.id);
 
-        const row = CardDropHelperMetadata.GenerateDropButtons({ card, series }, claimId, interaction.user.id);
-
-        try {
-            await interaction.editReply({
-                embeds: [ embed ],
-                files: [ attachment ],
-                components: [ row ],
-            });
+        await interaction.editReply({
+            embeds: [ embed ],
+            files: files,
+            components: [ row ],
+        });
         } catch (e) {
-            console.error(e);
-
-            if (e instanceof DiscordAPIError) {
-                await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}`);
-            } else {
-                await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN");
-            }
+            AppLogger.CatchError("Dropnumber", e);
+            await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening");
         }
-
-        CoreClient.ClaimId = claimId;
     }
 }
\ No newline at end of file
diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts
index be0a62d..f776930 100644
--- a/src/commands/stage/droprarity.ts
+++ b/src/commands/stage/droprarity.ts
@@ -1,12 +1,13 @@
-import { AttachmentBuilder, CacheType, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
+import { AttachmentBuilder, CacheType, CommandInteraction, SlashCommandBuilder } from "discord.js";
 import { Command } from "../../type/command";
-import { CardRarity, CardRarityParse } from "../../constants/CardRarity";
+import { CardRarity, CardRarityChoices, CardRarityParse } from "../../constants/CardRarity";
 import { readFileSync } from "fs";
 import Inventory from "../../database/entities/app/Inventory";
 import { v4 } from "uuid";
-import { CoreClient } from "../../client/client";
-import CardDropHelperMetadata from "../../helpers/CardDropHelperMetadata";
 import path from "path";
+import GetCardsHelper from "../../helpers/DropHelpers/GetCardsHelper";
+import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper";
+import AppLogger from "../../client/appLogger";
 
 export default class Droprarity extends Command {
     constructor() {
@@ -19,7 +20,8 @@ export default class Droprarity extends Command {
                 x
                     .setName("rarity")
                     .setDescription("The rarity you want to summon")
-                    .setRequired(true));
+                    .setRequired(true)
+                    .setChoices(CardRarityChoices));
     }
 
     public override async execute(interaction: CommandInteraction<CacheType>) {
@@ -39,52 +41,44 @@ export default class Droprarity extends Command {
             return;
         }
 
-        const card = await CardDropHelperMetadata.GetRandomCardByRarity(rarityType);
+        const card = GetCardsHelper.GetRandomCardByRarity(rarityType);
 
         if (!card) {
             await interaction.reply("Card not found");
             return;
         }
 
-        let image: Buffer;
-        const imageFileName = card.card.path.split("/").pop()!;
-
-        try {
-            image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
-        } catch {
-            await interaction.reply(`Unable to fetch image for card ${card.card.id}`);
-            return;
-        }
-
+        const claimId = v4();
         await interaction.deferReply();
 
-        const attachment = new AttachmentBuilder(image, { name: imageFileName });
-
-        const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.card.id);
-        const quantityClaimed = inventory ? inventory.Quantity : 0;
-
-        const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
-
-        const claimId = v4();
-
-        const row = CardDropHelperMetadata.GenerateDropButtons(card, claimId, interaction.user.id);
-
         try {
+            const files = [];
+            let imageFileName = "";
+
+            if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) {
+                const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
+                imageFileName = card.card.path.split("/").pop()!;
+
+                const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+                files.push(attachment);
+            }
+
+            const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.card.id);
+            const quantityClaimed = inventory ? inventory.Quantity : 0;
+
+            const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName);
+
+            const row = DropEmbedHelper.GenerateDropButtons(card, claimId, interaction.user.id);
+
             await interaction.editReply({
                 embeds: [ embed ],
-                files: [ attachment ],
+                files: files,
                 components: [ row ],
             });
         } catch (e) {
-            console.error(e);
-
-            if (e instanceof DiscordAPIError) {
-                await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}`);
-            } else {
-                await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN");
-            }
+            AppLogger.CatchError("Droprarity", e);
+            await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening");
         }
-
-        CoreClient.ClaimId = claimId;
     }
 }
\ No newline at end of file
diff --git a/src/commands/view.ts b/src/commands/view.ts
index 3ba5621..9a1d447 100644
--- a/src/commands/view.ts
+++ b/src/commands/view.ts
@@ -34,7 +34,7 @@ export default class View extends Command {
         await interaction.editReply({
             embeds: [ searchResult.embed ],
             components: [ searchResult.row ],
-            files: [ searchResult.attachment ],
+            files: searchResult.attachments,
         });
     }
 }
\ No newline at end of file
diff --git a/src/constants/CardConstants.ts b/src/constants/CardConstants.ts
index 3f9723b..60a7f59 100644
--- a/src/constants/CardConstants.ts
+++ b/src/constants/CardConstants.ts
@@ -7,4 +7,7 @@ export default class CardConstants {
     // Multidrop
     public static readonly MultidropCost = this.ClaimCost * 10;
     public static readonly MultidropQuantity = 11;
+
+    // Effects
+    public static readonly UnusedChanceUpChance = 0.5;
 }
\ No newline at end of file
diff --git a/src/constants/CardRarity.ts b/src/constants/CardRarity.ts
index 8817172..0fcdec7 100644
--- a/src/constants/CardRarity.ts
+++ b/src/constants/CardRarity.ts
@@ -9,6 +9,29 @@ export enum CardRarity {
     Legendary,
 }
 
+export const CardRarityChoices = [
+    {
+        name: "Bronze",
+        value: "bronze",
+    },
+    {
+        name: "Silver",
+        value: "silver",
+    },
+    {
+        name: "Gold",
+        value: "gold",
+    },
+    {
+        name: "Manga",
+        value: "manga",
+    },
+    {
+        name: "Legendary",
+        value: "legendary",
+    },
+];
+
 export function CardRarityToString(rarity: CardRarity): string {
     switch (rarity) {
     case CardRarity.Unknown:
diff --git a/src/constants/EffectDetails.ts b/src/constants/EffectDetails.ts
new file mode 100644
index 0000000..9d1f2b6
--- /dev/null
+++ b/src/constants/EffectDetails.ts
@@ -0,0 +1,23 @@
+class EffectDetail {
+    public readonly id: string;
+    public readonly friendlyName: string;
+    public readonly duration: number;
+    public readonly cost: number;
+    public readonly cooldown: number;
+
+    constructor(id: string, friendlyName: string, duration: number, cost: number, cooldown: number) {
+        this.id = id;
+        this.friendlyName = friendlyName;
+        this.duration = duration;
+        this.cost = cost;
+        this.cooldown = cooldown;
+    }
+};
+
+export const EffectDetails = new Map<string, EffectDetail>([
+    [ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ],
+]);
+
+export const EffectChoices = [
+    { name: "Unclaimed Chance Up", value: "unclaimed" },
+];
diff --git a/src/database/entities/app/UserEffect.ts b/src/database/entities/app/UserEffect.ts
index fa1b584..447b9c5 100644
--- a/src/database/entities/app/UserEffect.ts
+++ b/src/database/entities/app/UserEffect.ts
@@ -57,4 +57,30 @@ export default class UserEffect extends AppBaseEntity {
 
         return single;
     }
+
+    public static async FetchAllByUserIdPaginated(userId: string, page: number = 0, itemsPerPage: number = 10): Promise<[UserEffect[], number]> {
+        const repository = AppDataSource.getRepository(UserEffect);
+
+        const query = await repository.createQueryBuilder("effect")
+            .where("effect.UserId = :userId", { userId })
+            .andWhere("effect.Unused > 0")
+            .orderBy("effect.Name", "ASC")
+            .skip(page * itemsPerPage)
+            .take(itemsPerPage)
+            .getManyAndCount();
+
+        return query;
+    }
+
+    public static async FetchActiveEffectByUserId(userId: string): Promise<UserEffect | null> {
+        const repository = AppDataSource.getRepository(UserEffect);
+
+        const query = await repository.createQueryBuilder("effect")
+            .where("effect.UserId = :userId", { userId })
+            .andWhere("effect.WhenExpires IS NOT NULL")
+            .andWhere("effect.WhenExpires > :now", { now: new Date() })
+            .getOne();
+
+        return query;
+    }
 }
diff --git a/src/helpers/CardDropHelperMetadata.ts b/src/helpers/CardDropHelperMetadata.ts
deleted file mode 100644
index 8b9b273..0000000
--- a/src/helpers/CardDropHelperMetadata.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
-import { CardRarity, CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity";
-import CardRarityChances from "../constants/CardRarityChances";
-import { DropResult } from "../contracts/SeriesMetadata";
-import { CoreClient } from "../client/client";
-import AppLogger from "../client/appLogger";
-import CardConstants from "../constants/CardConstants";
-import StringTools from "./StringTools";
-
-export default class CardDropHelperMetadata {
-    public static GetRandomCard(): DropResult | undefined {
-        const randomRarity = Math.random() * 100;
-
-        let cardRarity: CardRarity;
-
-        const bronzeChance = CardRarityChances.Bronze;
-        const silverChance = bronzeChance + CardRarityChances.Silver;
-        const goldChance = silverChance + CardRarityChances.Gold;
-        const mangaChance = goldChance + CardRarityChances.Manga;
-
-        if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
-        else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
-        else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
-        else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga;
-        else cardRarity = CardRarity.Legendary;
-
-        const randomCard = this.GetRandomCardByRarity(cardRarity);
-
-        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCard", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`);
-
-        return randomCard;
-    }
-
-    public static GetRandomCardByRarity(rarity: CardRarity): DropResult | undefined {
-        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Parameters: rarity=${rarity}`);
-
-        const allCards = CoreClient.Cards
-            .flatMap(x => x.cards)
-            .filter(x => x.type == rarity);
-
-        const randomCardIndex = Math.floor(Math.random() * allCards.length);
-
-        const card = allCards[randomCardIndex];
-        const series = CoreClient.Cards
-            .find(x => x.cards.includes(card));
-
-        if (!series) {
-            AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`);
-
-            return undefined;
-        }
-
-        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Random card: ${card.id} ${card.name}`);
-
-        return {
-            series: series,
-            card: card,
-        };
-    }
-
-    public static GetCardByCardNumber(cardNumber: string): DropResult | undefined {
-        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`);
-
-        const card = CoreClient.Cards
-            .flatMap(x => x.cards)
-            .find(x => x.id == cardNumber);
-
-        const series = CoreClient.Cards
-            .find(x => x.cards.find(y => y.id == card?.id));
-
-        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Card: ${card?.id} ${card?.name}`);
-        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Series: ${series?.id} ${series?.name}`);
-
-        if (!card || !series) {
-            AppLogger.LogVerbose("CardDropHelperMetadata/GetCardByCardNumber", `Unable to find card metadata: ${cardNumber}`);
-            return undefined;
-        }
-
-        return { card, series };
-    }
-
-    public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder {
-        AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropEmbed", `Parameters: drop=${drop.card.id}, quantityClaimed=${quantityClaimed}, imageFileName=${imageFileName}`);
-
-        const description = drop.card.subseries ?? drop.series.name;
-        let colour = CardRarityToColour(drop.card.type);
-
-        if (drop.card.colour && StringTools.IsHexCode(drop.card.colour)) {
-            const hexCode = Number("0x" + drop.card.colour);
-
-            if (hexCode) {
-                colour = hexCode;
-            } else {
-                AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
-            }
-        } else if (drop.card.colour) {
-            AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
-        }
-
-        const embed = new EmbedBuilder()
-            .setTitle(drop.card.name)
-            .setDescription(description)
-            .setFooter({ text: `${CardRarityToString(drop.card.type)} ยท ${drop.card.id}` })
-            .setColor(colour)
-            .setImage(`attachment://${imageFileName}`)
-            .addFields([
-                {
-                    name: "Claimed",
-                    value: `${quantityClaimed}`,
-                    inline: true,
-                }
-            ]);
-
-        if (claimedBy != null) {
-            embed.addFields([
-                {
-                    name: "Claimed by",
-                    value: claimedBy,
-                    inline: true,
-                }
-            ]);
-        }
-
-        if (currency != null) {
-            embed.addFields([
-                {
-                    name: "Currency",
-                    value: `${currency}`,
-                    inline: true,
-                }
-            ]);
-        }
-
-        return embed;
-    }
-
-    public static GenerateDropButtons(drop: DropResult, claimId: string, userId: string, disabled: boolean = false): ActionRowBuilder<ButtonBuilder> {
-        AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropButtons", `Parameters: drop=${drop.card.id}, claimId=${claimId}, userId=${userId}`);
-
-        return new ActionRowBuilder<ButtonBuilder>()
-            .addComponents(
-                new ButtonBuilder()
-                    .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`)
-                    .setLabel(`Claim (${CardConstants.ClaimCost} ๐Ÿช™)`)
-                    .setStyle(ButtonStyle.Primary)
-                    .setDisabled(disabled),
-                new ButtonBuilder()
-                    .setCustomId("reroll")
-                    .setLabel("Reroll")
-                    .setStyle(ButtonStyle.Secondary));
-    }
-
-    public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder {
-        const dropEmbed = this.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency);
-
-        dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} ยท ${cardsRemaining} Remaining`});
-
-        return dropEmbed;
-    }
-
-    public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder<ButtonBuilder> {
-        return new ActionRowBuilder<ButtonBuilder>()
-            .addComponents(
-                new ButtonBuilder()
-                    .setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`)
-                    .setLabel("Keep")
-                    .setStyle(ButtonStyle.Primary)
-                    .setDisabled(disabled),
-                new ButtonBuilder()
-                    .setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`)
-                    .setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} ๐Ÿช™)`)
-                    .setStyle(ButtonStyle.Secondary));
-    }
-}
diff --git a/src/helpers/CardSearchHelper.ts b/src/helpers/CardSearchHelper.ts
index 29014f7..df0265c 100644
--- a/src/helpers/CardSearchHelper.ts
+++ b/src/helpers/CardSearchHelper.ts
@@ -1,16 +1,17 @@
 import {ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
 import Fuse from "fuse.js";
 import {CoreClient} from "../client/client.js";
-import CardDropHelperMetadata from "./CardDropHelperMetadata.js";
 import Inventory from "../database/entities/app/Inventory.js";
 import {readFileSync} from "fs";
 import path from "path";
 import AppLogger from "../client/appLogger.js";
+import GetCardsHelper from "./DropHelpers/GetCardsHelper.js";
+import DropEmbedHelper from "./DropHelpers/DropEmbedHelper.js";
 
 interface ReturnedPage {
     embed: EmbedBuilder,
     row: ActionRowBuilder<ButtonBuilder>,
-    attachment: AttachmentBuilder,
+    attachments: AttachmentBuilder[],
     results: string[],
 }
 
@@ -32,27 +33,26 @@ export default class CardSearchHelper {
             return undefined;
         }
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(entry.item.id);
+        const card = GetCardsHelper.GetCardByCardNumber(entry.item.id);
 
         if (!card) return undefined;
 
-        let image: Buffer;
-        const imageFileName = card.card.path.split("/").pop()!;
+        const attachments = [];
+        let imageFileName = "";
 
-        try {
-            image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
-        } catch {
-            AppLogger.LogError("CardSearchHelper/GenerateSearchQuery", `Unable to fetch image for card ${card.card.id}.`);
-            
-            return undefined;
+        if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) {
+            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
+            imageFileName = card.card.path.split("/").pop()!;
+
+            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+            attachments.push(attachment);
         }
 
-        const attachment = new AttachmentBuilder(image, { name: imageFileName });
-        
         const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
         const quantityClaimed = inventory?.Quantity ?? 0;
 
-        const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
+        const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName);
 
         const row = new ActionRowBuilder<ButtonBuilder>()
             .addComponents(
@@ -67,13 +67,13 @@ export default class CardSearchHelper {
                     .setStyle(ButtonStyle.Primary)
                     .setDisabled(pages == 1));
 
-        return { embed, row, attachment, results };
+        return { embed, row, attachments, results };
     }
 
     public static async GenerateSearchPageFromQuery(results: string[], userid: string, page: number): Promise<ReturnedPage | undefined> {
         const currentPageId = results[page - 1];
 
-        const card = CardDropHelperMetadata.GetCardByCardNumber(currentPageId);
+        const card = GetCardsHelper.GetCardByCardNumber(currentPageId);
 
         if (!card) {
             AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to find card by id: ${currentPageId}.`);
@@ -81,23 +81,22 @@ export default class CardSearchHelper {
             return undefined;
         }
 
-        let image: Buffer;
-        const imageFileName = card.card.path.split("/").pop()!;
+        const attachments = [];
+        let imageFileName = "";
 
-        try {
-            image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
-        } catch {
-            AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to fetch image for card ${card.card.id}.`);
+        if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) {
+            const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
+            imageFileName = card.card.path.split("/").pop()!;
 
-            return undefined;
+            const attachment = new AttachmentBuilder(image, { name: imageFileName });
+
+            attachments.push(attachment);
         }
 
-        const attachment = new AttachmentBuilder(image, { name: imageFileName });
-
         const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
         const quantityClaimed = inventory?.Quantity ?? 0;
 
-        const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
+        const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName);
 
         const row = new ActionRowBuilder<ButtonBuilder>()
             .addComponents(
@@ -112,6 +111,6 @@ export default class CardSearchHelper {
                     .setStyle(ButtonStyle.Primary)
                     .setDisabled(page == results.length));
 
-        return { embed, row, attachment, results };
+        return { embed, row, attachments, results };
     }
 }
diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts
new file mode 100644
index 0000000..08b5813
--- /dev/null
+++ b/src/helpers/DropHelpers/DropEmbedHelper.ts
@@ -0,0 +1,88 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
+import { DropResult } from "../../contracts/SeriesMetadata";
+import AppLogger from "../../client/appLogger";
+import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity";
+import StringTools from "../StringTools";
+
+export default class DropEmbedHelper {
+    public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder {
+        AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropEmbed", `Parameters: drop=${drop.card.id}, quantityClaimed=${quantityClaimed}, imageFileName=${imageFileName}`);
+
+        const description = drop.card.subseries ?? drop.series.name;
+        let colour = CardRarityToColour(drop.card.type);
+
+        if (drop.card.colour && StringTools.IsHexCode(drop.card.colour)) {
+            const hexCode = Number("0x" + drop.card.colour);
+
+            if (hexCode) {
+                colour = hexCode;
+            } else {
+                AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
+            }
+        } else if (drop.card.colour) {
+            AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
+        }
+
+        let imageUrl = `attachment://${imageFileName}`;
+
+        if (drop.card.path.startsWith("http://") || drop.card.path.startsWith("https://")) {
+            imageUrl = drop.card.path;
+        }
+
+        const embed = new EmbedBuilder()
+            .setTitle(drop.card.name)
+            .setDescription(description)
+            .setFooter({ text: `${CardRarityToString(drop.card.type)} ยท ${drop.card.id}` })
+            .setColor(colour)
+            .setImage(imageUrl)
+            .addFields([
+                {
+                    name: "Claimed",
+                    value: `${quantityClaimed}`,
+                    inline: true,
+                }
+            ]);
+
+        if (claimedBy != null) {
+            embed.addFields([
+                {
+                    name: "Claimed by",
+                    value: claimedBy,
+                    inline: true,
+                }
+            ]);
+        }
+
+        if (currency != null) {
+            embed.addFields([
+                {
+                    name: "Currency",
+                    value: `${currency}`,
+                    inline: true,
+                }
+            ]);
+        }
+
+        return embed;
+    }
+
+    public static GenerateDropButtons(drop: DropResult, claimId: string, userId: string, disabled: boolean = false): ActionRowBuilder<ButtonBuilder> {
+        AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropButtons", `Parameters: drop=${drop.card.id}, claimId=${claimId}, userId=${userId}`);
+
+        return new ActionRowBuilder<ButtonBuilder>()
+            .addComponents(
+                new ButtonBuilder()
+                    .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`)
+                    .setLabel("Claim")
+                    .setStyle(ButtonStyle.Success)
+                    .setDisabled(disabled),
+                new ButtonBuilder()
+                    .setCustomId(`sacrifice confirm ${userId} ${drop.card.id} 1`)
+                    .setLabel(`Sacrifice`)
+                    .setStyle(ButtonStyle.Danger),
+                new ButtonBuilder()
+                    .setCustomId("reroll")
+                    .setEmoji("๐Ÿ”")
+                    .setStyle(ButtonStyle.Primary),);
+    }
+}
\ No newline at end of file
diff --git a/src/helpers/DropHelpers/GetCardsHelper.ts b/src/helpers/DropHelpers/GetCardsHelper.ts
new file mode 100644
index 0000000..8bc4fe7
--- /dev/null
+++ b/src/helpers/DropHelpers/GetCardsHelper.ts
@@ -0,0 +1,91 @@
+import AppLogger from "../../client/appLogger";
+import { CoreClient } from "../../client/client";
+import CardConstants from "../../constants/CardConstants";
+import { CardRarity } from "../../constants/CardRarity";
+import CardRarityChances from "../../constants/CardRarityChances";
+import { DropResult } from "../../contracts/SeriesMetadata";
+import EffectHelper from "../EffectHelper";
+import GetUnclaimedCardsHelper from "./GetUnclaimedCardsHelper";
+
+export default class GetCardsHelper {
+    public static async FetchCard(userId: string): Promise<DropResult | undefined> {
+        const hasChanceUpEffect = await EffectHelper.HasEffect(userId, "unclaimed");
+
+        if (hasChanceUpEffect && Math.random() <= CardConstants.UnusedChanceUpChance) {
+            return await GetUnclaimedCardsHelper.GetRandomCardUnclaimed(userId);
+        }
+
+        return this.GetRandomCard();
+    }
+
+    public static GetRandomCard(): DropResult | undefined {
+        const randomRarity = Math.random() * 100;
+
+        let cardRarity: CardRarity;
+
+        const bronzeChance = CardRarityChances.Bronze;
+        const silverChance = bronzeChance + CardRarityChances.Silver;
+        const goldChance = silverChance + CardRarityChances.Gold;
+        const mangaChance = goldChance + CardRarityChances.Manga;
+
+        if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
+        else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
+        else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
+        else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga;
+        else cardRarity = CardRarity.Legendary;
+
+        const randomCard = this.GetRandomCardByRarity(cardRarity);
+
+        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCard", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`);
+
+        return randomCard;
+    }
+
+    public static GetRandomCardByRarity(rarity: CardRarity): DropResult | undefined {
+        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Parameters: rarity=${rarity}`);
+
+        const allCards = CoreClient.Cards
+            .flatMap(x => x.cards)
+            .filter(x => x.type == rarity);
+
+        const randomCardIndex = Math.floor(Math.random() * allCards.length);
+
+        const card = allCards[randomCardIndex];
+        const series = CoreClient.Cards
+            .find(x => x.cards.includes(card));
+
+        if (!series) {
+            AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`);
+
+            return undefined;
+        }
+
+        AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Random card: ${card.id} ${card.name}`);
+
+        return {
+            series: series,
+            card: card,
+        };
+    }
+
+    public static GetCardByCardNumber(cardNumber: string): DropResult | undefined {
+        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`);
+
+        const card = CoreClient.Cards
+            .flatMap(x => x.cards)
+            .find(x => x.id == cardNumber);
+
+        const series = CoreClient.Cards
+            .find(x => x.cards.find(y => y.id == card?.id));
+
+        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Card: ${card?.id} ${card?.name}`);
+        AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Series: ${series?.id} ${series?.name}`);
+
+        if (!card || !series) {
+            AppLogger.LogVerbose("CardDropHelperMetadata/GetCardByCardNumber", `Unable to find card metadata: ${cardNumber}`);
+            return undefined;
+        }
+
+        return { card, series };
+    }
+}
diff --git a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts
new file mode 100644
index 0000000..87fafea
--- /dev/null
+++ b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts
@@ -0,0 +1,63 @@
+import AppLogger from "../../client/appLogger";
+import { CoreClient } from "../../client/client";
+import { CardRarity } from "../../constants/CardRarity";
+import CardRarityChances from "../../constants/CardRarityChances";
+import { DropResult } from "../../contracts/SeriesMetadata";
+import Inventory from "../../database/entities/app/Inventory";
+import GetCardsHelper from "./GetCardsHelper";
+
+export default class GetUnclaimedCardsHelper {
+    public static async GetRandomCardUnclaimed(userId: string): Promise<DropResult | undefined> {
+        const randomRarity = Math.random() * 100;
+
+        let cardRarity: CardRarity;
+
+        const bronzeChance = CardRarityChances.Bronze;
+        const silverChance = bronzeChance + CardRarityChances.Silver;
+        const goldChance = silverChance + CardRarityChances.Gold;
+        const mangaChance = goldChance + CardRarityChances.Manga;
+
+        if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
+        else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
+        else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
+        else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga;
+        else cardRarity = CardRarity.Legendary;
+
+        const randomCard = await this.GetRandomCardByRarityUnclaimed(cardRarity, userId);
+
+        return randomCard;
+    }
+
+    public static async GetRandomCardByRarityUnclaimed(rarity: CardRarity, userId: string): Promise<DropResult | undefined> {
+        const claimedCards = await Inventory.FetchAllByUserId(userId);
+
+        if (!claimedCards) {
+            // They don't have any cards, so safe to get any random card
+            return GetCardsHelper.GetRandomCardByRarity(rarity);
+        }
+
+        const allCards = CoreClient.Cards
+            .flatMap(x => x.cards)
+            .filter(x => x.type == rarity)
+            .filter(x => !claimedCards.find(y => y.CardNumber == x.id));
+
+        if (!allCards) return undefined;
+
+        const randomCardIndex = Math.floor(Math.random() * allCards.length);
+
+        const card = allCards[randomCardIndex];
+        const series = CoreClient.Cards
+            .find(x => x.cards.includes(card));
+
+        if (!series) {
+            AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}`);
+
+            return undefined;
+        }
+
+        return {
+            series: series,
+            card: card,
+        };
+    }
+}
diff --git a/src/helpers/DropHelpers/MultidropEmbedHelper.ts b/src/helpers/DropHelpers/MultidropEmbedHelper.ts
new file mode 100644
index 0000000..526993e
--- /dev/null
+++ b/src/helpers/DropHelpers/MultidropEmbedHelper.ts
@@ -0,0 +1,28 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
+import { DropResult } from "../../contracts/SeriesMetadata";
+import { GetSacrificeAmount } from "../../constants/CardRarity";
+import DropEmbedHelper from "./DropEmbedHelper";
+
+export default class MultidropEmbedHelper {
+    public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder {
+        const dropEmbed = DropEmbedHelper.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency);
+
+        dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} ยท ${cardsRemaining} Remaining`});
+
+        return dropEmbed;
+    }
+
+    public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder<ButtonBuilder> {
+        return new ActionRowBuilder<ButtonBuilder>()
+            .addComponents(
+                new ButtonBuilder()
+                    .setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`)
+                    .setLabel("Keep")
+                    .setStyle(ButtonStyle.Primary)
+                    .setDisabled(disabled),
+                new ButtonBuilder()
+                    .setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`)
+                    .setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} ๐Ÿช™)`)
+                    .setStyle(ButtonStyle.Secondary));
+    }
+}
diff --git a/src/helpers/EffectHelper.ts b/src/helpers/EffectHelper.ts
index 14c2f43..235ea08 100644
--- a/src/helpers/EffectHelper.ts
+++ b/src/helpers/EffectHelper.ts
@@ -1,4 +1,10 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
 import UserEffect from "../database/entities/app/UserEffect";
+import EmbedColours from "../constants/EmbedColours";
+import { EffectDetails } from "../constants/EffectDetails";
+import User from "../database/entities/app/User";
+import CardConstants from "../constants/CardConstants";
+import AppLogger from "../client/appLogger";
 
 export default class EffectHelper {
     public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
@@ -14,6 +20,20 @@ export default class EffectHelper {
     }
 
     public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise<boolean> {
+        const canUseEffect = await this.CanUseEffect(userId, name);
+
+        if (!canUseEffect) return false;
+
+        const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
+
+        effect!.UseEffect(whenExpires);
+
+        await effect!.Save(UserEffect, effect!);
+
+        return true;
+    }
+
+    public static async CanUseEffect(userId: string, name: string): Promise<boolean> {
         const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
         const now = new Date();
 
@@ -21,13 +41,15 @@ export default class EffectHelper {
             return false;
         }
 
-        if (effect.WhenExpires && now < effect.WhenExpires) {
+        const effectDetail = EffectDetails.get(effect.Name);
+
+        if (!effectDetail) {
             return false;
         }
 
-        effect.UseEffect(whenExpires);
-
-        await effect.Save(UserEffect, effect);
+        if (effect.WhenExpires && now < new Date(effect.WhenExpires.getTime() + effectDetail.cooldown)) {
+            return false;
+        }
 
         return true;
     }
@@ -46,4 +68,127 @@ export default class EffectHelper {
 
         return true;
     }
+
+    public static async GenerateEffectListEmbed(userId: string, page: number): Promise<{
+        embed: EmbedBuilder,
+        row: ActionRowBuilder<ButtonBuilder>,
+    }> {
+        const itemsPerPage = 10;
+
+        const query = await UserEffect.FetchAllByUserIdPaginated(userId, page - 1, itemsPerPage);
+        const activeEffect = await UserEffect.FetchActiveEffectByUserId(userId);
+
+        const effects = query[0];
+        const count = query[1];
+
+        const totalPages = count > 0 ? Math.ceil(count / itemsPerPage) : 1;
+
+        let description = "*none*";
+
+        if (effects.length > 0) {
+            description = effects.map(x => `${EffectDetails.get(x.Name)?.friendlyName} x${x.Unused}`).join("\n");
+        }
+
+        const embed = new EmbedBuilder()
+            .setTitle("Effects")
+            .setDescription(description)
+            .setColor(EmbedColours.Ok)
+            .setFooter({ text: `Page ${page} of ${totalPages}` });
+
+        if (activeEffect) {
+            embed.addFields([
+                {
+                    name: "Active",
+                    value: `${EffectDetails.get(activeEffect.Name)?.friendlyName}`,
+                    inline: true,
+                },
+                {
+                    name: "Expires",
+                    value: `<t:${Math.round(activeEffect.WhenExpires!.getTime() / 1000)}>`,
+                    inline: true,
+                },
+            ]);
+        }
+
+        const row = new ActionRowBuilder<ButtonBuilder>()
+            .addComponents(
+                new ButtonBuilder()
+                    .setCustomId(`effects list ${page - 1}`)
+                    .setLabel("Previous")
+                    .setStyle(ButtonStyle.Primary)
+                    .setDisabled(page == 1),
+                new ButtonBuilder()
+                    .setCustomId(`effects list ${page + 1}`)
+                    .setLabel("Next")
+                    .setStyle(ButtonStyle.Primary)
+                    .setDisabled(page == totalPages),
+            );
+
+        return {
+            embed,
+            row,
+        };
+    }
+
+    public static async GenerateEffectBuyEmbed(userId: string, id: string, quantity: number, disabled: boolean): Promise<{
+        embed: EmbedBuilder,
+        row: ActionRowBuilder<ButtonBuilder>,
+    } | string> {
+        const effectDetail = EffectDetails.get(id);
+
+        if (!effectDetail) {
+            return "Effect detail not found!";
+        }
+
+        const totalCost = effectDetail.cost * quantity;
+
+        let user = await User.FetchOneById(User, userId);
+
+        if (!user) {
+            user = new User(userId, CardConstants.StartingCurrency);
+            await user.Save(User, user);
+
+            AppLogger.LogInfo("EffectHelper", `Created initial user entity for : ${userId}`);
+        }
+
+        if (user.Currency < totalCost) {
+            return `You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`;
+        }
+
+        const embed = new EmbedBuilder()
+            .setTitle("Buy Effect")
+            .setDescription(effectDetail.friendlyName)
+            .setColor(EmbedColours.Ok)
+            .addFields([
+                {
+                    name: "Cost",
+                    value: `${totalCost}`,
+                    inline: true,
+                },
+                {
+                    name: "Quantity",
+                    value: `${quantity}`,
+                    inline: true,
+                },
+            ]);
+
+        const row = new ActionRowBuilder<ButtonBuilder>()
+            .addComponents([
+                new ButtonBuilder()
+                    .setCustomId(`effects buy confirm ${id} ${quantity}`)
+                    .setLabel("Confirm")
+                    .setStyle(ButtonStyle.Success)
+                    .setDisabled(disabled),
+                new ButtonBuilder()
+                    .setCustomId(`effects buy cancel ${id} ${quantity}`)
+                    .setLabel("Cancel")
+                    .setStyle(ButtonStyle.Danger)
+                    .setDisabled(disabled),
+            ]);
+
+        return {
+            embed,
+            row,
+        }
+    }
 }
diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts
index 4e43977..6156938 100644
--- a/src/helpers/ImageHelper.ts
+++ b/src/helpers/ImageHelper.ts
@@ -3,7 +3,8 @@ import path from "path";
 import AppLogger from "../client/appLogger";
 import {existsSync} from "fs";
 import Inventory from "../database/entities/app/Inventory";
-import {Jimp} from "jimp";
+import { Bitmap, Jimp } from "jimp";
+import axios from "axios";
 
 interface CardInput {
     id: string;
@@ -25,36 +26,52 @@ export default class ImageHelper {
         const ctx = canvas.getContext("2d");
 
         for (let i = 0; i < cards.length; i++) {
-            const card = cards[i];
+            try {
+                const card = cards[i];
 
-            const filePath = path.join(process.env.DATA_DIR!, "cards", card.path);
+                const filePath = path.join(process.env.DATA_DIR!, "cards", card.path);
 
-            const exists = existsSync(filePath);
+                let bitmap: Bitmap;
 
-            if (!exists) {
-                AppLogger.LogError("ImageHelper/GenerateCardImageGrid", `Failed to load image from path ${card.path}`);
-                continue;
-            }
+                if (existsSync(filePath)) {
+                    const data = await Jimp.read(filePath);
 
-            const imageData = await Jimp.read(filePath);
+                    bitmap = data.bitmap;
+                } else if (card.path.startsWith("http://") || card.path.startsWith("https://")) {
+                    const response = await axios.get(card.path, { responseType: "arraybuffer" });
+                    const buffer = Buffer.from(response.data);
+                    const data = await Jimp.fromBuffer(buffer);
 
-            if (userId != null) {
-                const claimed = await Inventory.FetchOneByCardNumberAndUserId(userId, card.id);
-
-                if (!claimed || claimed.Quantity == 0) {
-                    imageData.greyscale();
+                    bitmap = data.bitmap;
+                } else {
+                    AppLogger.LogError("ImageHelper/GenerateCardImageGrid", `Failed to load image from path ${card.path}`);
+                    continue;
                 }
+
+                const imageData = Jimp.fromBitmap(bitmap);
+
+                if (userId != null) {
+                    const claimed = await Inventory.FetchOneByCardNumberAndUserId(userId, card.id);
+
+                    if (!claimed || claimed.Quantity == 0) {
+                        imageData.greyscale();
+                    }
+                }
+
+                const image = await loadImage(await imageData.getBuffer("image/png"));
+
+                const x = i % gridWidth;
+                const y = Math.floor(i / gridWidth);
+
+                const imageX = imageWidth * x;
+                const imageY = imageHeight * y;
+
+                ctx.drawImage(image, imageX, imageY);
+            }
+            catch {
+                // TODO: Enable once we've investigated a fix
+                //AppLogger.CatchError("ImageHelper", e);
             }
-
-            const image = await loadImage(await imageData.getBuffer("image/png"));
-
-            const x = i % gridWidth;
-            const y = Math.floor(i / gridWidth);
-
-            const imageX = imageWidth * x;
-            const imageY = imageHeight * y;
-
-            ctx.drawImage(image, imageX, imageY);
         }
 
         return canvas.toBuffer();
diff --git a/src/helpers/TimeLengthInput.ts b/src/helpers/TimeLengthInput.ts
index d1d8734..aef58ba 100644
--- a/src/helpers/TimeLengthInput.ts
+++ b/src/helpers/TimeLengthInput.ts
@@ -118,4 +118,19 @@ export default class TimeLengthInput {
 
         return desNumber;
     }
+
+    public static ConvertFromMilliseconds(ms: number): TimeLengthInput {
+        const seconds = Math.floor(ms / 1000);
+        const minutes = Math.floor(seconds / 60);
+        const hours = Math.floor(minutes / 60);
+        const days = Math.floor(hours / 24);
+
+        const remainingSeconds = seconds % 60;
+        const remainingMinutes = minutes % 60;
+        const remainingHours = hours % 24;
+
+        const timeString = `${days}d ${remainingHours}h ${remainingMinutes}m ${remainingSeconds}s`;
+
+        return new TimeLengthInput(timeString);
+    }
 }
\ No newline at end of file
diff --git a/src/registry.ts b/src/registry.ts
index e4e5d64..3ae885d 100644
--- a/src/registry.ts
+++ b/src/registry.ts
@@ -7,6 +7,7 @@ import AllBalance from "./commands/allbalance";
 import Balance from "./commands/balance";
 import Daily from "./commands/daily";
 import Drop from "./commands/drop";
+import Effects from "./commands/effects";
 import Gdrivesync from "./commands/gdrivesync";
 import Give from "./commands/give";
 import Id from "./commands/id";
@@ -25,6 +26,7 @@ import Droprarity from "./commands/stage/droprarity";
 
 // Button Event Imports
 import Claim from "./buttonEvents/Claim";
+import EffectsButtonEvent from "./buttonEvents/Effects";
 import InventoryButtonEvent from "./buttonEvents/Inventory";
 import MultidropButtonEvent from "./buttonEvents/Multidrop";
 import Reroll from "./buttonEvents/Reroll";
@@ -44,6 +46,7 @@ export default class Registry {
         CoreClient.RegisterCommand("balance", new Balance());
         CoreClient.RegisterCommand("daily", new Daily());
         CoreClient.RegisterCommand("drop", new Drop());
+        CoreClient.RegisterCommand("effects", new Effects());
         CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
         CoreClient.RegisterCommand("give", new Give());
         CoreClient.RegisterCommand("id", new Id());
@@ -63,6 +66,7 @@ export default class Registry {
 
     public static RegisterButtonEvents() {
         CoreClient.RegisterButtonEvent("claim", new Claim());
+        CoreClient.RegisterButtonEvent("effects", new EffectsButtonEvent());
         CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
         CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
         CoreClient.RegisterButtonEvent("reroll", new Reroll());
diff --git a/src/timers/PurgeClaims.ts b/src/timers/PurgeClaims.ts
index a0ed9d0..4ca8902 100644
--- a/src/timers/PurgeClaims.ts
+++ b/src/timers/PurgeClaims.ts
@@ -4,7 +4,7 @@ import Claim from "../database/entities/app/Claim";
 export default async function PurgeClaims() {
     const claims = await Claim.FetchAll(Claim);
 
-    const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago
+    const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago
 
     const expiredClaims = claims.filter(x => x.WhenCreated < whenLastClaimable);
 
diff --git a/tests/.gitkeep b/tests/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts b/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts
new file mode 100644
index 0000000..2199477
--- /dev/null
+++ b/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts
@@ -0,0 +1,23 @@
+import { ButtonInteraction } from "../../__types__/discord.js";
+
+export default function GenerateButtonInteractionMock(): ButtonInteraction {
+    return {
+        guild: {},
+        guildId: "guildId",
+        channel: {
+            isSendable: jest.fn().mockReturnValue(true),
+            send: jest.fn(),
+        },
+        deferUpdate: jest.fn(),
+        editReply: jest.fn(),
+        message: {
+            createdAt: new Date(1000 * 60 * 27),
+        },
+        user: {
+            id: "userId",
+        },
+        customId: "customId",
+        update: jest.fn(),
+        reply: jest.fn(),
+    };
+}
\ No newline at end of file
diff --git a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts
new file mode 100644
index 0000000..128c25b
--- /dev/null
+++ b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts
@@ -0,0 +1,17 @@
+import { CommandInteraction } from "../../__types__/discord.js";
+
+export default function GenerateCommandInteractionMock(options?: {
+    subcommand?: string,
+}): CommandInteraction {
+    return {
+        deferReply: jest.fn(),
+        editReply: jest.fn(),
+        isChatInputCommand: jest.fn().mockReturnValue(true),
+        options: {
+            getSubcommand: jest.fn().mockReturnValue(options?.subcommand),
+        },
+        user: {
+            id: "userId",
+        },
+    };
+}
\ No newline at end of file
diff --git a/tests/__types__/discord.js.ts b/tests/__types__/discord.js.ts
new file mode 100644
index 0000000..3304af4
--- /dev/null
+++ b/tests/__types__/discord.js.ts
@@ -0,0 +1,31 @@
+export type ButtonInteraction = {
+    guild: object | null,
+    guildId: string | null,
+    channel: {
+        isSendable: jest.Func,
+        send: jest.Func,
+    } | null,
+    deferUpdate: jest.Func,
+    editReply: jest.Func,
+    message: {
+        createdAt: Date,
+    } | null,
+    user: {
+        id: string,
+    } | null,
+    customId: string,
+    update: jest.Func,
+    reply: jest.Func,
+}
+
+export type CommandInteraction = {
+    deferReply: jest.Func,
+    editReply: jest.Func,
+    isChatInputCommand: jest.Func,
+    options: {
+        getSubcommand: jest.Func,
+    },
+    user: {
+        id: string,
+    },
+}
\ No newline at end of file
diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts
new file mode 100644
index 0000000..1e7027c
--- /dev/null
+++ b/tests/buttonEvents/Claim.test.ts
@@ -0,0 +1,90 @@
+import { ButtonInteraction, TextChannel } from "discord.js";
+import Claim from "../../src/buttonEvents/Claim";
+import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js";
+import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock";
+
+jest.mock("../../src/client/appLogger");
+
+let interaction: ButtonInteractionType;
+
+beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(1000 * 60 * 30);
+
+    interaction = GenerateButtonInteractionMock();
+    interaction.customId = "claim cardNumber claimId droppedBy userId";
+});
+
+afterAll(() => {
+    jest.useRealTimers();
+});
+
+test("GIVEN interaction.guild is null, EXPECT nothing to happen", async () => {
+    // Arrange
+    interaction.guild = null;
+
+    // Act
+    const claim = new Claim();
+    await claim.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(interaction.deferUpdate).not.toHaveBeenCalled();
+    expect(interaction.editReply).not.toHaveBeenCalled();
+    expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled();
+});
+
+test("GIVEN interaction.guildId is null, EXPECT nothing to happen", async () => {
+    // Arrange
+    interaction.guildId = null;
+
+    // Act
+    const claim = new Claim();
+    await claim.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(interaction.deferUpdate).not.toHaveBeenCalled();
+    expect(interaction.editReply).not.toHaveBeenCalled();
+    expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled();
+});
+
+test("GIVEN interaction.channel is null, EXPECT nothing to happen", async () => {
+    // Arrange
+    interaction.channel = null;
+
+    // Act
+    const claim = new Claim();
+    await claim.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(interaction.deferUpdate).not.toHaveBeenCalled();
+    expect(interaction.editReply).not.toHaveBeenCalled();
+});
+
+test("GIVEN channel is not sendable, EXPECT nothing to happen", async () => {
+    // Arrange
+    interaction.channel!.isSendable = jest.fn().mockReturnValue(false);
+
+    // Act
+    const claim = new Claim();
+    await claim.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(interaction.deferUpdate).not.toHaveBeenCalled();
+    expect(interaction.editReply).not.toHaveBeenCalled();
+    expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled();
+});
+
+test("GIVEN interaction.message was created more than 5 minutes ago, EXPECT error", async () => {
+    // Arrange
+    interaction.message!.createdAt = new Date(0);
+
+    // Act
+    const claim = new Claim();
+    await claim.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(interaction.channel!.send).toHaveBeenCalledTimes(1);
+    expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 2 minutes of it being dropped!");
+
+    expect(interaction.editReply).not.toHaveBeenCalled();
+});
\ No newline at end of file
diff --git a/tests/buttonEvents/Effects.test.ts b/tests/buttonEvents/Effects.test.ts
new file mode 100644
index 0000000..8fb1023
--- /dev/null
+++ b/tests/buttonEvents/Effects.test.ts
@@ -0,0 +1,68 @@
+import { ButtonInteraction } from "discord.js";
+import Effects from "../../src/buttonEvents/Effects";
+import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock";
+import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js";
+import List from "../../src/buttonEvents/Effects/List";
+import Use from "../../src/buttonEvents/Effects/Use";
+import AppLogger from "../../src/client/appLogger";
+
+jest.mock("../../src/client/appLogger");
+jest.mock("../../src/buttonEvents/Effects/List");
+jest.mock("../../src/buttonEvents/Effects/Use");
+
+let interaction: ButtonInteractionType;
+
+beforeEach(() => {
+    jest.resetAllMocks();
+
+    interaction = GenerateButtonInteractionMock();
+    interaction.customId = "effects";
+});
+
+test("GIVEN action is list, EXPECT list function to be called", async () => {
+    // Arrange
+    interaction.customId = "effects list";
+
+    // Act
+    const effects = new Effects();
+    await effects.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(List).toHaveBeenCalledTimes(1);
+    expect(List).toHaveBeenCalledWith(interaction);
+
+    expect(Use.Execute).not.toHaveBeenCalled();
+});
+
+test("GIVEN action is use, EXPECT use function to be called", async () => {
+    // Arrange
+    interaction.customId = "effects use";
+
+    // Act
+    const effects = new Effects();
+    await effects.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(Use.Execute).toHaveBeenCalledTimes(1);
+    expect(Use.Execute).toHaveBeenCalledWith(interaction);
+
+    expect(List).not.toHaveBeenCalled();
+});
+
+test.todo("GIVEN action is buy, EXPECT buy function to be called");
+
+test("GIVEN action is invalid, EXPECT nothing to be called", async () => {
+    // Arrange
+    interaction.customId = "effects invalid";
+
+    // Act
+    const effects = new Effects();
+    await effects.execute(interaction as unknown as ButtonInteraction);
+
+    // Assert
+    expect(List).not.toHaveBeenCalled();
+    expect(Use.Execute).not.toHaveBeenCalled();
+
+    expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+    expect(AppLogger.LogError).toHaveBeenCalledWith("Buttons/Effects", "Unknown action, invalid");
+});
diff --git a/tests/buttonEvents/Effects/Buy.test.ts b/tests/buttonEvents/Effects/Buy.test.ts
new file mode 100644
index 0000000..3898504
--- /dev/null
+++ b/tests/buttonEvents/Effects/Buy.test.ts
@@ -0,0 +1,350 @@
+import {ButtonInteraction} from "discord.js";
+import Buy from "../../../src/buttonEvents/Effects/Buy";
+import GenerateButtonInteractionMock from "../../__functions__/discord.js/GenerateButtonInteractionMock";
+import { ButtonInteraction as ButtonInteractionType } from "../../__types__/discord.js";
+import AppLogger from "../../../src/client/appLogger";
+import EffectHelper from "../../../src/helpers/EffectHelper";
+import EmbedColours from "../../../src/constants/EmbedColours";
+import User from "../../../src/database/entities/app/User";
+
+jest.mock("../../../src/client/appLogger");
+jest.mock("../../../src/helpers/EffectHelper");
+jest.mock("../../../src/database/entities/app/User");
+
+let interaction: ButtonInteractionType;
+
+beforeEach(() => {
+    jest.resetAllMocks();
+
+    interaction = GenerateButtonInteractionMock();
+    interaction.customId = "effects buy";
+
+});
+
+describe("Execute", () => {
+    test("GIVEN subaction is invalid, EXPECT error logged", async () => {
+        // Arrange
+        interaction.customId += " invalid";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy", "Unknown subaction, effects invalid");
+    });
+});
+
+describe("Confirm", () => {
+    let user: User;
+
+    beforeEach(() => {
+        interaction.customId += " confirm";
+
+        user = {
+            Currency: 1000,
+            Save: jest.fn(),
+            RemoveCurrency: jest.fn(),
+        } as unknown as User;
+
+        (User.FetchOneById as jest.Mock).mockResolvedValue(user);
+    });
+
+    test("EXPECT success embed generated", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+
+        const embed = {
+            id: "embed",
+            setColor: jest.fn(),
+            setFooter: jest.fn(),
+        };
+        const row = {
+            id: "row",
+        };
+        
+        (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue({
+            embed,
+            row,
+        });
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(interaction.update).toHaveBeenCalledTimes(1);
+        expect(interaction.update).toHaveBeenCalledWith({
+            embeds: [ embed ],
+            components: [ row ],
+        });
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledWith("userId", "unclaimed", 1, true);
+
+        expect(embed.setColor).toHaveBeenCalledTimes(1);
+        expect(embed.setColor).toHaveBeenCalledWith(EmbedColours.Success);
+
+        expect(embed.setFooter).toHaveBeenCalledTimes(1);
+        expect(embed.setFooter).toHaveBeenCalledWith({ text: "Purchased" });
+
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+
+        expect(User.FetchOneById).toHaveBeenCalledTimes(1);
+        expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId");
+
+        expect(user.RemoveCurrency).toHaveBeenCalledTimes(1);
+        expect(user.RemoveCurrency).toHaveBeenCalledWith(100);
+
+        expect(user.Save).toHaveBeenCalledTimes(1);
+        expect(user.Save).toHaveBeenCalledWith(User, user);
+
+        expect(EffectHelper.AddEffectToUserInventory).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.AddEffectToUserInventory).toHaveBeenCalledWith("userId", "unclaimed", 1);
+    });
+
+    test("GIVEN id is not supplied, EXPECT error", async () => {
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Not enough parameters");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is not supplied, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Not enough parameters");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is not a number, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed invalid";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Invalid number");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is 0, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed 0";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Invalid number");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN user is not found, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+
+        (User.FetchOneById as jest.Mock).mockResolvedValue(undefined);
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Unable to find user");
+
+        expect(EffectHelper.AddEffectToUserInventory).not.toHaveBeenCalled();
+
+        expect(interaction.update).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN user does not have enough currency, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+
+        user.Currency = 0;
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("You don't have enough currency to buy this! You have `0 Currency` and need `100 Currency`!");
+        
+        expect(EffectHelper.AddEffectToUserInventory).not.toHaveBeenCalled();
+
+        expect(interaction.update).not.toHaveBeenCalled();
+
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN GenerateEffectBuyEmbed returns with a string, EXPECT error replied", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+        
+        (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue("Test error");
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("Test error");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1);
+
+        expect(interaction.update).not.toHaveBeenCalled();
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+});
+
+describe("Cancel", () => {
+    beforeEach(() => {
+        interaction.customId += " cancel";
+    });
+
+    test("EXPECT embed generated", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+
+        const embed = {
+            id: "embed",
+            setColor: jest.fn(),
+            setFooter: jest.fn(),
+        };
+        const row = {
+            id: "row",
+        };
+        
+        (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue({
+            embed,
+            row,
+        });
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(interaction.update).toHaveBeenCalledTimes(1);
+        expect(interaction.update).toHaveBeenCalledWith({
+            embeds: [ embed ],
+            components: [ row ],
+        });
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledWith("userId", "unclaimed", 1, true);
+
+        expect(embed.setColor).toHaveBeenCalledTimes(1);
+        expect(embed.setColor).toHaveBeenCalledWith(EmbedColours.Error);
+
+        expect(embed.setFooter).toHaveBeenCalledTimes(1);
+        expect(embed.setFooter).toHaveBeenCalledWith({ text: "Cancelled" });
+
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN id is not supplied, EXPECT error", async () => {
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Not enough parameters");
+
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is not supplied, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Not enough parameters");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is not a number, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed invalid";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Invalid number");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN quantity is 0, EXPECT error", async () => {
+        // Assert
+        interaction.customId += " unclaimed 0";
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Invalid number");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled();
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN GenerateEffectBuyEmbed returns with a string, EXPECT error replied", async () => {
+        // Assert
+        interaction.customId += " unclaimed 1";
+        
+        (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue("Test error");
+
+        // Act
+        await Buy.Execute(interaction as unknown as ButtonInteraction);
+
+        // Assert
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("Test error");
+
+        expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1);
+
+        expect(interaction.update).not.toHaveBeenCalled();
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+});
diff --git a/tests/buttonEvents/Effects/List.test.ts b/tests/buttonEvents/Effects/List.test.ts
new file mode 100644
index 0000000..5b42c61
--- /dev/null
+++ b/tests/buttonEvents/Effects/List.test.ts
@@ -0,0 +1,50 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, EmbedBuilder } from "discord.js";
+import List from "../../../src/buttonEvents/Effects/List";
+import EffectHelper from "../../../src/helpers/EffectHelper";
+import { mock } from "jest-mock-extended";
+
+jest.mock("../../../src/helpers/EffectHelper");
+
+let interaction: ReturnType<typeof mock<ButtonInteraction>>;
+
+beforeEach(() => {
+    jest.resetAllMocks();
+
+    (EffectHelper.GenerateEffectListEmbed as jest.Mock).mockResolvedValue({
+        embed: mock<EmbedBuilder>(),
+        row: mock<ActionRowBuilder<ButtonBuilder>>(),
+    });
+
+    interaction = mock<ButtonInteraction>();
+    interaction.user.id = "userId";
+    interaction.customId = "effects list 1";
+});
+
+test("GIVEN pageOption is NOT a number, EXPECT error", async () => {
+    // Arrange
+    interaction.customId = "effects list invalid";
+
+    // Act
+    await List(interaction);
+
+    // Assert
+    expect(interaction.reply).toHaveBeenCalledTimes(1);
+    expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number")
+
+    expect(EffectHelper.GenerateEffectListEmbed).not.toHaveBeenCalled();
+    expect(interaction.update).not.toHaveBeenCalled();
+});
+
+test("GIVEN pageOption is a number, EXPECT interaction updated", async () => {
+    // Arrange
+    interaction.customId = "effects list 1";
+
+    // Act
+    await List(interaction);
+
+    // Assert
+    expect(EffectHelper.GenerateEffectListEmbed).toHaveBeenCalledTimes(1);
+    expect(EffectHelper.GenerateEffectListEmbed).toHaveBeenCalledWith("userId", 1);
+
+    expect(interaction.update).toHaveBeenCalledTimes(1);
+});
\ No newline at end of file
diff --git a/tests/buttonEvents/Effects/Use.test.ts b/tests/buttonEvents/Effects/Use.test.ts
new file mode 100644
index 0000000..86391cc
--- /dev/null
+++ b/tests/buttonEvents/Effects/Use.test.ts
@@ -0,0 +1,148 @@
+import { ButtonInteraction, InteractionResponse, InteractionUpdateOptions, MessagePayload } from "discord.js";
+import Use from "../../../src/buttonEvents/Effects/Use";
+import { mock } from "jest-mock-extended";
+import AppLogger from "../../../src/client/appLogger";
+import EffectHelper from "../../../src/helpers/EffectHelper";
+
+jest.mock("../../../src/client/appLogger");
+jest.mock("../../../src/helpers/EffectHelper");
+
+beforeEach(() => {
+    jest.resetAllMocks();
+
+    jest.useFakeTimers();
+    jest.setSystemTime(0);
+});
+
+afterAll(() => {
+    jest.useRealTimers();
+});
+
+describe("Execute", () => {
+    test("GIVEN subaction is unknown, EXPECT nothing to be called", async () => {
+        // Arrange
+        const interaction = mock<ButtonInteraction>();
+        interaction.customId = "effects use invalud";
+
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(interaction.reply).not.toHaveBeenCalled();
+        expect(interaction.update).not.toHaveBeenCalled();
+    });
+});
+
+describe("UseConfirm", () => {
+    let interaction = mock<ButtonInteraction>();
+
+    beforeEach(() => {
+        interaction = mock<ButtonInteraction>();
+        interaction.customId = "effects use confirm";
+    });
+
+    test("GIVEN effectDetail is not found, EXPECT error", async () => {
+        // Arrange
+        interaction.customId += " invalid";
+
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Button/Effects/Use", "Effect not found, invalid");
+
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("Effect not found in system!");
+    });
+
+    test("GIVEN EffectHelper.UseEffect failed, EXPECT error", async () => {
+        // Arrange
+        interaction.customId += " unclaimed";
+        interaction.user.id = "userId";
+
+        (EffectHelper.UseEffect as jest.Mock).mockResolvedValue(false);
+
+        const whenExpires = new Date(Date.now() + 10 * 60 * 1000);
+
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(EffectHelper.UseEffect).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.UseEffect).toHaveBeenCalledWith("userId", "unclaimed", whenExpires);
+
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
+    });
+
+    test("GIVEN EffectHelper.UseEffect succeeded, EXPECT interaction updated", async () => {
+        let updatedWith;
+
+        // Arrange
+        interaction.customId += " unclaimed";
+        interaction.user.id = "userId";
+        interaction.update.mockImplementation(async (opts: string | MessagePayload | InteractionUpdateOptions) => {
+            updatedWith = opts;
+
+            return mock<InteractionResponse<boolean>>();
+        });
+
+        (EffectHelper.UseEffect as jest.Mock).mockResolvedValue(true);
+
+        const whenExpires = new Date(Date.now() + 10 * 60 * 1000);
+
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(EffectHelper.UseEffect).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.UseEffect).toHaveBeenCalledWith("userId", "unclaimed", whenExpires);
+
+        expect(interaction.update).toHaveBeenCalledTimes(1);
+        expect(updatedWith).toMatchSnapshot();
+    });
+});
+
+describe("UseCancel", () => {
+    let interaction = mock<ButtonInteraction>();
+
+    beforeEach(() => {
+        interaction = mock<ButtonInteraction>();
+        interaction.customId = "effects use cancel";
+    });
+
+    test("GIVEN effectDetail is not found, EXPECT error", async () => {
+        // Arrange
+        interaction.customId += " invalid";
+
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Button/Effects/Cancel", "Effect not found, invalid");
+
+        expect(interaction.reply).toHaveBeenCalledTimes(1);
+        expect(interaction.reply).toHaveBeenCalledWith("Effect not found in system!");
+    });
+
+    test("GIVEN effectDetail is found, EXPECT interaction updated", async () => {
+        let updatedWith;
+
+        // Arrange
+        interaction.customId += " unclaimed";
+        interaction.user.id = "userId";
+        interaction.update.mockImplementation(async (opts: string | MessagePayload | InteractionUpdateOptions) => {
+            updatedWith = opts;
+
+            return mock<InteractionResponse<boolean>>();
+        });
+        // Act
+        await Use.Execute(interaction);
+
+        // Assert
+        expect(interaction.update).toHaveBeenCalledTimes(1);
+        expect(updatedWith).toMatchSnapshot();
+    });
+});
\ No newline at end of file
diff --git a/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap b/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap
new file mode 100644
index 0000000..6cec0f4
--- /dev/null
+++ b/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap
@@ -0,0 +1,95 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UseCancel GIVEN effectDetail is found, EXPECT interaction updated 1`] = `
+{
+  "components": [
+    {
+      "components": [
+        {
+          "custom_id": "effects use confirm unclaimed",
+          "disabled": true,
+          "emoji": undefined,
+          "label": "Confirm",
+          "style": 1,
+          "type": 2,
+        },
+        {
+          "custom_id": "effects use cancel unclaimed",
+          "disabled": true,
+          "emoji": undefined,
+          "label": "Cancel",
+          "style": 4,
+          "type": 2,
+        },
+      ],
+      "type": 1,
+    },
+  ],
+  "embeds": [
+    {
+      "color": 13882323,
+      "description": "The effect from your inventory has not been used",
+      "fields": [
+        {
+          "inline": true,
+          "name": "Effect",
+          "value": "Unclaimed Chance Up",
+        },
+        {
+          "inline": true,
+          "name": "Expires",
+          "value": "10m",
+        },
+      ],
+      "title": "Effect Use Cancelled",
+    },
+  ],
+}
+`;
+
+exports[`UseConfirm GIVEN EffectHelper.UseEffect succeeded, EXPECT interaction updated 1`] = `
+{
+  "components": [
+    {
+      "components": [
+        {
+          "custom_id": "effects use confirm unclaimed",
+          "disabled": true,
+          "emoji": undefined,
+          "label": "Confirm",
+          "style": 1,
+          "type": 2,
+        },
+        {
+          "custom_id": "effects use cancel unclaimed",
+          "disabled": true,
+          "emoji": undefined,
+          "label": "Cancel",
+          "style": 4,
+          "type": 2,
+        },
+      ],
+      "type": 1,
+    },
+  ],
+  "embeds": [
+    {
+      "color": 2263842,
+      "description": "You now have an active effect!",
+      "fields": [
+        {
+          "inline": true,
+          "name": "Effect",
+          "value": "Unclaimed Chance Up",
+        },
+        {
+          "inline": true,
+          "name": "Expires",
+          "value": "<t:600:f>",
+        },
+      ],
+      "title": "Effect Used",
+    },
+  ],
+}
+`;
diff --git a/tests/buttonEvents/Reroll.test.ts b/tests/buttonEvents/Reroll.test.ts
new file mode 100644
index 0000000..2021aac
--- /dev/null
+++ b/tests/buttonEvents/Reroll.test.ts
@@ -0,0 +1,7 @@
+describe("GIVEN valid conditions", () => {
+    test.todo("EXPECT user.RemoveCurrency to be called");
+
+    test.todo("GIVEN user is saved");
+});
+
+test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied");
\ No newline at end of file
diff --git a/tests/commands/__snapshots__/effects.test.ts.snap b/tests/commands/__snapshots__/effects.test.ts.snap
new file mode 100644
index 0000000..474b505
--- /dev/null
+++ b/tests/commands/__snapshots__/effects.test.ts.snap
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EXPECT CommandBuilder to be defined 1`] = `
+{
+  "contexts": undefined,
+  "default_member_permissions": undefined,
+  "default_permission": undefined,
+  "description": "Effects",
+  "description_localizations": undefined,
+  "dm_permission": undefined,
+  "integration_types": undefined,
+  "name": "effects",
+  "name_localizations": undefined,
+  "nsfw": undefined,
+  "options": [
+    {
+      "description": "List all effects I have",
+      "description_localizations": undefined,
+      "name": "list",
+      "name_localizations": undefined,
+      "options": [
+        {
+          "autocomplete": undefined,
+          "choices": undefined,
+          "description": "The page number",
+          "description_localizations": undefined,
+          "max_value": undefined,
+          "min_value": 1,
+          "name": "page",
+          "name_localizations": undefined,
+          "required": false,
+          "type": 10,
+        },
+      ],
+      "type": 1,
+    },
+    {
+      "description": "Use an effect in your inventory",
+      "description_localizations": undefined,
+      "name": "use",
+      "name_localizations": undefined,
+      "options": [
+        {
+          "autocomplete": undefined,
+          "choices": [
+            {
+              "name": "Unclaimed Chance Up",
+              "name_localizations": undefined,
+              "value": "unclaimed",
+            },
+          ],
+          "description": "The effect id to use",
+          "description_localizations": undefined,
+          "max_length": undefined,
+          "min_length": undefined,
+          "name": "id",
+          "name_localizations": undefined,
+          "required": true,
+          "type": 3,
+        },
+      ],
+      "type": 1,
+    },
+    {
+      "description": "Buy more effects",
+      "description_localizations": undefined,
+      "name": "buy",
+      "name_localizations": undefined,
+      "options": [
+        {
+          "autocomplete": undefined,
+          "choices": [
+            {
+              "name": "Unclaimed Chance Up",
+              "name_localizations": undefined,
+              "value": "unclaimed",
+            },
+          ],
+          "description": "The effect id to buy",
+          "description_localizations": undefined,
+          "max_length": undefined,
+          "min_length": undefined,
+          "name": "id",
+          "name_localizations": undefined,
+          "required": true,
+          "type": 3,
+        },
+        {
+          "autocomplete": undefined,
+          "choices": undefined,
+          "description": "The amount to buy",
+          "description_localizations": undefined,
+          "max_value": undefined,
+          "min_value": 1,
+          "name": "quantity",
+          "name_localizations": undefined,
+          "required": false,
+          "type": 10,
+        },
+      ],
+      "type": 1,
+    },
+  ],
+  "type": 1,
+}
+`;
diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts
new file mode 100644
index 0000000..81b5f4a
--- /dev/null
+++ b/tests/commands/drop.test.ts
@@ -0,0 +1,142 @@
+import { CommandInteraction } from "discord.js";
+import Drop from "../../src/commands/drop";
+import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock";
+import { CommandInteraction as CommandInteractionMock } from "../__types__/discord.js";
+import { CoreClient } from "../../src/client/client";
+import Config from "../../src/database/entities/app/Config";
+import User from "../../src/database/entities/app/User";
+import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper";
+import Inventory from "../../src/database/entities/app/Inventory";
+import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper";
+import CardConstants from "../../src/constants/CardConstants";
+import * as uuid from "uuid";
+
+jest.mock("../../src/database/entities/app/Config");
+jest.mock("../../src/database/entities/app/User");
+jest.mock("../../src/helpers/DropHelpers/GetCardsHelper");
+jest.mock("../../src/database/entities/app/Inventory");
+jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper");
+
+jest.mock("uuid");
+
+beforeEach(() => {
+    (Config.GetValue as jest.Mock).mockResolvedValue("false");
+});
+
+describe("execute", () => {
+    describe("GIVEN user is in the database", () => {
+        let interaction: CommandInteractionMock;
+        let user: User;
+        const randomCard = {
+            card: {
+                id: "cardId",
+                path: "https://google.com/",
+            }
+        };
+
+        beforeAll(async () => {
+            // Arrange
+            CoreClient.AllowDrops = true;
+
+            interaction = GenerateCommandInteractionMock();
+
+            user = {
+                Currency: 500,
+                RemoveCurrency: jest.fn().mockReturnValue(true),
+                Save: jest.fn(),
+            } as unknown as User;
+
+            (User.FetchOneById as jest.Mock).mockResolvedValue(user);
+            (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue(randomCard);
+            (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({
+                Quantity: 1,
+            });
+            (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockReturnValue({
+                type: "Embed",
+            });
+            (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockReturnValue({
+                type: "Button",
+            });
+
+            (uuid.v4 as jest.Mock).mockReturnValue("uuid");
+
+            // Act
+            const drop = new Drop();
+            await drop.execute(interaction as unknown as CommandInteraction);
+        });
+
+        test("EXPECT user to be fetched", () => {
+            expect(User.FetchOneById).toHaveBeenCalledTimes(1);
+            expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId");
+        });
+
+        test("EXPECT user.RemoveCurrency to be called", () => {
+            expect(user.RemoveCurrency).toHaveBeenCalledTimes(1);
+            expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost);
+        });
+
+        test("EXPECT user to be saved", () => {
+            expect(user.Save).toHaveBeenCalledTimes(1);
+            expect(user.Save).toHaveBeenCalledWith(User, user);
+        });
+
+        test("EXPECT random card to be fetched", () => {
+            expect(GetCardsHelper.FetchCard).toHaveBeenCalledTimes(1);
+            expect(GetCardsHelper.FetchCard).toHaveBeenCalledWith("userId");
+        });
+
+        test("EXPECT interaction to be deferred", () => {
+            expect(interaction.deferReply).toHaveBeenCalledTimes(1);
+        });
+
+        test("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called", () => {
+            expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledTimes(1);
+            expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledWith("userId", "cardId");
+        });
+
+        test("EXPECT DropEmbedHelper.GenerateDropEmbed to be called", () => {
+            expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledTimes(1);
+            expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledWith(randomCard, 1, "", undefined, 500);
+        });
+
+        test("EXPECT DropEmbedHelper.GenerateDropButtons to be called", () => {
+            expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledTimes(1);
+            expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledWith(randomCard, "uuid", "userId");
+        });
+
+        test("EXPECT interaction to be edited", () => {
+            expect(interaction.editReply).toHaveBeenCalledTimes(1);
+            expect(interaction.editReply).toHaveBeenCalledWith({
+                embeds: [ { type: "Embed" } ],
+                files: [],
+                components: [ { type: "Button" } ],
+            });
+        });
+
+        describe("AND randomCard path is not a url", () => {
+            test.todo("EXPECT image read from file system");
+
+            test.todo("EXPECT files on the embed to contain the image as an attachment");
+        });
+    });
+
+    describe("GIVEN user is not in the database", () => {
+        test.todo("EXPECT new user to be created");
+    });
+
+    describe("GIVEN user.RemoveCurrency fails", () => {
+        test.todo("EXPECT error replied");
+    });
+    
+    describe("GIVEN randomCard returns null", () => {
+        test.todo("EXPECT error logged");
+
+        test.todo("EXPECT error replied");
+    });
+
+    describe("GIVEN the code throws an error", () => {
+        test.todo("EXPECT error logged");
+
+        test.todo("EXPECT interaction edited with error");
+    });
+});
\ No newline at end of file
diff --git a/tests/commands/effects.test.ts b/tests/commands/effects.test.ts
new file mode 100644
index 0000000..33f612d
--- /dev/null
+++ b/tests/commands/effects.test.ts
@@ -0,0 +1,105 @@
+import Effects from "../../src/commands/effects";
+import List from "../../src/commands/effects/List";
+import Use from "../../src/commands/effects/Use";
+import Buy from "../../src/commands/effects/Buy";
+import AppLogger from "../../src/client/appLogger";
+import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock";
+import { CommandInteraction } from "discord.js";
+
+jest.mock("../../src/commands/effects/List");
+jest.mock("../../src/commands/effects/Use");
+jest.mock("../../src/commands/effects/Buy");
+jest.mock("../../src/client/appLogger");
+
+beforeEach(() => {
+    jest.resetAllMocks();
+});
+
+test("EXPECT CommandBuilder to be defined", async () => {
+    // Act
+    const effects = new Effects();
+
+    // Assert
+    expect(effects.CommandBuilder).toMatchSnapshot();
+});
+
+describe("execute", () => {
+    test("GIVEN interaction subcommand is list, EXPECT buy function called", async () => {
+        // Arrange
+        const interaction = GenerateCommandInteractionMock({
+            subcommand: "list",
+        });
+
+        // Act
+        const effects = new Effects();
+        await effects.execute(interaction as unknown as CommandInteraction);
+
+        // Assert
+        expect(List).toHaveBeenCalledTimes(1);
+        expect(List).toHaveBeenCalledWith(interaction);
+
+        expect(Use).not.toHaveBeenCalled();
+        expect(Buy).not.toHaveBeenCalled();
+
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN interaction subcommand is use, EXPECT buy function called", async () => {
+        // Arrange
+        const interaction = GenerateCommandInteractionMock({
+            subcommand: "use",
+        });
+
+        // Act
+        const effects = new Effects();
+        await effects.execute(interaction as unknown as CommandInteraction);
+
+        // Assert
+        expect(Use).toHaveBeenCalledTimes(1);
+        expect(Use).toHaveBeenCalledWith(interaction);
+
+        expect(List).not.toHaveBeenCalled();
+        expect(Buy).not.toHaveBeenCalled();
+
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN interaction subcommand is buy, EXPECT buy function called", async () => {
+        // Arrange
+        const interaction = GenerateCommandInteractionMock({
+            subcommand: "buy",
+        });
+
+        // Act
+        const effects = new Effects();
+        await effects.execute(interaction as unknown as CommandInteraction);
+
+        // Assert
+        expect(Buy).toHaveBeenCalledTimes(1);
+        expect(Buy).toHaveBeenCalledWith(interaction);
+
+        expect(List).not.toHaveBeenCalled();
+        expect(Use).not.toHaveBeenCalled();
+
+        expect(AppLogger.LogError).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN interaction subcommand is invalid, EXPECT error logged", async () => {
+        // Arrange
+        const interaction = GenerateCommandInteractionMock({
+            subcommand: "invalid",
+        });
+
+        // Act
+        const effects = new Effects();
+        await effects.execute(interaction as unknown as CommandInteraction);
+
+        // Assert
+        expect(AppLogger.LogError).toHaveBeenCalledTimes(1);
+        expect(AppLogger.LogError).toHaveBeenCalledWith("Commands/Effects", "Invalid subcommand: invalid");
+
+        expect(List).not.toHaveBeenCalled();
+        expect(Use).not.toHaveBeenCalled();
+        expect(Buy).not.toHaveBeenCalled();
+    });
+});
\ No newline at end of file
diff --git a/tests/commands/effects/Buy.test.ts b/tests/commands/effects/Buy.test.ts
new file mode 100644
index 0000000..87e4219
--- /dev/null
+++ b/tests/commands/effects/Buy.test.ts
@@ -0,0 +1,9 @@
+jest.mock("../../../src/helpers/EffectHelper");
+
+describe("Buy", () => {
+    test.todo("GIVEN result returns a string, EXPECT interaction replied with string");
+
+    test.todo("GIVEN result returns an embed, EXPECT interaction replied with embed and row");
+
+    test.todo("GIVEN quantity option is not supplied, EXPECT quantity to default to 1");
+});
diff --git a/tests/database/entities/app/UserEffect.test.ts b/tests/database/entities/app/UserEffect.test.ts
deleted file mode 100644
index 66992ec..0000000
--- a/tests/database/entities/app/UserEffect.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import UserEffect from "../../../../src/database/entities/app/UserEffect";
-
-let userEffect: UserEffect;
-const now = new Date();
-
-beforeEach(() => {
-    userEffect = new UserEffect("name", "userId", 1);
-});
-
-describe("AddUnused", () => {
-    beforeEach(() => {
-        userEffect.AddUnused(1);
-    });
-
-    test("EXPECT unused to be the amount more", () => {
-        expect(userEffect.Unused).toBe(2);
-    });
-});
-
-describe("UseEffect", () => {
-    describe("GIVEN Unused is 0", () => {
-        let result: boolean;
-
-        beforeEach(() => {
-            userEffect.Unused = 0;
-
-            result = userEffect.UseEffect(now);
-        });
-
-        test("EXPECT false returned", () => {
-            expect(result).toBe(false);
-        });
-
-        test("EXPECT details not to be changed", () => {
-            expect(userEffect.Unused).toBe(0);
-            expect(userEffect.WhenExpires).toBeUndefined();
-        });
-    });
-
-    describe("GIVEN Unused is greater than 0", () => {
-        let result: boolean;
-
-        beforeEach(() => {
-            result = userEffect.UseEffect(now);
-        });
-
-        test("EXPECT true returned", () => {
-            expect(result).toBe(true);
-        });
-
-        test("EXPECT Unused to be subtracted by 1", () => {
-            expect(userEffect.Unused).toBe(0);
-        });
-
-        test("EXPECT WhenExpires to be set", () => {
-            expect(userEffect.WhenExpires).toBe(now);
-        });
-    });
-});
-
-describe("IsEffectActive", () => {
-    describe("GIVEN WhenExpires is null", () => {
-        let result: boolean;
-
-        beforeEach(() => {
-            result = userEffect.IsEffectActive();
-        });
-
-        test("EXPECT false returned", () => {
-            expect(result).toBe(false);
-        });
-    });
-
-    describe("GIVEN WhenExpires is defined", () => {
-        describe("AND WhenExpires is in the past", () => {
-            let result: boolean;
-
-            beforeEach(() => {
-                userEffect.WhenExpires = new Date(now.getTime() - 100);
-
-                result = userEffect.IsEffectActive();
-            });
-
-            test("EXPECT false returned", () => {
-                expect(result).toBe(false);
-            });
-        });
-
-        describe("AND WhenExpires is in the future", () => {
-            let result: boolean;
-
-            beforeEach(() => {
-                userEffect.WhenExpires = new Date(now.getTime() + 100);
-
-                result = userEffect.IsEffectActive();
-            });
-
-            test("EXPECT true returned", () => {
-                expect(result).toBe(true);
-            });
-        });
-    });
-});
diff --git a/tests/helpers/DropHelpers/DropEmbedHelper.test.ts b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts
new file mode 100644
index 0000000..743f6c7
--- /dev/null
+++ b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts
@@ -0,0 +1,3 @@
+describe("GenerateDropButtons", () => {
+    test.todo("EXPECT row to be returned");
+});
\ No newline at end of file
diff --git a/tests/helpers/DropHelpers/GetCardsHelper.test.ts b/tests/helpers/DropHelpers/GetCardsHelper.test.ts
new file mode 100644
index 0000000..421d4ef
--- /dev/null
+++ b/tests/helpers/DropHelpers/GetCardsHelper.test.ts
@@ -0,0 +1,68 @@
+import GetCardsHelper from "../../../src/helpers/DropHelpers/GetCardsHelper";
+import EffectHelper from "../../../src/helpers/EffectHelper";
+import GetUnclaimedCardsHelper from "../../../src/helpers/DropHelpers/GetUnclaimedCardsHelper";
+import CardConstants from "../../../src/constants/CardConstants";
+
+jest.mock("../../../src/helpers/EffectHelper");
+jest.mock("../../../src/helpers/DropHelpers/GetUnclaimedCardsHelper");
+
+beforeEach(() => {
+    jest.resetAllMocks();
+});
+
+describe("FetchCard", () => {
+    test("GIVEN user has the unclaimed effect AND unused chance is within constraint, EXPECT unclaimed card returned", async () => {
+        // Arrange
+        (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(true);
+        GetCardsHelper.GetRandomCard = jest.fn();
+        Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance - 0.1);
+
+        // Act
+        await GetCardsHelper.FetchCard("userId");
+
+        // Assert
+        expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed");
+
+        expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).toHaveBeenCalledTimes(1);
+        expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).toHaveBeenCalledWith("userId");
+
+        expect(GetCardsHelper.GetRandomCard).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN user has unclaimed effect AND unused chance is NOT within constraint, EXPECT random card returned", async () => {
+        // Arrange
+        (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(true);
+        GetCardsHelper.GetRandomCard = jest.fn();
+        Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance + 0.1);
+
+        // Act
+        await GetCardsHelper.FetchCard("userId");
+
+        // Assert
+        expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed");
+
+        expect(GetCardsHelper.GetRandomCard).toHaveBeenCalledTimes(1);
+
+        expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).not.toHaveBeenCalled();
+    });
+
+    test("GIVEN user does NOT have unclaimed effect, EXPECT random card returned", async () => {
+        // Arrange
+        (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(false);
+        GetCardsHelper.GetRandomCard = jest.fn();
+        Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance + 0.1);
+
+        // Act
+        await GetCardsHelper.FetchCard("userId");
+
+        // Assert
+        expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1);
+        expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed");
+
+        expect(GetCardsHelper.GetRandomCard).toHaveBeenCalledTimes(1);
+
+        expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).not.toHaveBeenCalled();
+    });
+});
\ No newline at end of file
diff --git a/tests/helpers/EffectHelper.test.ts b/tests/helpers/EffectHelper.test.ts
index 343f06c..dcab744 100644
--- a/tests/helpers/EffectHelper.test.ts
+++ b/tests/helpers/EffectHelper.test.ts
@@ -1,281 +1,127 @@
-import UserEffect from "../../src/database/entities/app/UserEffect";
 import EffectHelper from "../../src/helpers/EffectHelper";
+import UserEffect from "../../src/database/entities/app/UserEffect";
 
-describe("AddEffectToUserInventory", () => {
-    describe("GIVEN effect is in database", () => {
-        const effectMock = {
-            AddUnused: jest.fn(),
-            Save: jest.fn(),
-        };
+jest.mock("../../src/database/entities/app/UserEffect");
 
-        beforeAll(async () => {
-            UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock);
+describe("GenerateEffectListEmbed", () => {
+    test("GIVEN user has an effect, EXPECT detailed embed to be returned", async () => {
+        // Arrange
+        (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([
+            [
+                {
+                    Name: "unclaimed",
+                    Unused: 1,
+                }
+            ],
+            1,
+        ]);
 
-            await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
-        });
+        // Act
+        const result = await EffectHelper.GenerateEffectListEmbed("userId", 1);
 
-        test("EXPECT database to be fetched", () => {
-            expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledTimes(1);
-            expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledWith("userId", "name");
-        });
-
-        test("EXPECT effect to be updated", () => {
-            expect(effectMock.AddUnused).toHaveBeenCalledTimes(1);
-            expect(effectMock.AddUnused).toHaveBeenCalledWith(1);
-        });
-
-        test("EXPECT effect to be saved", () => {
-            expect(effectMock.Save).toHaveBeenCalledTimes(1);
-            expect(effectMock.Save).toHaveBeenCalledWith(UserEffect, effectMock);
-        });
+        // Assert
+        expect(result).toMatchSnapshot();
     });
 
-    describe("GIVEN effect is not in database", () => {
-        beforeAll(async () => {
-            UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
-            UserEffect.prototype.Save = jest.fn();
+    test("GIVEN user has more than 1 page of effects, EXPECT pagination enabled", async () => {
+        const effects: {
+            Name: string,
+            Unused: number,
+        }[] = [];
 
-            await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
+        for (let i = 0; i < 15; i++) {
+            effects.push({
+                Name: "unclaimed",
+                Unused: 1,
+            });
+        }
+
+        // Arrange
+        (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([
+            effects,
+            15,
+        ]);
+
+        // Act
+        const result = await EffectHelper.GenerateEffectListEmbed("userId", 1);
+
+        // Assert
+        expect(result).toMatchSnapshot();
+    });
+
+    test("GIVEN user is on a page other than 1, EXPECT pagination enabled", async () => {
+        const effects: {
+            Name: string,
+            Unused: number,
+        }[] = [];
+
+        for (let i = 0; i < 15; i++) {
+            effects.push({
+                Name: "unclaimed",
+                Unused: 1,
+            });
+        }
+
+        // Arrange
+        (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([
+            effects,
+            15,
+        ]);
+
+        // Act
+        const result = await EffectHelper.GenerateEffectListEmbed("userId", 2);
+
+        // Assert
+        expect(result).toMatchSnapshot();
+    });
+
+    test("GIVEN user does NOT have an effect, EXPECT empty embed to be returned", async () => {
+        // Arrange
+        (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([
+            [],
+            0,
+        ]);
+
+        // Act
+        const result = await EffectHelper.GenerateEffectListEmbed("userId", 1);
+
+        // Assert
+        expect(result).toMatchSnapshot();
+    });
+
+    test("GIVEN there is an active effect, EXPECT field added", async () => {
+        // Arrange
+        (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([
+            [
+                {
+                    Name: "unclaimed",
+                    Unused: 1,
+                }
+            ],
+            1,
+        ]);
+
+        (UserEffect.FetchActiveEffectByUserId as jest.Mock).mockResolvedValue({
+            Name: "unclaimed",
+            WhenExpires: new Date(1738174052),
         });
 
-        test("EXPECT effect to be saved", () => {
-            expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1);
-            expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect));
-        });
+        // Act
+        const result = await EffectHelper.GenerateEffectListEmbed("userId", 1);
+
+        // Assert
+        expect(result).toMatchSnapshot();
     });
 });
 
-describe("UseEffect", () => {
-    describe("GIVEN effect is in database", () => {
-        describe("GIVEN now is before effect.WhenExpires", () => {
-            let result: boolean | undefined;
+describe("GenerateEffectBuyEmbed", () => {
+    test.todo("GIVEN Effect Details are not found, EXPECT error");
 
-            // nowMock < whenExpires
-            const nowMock = new Date(2024, 11, 3, 13, 30);
-            const whenExpires = new Date(2024, 11, 3, 14, 0);
+    test.todo("GIVEN user is not in database, EXPECT blank user created");
 
-            const userEffect = {
-                Unused: 1,
-                WhenExpires: whenExpires,
-            };
+    test.todo("GIVEN user does not have enough currency, EXPECT error");
 
-            beforeAll(async () => {
-                jest.setSystemTime(nowMock);
+    test.todo("GIVEN user does have enough currency, EXPECT embed returned");
 
-                UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                result = await EffectHelper.UseEffect("userId", "name", new Date());
-            });
-
-            test("EXPECT false returned", () => {
-                expect(result).toBe(false);
-            });
-        });
-
-        describe("GIVEN currently used effect is inactive", () => {
-            let result: boolean | undefined;
-
-            // nowMock > whenExpires
-            const nowMock = new Date(2024, 11, 3, 13, 30);
-            const whenExpires = new Date(2024, 11, 3, 13, 0);
-            const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
-
-            const userEffect = {
-                Unused: 1,
-                WhenExpires: whenExpires,
-                UseEffect: jest.fn(),
-                Save: jest.fn(),
-            };
-
-            beforeAll(async () => {
-                jest.setSystemTime(nowMock);
-
-                UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
-            });
-
-            test("EXPECT UseEffect to be called", () => {
-                expect(userEffect.UseEffect).toHaveReturnedTimes(1);
-                expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
-            });
-
-            test("EXPECT effect to be saved", () => {
-                expect(userEffect.Save).toHaveBeenCalledTimes(1);
-                expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
-            });
-
-            test("EXPECT true returned", () => {
-                expect(result).toBe(true);
-            });
-        });
-
-        describe("GIVEN effect.WhenExpires is null", () => {
-            let result: boolean | undefined;
-
-            // nowMock > whenExpires
-            const nowMock = new Date(2024, 11, 3, 13, 30);
-            const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
-
-            const userEffect = {
-                Unused: 1,
-                WhenExpires: null,
-                UseEffect: jest.fn(),
-                Save: jest.fn(),
-            };
-
-            beforeAll(async () => {
-                jest.setSystemTime(nowMock);
-
-                UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
-            });
-
-            test("EXPECT UseEffect to be called", () => {
-                expect(userEffect.UseEffect).toHaveBeenCalledTimes(1);
-                expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
-            });
-
-            test("EXPECT effect to be saved", () => {
-                expect(userEffect.Save).toHaveBeenCalledTimes(1);
-                expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
-            });
-
-            test("EXPECT true returned", () => {
-                expect(result).toBe(true);
-            });
-        });
-    });
-
-    describe("GIVEN effect is not in database", () => {
-        let result: boolean | undefined;
-
-        // nowMock > whenExpires
-        const nowMock = new Date(2024, 11, 3, 13, 30);
-        const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
-
-        beforeAll(async () => {
-            jest.setSystemTime(nowMock);
-
-            UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
-
-            result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
-        });
-
-        test("EXPECT false returned", () => {
-            expect(result).toBe(false);
-        });
-    });
-
-    describe("GIVEN effect.Unused is 0", () => {
-        let result: boolean | undefined;
-
-        // nowMock > whenExpires
-        const nowMock = new Date(2024, 11, 3, 13, 30);
-        const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
-
-        const userEffect = {
-            Unused: 0,
-            WhenExpires: null,
-            UseEffect: jest.fn(),
-            Save: jest.fn(),
-        };
-
-        beforeAll(async () => {
-            jest.setSystemTime(nowMock);
-
-            UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-            result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
-        });
-
-        test("EXPECT false returned", () => {
-            expect(result).toBe(false);
-        });
-    });
-});
-
-describe("HasEffect", () => {
-    describe("GIVEN effect is in database", () => {
-        describe("GIVEN effect.WhenExpires is defined", () => {
-            describe("GIVEN now is before effect.WhenExpires", () => {
-                let result: boolean | undefined;
-
-                const nowMock = new Date(2024, 11, 3, 13, 30);
-                const whenExpires = new Date(2024, 11, 3, 15, 0);
-
-                const userEffect = {
-                    WhenExpires: whenExpires,
-                };
-
-                beforeAll(async () => {
-                    jest.setSystemTime(nowMock);
-
-                    UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                    result = await EffectHelper.HasEffect("userId", "name");
-                });
-
-                test("EXPECT true returned", () => {
-                    expect(result).toBe(true);
-                });
-            });
-
-            describe("GIVEN now is after effect.WhenExpires", () => {
-                let result: boolean | undefined;
-
-                const nowMock = new Date(2024, 11, 3, 16, 30);
-                const whenExpires = new Date(2024, 11, 3, 15, 0);
-
-                const userEffect = {
-                    WhenExpires: whenExpires,
-                };
-
-                beforeAll(async () => {
-                    jest.setSystemTime(nowMock);
-
-                    UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                    result = await EffectHelper.HasEffect("userId", "name");
-                });
-
-                test("EXPECT false returned", () => {
-                    expect(result).toBe(false);
-                });
-            });
-        });
-
-        describe("GIVEN effect.WhenExpires is undefined", () => {
-            let result: boolean | undefined;
-
-            const userEffect = {
-                WhenExpires: undefined,
-            };
-
-            beforeAll(async () => {
-                UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
-
-                result = await EffectHelper.HasEffect("userId", "name");
-            });
-
-            test("EXPECT false returned", () => {
-                expect(result).toBe(false);
-            });
-        });
-    });
-
-    describe("GIVEN effect is not in database", () => {
-        let result: boolean | undefined;
-
-        beforeAll(async () => {
-            UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
-
-            result = await EffectHelper.HasEffect("userId", "name");
-        });
-
-        test("EXPECT false returned", () => {
-            expect(result).toBe(false);
-        });
-    });
-});
+    test.todo("GIVEN disabled boolean is true, EXPECT buttons to be disabled");
+});
\ No newline at end of file
diff --git a/tests/helpers/TimeLengthInput.test.ts b/tests/helpers/TimeLengthInput.test.ts
new file mode 100644
index 0000000..6a23d67
--- /dev/null
+++ b/tests/helpers/TimeLengthInput.test.ts
@@ -0,0 +1,38 @@
+import TimeLengthInput from "../../src/helpers/TimeLengthInput";
+
+describe("ConvertFromMilliseconds", () => {
+    test("EXPECT 1000ms to be outputted as a second", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(1000);
+        expect(timeLength.GetLengthShort()).toBe("1s");
+    });
+
+    test("EXPECT 60000ms to be outputted as a minute", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(60000);
+        expect(timeLength.GetLengthShort()).toBe("1m");
+    });
+
+    test("EXPECT 3600000ms to be outputted as an hour", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(3600000);
+        expect(timeLength.GetLengthShort()).toBe("1h");
+    });
+
+    test("EXPECT 86400000ms to be outputted as a day", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(86400000);
+        expect(timeLength.GetLengthShort()).toBe("1d");
+    });
+
+    test("EXPECT a combination to be outputted correctly", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(90061000);
+        expect(timeLength.GetLengthShort()).toBe("1d 1h 1m 1s");
+    });
+
+    test("EXPECT 0ms to be outputted as empty", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(0);
+        expect(timeLength.GetLengthShort()).toBe("");
+    });
+
+    test("EXPECT 123456789ms to be outputted correctly", () => {
+        const timeLength = TimeLengthInput.ConvertFromMilliseconds(123456789);
+        expect(timeLength.GetLengthShort()).toBe("1d 10h 17m 36s");
+    });
+});
\ No newline at end of file
diff --git a/tests/helpers/__snapshots__/EffectHelper.test.ts.snap b/tests/helpers/__snapshots__/EffectHelper.test.ts.snap
new file mode 100644
index 0000000..fc3317e
--- /dev/null
+++ b/tests/helpers/__snapshots__/EffectHelper.test.ts.snap
@@ -0,0 +1,216 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GenerateEffectListEmbed GIVEN there is an active effect, EXPECT field added 1`] = `
+{
+  "embed": {
+    "color": 3166394,
+    "description": "Unclaimed Chance Up x1",
+    "fields": [
+      {
+        "inline": true,
+        "name": "Active",
+        "value": "Unclaimed Chance Up",
+      },
+      {
+        "inline": true,
+        "name": "Expires",
+        "value": "<t:1738174>",
+      },
+    ],
+    "footer": {
+      "icon_url": undefined,
+      "text": "Page 1 of 1",
+    },
+    "title": "Effects",
+  },
+  "row": {
+    "components": [
+      {
+        "custom_id": "effects list 0",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Previous",
+        "style": 1,
+        "type": 2,
+      },
+      {
+        "custom_id": "effects list 2",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Next",
+        "style": 1,
+        "type": 2,
+      },
+    ],
+    "type": 1,
+  },
+}
+`;
+
+exports[`GenerateEffectListEmbed GIVEN user does NOT have an effect, EXPECT empty embed to be returned 1`] = `
+{
+  "embed": {
+    "color": 3166394,
+    "description": "*none*",
+    "footer": {
+      "icon_url": undefined,
+      "text": "Page 1 of 1",
+    },
+    "title": "Effects",
+  },
+  "row": {
+    "components": [
+      {
+        "custom_id": "effects list 0",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Previous",
+        "style": 1,
+        "type": 2,
+      },
+      {
+        "custom_id": "effects list 2",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Next",
+        "style": 1,
+        "type": 2,
+      },
+    ],
+    "type": 1,
+  },
+}
+`;
+
+exports[`GenerateEffectListEmbed GIVEN user has an effect, EXPECT detailed embed to be returned 1`] = `
+{
+  "embed": {
+    "color": 3166394,
+    "description": "Unclaimed Chance Up x1",
+    "footer": {
+      "icon_url": undefined,
+      "text": "Page 1 of 1",
+    },
+    "title": "Effects",
+  },
+  "row": {
+    "components": [
+      {
+        "custom_id": "effects list 0",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Previous",
+        "style": 1,
+        "type": 2,
+      },
+      {
+        "custom_id": "effects list 2",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Next",
+        "style": 1,
+        "type": 2,
+      },
+    ],
+    "type": 1,
+  },
+}
+`;
+
+exports[`GenerateEffectListEmbed GIVEN user has more than 1 page of effects, EXPECT pagination enabled 1`] = `
+{
+  "embed": {
+    "color": 3166394,
+    "description": "Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1",
+    "footer": {
+      "icon_url": undefined,
+      "text": "Page 1 of 2",
+    },
+    "title": "Effects",
+  },
+  "row": {
+    "components": [
+      {
+        "custom_id": "effects list 0",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Previous",
+        "style": 1,
+        "type": 2,
+      },
+      {
+        "custom_id": "effects list 2",
+        "disabled": false,
+        "emoji": undefined,
+        "label": "Next",
+        "style": 1,
+        "type": 2,
+      },
+    ],
+    "type": 1,
+  },
+}
+`;
+
+exports[`GenerateEffectListEmbed GIVEN user is on a page other than 1, EXPECT pagination enabled 1`] = `
+{
+  "embed": {
+    "color": 3166394,
+    "description": "Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1
+Unclaimed Chance Up x1",
+    "footer": {
+      "icon_url": undefined,
+      "text": "Page 2 of 2",
+    },
+    "title": "Effects",
+  },
+  "row": {
+    "components": [
+      {
+        "custom_id": "effects list 1",
+        "disabled": false,
+        "emoji": undefined,
+        "label": "Previous",
+        "style": 1,
+        "type": 2,
+      },
+      {
+        "custom_id": "effects list 3",
+        "disabled": true,
+        "emoji": undefined,
+        "label": "Next",
+        "style": 1,
+        "type": 2,
+      },
+    ],
+    "type": 1,
+  },
+}
+`;
diff --git a/tests/timers/PurgeClaims.test.ts b/tests/timers/PurgeClaims.test.ts
new file mode 100644
index 0000000..6d0d09b
--- /dev/null
+++ b/tests/timers/PurgeClaims.test.ts
@@ -0,0 +1,7 @@
+describe("PurgeClaims", () => {
+    test.todo("EXPECT claims to be fetched");
+
+    test.todo("EXPECT Claim.RemoveMany to remove the claims older than 2 minutes");
+
+    test.todo("EXPECT info logged");
+});
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 0313723..c63e681 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1591,6 +1591,15 @@ await-to-js@^3.0.0:
   resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-3.0.0.tgz#70929994185616f4675a91af6167eb61cc92868f"
   integrity sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==
 
+axios@^1.8.4:
+  version "1.8.4"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447"
+  integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==
+  dependencies:
+    follow-redirects "^1.15.6"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 babel-jest@^29.7.0:
   version "29.7.0"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
@@ -1794,6 +1803,14 @@ bytes@3.1.2:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
   integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
 
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+  integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+  dependencies:
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+
 call-bind@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@@ -2378,6 +2395,15 @@ dotenv@^16.0.0, dotenv@^16.0.3:
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
   integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
 
+dunder-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
+
 eastasianwidth@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -2454,12 +2480,39 @@ es-define-property@^1.0.0:
   dependencies:
     get-intrinsic "^1.2.4"
 
+es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
 es-errors@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
   integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
 
-escalade@^3.1.1, escalade@^3.2.0:
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+  integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+  dependencies:
+    es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+  integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+  dependencies:
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.6"
+    has-tostringtag "^1.0.2"
+    hasown "^2.0.2"
+
+escalade@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+  integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+
+escalade@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
   integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
@@ -2874,6 +2927,11 @@ fn.name@1.x.x:
   resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
   integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
 
+follow-redirects@^1.15.6:
+  version "1.15.9"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
+  integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
+
 foreground-child@^3.1.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
@@ -2891,6 +2949,16 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+form-data@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
+  integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    es-set-tostringtag "^2.1.0"
+    mime-types "^2.1.12"
+
 formidable@^1.2.1:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
@@ -2974,11 +3042,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
     has-symbols "^1.0.3"
     hasown "^2.0.0"
 
+get-intrinsic@^1.2.6:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+  dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    es-define-property "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.1.1"
+    function-bind "^1.1.2"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.1.0"
+
 get-package-type@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
   integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
 
+get-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+  dependencies:
+    dunder-proto "^1.0.1"
+    es-object-atoms "^1.0.0"
+
 get-stream@^6.0.0:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@@ -3087,6 +3179,11 @@ gopd@^1.0.1:
   dependencies:
     get-intrinsic "^1.1.3"
 
+gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
 graceful-fs@4.2.10:
   version "4.2.10"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
@@ -3136,6 +3233,18 @@ has-symbols@^1.0.3:
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
   integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
 
+has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+  integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+  dependencies:
+    has-symbols "^1.0.3"
+
 has-unicode@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -4328,6 +4437,11 @@ makeerror@1.0.12:
   dependencies:
     tmpl "1.0.5"
 
+math-intrinsics@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -4418,6 +4532,13 @@ mimic-response@^2.0.0:
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
   integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
 
+minimatch@9.0.5, minimatch@^9.0.0, minimatch@^9.0.4:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimatch@^10.0.0:
   version "10.0.1"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
@@ -4439,13 +4560,6 @@ minimatch@^5.0.1:
   dependencies:
     brace-expansion "^2.0.1"
 
-minimatch@^9.0.0, minimatch@^9.0.4:
-  version "9.0.5"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
-  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
-  dependencies:
-    brace-expansion "^2.0.1"
-
 minimist@^1.2.0:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -5007,7 +5121,12 @@ peek-readable@^4.1.0:
   resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
   integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
 
-picocolors@^1.0.0, picocolors@^1.1.0:
+picocolors@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
+  integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+
+picocolors@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
   integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -5098,6 +5217,11 @@ proxy-addr@~2.0.7:
     forwarded "0.2.0"
     ipaddr.js "1.9.1"
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 punycode@^2.1.0:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -5997,7 +6121,12 @@ type-fest@^3.0.0:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706"
   integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==
 
-type-fest@^4.18.2, type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1:
+type-fest@^4.18.2:
+  version "4.40.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.40.0.tgz#62bc09caccb99a75e1ad6b9b4653e8805e5e1eee"
+  integrity sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==
+
+type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1:
   version "4.26.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
   integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==
@@ -6412,6 +6541,6 @@ yoctocolors-cjs@^2.1.2:
   integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
 
 zod@^3.23.8:
-  version "3.23.8"
-  resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
-  integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
+  version "3.24.3"
+  resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87"
+  integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==