first commit v3 initialiazed a temporary repository for darmanet client

This commit is contained in:
2026-09-08 16:04:01 +03:30
commit 5c7fd95052
216 changed files with 30050 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { RateModel } from './rate.model';
import { UsersBaseModel } from './users-base.model';
export type AdminDocument = AdminModel & Document;
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'admin',
})
export class AdminModel extends UsersBaseModel {
@Prop({ type: Types.ObjectId, ref: 'FileUploadModel', required: false })
avatar?: Types.ObjectId;
@Prop()
password: string;
@Prop({ default: [] })
rate: RateModel[];
@Prop({ default: null })
resetToken: string | null;
@Prop({ default: true })
isActive: Boolean;
@Prop({ default: false })
onlineStatus: Boolean;
@Prop({ default: false })
available: Boolean;
@Prop({ default: 0 })
activeSessions: number;
@Prop({ default: 5 })
maxSessions: number;
/** Extra permissions beyond the role template (ADR 0001). */
@Prop({ type: [String], default: [] })
permissionGrants: string[];
/** Blocked permissions from the role template (ADR 0001). */
@Prop({ type: [String], default: [] })
permissionDenies: string[];
}
export const AdminSchema = SchemaFactory.createForClass(AdminModel);
AdminSchema.clearIndexes();
AdminSchema.pre('save', function (next) {
this.updatedAt = new Date();
// this.password = crypto.createHash('sha256').update(this.password).digest('hex'),
next();
});

View File

@@ -0,0 +1,74 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: false },
versionKey: false,
collection: 'audit_logs',
})
export class AuditLogModel extends Document {
@Prop({ type: Types.ObjectId, ref: 'UserModel', required: false })
userId?: Types.ObjectId;
@Prop({ type: String, required: true })
action: string; // e.g., 'joinRoom', 'sendMessage', 'expertOnline', etc.
@Prop({ type: String, required: false })
resource?: string; // e.g., 'Chat', 'Session', 'Expert', 'Message', etc.
@Prop({ type: String, required: false })
resourceId?: string; // e.g., sessionId, messageId, expertId, etc.
@Prop({ type: Object, required: false })
oldValues?: Record<string, any>; // Previous state before change
@Prop({ type: Object, required: false })
newValues?: Record<string, any>; // New state after change
@Prop({ type: Object, required: false })
changes?: Record<string, { from: any; to: any }>; // Detailed changes
@Prop({ type: String, required: false })
ipAddress?: string;
@Prop({ type: String, required: false })
userAgent?: string;
@Prop({ type: String, required: false })
method?: string; // For WebSocket: event name, for HTTP: GET, POST, etc.
@Prop({ type: String, required: false })
endpoint?: string; // For WebSocket: roomId or path, for HTTP: route path
@Prop({ type: Number, required: false })
statusCode?: number; // HTTP status code or success indicator
@Prop({ type: Object, required: false })
metadata?: Record<string, any>; // Additional context data
@Prop({ type: Date, default: Date.now })
createdAt: Date;
}
export const AuditLogSchema = SchemaFactory.createForClass(AuditLogModel);
// Create index for common queries
AuditLogSchema.index({ userId: 1, createdAt: -1 });
AuditLogSchema.index({ action: 1, createdAt: -1 });
AuditLogSchema.index({ resource: 1, resourceId: 1 });
AuditLogSchema.index({ createdAt: -1 });

View File

@@ -0,0 +1,42 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Types } from 'mongoose';
import { ReactEnum } from 'src/common/types/react.type';
import { Sender } from 'src/common/types/sender.type';
@Schema({
timestamps: { createdAt: 'createdAt' },
collection: 'reacts',
versionKey: false,
_id: true,
})
export class ReactsModel {
@Prop({ _id: true, default: new Types.ObjectId() })
sessionId: Types.ObjectId;
@Prop({ _id: true, default: new Types.ObjectId() })
messageId: Types.ObjectId;
@Prop({ _id: true, default: new Types.ObjectId() })
userId: Types.ObjectId;
@Prop({ default: false })
question: string;
@Prop({ default: false })
answer: string;
@Prop({ default: null, type: String, enum: Sender })
sender: Sender;
@Prop({ default: null, enum: ReactEnum })
react: string;
@Prop({ type: [String], required: true })
createdAt: [string, string]; // [time, date]
@Prop({ type: Date, default: Date.now })
createdISO: Date;
}
export const ReactSchema = SchemaFactory.createForClass(ReactsModel);

