@feardread/fear 1.1.3 → 1.2.0
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.
- package/controllers/payment.js +137 -20
- package/models/order.js +347 -48
- package/models/payment.js +198 -0
- package/package.json +1 -1
- package/routes/payment.js +17 -0
package/controllers/payment.js
CHANGED
|
@@ -1,31 +1,148 @@
|
|
|
1
1
|
const Razorpay = require("razorpay");
|
|
2
|
+
const paypal = require("@paypal/checkout-server-sdk");
|
|
3
|
+
const Payment = require("../models/payment");
|
|
4
|
+
const methods = require("./crud");
|
|
5
|
+
|
|
2
6
|
const instance = new Razorpay({
|
|
3
7
|
key_id: "rzp_test_HSSeDI22muUrLR",
|
|
4
8
|
key_secret: "sRO0YkBxvgMg0PvWHJN16Uf7",
|
|
5
9
|
});
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
// Razorpay instance
|
|
12
|
+
const razorpayInstance = new Razorpay({
|
|
13
|
+
key_id: "rzp_test_HSSeDI22muUrLR",
|
|
14
|
+
key_secret: "sRO0YkBxvgMg0PvWHJN16Uf7",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// PayPal environment setup
|
|
18
|
+
const paypalEnvironment = () => {
|
|
19
|
+
const clientId = process.env.PAYPAL_CLIENT_ID || "YOUR_PAYPAL_CLIENT_ID";
|
|
20
|
+
const clientSecret = process.env.PAYPAL_CLIENT_SECRET || "YOUR_PAYPAL_CLIENT_SECRET";
|
|
21
|
+
|
|
22
|
+
// Use sandbox for testing, live for production
|
|
23
|
+
return new paypal.core.SandboxEnvironment(clientId, clientSecret);
|
|
24
|
+
// For production: return new paypal.core.LiveEnvironment(clientId, clientSecret);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const paypalClient = () => {
|
|
28
|
+
return new paypal.core.PayPalHttpClient(paypalEnvironment());
|
|
18
29
|
};
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
exports.checkout = async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { amount } = req.body;
|
|
34
|
+
const option = {
|
|
35
|
+
amount: amount * 100,
|
|
36
|
+
currency: "INR",
|
|
37
|
+
};
|
|
38
|
+
const order = await razorpayInstance.orders.create(option);
|
|
39
|
+
res.json({
|
|
40
|
+
success: true,
|
|
41
|
+
order,
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.status(500).json({
|
|
45
|
+
success: false,
|
|
46
|
+
message: "Error creating Razorpay order",
|
|
47
|
+
error: error.message,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
26
50
|
};
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
exports.paymentVerification = async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const { razorpayOrderId, razorpayPaymentId, razorpaySignature } = req.body;
|
|
55
|
+
|
|
56
|
+
// Verify signature for security
|
|
57
|
+
const crypto = require("crypto");
|
|
58
|
+
const generatedSignature = crypto
|
|
59
|
+
.createHmac("sha256", "sRO0YkBxvgMg0PvWHJN16Uf7")
|
|
60
|
+
.update(`${razorpayOrderId}|${razorpayPaymentId}`)
|
|
61
|
+
.digest("hex");
|
|
62
|
+
|
|
63
|
+
if (generatedSignature === razorpaySignature) {
|
|
64
|
+
res.json({
|
|
65
|
+
success: true,
|
|
66
|
+
razorpayOrderId,
|
|
67
|
+
razorpayPaymentId,
|
|
68
|
+
verified: true,
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
res.status(400).json({
|
|
72
|
+
success: false,
|
|
73
|
+
message: "Payment verification failed",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res.status(500).json({
|
|
78
|
+
success: false,
|
|
79
|
+
message: "Error verifying payment",
|
|
80
|
+
error: error.message,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
31
83
|
};
|
|
84
|
+
|
|
85
|
+
// ============ PAYPAL METHODS ============
|
|
86
|
+
|
|
87
|
+
const createPayPalOrder = async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { amount, currency = "USD" } = req.body;
|
|
90
|
+
|
|
91
|
+
const request = new paypal.orders.OrdersCreateRequest();
|
|
92
|
+
request.prefer("return=representation");
|
|
93
|
+
request.requestBody({
|
|
94
|
+
intent: "CAPTURE",
|
|
95
|
+
purchase_units: [{
|
|
96
|
+
amount: {
|
|
97
|
+
currency_code: currency,
|
|
98
|
+
value: amount.toString(),
|
|
99
|
+
},
|
|
100
|
+
}],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const order = await paypalClient().execute(request);
|
|
104
|
+
|
|
105
|
+
res.json({
|
|
106
|
+
success: true,
|
|
107
|
+
orderId: order.result.id,
|
|
108
|
+
order: order.result,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
res.status(500).json({
|
|
112
|
+
success: false,
|
|
113
|
+
message: "Error creating PayPal order",
|
|
114
|
+
error: error.message,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const capturePayPalOrder = async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { orderId } = req.body;
|
|
122
|
+
|
|
123
|
+
const request = new paypal.orders.OrdersCaptureRequest(orderId);
|
|
124
|
+
request.requestBody({});
|
|
125
|
+
|
|
126
|
+
const capture = await paypalClient().execute(request);
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
captureId: capture.result.id,
|
|
131
|
+
status: capture.result.status,
|
|
132
|
+
capture: capture.result,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
res.status(500).json({
|
|
136
|
+
success: false,
|
|
137
|
+
message: "Error capturing PayPal order",
|
|
138
|
+
error: error.message,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const crud = methods.crudController( Payment );
|
|
144
|
+
for(prop in crud) {
|
|
145
|
+
if(crud.hasOwnProperty(prop)) {
|
|
146
|
+
module.exports[prop] = crud[prop];
|
|
147
|
+
}
|
|
148
|
+
}
|
package/models/order.js
CHANGED
|
@@ -1,26 +1,152 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const orderItemSchema = new mongoose.Schema({
|
|
4
|
+
productId: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: "Product",
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
name: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
quantity: {
|
|
14
|
+
type: Number,
|
|
15
|
+
required: true,
|
|
16
|
+
min: 1,
|
|
17
|
+
},
|
|
18
|
+
price: {
|
|
19
|
+
type: Number,
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
discount: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: 0,
|
|
25
|
+
},
|
|
26
|
+
tax: {
|
|
27
|
+
type: Number,
|
|
28
|
+
default: 0,
|
|
29
|
+
},
|
|
30
|
+
subtotal: {
|
|
31
|
+
type: Number,
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
image: String,
|
|
35
|
+
sku: String,
|
|
36
|
+
attributes: {
|
|
37
|
+
type: Map,
|
|
38
|
+
of: String, // For size, color, etc.
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const orderSchema = new mongoose.Schema(
|
|
5
43
|
{
|
|
6
|
-
|
|
44
|
+
orderNumber: {
|
|
45
|
+
type: String,
|
|
46
|
+
unique: true,
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
userId: {
|
|
7
50
|
type: mongoose.Schema.Types.ObjectId,
|
|
8
51
|
ref: "User",
|
|
9
52
|
required: true,
|
|
53
|
+
index: true,
|
|
54
|
+
},
|
|
55
|
+
// Order Items
|
|
56
|
+
items: [orderItemSchema],
|
|
57
|
+
|
|
58
|
+
// Pricing
|
|
59
|
+
subtotal: {
|
|
60
|
+
type: Number,
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
discount: {
|
|
64
|
+
type: Number,
|
|
65
|
+
default: 0,
|
|
66
|
+
},
|
|
67
|
+
tax: {
|
|
68
|
+
type: Number,
|
|
69
|
+
default: 0,
|
|
70
|
+
},
|
|
71
|
+
shippingCost: {
|
|
72
|
+
type: Number,
|
|
73
|
+
default: 0,
|
|
74
|
+
},
|
|
75
|
+
total: {
|
|
76
|
+
type: Number,
|
|
77
|
+
required: true,
|
|
78
|
+
},
|
|
79
|
+
currency: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: "INR",
|
|
82
|
+
enum: ["INR", "USD", "EUR", "GBP"],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Payment Information
|
|
86
|
+
paymentMethod: {
|
|
87
|
+
type: String,
|
|
88
|
+
enum: ["razorpay", "paypal", "card", "upi", "netbanking", "cod"],
|
|
89
|
+
required: true,
|
|
10
90
|
},
|
|
11
|
-
|
|
12
|
-
|
|
91
|
+
paymentStatus: {
|
|
92
|
+
type: String,
|
|
93
|
+
enum: ["pending", "processing", "completed", "failed", "refunded", "partially_refunded"],
|
|
94
|
+
default: "pending",
|
|
95
|
+
index: true,
|
|
96
|
+
},
|
|
97
|
+
paymentDetails: {
|
|
98
|
+
// Razorpay
|
|
99
|
+
razorpayOrderId: String,
|
|
100
|
+
razorpayPaymentId: String,
|
|
101
|
+
razorpaySignature: String,
|
|
102
|
+
|
|
103
|
+
// PayPal
|
|
104
|
+
paypalOrderId: String,
|
|
105
|
+
paypalCaptureId: String,
|
|
106
|
+
|
|
107
|
+
// Common
|
|
108
|
+
transactionId: String,
|
|
109
|
+
paymentMethodId: {
|
|
110
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
111
|
+
ref: "PaymentMethod",
|
|
112
|
+
},
|
|
113
|
+
paidAt: Date,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Order Status
|
|
117
|
+
orderStatus: {
|
|
118
|
+
type: String,
|
|
119
|
+
enum: [
|
|
120
|
+
"pending",
|
|
121
|
+
"confirmed",
|
|
122
|
+
"processing",
|
|
123
|
+
"shipped",
|
|
124
|
+
"out_for_delivery",
|
|
125
|
+
"delivered",
|
|
126
|
+
"cancelled",
|
|
127
|
+
"returned",
|
|
128
|
+
"refunded"
|
|
129
|
+
],
|
|
130
|
+
default: "pending",
|
|
131
|
+
index: true,
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Shipping Information
|
|
135
|
+
shippingAddress: {
|
|
136
|
+
name: {
|
|
13
137
|
type: String,
|
|
14
138
|
required: true,
|
|
15
139
|
},
|
|
16
|
-
|
|
140
|
+
phone: {
|
|
17
141
|
type: String,
|
|
18
142
|
required: true,
|
|
19
143
|
},
|
|
20
|
-
|
|
144
|
+
email: String,
|
|
145
|
+
line1: {
|
|
21
146
|
type: String,
|
|
22
147
|
required: true,
|
|
23
148
|
},
|
|
149
|
+
line2: String,
|
|
24
150
|
city: {
|
|
25
151
|
type: String,
|
|
26
152
|
required: true,
|
|
@@ -29,60 +155,88 @@ var orderSchema = new mongoose.Schema(
|
|
|
29
155
|
type: String,
|
|
30
156
|
required: true,
|
|
31
157
|
},
|
|
32
|
-
|
|
158
|
+
postalCode: {
|
|
33
159
|
type: String,
|
|
34
|
-
},
|
|
35
|
-
pincode: {
|
|
36
|
-
type: Number,
|
|
37
160
|
required: true,
|
|
38
161
|
},
|
|
39
|
-
|
|
40
|
-
paymentInfo: {
|
|
41
|
-
razorpayOrderId: {
|
|
42
|
-
type: String,
|
|
43
|
-
required: true,
|
|
44
|
-
},
|
|
45
|
-
razorpayPaymentId: {
|
|
162
|
+
country: {
|
|
46
163
|
type: String,
|
|
47
164
|
required: true,
|
|
165
|
+
default: "IN",
|
|
48
166
|
},
|
|
167
|
+
landmark: String,
|
|
49
168
|
},
|
|
50
|
-
|
|
169
|
+
|
|
170
|
+
billingAddress: {
|
|
171
|
+
name: String,
|
|
172
|
+
phone: String,
|
|
173
|
+
email: String,
|
|
174
|
+
line1: String,
|
|
175
|
+
line2: String,
|
|
176
|
+
city: String,
|
|
177
|
+
state: String,
|
|
178
|
+
postalCode: String,
|
|
179
|
+
country: String,
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Shipping Details
|
|
183
|
+
shippingProvider: String,
|
|
184
|
+
trackingNumber: String,
|
|
185
|
+
estimatedDeliveryDate: Date,
|
|
186
|
+
actualDeliveryDate: Date,
|
|
187
|
+
|
|
188
|
+
// Status History
|
|
189
|
+
statusHistory: [
|
|
51
190
|
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
191
|
+
status: String,
|
|
192
|
+
timestamp: {
|
|
193
|
+
type: Date,
|
|
194
|
+
default: Date.now,
|
|
56
195
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
price: {
|
|
62
|
-
type: Number,
|
|
63
|
-
required: true,
|
|
196
|
+
note: String,
|
|
197
|
+
updatedBy: {
|
|
198
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
199
|
+
ref: "User",
|
|
64
200
|
},
|
|
65
201
|
},
|
|
66
202
|
],
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
month: {
|
|
203
|
+
|
|
204
|
+
// Discount/Coupon
|
|
205
|
+
couponCode: String,
|
|
206
|
+
couponDiscount: {
|
|
72
207
|
type: Number,
|
|
73
|
-
default:
|
|
208
|
+
default: 0,
|
|
74
209
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
210
|
+
|
|
211
|
+
// Notes
|
|
212
|
+
customerNote: String,
|
|
213
|
+
internalNote: String,
|
|
214
|
+
|
|
215
|
+
// Cancellation/Return
|
|
216
|
+
cancellationReason: String,
|
|
217
|
+
cancelledAt: Date,
|
|
218
|
+
cancelledBy: {
|
|
219
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
220
|
+
ref: "User",
|
|
82
221
|
},
|
|
83
|
-
|
|
222
|
+
|
|
223
|
+
returnReason: String,
|
|
224
|
+
returnedAt: Date,
|
|
225
|
+
|
|
226
|
+
// Refund Information
|
|
227
|
+
refundAmount: Number,
|
|
228
|
+
refundStatus: {
|
|
84
229
|
type: String,
|
|
85
|
-
|
|
230
|
+
enum: ["none", "pending", "processing", "completed", "failed"],
|
|
231
|
+
default: "none",
|
|
232
|
+
},
|
|
233
|
+
refundedAt: Date,
|
|
234
|
+
refundTransactionId: String,
|
|
235
|
+
|
|
236
|
+
// Metadata
|
|
237
|
+
metadata: {
|
|
238
|
+
type: Map,
|
|
239
|
+
of: mongoose.Schema.Types.Mixed,
|
|
86
240
|
},
|
|
87
241
|
},
|
|
88
242
|
{
|
|
@@ -90,5 +244,150 @@ var orderSchema = new mongoose.Schema(
|
|
|
90
244
|
}
|
|
91
245
|
);
|
|
92
246
|
|
|
93
|
-
//
|
|
94
|
-
|
|
247
|
+
// Indexes for better query performance
|
|
248
|
+
orderSchema.index({ orderNumber: 1 });
|
|
249
|
+
orderSchema.index({ userId: 1, createdAt: -1 });
|
|
250
|
+
orderSchema.index({ orderStatus: 1, createdAt: -1 });
|
|
251
|
+
orderSchema.index({ paymentStatus: 1 });
|
|
252
|
+
orderSchema.index({ "paymentDetails.razorpayOrderId": 1 });
|
|
253
|
+
orderSchema.index({ "paymentDetails.paypalOrderId": 1 });
|
|
254
|
+
orderSchema.index({ trackingNumber: 1 });
|
|
255
|
+
|
|
256
|
+
// Pre-save middleware to generate order number
|
|
257
|
+
orderSchema.pre("save", async function (next) {
|
|
258
|
+
if (!this.orderNumber) {
|
|
259
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
260
|
+
const random = Math.random().toString(36).substring(2, 7).toUpperCase();
|
|
261
|
+
this.orderNumber = `ORD-${timestamp}-${random}`;
|
|
262
|
+
}
|
|
263
|
+
next();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Pre-save middleware to add status history
|
|
267
|
+
orderSchema.pre("save", function (next) {
|
|
268
|
+
if (this.isModified("orderStatus")) {
|
|
269
|
+
this.statusHistory.push({
|
|
270
|
+
status: this.orderStatus,
|
|
271
|
+
timestamp: new Date(),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
next();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Static method to get orders by user
|
|
278
|
+
orderSchema.statics.getUserOrders = async function (userId, options = {}) {
|
|
279
|
+
const { status, limit = 10, skip = 0 } = options;
|
|
280
|
+
const query = { userId };
|
|
281
|
+
|
|
282
|
+
if (status) {
|
|
283
|
+
query.orderStatus = status;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return await this.find(query)
|
|
287
|
+
.sort({ createdAt: -1 })
|
|
288
|
+
.limit(limit)
|
|
289
|
+
.skip(skip)
|
|
290
|
+
.populate("items.productId", "name images")
|
|
291
|
+
.populate("paymentDetails.paymentMethodId");
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Static method to get order statistics
|
|
295
|
+
orderSchema.statics.getOrderStats = async function (userId) {
|
|
296
|
+
const stats = await this.aggregate([
|
|
297
|
+
{ $match: { userId: mongoose.Types.ObjectId(userId) } },
|
|
298
|
+
{
|
|
299
|
+
$group: {
|
|
300
|
+
_id: "$orderStatus",
|
|
301
|
+
count: { $sum: 1 },
|
|
302
|
+
totalAmount: { $sum: "$total" },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
return stats;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Instance method to mark as paid
|
|
311
|
+
orderSchema.methods.markAsPaid = async function (paymentInfo) {
|
|
312
|
+
this.paymentStatus = "completed";
|
|
313
|
+
this.paymentDetails = {
|
|
314
|
+
...this.paymentDetails,
|
|
315
|
+
...paymentInfo,
|
|
316
|
+
paidAt: new Date(),
|
|
317
|
+
};
|
|
318
|
+
this.orderStatus = "confirmed";
|
|
319
|
+
return await this.save();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Instance method to update order status
|
|
323
|
+
orderSchema.methods.updateStatus = async function (status, note, updatedBy) {
|
|
324
|
+
this.orderStatus = status;
|
|
325
|
+
this.statusHistory.push({
|
|
326
|
+
status,
|
|
327
|
+
timestamp: new Date(),
|
|
328
|
+
note,
|
|
329
|
+
updatedBy,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Update specific dates based on status
|
|
333
|
+
if (status === "delivered") {
|
|
334
|
+
this.actualDeliveryDate = new Date();
|
|
335
|
+
} else if (status === "cancelled") {
|
|
336
|
+
this.cancelledAt = new Date();
|
|
337
|
+
this.cancelledBy = updatedBy;
|
|
338
|
+
} else if (status === "returned") {
|
|
339
|
+
this.returnedAt = new Date();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return await this.save();
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Instance method to process refund
|
|
346
|
+
orderSchema.methods.processRefund = async function (amount, transactionId) {
|
|
347
|
+
this.refundAmount = amount || this.total;
|
|
348
|
+
this.refundStatus = "completed";
|
|
349
|
+
this.refundedAt = new Date();
|
|
350
|
+
this.refundTransactionId = transactionId;
|
|
351
|
+
this.paymentStatus = amount >= this.total ? "refunded" : "partially_refunded";
|
|
352
|
+
this.orderStatus = "refunded";
|
|
353
|
+
return await this.save();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Instance method to add tracking information
|
|
357
|
+
orderSchema.methods.addTracking = async function (provider, trackingNumber, estimatedDelivery) {
|
|
358
|
+
this.shippingProvider = provider;
|
|
359
|
+
this.trackingNumber = trackingNumber;
|
|
360
|
+
this.estimatedDeliveryDate = estimatedDelivery;
|
|
361
|
+
this.orderStatus = "shipped";
|
|
362
|
+
return await this.save();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Virtual for order age in days
|
|
366
|
+
orderSchema.virtual("orderAge").get(function () {
|
|
367
|
+
return Math.floor((Date.now() - this.createdAt) / (1000 * 60 * 60 * 24));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Virtual for items count
|
|
371
|
+
orderSchema.virtual("itemsCount").get(function () {
|
|
372
|
+
return this.items.reduce((total, item) => total + item.quantity, 0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Virtual for checking if order can be cancelled
|
|
376
|
+
orderSchema.virtual("canCancel").get(function () {
|
|
377
|
+
return ["pending", "confirmed", "processing"].includes(this.orderStatus);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Virtual for checking if order can be returned
|
|
381
|
+
orderSchema.virtual("canReturn").get(function () {
|
|
382
|
+
const returnWindow = 7; // days
|
|
383
|
+
return (
|
|
384
|
+
this.orderStatus === "delivered" &&
|
|
385
|
+
this.orderAge <= returnWindow
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Ensure virtuals are included in JSON
|
|
390
|
+
orderSchema.set("toJSON", { virtuals: true });
|
|
391
|
+
orderSchema.set("toObject", { virtuals: true });
|
|
392
|
+
|
|
393
|
+
module.exports = mongoose.model("Order", orderSchema);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
const paymentMethodSchema = new mongoose.Schema(
|
|
5
|
+
{
|
|
6
|
+
userId: {
|
|
7
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
8
|
+
ref: "User",
|
|
9
|
+
required: true,
|
|
10
|
+
index: true,
|
|
11
|
+
},
|
|
12
|
+
paymentType: {
|
|
13
|
+
type: String,
|
|
14
|
+
enum: ["razorpay", "paypal", "card", "upi", "netbanking"],
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
paymentToken: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
// This stores the tokenized payment method from the payment gateway
|
|
21
|
+
},
|
|
22
|
+
isDefault: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
isActive: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: true,
|
|
29
|
+
},
|
|
30
|
+
// Card/Payment details (only non-sensitive info for display)
|
|
31
|
+
cardDetails: {
|
|
32
|
+
last4: {
|
|
33
|
+
type: String,
|
|
34
|
+
maxlength: 4,
|
|
35
|
+
},
|
|
36
|
+
brand: {
|
|
37
|
+
type: String, // Visa, Mastercard, Amex, etc.
|
|
38
|
+
},
|
|
39
|
+
expiryMonth: {
|
|
40
|
+
type: String,
|
|
41
|
+
maxlength: 2,
|
|
42
|
+
},
|
|
43
|
+
expiryYear: {
|
|
44
|
+
type: String,
|
|
45
|
+
maxlength: 4,
|
|
46
|
+
},
|
|
47
|
+
cardholderName: {
|
|
48
|
+
type: String,
|
|
49
|
+
},
|
|
50
|
+
fingerprint: {
|
|
51
|
+
type: String, // Unique identifier for the card
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
// PayPal details
|
|
55
|
+
paypalDetails: {
|
|
56
|
+
email: {
|
|
57
|
+
type: String,
|
|
58
|
+
},
|
|
59
|
+
payerId: {
|
|
60
|
+
type: String,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
// UPI details
|
|
64
|
+
upiDetails: {
|
|
65
|
+
vpa: {
|
|
66
|
+
type: String, // Virtual Payment Address (e.g., user@paytm)
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// Billing address
|
|
70
|
+
billingAddress: {
|
|
71
|
+
name: String,
|
|
72
|
+
line1: String,
|
|
73
|
+
line2: String,
|
|
74
|
+
city: String,
|
|
75
|
+
state: String,
|
|
76
|
+
postalCode: String,
|
|
77
|
+
country: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: "IN",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
// Metadata for additional information
|
|
83
|
+
metadata: {
|
|
84
|
+
type: Map,
|
|
85
|
+
of: String,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
timestamps: true,
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Indexes for better query performance
|
|
94
|
+
paymentMethodSchema.index({ userId: 1, isDefault: 1 });
|
|
95
|
+
paymentMethodSchema.index({ userId: 1, isActive: 1 });
|
|
96
|
+
paymentMethodSchema.index({ createdAt: -1 });
|
|
97
|
+
|
|
98
|
+
// Middleware to ensure only one default payment method per user
|
|
99
|
+
paymentMethodSchema.pre("save", async function (next) {
|
|
100
|
+
if (this.isDefault && this.isModified("isDefault")) {
|
|
101
|
+
// Remove default flag from other payment methods for this user
|
|
102
|
+
await mongoose.model("PaymentMethod").updateMany(
|
|
103
|
+
{
|
|
104
|
+
userId: this.userId,
|
|
105
|
+
_id: { $ne: this._id },
|
|
106
|
+
isDefault: true,
|
|
107
|
+
},
|
|
108
|
+
{ isDefault: false }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
paymentMethodSchema.statics.getDefaultPaymentMethod = async function (userId) {
|
|
114
|
+
return await this.findOne({
|
|
115
|
+
userId,
|
|
116
|
+
isDefault: true,
|
|
117
|
+
isActive: true,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
paymentMethodSchema.statics.getUserPaymentMethods = async function (userId) {
|
|
121
|
+
return await this.find({
|
|
122
|
+
userId,
|
|
123
|
+
isActive: true,
|
|
124
|
+
}).sort({ isDefault: -1, createdAt: -1 });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
paymentMethodSchema.methods.setAsDefault = async function () {
|
|
128
|
+
await mongoose.model("PaymentMethod").updateMany(
|
|
129
|
+
{
|
|
130
|
+
userId: this.userId,
|
|
131
|
+
_id: { $ne: this._id },
|
|
132
|
+
},
|
|
133
|
+
{ isDefault: false }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
this.isDefault = true;
|
|
137
|
+
return await this.save();
|
|
138
|
+
};
|
|
139
|
+
paymentMethodSchema.methods.softDelete = async function () {
|
|
140
|
+
this.isActive = false;
|
|
141
|
+
if (this.isDefault) {
|
|
142
|
+
this.isDefault = false;
|
|
143
|
+
// Optionally set another method as default
|
|
144
|
+
const otherMethod = await mongoose.model("PaymentMethod").findOne({
|
|
145
|
+
userId: this.userId,
|
|
146
|
+
_id: { $ne: this._id },
|
|
147
|
+
isActive: true,
|
|
148
|
+
});
|
|
149
|
+
if (otherMethod) {
|
|
150
|
+
otherMethod.isDefault = true;
|
|
151
|
+
await otherMethod.save();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return await this.save();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Virtual for masked card number display
|
|
158
|
+
paymentMethodSchema.virtual("displayNumber").get(function () {
|
|
159
|
+
if (this.cardDetails?.last4) {
|
|
160
|
+
return `•••• •••• •••• ${this.cardDetails.last4}`;
|
|
161
|
+
}
|
|
162
|
+
if (this.paypalDetails?.email) {
|
|
163
|
+
return this.paypalDetails.email;
|
|
164
|
+
}
|
|
165
|
+
if (this.upiDetails?.vpa) {
|
|
166
|
+
return this.upiDetails.vpa;
|
|
167
|
+
}
|
|
168
|
+
return "Payment Method";
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Virtual for expiry display
|
|
172
|
+
paymentMethodSchema.virtual("expiryDisplay").get(function () {
|
|
173
|
+
if (this.cardDetails?.expiryMonth && this.cardDetails?.expiryYear) {
|
|
174
|
+
return `${this.cardDetails.expiryMonth}/${this.cardDetails.expiryYear}`;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Virtual for checking if card is expired
|
|
180
|
+
paymentMethodSchema.virtual("isExpired").get(function () {
|
|
181
|
+
if (this.cardDetails?.expiryMonth && this.cardDetails?.expiryYear) {
|
|
182
|
+
const now = new Date();
|
|
183
|
+
const expiryDate = new Date(
|
|
184
|
+
parseInt(this.cardDetails.expiryYear),
|
|
185
|
+
parseInt(this.cardDetails.expiryMonth) - 1
|
|
186
|
+
);
|
|
187
|
+
return now > expiryDate;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Ensure virtuals are included in JSON
|
|
193
|
+
paymentMethodSchema.set("toJSON", { virtuals: true });
|
|
194
|
+
paymentMethodSchema.set("toObject", { virtuals: true });
|
|
195
|
+
|
|
196
|
+
const PaymentMethod = mongoose.model("PaymentMethod", paymentMethodSchema);
|
|
197
|
+
|
|
198
|
+
module.exports = PaymentMethod;
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const Payment = require('../controllers/payment');
|
|
2
|
+
|
|
3
|
+
module.exports = (fear) => {
|
|
4
|
+
const router = fear.createRouter();
|
|
5
|
+
const handler = fear.getHandler();
|
|
6
|
+
const validator = fear.getValidator();
|
|
7
|
+
|
|
8
|
+
router.get("/all", Payment.list);
|
|
9
|
+
router.post("/new", Payment.create);
|
|
10
|
+
|
|
11
|
+
router.route('/:id')
|
|
12
|
+
.get(Payment.read)
|
|
13
|
+
.put(Payment.update);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
return router;
|
|
17
|
+
}
|