Some checks failed
🚀 Continuous Integration / 🔧 Backend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🔧 Backend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🔍 Code Quality (push) Has been cancelled
🚀 Continuous Integration / 🔒 Security Checks (push) Has been cancelled
🚀 Continuous Integration / 🎨 Theme Tests (push) Has been cancelled
🚀 Continuous Integration / ♿ Accessibility Tests (push) Has been cancelled
🚀 Continuous Integration / 📱 Cross-Browser Tests (push) Has been cancelled
🚀 Continuous Integration / 🏗️ Build Tests (push) Has been cancelled
🚀 Continuous Integration / 📊 Performance Tests (push) Has been cancelled
🚀 Continuous Integration / 🎯 Integration Tests (push) Has been cancelled
🚀 Continuous Integration / ✅ All Tests Passed (push) Has been cancelled
359 lines
8.2 KiB
JavaScript
359 lines
8.2 KiB
JavaScript
const mongoose = require('mongoose');
|
|
|
|
// Schema für Belohnungen
|
|
const rewardSchema = new mongoose.Schema({
|
|
title: {
|
|
type: String,
|
|
required: true,
|
|
trim: true,
|
|
maxlength: 100
|
|
},
|
|
description: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 500
|
|
},
|
|
icon: {
|
|
type: String,
|
|
default: 'gift',
|
|
enum: [
|
|
'gift', 'tv', 'game', 'movie', 'ice-cream', 'toy', 'book',
|
|
'outing', 'money', 'treat', 'activity', 'privilege', 'special',
|
|
'electronics', 'sports', 'art', 'music', 'food', 'other'
|
|
]
|
|
},
|
|
cost: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1,
|
|
max: 10000
|
|
},
|
|
category: {
|
|
type: String,
|
|
enum: [
|
|
'screen-time', 'treats', 'toys', 'activities', 'outings',
|
|
'privileges', 'money', 'special', 'electronics', 'books',
|
|
'sports', 'art', 'other'
|
|
],
|
|
default: 'treats'
|
|
},
|
|
createdBy: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User',
|
|
required: true
|
|
},
|
|
availableFor: [{
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Child'
|
|
}], // Leer = für alle Kinder verfügbar
|
|
ageRestriction: {
|
|
minAge: {
|
|
type: Number,
|
|
min: 3,
|
|
max: 18
|
|
},
|
|
maxAge: {
|
|
type: Number,
|
|
min: 3,
|
|
max: 18
|
|
}
|
|
},
|
|
availability: {
|
|
isLimited: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
totalQuantity: {
|
|
type: Number,
|
|
min: 1,
|
|
required: function() { return this.availability.isLimited; }
|
|
},
|
|
remainingQuantity: {
|
|
type: Number,
|
|
min: 0,
|
|
required: function() { return this.availability.isLimited; }
|
|
},
|
|
resetPeriod: {
|
|
type: String,
|
|
enum: ['daily', 'weekly', 'monthly', 'never'],
|
|
default: 'never'
|
|
},
|
|
lastReset: Date
|
|
},
|
|
timeRestrictions: {
|
|
validFrom: Date,
|
|
validUntil: Date,
|
|
daysOfWeek: [{
|
|
type: Number,
|
|
min: 0,
|
|
max: 6 // 0 = Sonntag, 6 = Samstag
|
|
}],
|
|
timeOfDay: {
|
|
startTime: String, // Format: "HH:MM"
|
|
endTime: String // Format: "HH:MM"
|
|
}
|
|
},
|
|
requiresApproval: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
isInstant: {
|
|
type: Boolean,
|
|
default: false // true = sofort verfügbar, false = muss genehmigt werden
|
|
},
|
|
tags: [{
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 20
|
|
}],
|
|
image: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
instructions: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 1000
|
|
},
|
|
isActive: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
isPopular: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
sortOrder: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
stats: {
|
|
timesRedeemed: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
totalPointsSpent: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
lastRedeemed: Date
|
|
},
|
|
createdAt: {
|
|
type: Date,
|
|
default: Date.now
|
|
},
|
|
updatedAt: {
|
|
type: Date,
|
|
default: Date.now
|
|
}
|
|
});
|
|
|
|
// Schema für Belohnungs-Anfragen
|
|
const rewardRequestSchema = new mongoose.Schema({
|
|
reward: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Reward',
|
|
required: true
|
|
},
|
|
child: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Child',
|
|
required: true
|
|
},
|
|
pointsSpent: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1
|
|
},
|
|
status: {
|
|
type: String,
|
|
enum: ['pending', 'approved', 'rejected', 'redeemed'],
|
|
default: 'pending'
|
|
},
|
|
requestedAt: {
|
|
type: Date,
|
|
default: Date.now
|
|
},
|
|
processedAt: Date,
|
|
processedBy: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User'
|
|
},
|
|
redeemedAt: Date,
|
|
rejectionReason: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 200
|
|
},
|
|
notes: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 500
|
|
}
|
|
});
|
|
|
|
// Indizes für bessere Performance
|
|
rewardSchema.index({ createdBy: 1, isActive: 1 });
|
|
rewardSchema.index({ cost: 1, isActive: 1 });
|
|
rewardSchema.index({ category: 1, isActive: 1 });
|
|
rewardSchema.index({ isPopular: -1, sortOrder: 1 });
|
|
|
|
rewardRequestSchema.index({ child: 1, status: 1, requestedAt: -1 });
|
|
rewardRequestSchema.index({ status: 1, requestedAt: -1 });
|
|
|
|
// Middleware: updatedAt automatisch setzen
|
|
rewardSchema.pre('save', function(next) {
|
|
this.updatedAt = new Date();
|
|
next();
|
|
});
|
|
|
|
// Methode: Verfügbarkeit prüfen
|
|
rewardSchema.methods.isAvailableFor = function(child) {
|
|
// Altersrestriktion prüfen
|
|
if (this.ageRestriction.minAge && child.age < this.ageRestriction.minAge) {
|
|
return false;
|
|
}
|
|
if (this.ageRestriction.maxAge && child.age > this.ageRestriction.maxAge) {
|
|
return false;
|
|
}
|
|
|
|
// Verfügbarkeit für spezifische Kinder prüfen
|
|
if (this.availableFor.length > 0 && !this.availableFor.includes(child._id)) {
|
|
return false;
|
|
}
|
|
|
|
// Mengenbegrenzung prüfen
|
|
if (this.availability.isLimited && this.availability.remainingQuantity <= 0) {
|
|
return false;
|
|
}
|
|
|
|
// Zeitrestriktionen prüfen
|
|
const now = new Date();
|
|
if (this.timeRestrictions.validFrom && now < this.timeRestrictions.validFrom) {
|
|
return false;
|
|
}
|
|
if (this.timeRestrictions.validUntil && now > this.timeRestrictions.validUntil) {
|
|
return false;
|
|
}
|
|
|
|
return this.isActive;
|
|
};
|
|
|
|
// Methode: Belohnung einlösen
|
|
rewardSchema.methods.redeem = async function(childId) {
|
|
const Child = mongoose.model('Child');
|
|
const child = await Child.findById(childId);
|
|
|
|
if (!child) {
|
|
throw new Error('Kind nicht gefunden');
|
|
}
|
|
|
|
if (!this.isAvailableFor(child)) {
|
|
throw new Error('Belohnung nicht verfügbar für dieses Kind');
|
|
}
|
|
|
|
if (child.points.available < this.cost) {
|
|
throw new Error('Nicht genügend Punkte verfügbar');
|
|
}
|
|
|
|
// Punkte abziehen
|
|
await child.spendPoints(this.cost);
|
|
|
|
// Verfügbare Menge reduzieren
|
|
if (this.availability.isLimited) {
|
|
this.availability.remainingQuantity -= 1;
|
|
}
|
|
|
|
// Statistiken aktualisieren
|
|
this.stats.timesRedeemed += 1;
|
|
this.stats.totalPointsSpent += this.cost;
|
|
this.stats.lastRedeemed = new Date();
|
|
|
|
await this.save();
|
|
|
|
// Belohnungs-Anfrage erstellen (falls Genehmigung erforderlich)
|
|
if (this.requiresApproval) {
|
|
const RewardRequest = mongoose.model('RewardRequest');
|
|
const request = new RewardRequest({
|
|
reward: this._id,
|
|
child: childId,
|
|
pointsSpent: this.cost,
|
|
status: 'pending'
|
|
});
|
|
await request.save();
|
|
return request;
|
|
} else {
|
|
// Sofortige Einlösung
|
|
const RewardRequest = mongoose.model('RewardRequest');
|
|
const request = new RewardRequest({
|
|
reward: this._id,
|
|
child: childId,
|
|
pointsSpent: this.cost,
|
|
status: 'redeemed',
|
|
redeemedAt: new Date()
|
|
});
|
|
await request.save();
|
|
return request;
|
|
}
|
|
};
|
|
|
|
// Statische Methode: Verfügbare Belohnungen für ein Kind
|
|
rewardSchema.statics.findAvailableFor = function(child) {
|
|
const query = {
|
|
isActive: true,
|
|
$or: [
|
|
{ availableFor: { $size: 0 } }, // Für alle verfügbar
|
|
{ availableFor: child._id } // Spezifisch für dieses Kind
|
|
]
|
|
};
|
|
|
|
// Altersrestriktionen
|
|
if (child.age) {
|
|
query.$and = [
|
|
{ $or: [{ 'ageRestriction.minAge': { $exists: false } }, { 'ageRestriction.minAge': { $lte: child.age } }] },
|
|
{ $or: [{ 'ageRestriction.maxAge': { $exists: false } }, { 'ageRestriction.maxAge': { $gte: child.age } }] }
|
|
];
|
|
}
|
|
|
|
return this.find(query).sort({ isPopular: -1, sortOrder: 1, cost: 1 });
|
|
};
|
|
|
|
// Methoden für RewardRequest
|
|
rewardRequestSchema.methods.approve = function(approvedByUserId) {
|
|
this.status = 'approved';
|
|
this.processedAt = new Date();
|
|
this.processedBy = approvedByUserId;
|
|
return this.save();
|
|
};
|
|
|
|
rewardRequestSchema.methods.reject = function(reason, rejectedByUserId) {
|
|
this.status = 'rejected';
|
|
this.rejectionReason = reason;
|
|
this.processedAt = new Date();
|
|
this.processedBy = rejectedByUserId;
|
|
|
|
// Punkte zurückgeben
|
|
return this.refundPoints();
|
|
};
|
|
|
|
rewardRequestSchema.methods.markRedeemed = function() {
|
|
this.status = 'redeemed';
|
|
this.redeemedAt = new Date();
|
|
return this.save();
|
|
};
|
|
|
|
rewardRequestSchema.methods.refundPoints = async function() {
|
|
const Child = mongoose.model('Child');
|
|
const child = await Child.findById(this.child);
|
|
if (child) {
|
|
child.points.available += this.pointsSpent;
|
|
child.points.spent -= this.pointsSpent;
|
|
await child.save();
|
|
}
|
|
return this.save();
|
|
};
|
|
|
|
const Reward = mongoose.model('Reward', rewardSchema);
|
|
const RewardRequest = mongoose.model('RewardRequest', rewardRequestSchema);
|
|
|
|
module.exports = { Reward, RewardRequest }; |