View File

@@ -0,0 +1,69 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export interface TimeInterval {
start: string; // HH:mm format
end: string; // HH:mm format
}
export interface WeeklyWindow {
day: 'saturday' | 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday';
intervals: TimeInterval[];
}
export interface Exception {
date: string; // YYYY-MM-DD format
intervals: TimeInterval[]; // Empty array means closed
reason?: string;
}
export interface BusinessHoursConfig {
timezone: string; // IANA timezone (e.g., 'Asia/Tehran')
globalToggle: boolean; // Master switch
weeklyWindows: WeeklyWindow[];
exceptions: Exception[];
policy?: {
onClose: 'allow_existing_until_end' | 'hard_close';
};
messageTemplate?: {
key: string;
default: string;
};
}
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'business_hours',
_id: true,
})
export class BusinessHoursModel extends Document {
@Prop({ type: Object, required: true })
config: BusinessHoursConfig;
@Prop({ type: [String], default: [] })
createdBy: string[]; // Array of admin usernames
@Prop({ type: String, required: false })
updatedBy?: string; // Last admin username who updated
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
@Prop({ type: Boolean, default: true })
isActive: boolean; // Only one should be active at a time
}
export const BusinessHoursSchema = SchemaFactory.createForClass(BusinessHoursModel);

View File

@@ -0,0 +1,29 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'categories',
_id: true,
})
export class CategoriesModel {
@Prop({ type: String })
title: string;
@Prop({ type: String })
enTitle: string;
@Prop({ type: Number })
priority: number;
@Prop({ type: String })
icon: string;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
}
export const CategoriesSchema = SchemaFactory.createForClass(CategoriesModel);

View File

@@ -0,0 +1,52 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import type { ChatAttachmentFileType } from 'src/storage/storage.types';
@Schema({
collection: 'chat_message_attachments',
timestamps: { createdAt: 'uploadedAt', updatedAt: false },
versionKey: false,
})
export class ChatMessageAttachmentModel extends Document {
@Prop({ type: Types.ObjectId, required: true })
messageId: Types.ObjectId;
@Prop({ type: Types.ObjectId, required: true })
sessionId: Types.ObjectId;
@Prop({ required: true })
uploaderId: string;
@Prop({ required: true, enum: ['User', 'Expert'] })
uploaderRole: 'User' | 'Expert';
@Prop({ required: true, enum: ['voice', 'image', 'document'] })
fileType: ChatAttachmentFileType;
@Prop({ required: true })
storageKey: string;
@Prop({ required: true, default: 'private' })
bucket: string;
@Prop({ required: true })
originalFilename: string;
@Prop({ required: true })
mimeType: string;
@Prop({ required: true })
size: number;
@Prop({ type: Number, required: false })
durationSec?: number;
@Prop({ required: true, default: 'ready', enum: ['ready'] })
status: 'ready';
}
export const ChatMessageAttachmentSchema = SchemaFactory.createForClass(
ChatMessageAttachmentModel,
);
ChatMessageAttachmentSchema.index({ sessionId: 1, messageId: 1 }, { unique: true });

View File

@@ -0,0 +1,39 @@
import { Prop, Schema } from '@nestjs/mongoose';
import { Types } from 'mongoose';
import { MessagesModel } from './messages.model';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
})
export class ChatModel {
@Prop({ _id: true, default: new Types.ObjectId() })
sessionId: Types.ObjectId;
@Prop({ type: String })
chatTitle: string;
@Prop({ default: false })
chatClosed: boolean;
@Prop({ default: false })
connectedToExpert: boolean;
@Prop({ default: false })
onlineChatClosed: boolean;
@Prop({ default: null })
expertRate: number | null;
@Prop({ type: String })
expert: string;
@Prop({ type: [MessagesModel], default: [] })
messages: MessagesModel[];
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
}

View File

