Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
478 changes: 478 additions & 0 deletions LOYALTY_BADGE_IMPLEMENTATION.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const vaultRegistryRoutes = require("./routes/vaultRegistry");
const contractUpgradeRoutes = require("./routes/contractUpgrade");
const conversionAnalyticsRoutes = require("./routes/conversionAnalytics");
const correlationRoutes = require("./routes/correlationRoutes");
const loyaltyBadgeRoutes = require("./routes/loyaltyBadgeRoutes");

app.get("/", (req, res) => {
res.json({ message: "Vesting Vault API is running!" });
Expand Down Expand Up @@ -401,6 +402,9 @@ app.use("/api/conversions", conversionAnalyticsRoutes);
// Mount TVL-price correlation analysis routes
app.use("/api/correlation", correlationRoutes);

// Mount loyalty badge routes
app.use("/api/loyalty-badges", loyaltyBadgeRoutes);

// Historical price tracking job management endpoints
app.post("/api/admin/jobs/historical-prices/start", async (req, res) => {
try {
Expand Down
123 changes: 123 additions & 0 deletions backend/src/models/LoyaltyBadge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const { DataTypes } = require("sequelize");
const { sequelize } = require("../database/connection");

const LoyaltyBadge = sequelize.define(
"LoyaltyBadge",
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
beneficiary_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: "beneficiaries",
key: "id",
},
onUpdate: "CASCADE",
onDelete: "CASCADE",
comment: "Reference to the beneficiary who earned this badge",
},
badge_type: {
type: DataTypes.ENUM('diamond_hands', 'platinum_hodler', 'gold_holder', 'silver_holder'),
allowNull: false,
defaultValue: 'diamond_hands',
comment: "Type of loyalty badge earned",
},
awarded_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: "Timestamp when the badge was awarded",
},
retention_period_days: {
type: DataTypes.INTEGER,
allowNull: false,
comment: "Number of days the beneficiary maintained 100% retention",
},
initial_vested_amount: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
comment: "Initial amount of vested tokens when monitoring started",
},
current_balance: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
comment: "Current token balance at time of badge award",
},
nft_metadata_uri: {
type: DataTypes.STRING,
allowNull: true,
comment: "URI to NFT metadata if badge is minted as NFT",
},
discord_role_granted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: "Flag indicating if Discord role was granted",
},
priority_access_granted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: "Flag indicating if priority access was granted",
},
monitoring_start_date: {
type: DataTypes.DATE,
allowNull: false,
comment: "Date when balance monitoring started for this badge",
},
last_balance_check: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: "Last time the balance was checked",
},
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: "Flag indicating if the badge is still active",
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
tableName: "loyalty_badges",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{
fields: ["beneficiary_id"],
},
{
fields: ["badge_type"],
},
{
fields: ["awarded_at"],
},
{
fields: ["beneficiary_id", "badge_type"],
unique: true,
},
],
},
);

LoyaltyBadge.associate = function (models) {
LoyaltyBadge.belongsTo(models.Beneficiary, {
foreignKey: 'beneficiary_id',
as: 'beneficiary'
});
};

module.exports = LoyaltyBadge;
2 changes: 2 additions & 0 deletions backend/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ContractUpgradeAuditLog = require("./contractUpgradeAuditLog");
const CertifiedBuild = require("./certifiedBuild");
const ConversionEvent = require("./conversionEvent");
const MilestoneCelebrationWebhook = require("./milestoneCelebrationWebhook");
const LoyaltyBadge = require("./loyaltyBadge");

const { Token, initTokenModel } = require("./token");
const {
Expand Down Expand Up @@ -84,6 +85,7 @@ const models = {
CertifiedBuild,
ConversionEvent,
MilestoneCelebrationWebhook,
LoyaltyBadge,
sequelize,
};

Expand Down
179 changes: 179 additions & 0 deletions backend/src/routes/loyaltyBadgeRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const express = require('express');
const router = express.Router();
const BeneficiaryLoyaltyBadgeService = require('../services/beneficiaryLoyaltyBadgeService');
const authService = require('../services/authService');

const loyaltyBadgeService = new BeneficiaryLoyaltyBadgeService();

// POST /api/loyalty-badges/monitoring/start
// Start monitoring a beneficiary for Diamond Hands badge
router.post(
'/monitoring/start',
authService.authenticate(true), // Require authentication
async (req, res) => {
try {
const { beneficiaryId, startDate } = req.body;

if (!beneficiaryId) {
return res.status(400).json({
success: false,
message: 'beneficiaryId is required'
});
}

const result = await loyaltyBadgeService.startMonitoring(beneficiaryId, startDate);

res.json(result);
} catch (error) {
console.error('Error starting loyalty badge monitoring:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// POST /api/loyalty-badges/monitoring/check
// Check and update all active monitoring records (admin only)
router.post(
'/monitoring/check',
authService.authenticate(true), // Require admin authentication
async (req, res) => {
try {
const result = await loyaltyBadgeService.checkAndUpdateRetentionPeriods();

res.json({
success: true,
message: 'Monitoring check completed',
data: result
});
} catch (error) {
console.error('Error checking retention periods:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// GET /api/loyalty-badges/beneficiary/:beneficiaryId
// Get all badges for a specific beneficiary
router.get(
'/beneficiary/:beneficiaryId',
authService.authenticate(true), // Require authentication
async (req, res) => {
try {
const { beneficiaryId } = req.params;
const badges = await loyaltyBadgeService.getBeneficiaryBadges(beneficiaryId);

res.json({
success: true,
data: badges
});
} catch (error) {
console.error('Error fetching beneficiary badges:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// GET /api/loyalty-badges/diamond-hands
// Get all Diamond Hands badge holders (admin only)
router.get(
'/diamond-hands',
authService.authenticate(true), // Require admin authentication
async (req, res) => {
try {
const holders = await loyaltyBadgeService.getDiamondHandsHolders();

res.json({
success: true,
data: holders
});
} catch (error) {
console.error('Error fetching Diamond Hands holders:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// GET /api/loyalty-badges/statistics
// Get monitoring statistics (admin only)
router.get(
'/statistics',
authService.authenticate(true), // Require admin authentication
async (req, res) => {
try {
const stats = await loyaltyBadgeService.getMonitoringStatistics();

res.json({
success: true,
data: stats
});
} catch (error) {
console.error('Error fetching monitoring statistics:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// POST /api/loyalty-badges/:badgeId/award
// Manually award a badge (admin only)
router.post(
'/:badgeId/award',
authService.authenticate(true), // Require admin authentication
async (req, res) => {
try {
const { badgeId } = req.params;
const result = await loyaltyBadgeService.awardDiamondHandsBadge(badgeId);

res.json(result);
} catch (error) {
console.error('Error awarding badge:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

// GET /api/loyalty-badges/balance/:walletAddress
// Get current wallet balance (helper endpoint)
router.get(
'/balance/:walletAddress',
authService.authenticate(true), // Require authentication
async (req, res) => {
try {
const { walletAddress } = req.params;
const balance = await loyaltyBadgeService.getWalletBalance(walletAddress);

res.json({
success: true,
data: {
walletAddress,
balance: balance.toString()
}
});
} catch (error) {
console.error('Error fetching wallet balance:', error);
res.status(500).json({
success: false,
message: error.message
});
}
}
);

module.exports = router;
Loading
Loading