308 lines
8.0 KiB
JavaScript
308 lines
8.0 KiB
JavaScript
const mongoose = require('mongoose');
|
|
|
|
// Mock Express Request and Response objects
|
|
class MockRequest {
|
|
constructor(body = {}) {
|
|
this.body = body;
|
|
}
|
|
}
|
|
|
|
class MockResponse {
|
|
constructor() {
|
|
this.statusCode = 200;
|
|
this.data = null;
|
|
}
|
|
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
}
|
|
|
|
json(data) {
|
|
this.data = data;
|
|
console.log('Response Status:', this.statusCode);
|
|
console.log('Response Data:', JSON.stringify(data, null, 2));
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// Define schemas
|
|
const tenantSchema = new mongoose.Schema({
|
|
name: { type: String, required: true },
|
|
email: { type: String, required: true }
|
|
}, { timestamps: true });
|
|
|
|
const creditOperationSchema = new mongoose.Schema({
|
|
tenantId: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Tenant',
|
|
required: true,
|
|
index: true
|
|
},
|
|
userId: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User',
|
|
required: true,
|
|
index: true
|
|
},
|
|
operationType: {
|
|
type: String,
|
|
required: true,
|
|
enum: ['evaluation', 'credit_purchase', 'credit_refund', 'rut_lookup', 'other'],
|
|
index: true
|
|
},
|
|
creditsChanged: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
balanceAfter: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
description: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
metadata: {
|
|
type: mongoose.Schema.Types.Mixed,
|
|
default: {}
|
|
}
|
|
}, {
|
|
timestamps: true
|
|
});
|
|
|
|
const tenantBillingSchema = new mongoose.Schema({
|
|
tenantId: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Tenant',
|
|
required: true,
|
|
index: true
|
|
},
|
|
billingMonth: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1,
|
|
max: 12,
|
|
index: true
|
|
},
|
|
billingYear: {
|
|
type: Number,
|
|
required: true,
|
|
min: 2020,
|
|
index: true
|
|
},
|
|
billingDate: {
|
|
type: Date,
|
|
required: true,
|
|
index: true
|
|
},
|
|
totalOperations: {
|
|
type: Number,
|
|
required: true,
|
|
default: 0
|
|
},
|
|
totalCreditsUsed: {
|
|
type: Number,
|
|
required: true,
|
|
default: 0
|
|
},
|
|
totalAmount: {
|
|
type: Number,
|
|
required: true,
|
|
default: 0
|
|
},
|
|
status: {
|
|
type: String,
|
|
required: true,
|
|
enum: ['pending', 'paid', 'overdue'],
|
|
default: 'pending',
|
|
index: true
|
|
},
|
|
operationsDetail: {
|
|
evaluations: {
|
|
count: { type: Number, default: 0 },
|
|
credits: { type: Number, default: 0 },
|
|
amount: { type: Number, default: 0 }
|
|
},
|
|
rutLookups: {
|
|
count: { type: Number, default: 0 },
|
|
credits: { type: Number, default: 0 },
|
|
amount: { type: Number, default: 0 }
|
|
},
|
|
other: {
|
|
count: { type: Number, default: 0 },
|
|
credits: { type: Number, default: 0 },
|
|
amount: { type: Number, default: 0 }
|
|
}
|
|
},
|
|
creditOperations: {
|
|
type: String, // JSON string of detailed credit operations
|
|
default: '[]'
|
|
}
|
|
}, {
|
|
timestamps: true
|
|
});
|
|
|
|
// Create compound unique index
|
|
tenantBillingSchema.index({ tenantId: 1, billingMonth: 1, billingYear: 1 }, { unique: true });
|
|
|
|
const Tenant = mongoose.model('Tenant', tenantSchema);
|
|
const CreditOperation = mongoose.model('CreditOperation', creditOperationSchema);
|
|
const TenantBilling = mongoose.model('TenantBilling', tenantBillingSchema);
|
|
|
|
// Simplified billing generation function
|
|
async function generateAllMonthlyBillings(req, res) {
|
|
try {
|
|
const { month, year } = req.body;
|
|
|
|
if (!month || !year) {
|
|
return res.status(400).json({ error: 'Month and year are required' });
|
|
}
|
|
|
|
const startDate = new Date(year, month - 1, 1);
|
|
const endDate = new Date(year, month, 0, 23, 59, 59, 999);
|
|
const billingDate = new Date();
|
|
|
|
console.log(`Generating billings for ${month}/${year}`);
|
|
console.log(`Date range: ${startDate} to ${endDate}`);
|
|
|
|
const tenants = await Tenant.find({});
|
|
console.log(`Found ${tenants.length} tenants`);
|
|
|
|
const results = [];
|
|
const errors = [];
|
|
const costPerCredit = 0.10; // $0.10 per credit
|
|
|
|
for (const tenant of tenants) {
|
|
try {
|
|
console.log(`Processing tenant: ${tenant.name}`);
|
|
|
|
// Get credit operations for this tenant in the specified period
|
|
const creditOperations = await CreditOperation.find({
|
|
tenantId: tenant._id,
|
|
creditsChanged: { $lt: 0 }, // Only deductions
|
|
createdAt: {
|
|
$gte: startDate,
|
|
$lte: endDate
|
|
}
|
|
}).sort({ createdAt: 1 });
|
|
|
|
console.log(`Found ${creditOperations.length} credit operations for ${tenant.name}`);
|
|
|
|
if (creditOperations.length > 0) {
|
|
// Aggregate operations by type
|
|
const operationsDetail = {
|
|
evaluations: { count: 0, credits: 0, amount: 0 },
|
|
rutLookups: { count: 0, credits: 0, amount: 0 },
|
|
other: { count: 0, credits: 0, amount: 0 }
|
|
};
|
|
|
|
let totalCreditsUsed = 0;
|
|
|
|
creditOperations.forEach(op => {
|
|
const credits = Math.abs(op.creditsChanged);
|
|
totalCreditsUsed += credits;
|
|
const amount = credits * costPerCredit;
|
|
|
|
if (op.operationType === 'evaluation') {
|
|
operationsDetail.evaluations.count++;
|
|
operationsDetail.evaluations.credits += credits;
|
|
operationsDetail.evaluations.amount += amount;
|
|
} else if (op.operationType === 'rut_lookup') {
|
|
operationsDetail.rutLookups.count++;
|
|
operationsDetail.rutLookups.credits += credits;
|
|
operationsDetail.rutLookups.amount += amount;
|
|
} else {
|
|
operationsDetail.other.count++;
|
|
operationsDetail.other.credits += credits;
|
|
operationsDetail.other.amount += amount;
|
|
}
|
|
});
|
|
|
|
const totalOperations = creditOperations.length;
|
|
const totalAmount = totalCreditsUsed * costPerCredit;
|
|
|
|
// Check if billing already exists
|
|
const existingBilling = await TenantBilling.findOne({
|
|
tenantId: tenant._id,
|
|
billingMonth: month,
|
|
billingYear: year
|
|
});
|
|
|
|
if (existingBilling) {
|
|
console.log(`Billing already exists for ${tenant.name}`);
|
|
continue;
|
|
}
|
|
|
|
// Create new billing
|
|
const billing = new TenantBilling({
|
|
tenantId: tenant._id,
|
|
billingMonth: month,
|
|
billingYear: year,
|
|
billingDate,
|
|
totalOperations,
|
|
totalCreditsUsed,
|
|
totalAmount,
|
|
status: 'pending',
|
|
operationsDetail,
|
|
creditOperations: JSON.stringify(creditOperations)
|
|
});
|
|
|
|
await billing.save();
|
|
await billing.populate('tenantId', 'name');
|
|
results.push(billing);
|
|
|
|
console.log(`Created billing for ${tenant.name}: ${totalOperations} operations, ${totalCreditsUsed} credits, $${totalAmount.toFixed(2)}`);
|
|
} else {
|
|
console.log(`No operations found for ${tenant.name}`);
|
|
}
|
|
} catch (tenantError) {
|
|
console.error(`Error processing tenant ${tenant.name}:`, tenantError);
|
|
errors.push({
|
|
tenantId: tenant._id,
|
|
tenantName: tenant.name,
|
|
error: tenantError.message
|
|
});
|
|
}
|
|
}
|
|
|
|
res.status(201).json({
|
|
message: `Generation completed. ${results.length} billings created, ${errors.length} errors`,
|
|
billings: results,
|
|
errors: errors,
|
|
summary: {
|
|
totalTenants: tenants.length,
|
|
successfulBillings: results.length,
|
|
failedBillings: errors.length
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error generating all monthly billings:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
|
|
async function testBillingGeneration() {
|
|
try {
|
|
await mongoose.connect('mongodb://localhost:27017/duxiter');
|
|
console.log('Connected to MongoDB');
|
|
|
|
const req = new MockRequest({
|
|
month: 7,
|
|
year: 2025
|
|
});
|
|
|
|
const res = new MockResponse();
|
|
|
|
await generateAllMonthlyBillings(req, res);
|
|
|
|
await mongoose.disconnect();
|
|
console.log('Test completed');
|
|
} catch (error) {
|
|
console.error('Test error:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
testBillingGeneration(); |