@@ -0,0 +1,31 @@
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { Document, HydratedDocument } from 'mongoose';
export interface ClientInterface {
name_fa: string;
name_en: string;
apiKey: string;
status: string;
}
export type ClientDocument = HydratedDocument<ClientModel>;
@Schema({ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } })
export class ClientModel extends Document {
@Prop({ required: true })
name: string;
@Prop({ required: true })
enName: string;
@Prop({ required: true, unique: true })
apiKey: string;
@Prop({ enum: ['active', 'inactive'], default: 'active' })
status: string;
}
export const ClientSchema = SchemaFactory.createForClass(ClientModel);

View File

@@ -0,0 +1,77 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { QuestionsModel } from './questions.model';
export interface UpdatedBy {
name: string; // User's mobile number
updatedAt: {
time: string; // Time of update
date: string; // Date of update
};
}
@Schema({
timestamps: false,
versionKey: false,
collection: 'dictionaries',
_id: true,
})
export class DictionariesModel {
@Prop({ type: String })
title: string;
@Prop({ type: 'string' })
category: string;
@Prop({ type: String })
type: 'Regulation' | 'Method';
@Prop()
questions: QuestionsModel[];
@Prop({ type: Boolean })
isActive: boolean;
@Prop({ type: [String] })
fileId: string[];
@Prop({ type: String })
icon: string;
@Prop({ type: String, required: false })
aiCollectionId: string;
@Prop({ type: String, required: false })
aiCollectionName: string;
/** Cached from AI get-collections-with-descriptions / get-collection-description. */
@Prop({ type: String, required: false })
description?: string;
@Prop({ type: [String], default: [] })
keywords?: string[];
/**
* Sync bookkeeping vs AI SoT.
* ok | missing | deactivated | stale
*/
@Prop({ type: String, required: false, default: 'ok' })
aiSyncStatus?: string;
@Prop({ type: Date, required: false })
lastSyncedAt?: Date;
@Prop({ type: Array })
createdAt: [];
@Prop({ type: Date })
createdISO: Date;
@Prop({ type: Date })
updatedAt: Date;
@Prop({ type: [Object] })
updatedBy: UpdatedBy[];
}
export const DictionariesSchema =
SchemaFactory.createForClass(DictionariesModel);

View File

@@ -0,0 +1,34 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({
collection: 'dictionary_file_assets',
timestamps: { createdAt: 'uploadedAt', updatedAt: false },
versionKey: false,
})
export class DictionaryFileAssetModel extends Document {
@Prop({ required: true })
category: string;
@Prop({ type: Types.ObjectId, ref: 'AdminModel', required: true })
uploadedBy: Types.ObjectId;
@Prop({ required: true })
originalFilename: string;
/** Key inside the private bucket (no bucket name). */
@Prop({ required: true, unique: true })
storageKey: string;
@Prop({ required: true, default: 'private' })
bucket: string;
@Prop({ required: true })
mimeType: string;
@Prop({ required: true })
size: number;
}
export const DictionaryFileAssetSchema =
SchemaFactory.createForClass(DictionaryFileAssetModel);

View File

@@ -0,0 +1,54 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import {
ExpertPreparedMessageCategory,
EXPERT_PREPARED_MESSAGE_CATEGORIES,
} from 'src/common/types/expert-prepared-message-category.type';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'expert_prepared_messages',
_id: true,
})
export class ExpertPreparedMessageModel extends Document {
@Prop({ type: String, required: true, trim: true })
message: string;
@Prop({
type: String,
required: true,
enum: EXPERT_PREPARED_MESSAGE_CATEGORIES,
})
category: ExpertPreparedMessageCategory;
@Prop({ type: String, trim: true, default: '' })
label: string;
@Prop({ type: Boolean, default: true })
isEnabled: boolean;
@Prop({ type: Number, default: 0 })
sortOrder: number;
@Prop({ type: String, default: 'fa', trim: true })
locale: string;
@Prop({ type: String })
createdBy?: string;
@Prop({ type: String })
updatedBy?: string;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
}
export const ExpertPreparedMessageSchema = SchemaFactory.createForClass(
ExpertPreparedMessageModel,
);
ExpertPreparedMessageSchema.index({ isEnabled: 1, category: 1, sortOrder: 1 });

View File

