@mac777/project-pinecone-models 1.1.22 → 1.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ import mongoose from "mongoose";
2
+
3
+ /**
4
+ * Payment Details Schema
5
+ * Stores payment/payout information separate from User
6
+ */
7
+ const paymentDetailsSchema = new mongoose.Schema({
8
+ userId: {
9
+ type: mongoose.Schema.Types.ObjectId,
10
+ ref: 'User',
11
+ required: true,
12
+ unique: true,
13
+ index: true
14
+ },
15
+
16
+ // Payment Method
17
+ method: {
18
+ type: String,
19
+ enum: ['bkash', 'nagad', 'rocket', 'bank_transfer'],
20
+ required: true,
21
+ index: true
22
+ },
23
+
24
+ // Mobile Money (bKash, Nagad, Rocket)
25
+ mobileNumber: {
26
+ type: String,
27
+ trim: true
28
+ },
29
+ accountHolderName: {
30
+ type: String,
31
+ trim: true
32
+ },
33
+
34
+ // Bank Transfer
35
+ bankName: {
36
+ type: String,
37
+ trim: true
38
+ },
39
+ accountNumber: {
40
+ type: String,
41
+ trim: true
42
+ },
43
+ branchName: {
44
+ type: String,
45
+ trim: true
46
+ },
47
+ routingNumber: {
48
+ type: String,
49
+ trim: true
50
+ },
51
+ swiftCode: {
52
+ type: String,
53
+ trim: true
54
+ },
55
+
56
+ // Verification
57
+ verified: {
58
+ type: Boolean,
59
+ default: false,
60
+ index: true
61
+ },
62
+ verifiedAt: {
63
+ type: Date
64
+ },
65
+ verifiedBy: {
66
+ type: mongoose.Schema.Types.ObjectId,
67
+ ref: 'User'
68
+ },
69
+ rejectionReason: {
70
+ type: String
71
+ },
72
+
73
+ // Metadata
74
+ lastUpdatedBy: {
75
+ type: mongoose.Schema.Types.ObjectId,
76
+ ref: 'User'
77
+ }
78
+ }, {
79
+ timestamps: true
80
+ });
81
+
82
+ // Indexes
83
+ paymentDetailsSchema.index({ userId: 1 });
84
+ paymentDetailsSchema.index({ verified: 1, createdAt: -1 });
85
+ paymentDetailsSchema.index({ method: 1, verified: 1 });
86
+
87
+ // Pre-save validation: Ensure required fields based on method
88
+ paymentDetailsSchema.pre('save', function(next) {
89
+ if (this.method === 'bank_transfer') {
90
+ if (!this.bankName || !this.accountNumber || !this.accountHolderName) {
91
+ return next(new Error('Bank name, account number, and account holder name are required for bank transfer'));
92
+ }
93
+ } else {
94
+ // Mobile money methods
95
+ if (!this.mobileNumber || !this.accountHolderName) {
96
+ return next(new Error('Mobile number and account holder name are required for mobile money'));
97
+ }
98
+ }
99
+ next();
100
+ });
101
+
102
+ // Methods
103
+ paymentDetailsSchema.methods.markAsVerified = function(verifiedBy: mongoose.Types.ObjectId) {
104
+ this.verified = true;
105
+ this.verifiedAt = new Date();
106
+ this.verifiedBy = verifiedBy;
107
+ this.rejectionReason = undefined;
108
+ return this.save();
109
+ };
110
+
111
+ paymentDetailsSchema.methods.reject = function(reason: string, rejectedBy: mongoose.Types.ObjectId) {
112
+ this.verified = false;
113
+ this.verifiedAt = undefined;
114
+ this.rejectionReason = reason;
115
+ this.lastUpdatedBy = rejectedBy;
116
+ return this.save();
117
+ };
118
+
119
+ paymentDetailsSchema.methods.isMobileMoney = function() {
120
+ return ['bkash', 'nagad', 'rocket'].includes(this.method);
121
+ };
122
+
123
+ paymentDetailsSchema.methods.isBankTransfer = function() {
124
+ return this.method === 'bank_transfer';
125
+ };
126
+
127
+ export default paymentDetailsSchema;
@@ -0,0 +1,93 @@
1
+ import mongoose from "mongoose";
2
+
3
+ /**
4
+ * Phone Verification Schema
5
+ * Handles OTP generation, verification, and rate limiting
6
+ */
7
+ const phoneVerificationSchema = new mongoose.Schema({
8
+ userId: {
9
+ type: mongoose.Schema.Types.ObjectId,
10
+ ref: 'User',
11
+ required: true,
12
+ index: true
13
+ },
14
+ phoneNumber: {
15
+ type: String,
16
+ required: true,
17
+ trim: true
18
+ },
19
+ otp: {
20
+ type: String,
21
+ required: true
22
+ },
23
+ expiresAt: {
24
+ type: Date,
25
+ required: true,
26
+ index: true
27
+ },
28
+ verified: {
29
+ type: Boolean,
30
+ default: false
31
+ },
32
+ verifiedAt: {
33
+ type: Date
34
+ },
35
+ // Attempt tracking (security)
36
+ attempts: {
37
+ type: Number,
38
+ default: 0
39
+ },
40
+ lastAttemptAt: {
41
+ type: Date
42
+ },
43
+ maxAttempts: {
44
+ type: Number,
45
+ default: 5
46
+ },
47
+ // Rate limiting
48
+ sentCount: {
49
+ type: Number,
50
+ default: 1
51
+ },
52
+ lastSentAt: {
53
+ type: Date,
54
+ default: Date.now
55
+ },
56
+ // Metadata
57
+ sentFrom: {
58
+ type: String // IP address
59
+ }
60
+ }, {
61
+ timestamps: true
62
+ });
63
+
64
+ // Indexes
65
+ phoneVerificationSchema.index({ userId: 1, phoneNumber: 1 });
66
+ phoneVerificationSchema.index({ userId: 1, verified: 1 });
67
+ phoneVerificationSchema.index({ phoneNumber: 1, verified: 1 });
68
+
69
+ // TTL Index: Auto-delete expired OTPs after they expire
70
+ phoneVerificationSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
71
+
72
+ // Methods
73
+ phoneVerificationSchema.methods.isExpired = function() {
74
+ return this.expiresAt < new Date();
75
+ };
76
+
77
+ phoneVerificationSchema.methods.isMaxAttemptsReached = function() {
78
+ return this.attempts >= this.maxAttempts;
79
+ };
80
+
81
+ phoneVerificationSchema.methods.incrementAttempts = function() {
82
+ this.attempts += 1;
83
+ this.lastAttemptAt = new Date();
84
+ return this.save();
85
+ };
86
+
87
+ phoneVerificationSchema.methods.canResend = function(cooldownSeconds = 60) {
88
+ if (!this.lastSentAt) return true;
89
+ const cooldownMs = cooldownSeconds * 1000;
90
+ return Date.now() - this.lastSentAt.getTime() > cooldownMs;
91
+ };
92
+
93
+ export default phoneVerificationSchema;
package/src/Review.ts ADDED
@@ -0,0 +1,268 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ export interface IReview {
4
+ eventId: mongoose.Types.ObjectId;
5
+ userId: mongoose.Types.ObjectId;
6
+ ticketId: mongoose.Types.ObjectId;
7
+
8
+ // Review Content
9
+ rating: number; // 1-5 stars
10
+ title: string;
11
+ comment: string;
12
+
13
+ // Context Metadata (smart dual-context approach)
14
+ reviewContext: {
15
+ hasCheckedIn: boolean;
16
+ checkInTime?: Date;
17
+ ticketTier: string;
18
+ ticketType: string;
19
+ };
20
+
21
+ // Status & Moderation
22
+ status: 'pending' | 'approved' | 'hidden' | 'flagged';
23
+ isVisible: boolean;
24
+ moderationNotes?: string;
25
+
26
+ // Timestamps
27
+ submittedAt: Date;
28
+ moderatedAt?: Date;
29
+ moderatorId?: mongoose.Types.ObjectId;
30
+
31
+ // Community Features
32
+ helpfulVotes: number;
33
+ reportedCount: number;
34
+ reports: Array<{
35
+ userId: mongoose.Types.ObjectId;
36
+ reason: string;
37
+ reportedAt: Date;
38
+ }>;
39
+ }
40
+
41
+ const reviewSchema = new mongoose.Schema<IReview>({
42
+ eventId: {
43
+ type: mongoose.Schema.Types.ObjectId,
44
+ ref: 'Event',
45
+ required: true,
46
+ index: true
47
+ },
48
+
49
+ userId: {
50
+ type: mongoose.Schema.Types.ObjectId,
51
+ ref: 'User',
52
+ required: true,
53
+ index: true
54
+ },
55
+
56
+ ticketId: {
57
+ type: mongoose.Schema.Types.ObjectId,
58
+ ref: 'Ticket',
59
+ required: true,
60
+ index: true
61
+ },
62
+
63
+ // Review Content
64
+ rating: {
65
+ type: Number,
66
+ required: true,
67
+ min: 1,
68
+ max: 5,
69
+ validate: {
70
+ validator: Number.isInteger,
71
+ message: 'Rating must be a whole number'
72
+ }
73
+ },
74
+
75
+ title: {
76
+ type: String,
77
+ required: true,
78
+ trim: true,
79
+ maxlength: 100
80
+ },
81
+
82
+ comment: {
83
+ type: String,
84
+ required: true,
85
+ trim: true,
86
+ maxlength: 1000
87
+ },
88
+
89
+ // Context Metadata
90
+ reviewContext: {
91
+ hasCheckedIn: {
92
+ type: Boolean,
93
+ required: true
94
+ },
95
+ checkInTime: Date,
96
+ ticketTier: {
97
+ type: String,
98
+ required: true
99
+ },
100
+ ticketType: {
101
+ type: String,
102
+ required: true
103
+ }
104
+ },
105
+
106
+ // Status & Moderation
107
+ status: {
108
+ type: String,
109
+ enum: ['pending', 'approved', 'hidden', 'flagged'],
110
+ default: 'pending',
111
+ index: true
112
+ },
113
+
114
+ isVisible: {
115
+ type: Boolean,
116
+ default: true
117
+ },
118
+
119
+ moderationNotes: {
120
+ type: String,
121
+ trim: true,
122
+ maxlength: 500
123
+ },
124
+
125
+ // Timestamps
126
+ submittedAt: {
127
+ type: Date,
128
+ default: Date.now,
129
+ index: true
130
+ },
131
+
132
+ moderatedAt: Date,
133
+
134
+ moderatorId: {
135
+ type: mongoose.Schema.Types.ObjectId,
136
+ ref: 'User'
137
+ },
138
+
139
+ // Community Features
140
+ helpfulVotes: {
141
+ type: Number,
142
+ default: 0,
143
+ min: 0
144
+ },
145
+
146
+ reportedCount: {
147
+ type: Number,
148
+ default: 0,
149
+ min: 0
150
+ },
151
+
152
+ reports: [{
153
+ userId: {
154
+ type: mongoose.Schema.Types.ObjectId,
155
+ ref: 'User',
156
+ required: true
157
+ },
158
+ reason: {
159
+ type: String,
160
+ required: true,
161
+ enum: ['spam', 'abusive', 'fake', 'irrelevant', 'other']
162
+ },
163
+ reportedAt: {
164
+ type: Date,
165
+ default: Date.now
166
+ }
167
+ }]
168
+ });
169
+
170
+ // Compound indexes for efficient queries
171
+ reviewSchema.index({ eventId: 1, status: 1, isVisible: 1 });
172
+ reviewSchema.index({ userId: 1, eventId: 1 }, { unique: true }); // One review per user per event
173
+ reviewSchema.index({ submittedAt: -1 });
174
+ reviewSchema.index({ 'reviewContext.hasCheckedIn': 1 });
175
+
176
+ // Virtual for review type classification
177
+ reviewSchema.virtual('reviewType').get(function() {
178
+ return this.reviewContext.hasCheckedIn ? 'attended' : 'entry_access';
179
+ });
180
+
181
+ // Method to mark as helpful
182
+ reviewSchema.methods.markHelpful = function() {
183
+ this.helpfulVotes += 1;
184
+ return this.save();
185
+ };
186
+
187
+ // Method to report review
188
+ reviewSchema.methods.report = function(userId: string, reason: string) {
189
+ // Check if user already reported
190
+ const existingReport = this.reports.find((report: any) =>
191
+ report.userId.toString() === userId
192
+ );
193
+
194
+ if (!existingReport) {
195
+ this.reports.push({
196
+ userId: new mongoose.Types.ObjectId(userId),
197
+ reason,
198
+ reportedAt: new Date()
199
+ });
200
+ this.reportedCount += 1;
201
+
202
+ // Auto-flag if too many reports
203
+ if (this.reportedCount >= 3) {
204
+ this.status = 'flagged';
205
+ }
206
+ }
207
+
208
+ return this.save();
209
+ };
210
+
211
+ // Method for admin moderation
212
+ reviewSchema.methods.moderate = function(moderatorId: string, action: 'approve' | 'hide' | 'flag', notes?: string) {
213
+ this.moderatorId = new mongoose.Types.ObjectId(moderatorId);
214
+ this.moderatedAt = new Date();
215
+
216
+ switch (action) {
217
+ case 'approve':
218
+ this.status = 'approved';
219
+ this.isVisible = true;
220
+ break;
221
+ case 'hide':
222
+ this.status = 'hidden';
223
+ this.isVisible = false;
224
+ break;
225
+ case 'flag':
226
+ this.status = 'flagged';
227
+ this.isVisible = false;
228
+ break;
229
+ }
230
+
231
+ if (notes) {
232
+ this.moderationNotes = notes;
233
+ }
234
+
235
+ return this.save();
236
+ };
237
+
238
+ // Static method to calculate weighted rating for an event
239
+ reviewSchema.statics.calculateWeightedRating = async function(eventId: string) {
240
+ const reviews = await this.find({
241
+ eventId: new mongoose.Types.ObjectId(eventId),
242
+ status: 'approved',
243
+ isVisible: true
244
+ });
245
+
246
+ if (reviews.length === 0) return 0;
247
+
248
+ let totalWeightedRating = 0;
249
+ let totalWeight = 0;
250
+
251
+ reviews.forEach((review: any) => {
252
+ const weight = review.reviewContext.hasCheckedIn ? 1.0 : 0.6;
253
+ totalWeightedRating += review.rating * weight;
254
+ totalWeight += weight;
255
+ });
256
+
257
+ return totalWeight > 0 ? totalWeightedRating / totalWeight : 0;
258
+ };
259
+
260
+ // Pre-save middleware to set default status
261
+ reviewSchema.pre('save', function(next) {
262
+ if (this.isNew && !this.status) {
263
+ this.status = 'pending';
264
+ }
265
+ next();
266
+ });
267
+
268
+ export default reviewSchema;