@@ -0,0 +1,42 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'externalTokens',
_id: true,
})
export class ExternalTokensModel {
@Prop({ type: String })
token: string;
@Prop({ type: 'string' })
url: string;
@Prop({ type: Boolean })
isActive: boolean;
@Prop({ type: String })
method: 'access' | 'refresh';
@Prop({ type: String })
tokenType: 'Bearer' | 'Basic';
@Prop({ type: String })
expiresIn: string;
@Prop({ type: String })
scope: string;
@Prop({ type: Number })
timestamps: Number;
@Prop({ type: Array })
createdAt: [];
@Prop({ type: Array })
updatedAt: [];
}
export const ExternalTokenSchema =
SchemaFactory.createForClass(ExternalTokensModel);

View File

@@ -0,0 +1,34 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type FileUploadDocument = FileUploadModel & Document;
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'file',
_id: true,
})
export class FileUploadModel {
@Prop({ required: true })
originalName: string;
@Prop({ required: true })
filename: string;
@Prop({ required: true })
filePath: string;
@Prop({ required: true })
mimetype: string;
@Prop({ required: true })
size: number;
@Prop({ required: true })
type: string; // e.g., 'image', 'document'
@Prop({ required: true })
category: string; // e.g., 'profile', 'invoice'
}
export const FileUploadSchema = SchemaFactory.createForClass(FileUploadModel);

View File

@@ -0,0 +1,91 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'installments',
_id: true,
})
export class InstallmentModel {
@Prop({ type: Number, required: true, index: true })
policyId: number;
@Prop({ type: Types.ObjectId, required: true, index: true })
userId: Types.ObjectId;
@Prop({ type: String, required: true, unique: true })
installmentId: string;
@Prop({ type: String, required: false })
paymentMethod: string;
@Prop({ type: String, required: false })
installmentYear: string;
@Prop({ type: Number, required: false })
installmentNumber: number;
@Prop({ type: Number, required: false })
installmentPaymentAmount: number;
@Prop({ type: Number, required: false })
remainingAmount: number;
@Prop({ type: String, required: false })
installmentDate: string;
@Prop({ type: String, required: false })
installmentPaymentDate: string;
@Prop({ type: String, required: false })
installmentStatus: string;
@Prop({ type: Number, required: false })
totalInstallments: number;
@Prop({ type: Number, required: false })
paidInstallments: number;
@Prop({ type: Number, required: false })
upcomingInstallmentCount: number;
@Prop({ type: Number, required: false })
overdueInstallmentCount: number;
@Prop({ type: String, required: false })
bankId: string;
@Prop({ type: Number, required: false })
totalPremiumCollected: number;
@Prop({ type: Boolean, default: true, index: true })
isActive: boolean;
@Prop({ type: String, required: false, index: true })
syncBatchId: string;
@Prop({ type: Date, required: false })
lastSyncedAt: Date;
@Prop({ type: Date, default: Date.now })
lastUpdated: Date;
@Prop({ type: Date })
createdAt: Date;
@Prop({ type: Date })
updatedAt: Date;
}
export const InstallmentSchema = SchemaFactory.createForClass(InstallmentModel);
// Create compound indexes for efficient queries
InstallmentSchema.index({ policyId: 1, userId: 1 });
InstallmentSchema.index({ userId: 1 });
InstallmentSchema.index({ policyId: 1, installmentStatus: 1 });
InstallmentSchema.index({ installmentId: 1 }, { unique: true });
InstallmentSchema.index({ userId: 1, isActive: 1 });
InstallmentSchema.index({ userId: 1, syncBatchId: 1 });
// Made with Bob

View File

@@ -0,0 +1,26 @@
import { Prop, Schema } from '@nestjs/mongoose';
import { Types } from 'mongoose';
import { ReactEnum } from 'src/common/types/react.type';
import { Sender } from 'src/common/types/sender.type';
@Schema({
timestamps: { createdAt: 'createdAt' },
versionKey: false,
_id: true,
})
export class MessagesModel {
@Prop({ _id: true, default: new Types.ObjectId() })
messageId: Types.ObjectId;
@Prop({ default: false })
text: string;
@Prop({ default: null, type: Sender })
sender: Sender;
@Prop({ default: null, enum: ReactEnum })
react: string;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
}

View File

@@ -0,0 +1,105 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'policies',
_id: true,
})
export class PolicyModel {
@Prop({ type: Types.ObjectId, required: true, index: true })
userId: Types.ObjectId;
@Prop({ type: Number, required: true, index: true })
policyId: number;
@Prop({ type: String, required: true })
policyNumber: string;
@Prop({ type: String, required: true })
insuranceLineName: string;
@Prop({ type: Number, required: true })
insuranceLineCode: number;
@Prop({ type: String, required: true })
policyHolderName: string;
@Prop({ type: String, required: true })
insuredName: string;
@Prop({ type: String, required: true })
policyIssueDate: string;
@Prop({ type: String, required: true })
policyBeginDate: string;
@Prop({ type: String, required: true })
policyEndDate: string;
@Prop({ type: String, required: true })
policyStatus: string;
@Prop({ type: Number, required: true })
policyStatusCode: number;
@Prop({ type: String, required: true })
uniquePolicyCode: string;
@Prop({ type: String, required: false })
referrerName: string;
@Prop({ type: String, required: false })
issuingUnit: string;
@Prop({ type: String, required: false })
policyHolderCode: string;
@Prop({ type: Number, required: false })
agentCode: number;
@Prop({ type: String, required: true })
policyType: string; // 'life', 'car', 'health', 'fire', 'cargo', 'equipment', 'travel', 'liability', 'personalAccident'
@Prop({ type: String, required: false })
address: string;
@Prop({ type: String, required: false })
licensePlate: string;
@Prop({ type: String, required: false })
vehicleType: string;
@Prop({ type: Object, required: false })
additionalData: Record<string, unknown>;
@Prop({ type: Boolean, default: true, index: true })
isActive: boolean;
@Prop({ type: String, required: false, index: true })
syncBatchId: string;
@Prop({ type: Date, required: false })
lastSyncedAt: Date;
@Prop({ type: Date, default: Date.now })
lastUpdated: Date;
@Prop({ type: Date })
createdAt: Date;
@Prop({ type: Date })
updatedAt: Date;
}
export const PolicySchema = SchemaFactory.createForClass(PolicyModel);
// Create compound index for efficient queries
PolicySchema.index({ userId: 1, policyId: 1 }, { unique: true });
PolicySchema.index({ policyId: 1 });
PolicySchema.index({ userId: 1, policyStatusCode: 1 });
PolicySchema.index({ userId: 1, isActive: 1 });
PolicySchema.index({ userId: 1, syncBatchId: 1 });
// Made with Bob

View File

@@ -0,0 +1,56 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Types } from 'mongoose';
export interface requestedBy {
name: string; // User's mobile number
_id: Types.ObjectId;
}
@Schema({ collection: 'questionModification' })
export class QuestionModificationModel {
@Prop({ type: Types.ObjectId, ref: 'DictionariesModel' })
dictionaryId: Types.ObjectId;
@Prop({ type: Types.ObjectId })
questionId: Types.ObjectId;
@Prop({ type: String })
oldQuestion: string;
@Prop({ type: String })
oldAnswer: string;
@Prop({ type: String })
newQuestion: string;
@Prop({ type: String })
newAnswer: string;
@Prop({ type: Types.ObjectId })
requestedBy: requestedBy; // expert's mobile or ID
@Prop({ type: String })
status: 'pending' | 'approved' | 'rejected';
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Array })
createdAtPersian: [];
@Prop({ type: Date })
reviewedAt?: Date;
@Prop({ type: Array })
reviewedAtPersian: [];
@Prop({ type: Types.ObjectId })
reviewedBy?: requestedBy; // admin's ID
@Prop({ type: String })
rejectionReason?: string;
}
export const QuestionModificationSchema = SchemaFactory.createForClass(
QuestionModificationModel,
);

View File

@@ -0,0 +1,44 @@
import { Prop, Schema } from '@nestjs/mongoose';
import { Types } from 'mongoose';
export interface UpdatedBy {
name: string; // User's mobile number
updatedAt: {
time: string; // Time of update
date: string; // Date of update
};
}
@Schema({
timestamps: false,
versionKey: false,
_id: true,
})
export class QuestionsModel {
@Prop({ _id: true, default: new Types.ObjectId() })
_id: Types.ObjectId;
@Prop({ type: 'string' })
question: string;
@Prop({ type: 'string' })
answer: string;
/** AI sync/export item id (UUID). Required for AI-first PUT/DELETE. */
@Prop({ type: String, required: false })
aiItemId?: string;
@Prop({ type: Boolean, default: false })
deleted: boolean;
@Prop({ type: Array })
createdAt: any[];
@Prop({ type: Number })
createdISO: number;
@Prop({ type: Array })
updatedAt: any[];
@Prop({ type: [Object] })
updatedBy: UpdatedBy[];
}

View File

@@ -0,0 +1,20 @@
import { Prop, Schema } from '@nestjs/mongoose';
import { Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
})
export class RateModel {
@Prop({ _id: true, default: new Types.ObjectId() })
sessionId: Types.ObjectId;
@Prop({ type: String })
rate: string;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
createdAtDate: Date;
}

View File

@@ -0,0 +1,33 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'reassigned_logs',
})
export class ReassignedLogsModel extends Document {
@Prop({ type: Types.ObjectId, ref: 'SessionModel', required: true })
sessionId: Types.ObjectId;
@Prop({ type: Types.ObjectId, ref: 'UserModel', required: true })
userId: Types.ObjectId;
@Prop({ type: String, required: true })
from: string; // Expert email (from expert)
@Prop({ type: String, required: false, default: null })
to: string | null; // Expert email (to expert), or null if no expert available
@Prop({ type: String, required: false, default: 'auto' })
source: 'manual' | 'auto';
@Prop({ type: String, required: false })
mode?: 'selective' | 'random';
@Prop({ type: String, required: false })
reason?: string;
}
export const ReassignedLogsSchema = SchemaFactory.createForClass(ReassignedLogsModel);

View File

@@ -0,0 +1,125 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { Sender } from 'src/common/types/sender.type';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'sessions', // Name of the new collection
})
export class SessionModel extends Document {
@Prop({ type: Types.ObjectId, ref: 'UserModel', required: true })
userId: Types.ObjectId; // Reference to the user who owns this session
@Prop({ required: true })
chatTitle: string;
@Prop({ required: true, default: false })
connectedToExpert: boolean;
/** AI offered a human handoff; user still chooses whether to open online chat. */
@Prop({ required: false, default: false })
escalationOffered: boolean;
@Prop({ required: true, default: false })
onlineChatClosed: boolean;
@Prop({ type: Date, required: false, default: null })
onlineStartDate: Date;
@Prop({ type: Date, required: false, default: null })
onlineEndDate: Date;
@Prop({ required: true, default: false })
chatClosed: boolean;
@Prop({ required: false })
expert: string;
/** Manual expert transfers per session (max 1). Auto-reassign does not increment this. */
@Prop({ required: true, default: 0 })
transferCount: number;
@Prop({ required: false })
expertRate: number;
@Prop({ required: false })
adminRateToExpert: boolean;
@Prop({ required: false })
adminRateToBot: boolean;
@Prop({ required: false })
roomId: string;
@Prop({ type: [String], required: true })
createdAt: [string, string]; // [time, date]
@Prop({ type: Date, default: Date.now })
createdISO: Date;
@Prop({
type: [
{
messageId: { type: Types.ObjectId, required: true },
text: { type: String, required: true },
sender: { type: String, required: true },
react: { type: String, default: 'Nothing' },
runId: { type: String, required: false },
aiStatus: { type: String, required: false },
escalation: { type: Object, required: false },
createdAt: { type: [String], required: true }, // [time, date]
createdISO: { type: Date, required: true },
edited: { type: Boolean, default: false },
messageType: {
type: String,
enum: ['text', 'image', 'voice', 'document'],
default: 'text',
},
voiceDurationSec: { type: Number, required: false },
voiceMimeType: { type: String, required: false },
/** Image/document or generic attachment MIME (voice still uses voiceMimeType for compatibility). */
mimeType: { type: String, required: false },
replyTo: {
type: {
messageId: { type: Types.ObjectId, required: true },
textPreview: { type: String, default: '' },
sender: { type: String, required: true },
createdISO: { type: Date, required: false },
unavailable: { type: Boolean, default: false },
},
required: false,
},
},
],
default: [],
})
messages: {
messageId: Types.ObjectId;
text: string;
sender: Sender;
react: string;
createdAt: [string, string];
createdISO: Date;
edited: boolean;
runId?: string;
aiStatus?: string;
escalation?: {
summary?: string;
handoff_context?: Record<string, unknown>;
};
messageType?: 'text' | 'image' | 'voice' | 'document';
voiceDurationSec?: number;
voiceMimeType?: string;
mimeType?: string;
replyTo?: {
messageId: Types.ObjectId;
textPreview: string;
sender: Sender;
createdISO?: Date;
unavailable: boolean;
};
}[];
}
export const SessionSchema = SchemaFactory.createForClass(SessionModel);

View File

@@ -0,0 +1,33 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type StaffRoleDocument = StaffRoleModel & Document;
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'staff_roles',
})
export class StaffRoleModel {
/** Unique role slug stored on admin.role (e.g. admin, expert, content_manager). */
@Prop({ required: true, unique: true, index: true })
name: string;
@Prop({ required: false })
displayName?: string;
@Prop({ type: [String], default: [] })
permissions: string[];
/** System roles cannot be deleted or renamed. */
@Prop({ default: false })
isSystem: boolean;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
}
export const StaffRoleSchema = SchemaFactory.createForClass(StaffRoleModel);

View File

@@ -0,0 +1,35 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Types } from 'mongoose';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'user_insurance_snapshots',
_id: true,
})
export class UserInsuranceSnapshotModel {
@Prop({ type: Types.ObjectId, required: true, unique: true, index: true })
userId: Types.ObjectId;
@Prop({ type: String, required: true })
syncBatchId: string;
@Prop({ type: Date, required: true })
syncedAt: Date;
@Prop({ type: Object, required: true })
user_insurance_data: Record<string, unknown>;
@Prop({ type: Object, required: true })
user_installments_data: Record<string, unknown>;
@Prop({ type: Date })
createdAt: Date;
@Prop({ type: Date })
updatedAt: Date;
}
export const UserInsuranceSnapshotSchema = SchemaFactory.createForClass(
UserInsuranceSnapshotModel,
);

View File

@@ -0,0 +1,37 @@
import * as crypto from 'node:crypto';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ChatModel } from './chat.model';
import { UsersBaseModel } from './users-base.model';
@Schema({
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
versionKey: false,
collection: 'user',
_id: true,
})
export class UserModel extends UsersBaseModel {
@Prop({ default: [] })
chat: ChatModel[];
@Prop({
required: false,
select: true,
set: (otp: string) =>
otp ? crypto.createHash('sha256').update(otp).digest('hex') : null,
expires: 120, // The OTP will automatically be removed after 120 seconds
})
otp: string | null;
@Prop({ type: Date, default: null })
otpCreatedAt: Date | null;
@Prop({ type: Number, default: 0 })
otpAttempts: number;
}
export const UserSchema = SchemaFactory.createForClass(UserModel);
UserSchema.pre('save', function (next) {
this.updatedAt = new Date();
next();
});

View File

@@ -0,0 +1,76 @@
import * as crypto from 'node:crypto';
import { Prop, Schema } from '@nestjs/mongoose';
import { Role } from 'src/common/types/role.type';
@Schema()
export class UsersBaseModel {
@Prop()
name: string;
@Prop()
family: string;
@Prop({ required: true })
role: Role;
@Prop()
fatherName: string;
@Prop()
shenasnameseri: string;
@Prop()
shenasnameserial: string;
@Prop()
birthDate: string;
@Prop()
gender: string;
@Prop({
required: false,
match: /^[0-9]{11}$/,
default: undefined,
})
mobile: string;
@Prop()
birthday: string;
@Prop()
nationalCode: string;
@Prop({ match: /^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$/ })
email: string;
@Prop()
address: string;
@Prop({
default: function () {
// Generate 6-digit number or use mobile
return (
this.mobile || Math.floor(100000 + Math.random() * 900000).toString()
);
},
})
username: string;
@Prop({ type: Date })
last_login: Date;
@Prop({ type: Date, default: Date.now })
createdAt: Date;
@Prop({ type: Date, default: Date.now })
updatedAt: Date;
static validateOtp(otpInput: string, storedOtpHash: string): boolean {
const inputOtpHash = crypto
.createHash('sha256')
.update(otpInput)
.digest('hex');
return inputOtpHash === storedOtpHash;
}
}