forked from Yara724/api
Compare commits
10 Commits
c6f7edabd2
...
5618d120e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 5618d120e3 | |||
|
|
d9b1537ee4 | ||
| f01882dc3a | |||
|
|
8284aba825 | ||
| 3c15901ecc | |||
|
|
1e1edf9136 | ||
| daad9539ea | |||
|
|
194b71cdb2 | ||
|
|
04ba46ed2a | ||
|
|
95bfc095ce |
49
package-lock.json
generated
49
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@nestjs-modules/mailer": "^1.8.1",
|
"@nestjs-modules/mailer": "^1.8.1",
|
||||||
"@nestjs/axios": "^3.1.3",
|
"@nestjs/axios": "^3.1.3",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
@@ -4651,6 +4652,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/config": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "17.4.1",
|
||||||
|
"dotenv-expand": "12.0.3",
|
||||||
|
"lodash": "4.18.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nestjs/config/node_modules/dotenv": {
|
||||||
|
"version": "17.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
|
||||||
|
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nestjs/config/node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nestjs/core": {
|
"node_modules/@nestjs/core": {
|
||||||
"version": "10.4.15",
|
"version": "10.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz",
|
||||||
@@ -8441,6 +8475,21 @@
|
|||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv-expand": {
|
||||||
|
"version": "12.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
|
||||||
|
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@nestjs-modules/mailer": "^1.8.1",
|
"@nestjs-modules/mailer": "^1.8.1",
|
||||||
"@nestjs/axios": "^3.1.3",
|
"@nestjs/axios": "^3.1.3",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import { LoginDtoRs } from "src/auth/dto/user/login.dto";
|
|||||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
import { OtpService } from "src/utils/otp/otp.service";
|
import { OtpService } from "src/utils/otp/otp.service";
|
||||||
import { SmsManagerService } from "src/utils/sms-manager/sms-manager.service";
|
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||||
import { describeSmsError } from "src/utils/sms-manager/sms-provider.exception";
|
|
||||||
|
|
||||||
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
|
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -26,7 +25,7 @@ export class UserAuthService {
|
|||||||
private readonly userDbService: UserDbService,
|
private readonly userDbService: UserDbService,
|
||||||
private readonly hashService: HashService,
|
private readonly hashService: HashService,
|
||||||
private readonly otpCreator: OtpService,
|
private readonly otpCreator: OtpService,
|
||||||
private readonly smsManagerService: SmsManagerService,
|
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateUser(username: string, pass: string): Promise<any> {
|
async validateUser(username: string, pass: string): Promise<any> {
|
||||||
@@ -118,26 +117,19 @@ export class UserAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async smsSender(otp: string, mobile: string) {
|
private async smsSender(otp: string, mobile: string) {
|
||||||
try {
|
const ok = await this.smsOrchestrationService.sendAuthOtp(
|
||||||
await this.smsManagerService.verifyLookUp({
|
mobile,
|
||||||
token: otp,
|
otp,
|
||||||
template: process.env.AUTH_SMS_TEMPLATE,
|
process.env.AUTH_SMS_TEMPLATE,
|
||||||
receptor: mobile,
|
|
||||||
});
|
|
||||||
this.logger.log(
|
|
||||||
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
if (!ok) {
|
||||||
this.logger.error(
|
|
||||||
`Auth OTP SMS failed phone=${mobile} otp=${otp} ${describeSmsError(err)}`,
|
|
||||||
);
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
"auth sms send failed",
|
"auth sms send failed",
|
||||||
HttpStatus.BAD_GATEWAY,
|
HttpStatus.BAD_GATEWAY,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.logger.log(
|
||||||
|
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { UsersModule } from "src/users/users.module";
|
|||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
// import { MailModule } from "src/utils/mail/mail.module";
|
// import { MailModule } from "src/utils/mail/mail.module";
|
||||||
import { OtpModule } from "src/utils/otp/otp.module";
|
import { OtpModule } from "src/utils/otp/otp.module";
|
||||||
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -22,7 +22,7 @@ import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
|||||||
HashModule,
|
HashModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
SmsManagerModule,
|
SmsOrchestrationModule,
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
signOptions: { expiresIn: "1h" },
|
signOptions: { expiresIn: "1h" },
|
||||||
global: true,
|
global: true,
|
||||||
|
|||||||
@@ -4987,14 +4987,14 @@ export class ClaimRequestManagementService {
|
|||||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||||
|
|
||||||
const requiredDocs = claim.requiredDocuments as any;
|
const requiredDocs = claim.requiredDocuments as any;
|
||||||
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileId?: string }> = {};
|
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileUrl?: string }> = {};
|
||||||
if (requiredDocs) {
|
if (requiredDocs) {
|
||||||
const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs);
|
const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs);
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
||||||
requiredDocumentsStatus[k] = {
|
requiredDocumentsStatus[k] = {
|
||||||
uploaded: !!doc?.uploaded,
|
uploaded: !!doc?.uploaded,
|
||||||
fileId: doc?.fileId?.toString(),
|
fileUrl: doc?.filePath ? buildFileLink(doc.filePath) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5041,6 +5041,25 @@ export class ClaimRequestManagementService {
|
|||||||
s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined;
|
s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined;
|
||||||
const maskNationalCode = (s?: string) =>
|
const maskNationalCode = (s?: string) =>
|
||||||
s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined;
|
s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined;
|
||||||
|
const isExpertViewer = actor?.role === RoleEnum.FIELD_EXPERT;
|
||||||
|
const ownerData = claim.owner
|
||||||
|
? {
|
||||||
|
userId: claim.owner.userId?.toString(),
|
||||||
|
fullName: claim.owner.fullName,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const moneyForUser = claim.money
|
||||||
|
? {
|
||||||
|
sheba: maskSheba(claim.money.sheba),
|
||||||
|
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const moneyForExpert = claim.money
|
||||||
|
? {
|
||||||
|
sheba: claim.money.sheba,
|
||||||
|
nationalCodeOfOwner: claim.money.nationalCodeOfInsurer,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claim._id.toString(),
|
claimRequestId: claim._id.toString(),
|
||||||
@@ -5052,25 +5071,17 @@ export class ClaimRequestManagementService {
|
|||||||
nextStep: claim.workflow?.nextStep,
|
nextStep: claim.workflow?.nextStep,
|
||||||
blameRequestId: claim.blameRequestId?.toString(),
|
blameRequestId: claim.blameRequestId?.toString(),
|
||||||
blameRequestNo: claim.blameRequestNo,
|
blameRequestNo: claim.blameRequestNo,
|
||||||
owner: claim.owner
|
...(isExpertViewer ? { owner: ownerData } : {}),
|
||||||
? {
|
|
||||||
userId: claim.owner.userId?.toString(),
|
|
||||||
fullName: claim.owner.fullName,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
vehicle: claim.vehicle,
|
vehicle: claim.vehicle,
|
||||||
selectedParts: claim.damage?.selectedParts,
|
selectedParts: claim.damage?.selectedParts,
|
||||||
otherParts: claim.damage?.otherParts,
|
otherParts: claim.damage?.otherParts,
|
||||||
money: claim.money
|
money: isExpertViewer ? moneyForExpert : moneyForUser,
|
||||||
? {
|
|
||||||
sheba: maskSheba(claim.money.sheba),
|
|
||||||
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
|
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
|
||||||
carAngles,
|
carAngles,
|
||||||
damagedParts,
|
damagedParts,
|
||||||
expertResend,
|
expertResend,
|
||||||
|
...(isExpertViewer
|
||||||
|
? {
|
||||||
userRating: claim.userRating
|
userRating: claim.userRating
|
||||||
? {
|
? {
|
||||||
progressSpeed: claim.userRating.progressSpeed,
|
progressSpeed: claim.userRating.progressSpeed,
|
||||||
@@ -5080,6 +5091,8 @@ export class ClaimRequestManagementService {
|
|||||||
createdAt: claim.userRating.createdAt,
|
createdAt: claim.userRating.createdAt,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
createdAt: (claim as any).createdAt,
|
createdAt: (claim as any).createdAt,
|
||||||
updatedAt: (claim as any).updatedAt,
|
updatedAt: (claim as any).updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
})
|
})
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get Claim Details (V2)",
|
summary: "Get Claim Details (V2)",
|
||||||
description: "Get full details of a claim request. Only the claim owner can access.",
|
description:
|
||||||
|
"Get claim details for current actor. USER receives only minimal own-needed payload (no extra owner/security fields). FIELD_EXPERT keeps full view for expert workflows.",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -560,8 +561,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
description: `
|
description: `
|
||||||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
||||||
|
|
||||||
**Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
||||||
- car_green_card
|
|
||||||
- damaged_driving_license_front/back
|
- damaged_driving_license_front/back
|
||||||
- damaged_chassis_number, damaged_engine_photo
|
- damaged_chassis_number, damaged_engine_photo
|
||||||
- damaged_car_card_front/back, damaged_metal_plate
|
- damaged_car_card_front/back, damaged_metal_plate
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export class ClaimDetailsV2ResponseDto {
|
|||||||
nationalCodeOfOwner?: string;
|
nationalCodeOfOwner?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Required documents status' })
|
@ApiPropertyOptional({ description: 'Required documents status (link instead of id)' })
|
||||||
requiredDocuments?: Record<string, { uploaded: boolean; fileId?: string }>;
|
requiredDocuments?: Record<string, { uploaded: boolean; fileUrl?: string }>;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Car angles captured' })
|
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||||
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ export class ResendRequestDto {
|
|||||||
description: "Array of resend requests (can include first party, second party, or both)",
|
description: "Array of resend requests (can include first party, second party, or both)",
|
||||||
example: [
|
example: [
|
||||||
{
|
{
|
||||||
partyId: "507f1f77bcf86cd799439011",
|
partyId: "681b3b3fb237e5856429e444",
|
||||||
requestedItems: ["drivingLicense", "carCertificate"],
|
requestedItems: ["drivingLicense"],
|
||||||
description: "Please resend with better quality",
|
description: "DESC TEST1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyId: "68b6ba552a6e897df34e0f90",
|
||||||
|
requestedItems: ["carCertificate"],
|
||||||
|
description: "DESC TEST2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ function toIranFaDateTimeTuple(input: Date): [string, string] {
|
|||||||
return [dateFa, timeFa];
|
return [dateFa, timeFa];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMissingFaTuple(value: unknown): boolean {
|
||||||
|
if (value == null) return true;
|
||||||
|
if (!Array.isArray(value)) return false;
|
||||||
|
if (value.length < 2) return true;
|
||||||
|
const [d, t] = value;
|
||||||
|
return !d || !t;
|
||||||
|
}
|
||||||
|
|
||||||
function writeFaTimestampsOnUpdate(query: Query<any, any>): void {
|
function writeFaTimestampsOnUpdate(query: Query<any, any>): void {
|
||||||
const nowFa = toIranFaDateTimeTuple(new Date());
|
const nowFa = toIranFaDateTimeTuple(new Date());
|
||||||
const update = (query.getUpdate() ?? {}) as Record<string, any>;
|
const update = (query.getUpdate() ?? {}) as Record<string, any>;
|
||||||
@@ -46,15 +54,24 @@ function writeFaTimestampsOnUpdate(query: Query<any, any>): void {
|
|||||||
update.$set = {};
|
update.$set = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.$set.createdAtFa == null && update.createdAtFa == null) {
|
|
||||||
update.$set.createdAtFa = nowFa;
|
|
||||||
}
|
|
||||||
update.$set.updatedAtFa = nowFa;
|
update.$set.updatedAtFa = nowFa;
|
||||||
|
|
||||||
|
// Keep createdAtFa immutable on regular updates; set only for upsert inserts.
|
||||||
|
const opts = (query as any).getOptions?.() ?? {};
|
||||||
|
if (opts.upsert) {
|
||||||
|
if (!update.$setOnInsert || typeof update.$setOnInsert !== "object") {
|
||||||
|
update.$setOnInsert = {};
|
||||||
|
}
|
||||||
|
if (isMissingFaTuple(update.$setOnInsert.createdAtFa)) {
|
||||||
|
update.$setOnInsert.createdAtFa = nowFa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query.setUpdate(update);
|
query.setUpdate(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds `createdAtFa` / `updatedAtFa` (Iran timezone, UTC+03:30 offset string)
|
* Adds `createdAtFa` / `updatedAtFa` (Iran timezone tuple: [date, time])
|
||||||
* to all schemas that use mongoose timestamps.
|
* to all schemas that use mongoose timestamps.
|
||||||
*/
|
*/
|
||||||
export function applyIranFaTimestampPlugin(connection: Connection): void {
|
export function applyIranFaTimestampPlugin(connection: Connection): void {
|
||||||
@@ -69,7 +86,7 @@ export function applyIranFaTimestampPlugin(connection: Connection): void {
|
|||||||
|
|
||||||
schema.pre("save", function (next) {
|
schema.pre("save", function (next) {
|
||||||
const nowFa = toIranFaDateTimeTuple(new Date());
|
const nowFa = toIranFaDateTimeTuple(new Date());
|
||||||
if (!(this as any).createdAtFa) {
|
if (isMissingFaTuple((this as any).createdAtFa)) {
|
||||||
(this as any).createdAtFa = nowFa;
|
(this as any).createdAtFa = nowFa;
|
||||||
}
|
}
|
||||||
(this as any).updatedAtFa = nowFa;
|
(this as any).updatedAtFa = nowFa;
|
||||||
@@ -79,7 +96,7 @@ export function applyIranFaTimestampPlugin(connection: Connection): void {
|
|||||||
schema.pre("insertMany", function (next, docs: any[]) {
|
schema.pre("insertMany", function (next, docs: any[]) {
|
||||||
const nowFa = toIranFaDateTimeTuple(new Date());
|
const nowFa = toIranFaDateTimeTuple(new Date());
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (!doc.createdAtFa) doc.createdAtFa = nowFa;
|
if (isMissingFaTuple(doc.createdAtFa)) doc.createdAtFa = nowFa;
|
||||||
doc.updatedAtFa = nowFa;
|
doc.updatedAtFa = nowFa;
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class SendExpertInitiatedLinkV2Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
"Frontend route path (same as add-second-party flow), e.g. requestManagement/firstParty",
|
||||||
|
example: "requestManagement/firstParty",
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
frontendRoute: string;
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signatu
|
|||||||
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
||||||
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
||||||
import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
|
import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
|
||||||
|
import { SendExpertInitiatedLinkV2Dto } from "./dto/send-expert-initiated-link.v2.dto";
|
||||||
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
|
||||||
@@ -156,12 +157,13 @@ export class ExpertInitiatedV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "[V2] Send blame link to party/parties (LINK)",
|
summary: "[V2] Send blame link to party/parties (LINK)",
|
||||||
description:
|
description:
|
||||||
"For expert-initiated LINK files only. Sends the blame link to the first party (and second party for THIRD_PARTY). SMS delivery is mocked for now; first party opens the link and fills the form via the normal flow. Call after create when creationMethod is LINK.",
|
"For expert-initiated LINK files only. Sends SMS with template `yara-field-expert-link` to first party (and second party for THIRD_PARTY). Tokens: token=#1(بدنه/ثالث), token2=#2(expert name), token3=#3(invite link built exactly like add-second-party flow: `${URL}/{frontendRoute}?token={requestId}`).",
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: "Link sent (mocked); recipients can open the link to fill the form",
|
description: "Link sent; recipients can open and fill via normal user flow",
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -174,6 +176,7 @@ export class ExpertInitiatedV2Controller {
|
|||||||
properties: {
|
properties: {
|
||||||
role: { type: "string", enum: ["FIRST", "SECOND"] },
|
role: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
phoneNumber: { type: "string" },
|
phoneNumber: { type: "string" },
|
||||||
|
smsSent: { type: "boolean" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -183,8 +186,9 @@ export class ExpertInitiatedV2Controller {
|
|||||||
async sendLinkV2(
|
async sendLinkV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: SendExpertInitiatedLinkV2Dto,
|
||||||
) {
|
) {
|
||||||
return this.requestManagementService.sendLinkV2(expert, requestId);
|
return this.requestManagementService.sendLinkV2(expert, requestId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("send-party-otps/:requestId")
|
@Post("send-party-otps/:requestId")
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import { BlameRequestDbService } from "src/request-management/entities/db-servic
|
|||||||
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { CronModule } from "src/utils/cron/cron.module";
|
import { CronModule } from "src/utils/cron/cron.module";
|
||||||
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
|
||||||
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
||||||
import { AuthModule } from "src/auth/auth.module";
|
import { AuthModule } from "src/auth/auth.module";
|
||||||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||||||
@@ -45,7 +45,7 @@ import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
SandHubModule,
|
SandHubModule,
|
||||||
SmsManagerModule,
|
SmsOrchestrationModule,
|
||||||
PublicIdModule,
|
PublicIdModule,
|
||||||
HashModule,
|
HashModule,
|
||||||
WorkflowStepManagementModule,
|
WorkflowStepManagementModule,
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
|||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||||
import { AutoCloseRequestService } from "src/utils/cron/cron.service";
|
import { AutoCloseRequestService } from "src/utils/cron/cron.service";
|
||||||
import { SmsManagerService } from "src/utils/sms-manager/sms-manager.service";
|
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||||
import { describeSmsError } from "src/utils/sms-manager/sms-provider.exception";
|
|
||||||
import {
|
import {
|
||||||
DescriptionDto,
|
DescriptionDto,
|
||||||
LocationDto,
|
LocationDto,
|
||||||
@@ -79,6 +78,7 @@ import {
|
|||||||
buildMutualAgreementExpertDecision,
|
buildMutualAgreementExpertDecision,
|
||||||
MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS,
|
MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS,
|
||||||
} from "src/helpers/blame-party-agreement-decision";
|
} from "src/helpers/blame-party-agreement-decision";
|
||||||
|
import { buildFileLink } from "src/helpers/urlCreator";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestManagementService {
|
export class RequestManagementService {
|
||||||
@@ -307,7 +307,7 @@ export class RequestManagementService {
|
|||||||
private readonly clientService: ClientService,
|
private readonly clientService: ClientService,
|
||||||
private readonly blameVoiceDbService: BlameVoiceDbService,
|
private readonly blameVoiceDbService: BlameVoiceDbService,
|
||||||
private readonly userSign: UserSignDbService,
|
private readonly userSign: UserSignDbService,
|
||||||
private readonly smsManagerService: SmsManagerService,
|
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||||
private readonly expertDbService: ExpertDbService,
|
private readonly expertDbService: ExpertDbService,
|
||||||
private readonly autoCloseRequestService: AutoCloseRequestService,
|
private readonly autoCloseRequestService: AutoCloseRequestService,
|
||||||
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
|
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
|
||||||
@@ -1304,21 +1304,14 @@ export class RequestManagementService {
|
|||||||
metadata: { secondPartyPhone, expertInitiatedLink: true },
|
metadata: { secondPartyPhone, expertInitiatedLink: true },
|
||||||
} as any);
|
} as any);
|
||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
const url = `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
const url = this.smsOrchestrationService.buildInviteLink(
|
||||||
try {
|
frontendRoute,
|
||||||
await this.smsManagerService.verifyLookUp({
|
requestId,
|
||||||
token: url,
|
|
||||||
template: "yara724-invite-link",
|
|
||||||
receptor: secondPartyPhone,
|
|
||||||
});
|
|
||||||
this.logger.log(
|
|
||||||
`[SMS] Invitation resent to ${secondPartyPhone} for expert-initiated request ${req.publicId}`,
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
await this.smsOrchestrationService.sendInviteLink(
|
||||||
this.logger.error(
|
secondPartyPhone,
|
||||||
`[SMS] Failed to send invitation to ${secondPartyPhone}: ${describeSmsError(err)}`,
|
url,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
requestId: req._id,
|
requestId: req._id,
|
||||||
publicId: req.publicId,
|
publicId: req.publicId,
|
||||||
@@ -1361,22 +1354,11 @@ export class RequestManagementService {
|
|||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
|
|
||||||
// Send SMS invitation
|
// Send SMS invitation
|
||||||
const url = `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
const url = this.smsOrchestrationService.buildInviteLink(
|
||||||
try {
|
frontendRoute,
|
||||||
await this.smsManagerService.verifyLookUp({
|
requestId,
|
||||||
token: url,
|
|
||||||
template: "yara724-invite-link",
|
|
||||||
receptor: phoneNumber,
|
|
||||||
});
|
|
||||||
this.logger.log(
|
|
||||||
`[SMS] Invitation sent to ${phoneNumber} for request ${req.publicId}`,
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
await this.smsOrchestrationService.sendInviteLink(phoneNumber, url);
|
||||||
this.logger.error(
|
|
||||||
`[SMS] Failed to send invitation to ${phoneNumber}: ${describeSmsError(err)}`,
|
|
||||||
);
|
|
||||||
// Don't block the request flow if SMS fails
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestId: req._id,
|
requestId: req._id,
|
||||||
@@ -2294,21 +2276,11 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the SMS after the database update is complete.
|
// Send the SMS after the database update is complete.
|
||||||
const URL = `${process.env.URL}/${frontendRoutes}?token=${requestId}`;
|
const URL = this.smsOrchestrationService.buildInviteLink(
|
||||||
try {
|
frontendRoutes,
|
||||||
await this.smsManagerService.verifyLookUp({
|
requestId,
|
||||||
token: URL,
|
|
||||||
template: "yara724-invite-link",
|
|
||||||
receptor: phoneNumber,
|
|
||||||
});
|
|
||||||
this.logger.log(
|
|
||||||
`[SMS] Invite link verifyLookUp ok receptor=${phoneNumber}`,
|
|
||||||
);
|
);
|
||||||
} catch (er) {
|
await this.smsOrchestrationService.sendInviteLink(phoneNumber, URL);
|
||||||
this.logger.error(
|
|
||||||
`[SMS] Invite link failed receptor=${phoneNumber}: ${describeSmsError(er)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { url: URL };
|
return { url: URL };
|
||||||
}
|
}
|
||||||
@@ -2837,19 +2809,13 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
// TODO SMS Sending
|
// TODO SMS Sending
|
||||||
if (phoneNumberToNotify) {
|
if (phoneNumberToNotify) {
|
||||||
const message = `متاسفانه طرف مقابل با نظر کارشناس مخالفت کرد. فرآیند آنلاین این پرونده بسته شده است و جهت پیگیری حضوری اقدام نمایید.`;
|
await this.smsOrchestrationService.sendTextByKey(
|
||||||
try {
|
phoneNumberToNotify,
|
||||||
await this.smsManagerService.sendMessage({
|
"parties_disagree_notify",
|
||||||
receptor: phoneNumberToNotify,
|
"متاسفانه طرف مقابل با نظر کارشناس مخالفت کرد. فرآیند آنلاین این پرونده بسته شده است و جهت پیگیری حضوری اقدام نمایید.",
|
||||||
message,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
`[SMS] PartiesDisagree notify failed receptor=${phoneNumberToNotify}: ${describeSmsError(err)}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePostReplyStateChanges(
|
private async handlePostReplyStateChanges(
|
||||||
updatedReq: any,
|
updatedReq: any,
|
||||||
@@ -3027,18 +2993,16 @@ export class RequestManagementService {
|
|||||||
: originalReq.firstPartyDetails?.firstPartyPhoneNumber;
|
: originalReq.firstPartyDetails?.firstPartyPhoneNumber;
|
||||||
|
|
||||||
if (phoneNumber) {
|
if (phoneNumber) {
|
||||||
// TODO FIX SMS SENDING FOR PARTIES
|
const key =
|
||||||
try {
|
message.includes("24 ساعت")
|
||||||
await this.smsManagerService.sendMessage({
|
? "one_party_accepted_wait_24h"
|
||||||
receptor: phoneNumber,
|
: "one_party_signed_wait_signature";
|
||||||
|
await this.smsOrchestrationService.sendTextByKey(
|
||||||
|
phoneNumber,
|
||||||
|
key,
|
||||||
message,
|
message,
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
`[SMS] One-party-replied notify failed receptor=${phoneNumber}: ${describeSmsError(err)}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await this.autoCloseRequestService.scheduleAutoClose({
|
await this.autoCloseRequestService.scheduleAutoClose({
|
||||||
requestId: updatedReq._id.toString(),
|
requestId: updatedReq._id.toString(),
|
||||||
@@ -3740,10 +3704,11 @@ export class RequestManagementService {
|
|||||||
async sendLinkV2(
|
async sendLinkV2(
|
||||||
expert: any,
|
expert: any,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
|
dto: { frontendRoute: string },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
sent: boolean;
|
sent: boolean;
|
||||||
linkUrl: string;
|
linkUrl: string;
|
||||||
sentTo: { role: string; phoneNumber: string }[];
|
sentTo: { role: string; phoneNumber: string; smsSent: boolean }[];
|
||||||
}> {
|
}> {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
@@ -3754,29 +3719,43 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const baseUrl =
|
if (!dto?.frontendRoute) {
|
||||||
process.env.FRONTEND_BASE_URL || process.env.APP_URL || "";
|
throw new BadRequestException("frontendRoute is required");
|
||||||
const linkUrl = baseUrl
|
}
|
||||||
? `${baseUrl.replace(/\/$/, "")}/blame/${requestId}`
|
if (!process.env.URL) {
|
||||||
: "";
|
throw new InternalServerErrorException(
|
||||||
|
"URL environment variable is not configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const linkUrl = this.smsOrchestrationService.buildInviteLink(
|
||||||
|
dto.frontendRoute,
|
||||||
|
requestId,
|
||||||
|
);
|
||||||
|
const expertName =
|
||||||
|
`${expert?.lastName || ""}`.trim() || "کارشناس";
|
||||||
|
|
||||||
|
const sentTo: { role: string; phoneNumber: string; smsSent: boolean }[] = [];
|
||||||
|
const sendSms = async (phone: string, role: PartyRole): Promise<boolean> => {
|
||||||
|
return this.smsOrchestrationService.sendFieldExpertLink({
|
||||||
|
receptor: phone,
|
||||||
|
type: req.type,
|
||||||
|
expertLastName: expertName,
|
||||||
|
link: linkUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const sentTo: { role: string; phoneNumber: string }[] = [];
|
|
||||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) {
|
if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) {
|
||||||
const phone = req.parties[firstIdx].person.phoneNumber;
|
const phone = req.parties[firstIdx].person.phoneNumber;
|
||||||
this.logger.log(
|
const smsSent = await sendSms(phone, PartyRole.FIRST);
|
||||||
`[MOCK SMS] Would send link to first party ${phone}: ${linkUrl}`,
|
sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone, smsSent });
|
||||||
);
|
|
||||||
sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone });
|
|
||||||
}
|
}
|
||||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||||
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
|
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
|
||||||
if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) {
|
if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) {
|
||||||
const phone = req.parties[secondIdx].person.phoneNumber;
|
const phone = req.parties[secondIdx].person.phoneNumber;
|
||||||
this.logger.log(
|
const smsSent = await sendSms(phone, PartyRole.SECOND);
|
||||||
`[MOCK SMS] Would send link to second party ${phone}: ${linkUrl}`,
|
sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone, smsSent });
|
||||||
);
|
|
||||||
sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3788,11 +3767,20 @@ export class RequestManagementService {
|
|||||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||||
actorType: "field_expert",
|
actorType: "field_expert",
|
||||||
},
|
},
|
||||||
metadata: { linkUrl, sentTo },
|
metadata: {
|
||||||
|
linkUrl,
|
||||||
|
sentTo,
|
||||||
|
template: "yara-field-expert-link",
|
||||||
|
frontendRoute: dto.frontendRoute,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
|
|
||||||
return { sent: true, linkUrl, sentTo };
|
return {
|
||||||
|
sent: sentTo.length > 0 && sentTo.every((x) => x.smsSent),
|
||||||
|
linkUrl,
|
||||||
|
sentTo,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4117,6 +4105,133 @@ export class RequestManagementService {
|
|||||||
return none;
|
return none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Party payload for user blame detail: no national codes, licenses, phone, birthdays, or vehicle inquiry blobs. */
|
||||||
|
private async sanitizePartyForBlameUserView(
|
||||||
|
party: any,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (!party || typeof party !== "object") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const person = party.person;
|
||||||
|
const safePerson = person
|
||||||
|
? {
|
||||||
|
userId:
|
||||||
|
person.userId != null ? String(person.userId) : undefined,
|
||||||
|
fullName: person.fullName,
|
||||||
|
clientId:
|
||||||
|
person.clientId != null ? String(person.clientId) : undefined,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const vehicle = party.vehicle
|
||||||
|
? {
|
||||||
|
plateId: party.vehicle.plateId,
|
||||||
|
name: party.vehicle.name,
|
||||||
|
model: party.vehicle.model,
|
||||||
|
type: party.vehicle.type,
|
||||||
|
isNew: party.vehicle.isNew,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const evidenceRaw = party.evidence as Record<string, unknown> | undefined;
|
||||||
|
const evidence: Record<string, unknown> | undefined = evidenceRaw
|
||||||
|
? { ...evidenceRaw }
|
||||||
|
: undefined;
|
||||||
|
if (evidence) {
|
||||||
|
if (party.role === PartyRole.FIRST && evidence.videoId) {
|
||||||
|
const videoDoc = await this.blameVideoDbService.findById(
|
||||||
|
String(evidence.videoId),
|
||||||
|
);
|
||||||
|
if (videoDoc?.path) {
|
||||||
|
evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete evidence.videoId;
|
||||||
|
|
||||||
|
const voiceIds = evidence.voices;
|
||||||
|
if (Array.isArray(voiceIds)) {
|
||||||
|
const voiceUrls: string[] = [];
|
||||||
|
for (const voiceId of voiceIds) {
|
||||||
|
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||||
|
String(voiceId),
|
||||||
|
);
|
||||||
|
if (voiceDoc?.path) {
|
||||||
|
voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evidence.voiceUrls = voiceUrls;
|
||||||
|
}
|
||||||
|
delete evidence.voices;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: party.role,
|
||||||
|
person: safePerson,
|
||||||
|
carBodyFirstForm: party.carBodyFirstForm,
|
||||||
|
location: party.location,
|
||||||
|
vehicle,
|
||||||
|
insurance: party.insurance,
|
||||||
|
statement: party.statement,
|
||||||
|
evidence,
|
||||||
|
confirmation: party.confirmation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveBlameExpertDisplayNameFromPlain(
|
||||||
|
expert: Record<string, unknown>,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const decision = expert.decision as Record<string, unknown> | undefined;
|
||||||
|
const decided = decision?.decidedByExpertId;
|
||||||
|
const assigned = expert.assignedExpertId;
|
||||||
|
const raw = decided ?? assigned;
|
||||||
|
if (raw == null || raw === "") return undefined;
|
||||||
|
const sid = String(raw);
|
||||||
|
if (!Types.ObjectId.isValid(sid)) return undefined;
|
||||||
|
const doc = await this.expertDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(sid),
|
||||||
|
});
|
||||||
|
if (!doc) return undefined;
|
||||||
|
return `${doc.firstName || ""} ${doc.lastName || ""}`.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expert subdocument for user view: string ids, trimmed snapshot (name only), plus `expertName`. */
|
||||||
|
private async buildExpertForBlameUserView(
|
||||||
|
expertRaw: any,
|
||||||
|
): Promise<Record<string, unknown> | undefined> {
|
||||||
|
if (!expertRaw || typeof expertRaw !== "object") return undefined;
|
||||||
|
const out = JSON.parse(JSON.stringify(expertRaw)) as Record<string, unknown>;
|
||||||
|
const decision = out.decision as Record<string, unknown> | undefined;
|
||||||
|
let expertName: string | undefined;
|
||||||
|
|
||||||
|
if (decision) {
|
||||||
|
if (decision.guiltyPartyId != null) {
|
||||||
|
decision.guiltyPartyId = String(decision.guiltyPartyId);
|
||||||
|
}
|
||||||
|
if (decision.decidedByExpertId != null) {
|
||||||
|
decision.decidedByExpertId = String(decision.decidedByExpertId);
|
||||||
|
}
|
||||||
|
const snap = decision.expertProfileSnapshot as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
if (snap) {
|
||||||
|
expertName =
|
||||||
|
`${snap.firstName ?? ""} ${snap.lastName ?? ""}`.trim() || undefined;
|
||||||
|
decision.expertProfileSnapshot = {
|
||||||
|
firstName: snap.firstName,
|
||||||
|
lastName: snap.lastName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.assignedExpertId != null) {
|
||||||
|
out.assignedExpertId = String(out.assignedExpertId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expertName) {
|
||||||
|
expertName = await this.resolveBlameExpertDisplayNameFromPlain(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...out, expertName };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2: Get one blame request by id. Access allowed if current user is a party (by userId or phone)
|
* V2: Get one blame request by id. Access allowed if current user is a party (by userId or phone)
|
||||||
* or the initiating field expert. For expert-initiated LINK, party access by phone is sufficient.
|
* or the initiating field expert. For expert-initiated LINK, party access by phone is sufficient.
|
||||||
@@ -4165,7 +4280,36 @@ export class RequestManagementService {
|
|||||||
typeof (req as any).toObject === "function"
|
typeof (req as any).toObject === "function"
|
||||||
? (req as any).toObject({ versionKey: false })
|
? (req as any).toObject({ versionKey: false })
|
||||||
: { ...(req as any) };
|
: { ...(req as any) };
|
||||||
return { ...plain, claimCreation };
|
|
||||||
|
const allParties = Array.isArray(plain.parties) ? plain.parties : [];
|
||||||
|
const visibleParties =
|
||||||
|
isFieldExpertOwner || isRegistrarOwner
|
||||||
|
? allParties
|
||||||
|
: allParties.filter((p: any) => {
|
||||||
|
const pid = p?.person?.userId ? String(p.person.userId) : null;
|
||||||
|
const phone = p?.person?.phoneNumber;
|
||||||
|
return (
|
||||||
|
(pid && pid === String(user?.sub)) ||
|
||||||
|
(phone && phone === user?.username)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const parties = await Promise.all(
|
||||||
|
visibleParties.map((p: any) => this.sanitizePartyForBlameUserView(p)),
|
||||||
|
);
|
||||||
|
const expert = await this.buildExpertForBlameUserView(plain.expert);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestNo: plain.requestNo,
|
||||||
|
publicId: plain.publicId,
|
||||||
|
type: plain.type,
|
||||||
|
status: plain.status,
|
||||||
|
blameStatus: plain.blameStatus,
|
||||||
|
workflow: plain.workflow,
|
||||||
|
parties,
|
||||||
|
expert,
|
||||||
|
carBodyInsuranceDetail: plain.carBodyInsuranceDetail,
|
||||||
|
claimCreation,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export class RequestManagementV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get one blame request (v2)",
|
summary: "Get one blame request (v2)",
|
||||||
description:
|
description:
|
||||||
"Response is the blame document plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }) — computed for the current user so the app can route the damaged party to create-claim when the blame is COMPLETED and no v2 claim exists yet.",
|
"Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.",
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { FilterQuery, Model } from "mongoose";
|
||||||
|
import { SmsText, SmsTextDocument } from "../schema/sms-text.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmsTextDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(SmsText.name)
|
||||||
|
private readonly smsTextModel: Model<SmsTextDocument>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findOne(filter: FilterQuery<SmsText>): Promise<SmsTextDocument | null> {
|
||||||
|
return this.smsTextModel.findOne(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert(key: string, text: string): Promise<void> {
|
||||||
|
await this.smsTextModel.updateOne(
|
||||||
|
{ key },
|
||||||
|
{ $setOnInsert: { key, text, active: true } },
|
||||||
|
{ upsert: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/sms-orchestration/entities/schema/sms-text.schema.ts
Normal file
17
src/sms-orchestration/entities/schema/sms-text.schema.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
|
@Schema({ collection: "sms-texts", timestamps: true, versionKey: false })
|
||||||
|
export class SmsText {
|
||||||
|
@Prop({ required: true, unique: true, index: true })
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
text: string;
|
||||||
|
|
||||||
|
@Prop({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SmsTextDocument = HydratedDocument<SmsText>;
|
||||||
|
export const SmsTextSchema = SchemaFactory.createForClass(SmsText);
|
||||||
18
src/sms-orchestration/provider/sms-gateway.module.ts
Normal file
18
src/sms-orchestration/provider/sms-gateway.module.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { KavenegarModule } from "@fraybabak/kavenegar_nest";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import { SmsGatewayService } from "./sms-gateway.service";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
KavenegarModule.forRoot({
|
||||||
|
apikey: process.env.SMS_API_KEY || "",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [SmsGatewayService],
|
||||||
|
exports: [SmsGatewayService],
|
||||||
|
})
|
||||||
|
export class SmsGatewayModule {}
|
||||||
@@ -14,19 +14,20 @@ import {
|
|||||||
|
|
||||||
export interface SendMessage {
|
export interface SendMessage {
|
||||||
message: string;
|
message: string;
|
||||||
// sender: string;
|
|
||||||
receptor: string;
|
receptor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifyLookUpMessage {
|
export interface VerifyLookUpMessage {
|
||||||
template: string;
|
template: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
token2?: string;
|
||||||
|
token3?: string;
|
||||||
receptor: string;
|
receptor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmsManagerService {
|
export class SmsGatewayService {
|
||||||
private readonly logger = new Logger(SmsManagerService.name);
|
private readonly logger = new Logger(SmsGatewayService.name);
|
||||||
|
|
||||||
constructor(private readonly sender: KavenegarService) {}
|
constructor(private readonly sender: KavenegarService) {}
|
||||||
|
|
||||||
16
src/sms-orchestration/sms-orchestration.module.ts
Normal file
16
src/sms-orchestration/sms-orchestration.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
|
import { SmsTextDbService } from "./entities/db-service/sms-text.db.service";
|
||||||
|
import { SmsText, SmsTextSchema } from "./entities/schema/sms-text.schema";
|
||||||
|
import { SmsGatewayModule } from "./provider/sms-gateway.module";
|
||||||
|
import { SmsOrchestrationService } from "./sms-orchestration.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
SmsGatewayModule,
|
||||||
|
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
|
||||||
|
],
|
||||||
|
providers: [SmsTextDbService, SmsOrchestrationService],
|
||||||
|
exports: [SmsOrchestrationService],
|
||||||
|
})
|
||||||
|
export class SmsOrchestrationModule {}
|
||||||
111
src/sms-orchestration/sms-orchestration.service.ts
Normal file
111
src/sms-orchestration/sms-orchestration.service.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { SmsTextDbService } from "./entities/db-service/sms-text.db.service";
|
||||||
|
import { SmsGatewayService } from "./provider/sms-gateway.service";
|
||||||
|
import { describeSmsError } from "./provider/sms-provider.exception";
|
||||||
|
|
||||||
|
type TemplateArgs = {
|
||||||
|
template: string;
|
||||||
|
receptor: string;
|
||||||
|
token: string;
|
||||||
|
token2?: string;
|
||||||
|
token3?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmsOrchestrationService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SmsOrchestrationService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly smsGatewayService: SmsGatewayService,
|
||||||
|
private readonly smsTextDbService: SmsTextDbService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
await this.smsTextDbService.upsert(
|
||||||
|
"parties_disagree_notify",
|
||||||
|
"متاسفانه طرف مقابل با نظر کارشناس مخالفت کرد. فرآیند آنلاین این پرونده بسته شده است و جهت پیگیری حضوری اقدام نمایید.",
|
||||||
|
);
|
||||||
|
await this.smsTextDbService.upsert(
|
||||||
|
"one_party_signed_wait_signature",
|
||||||
|
"طرف مقابل توافق را امضا کرده است. لطفا جهت نهایی کردن پرونده، امضای خود را ثبت نمایید.",
|
||||||
|
);
|
||||||
|
await this.smsTextDbService.upsert(
|
||||||
|
"one_party_accepted_wait_24h",
|
||||||
|
"طرف مقابل نظر کارشناس را قبول و امضا کرده است. در طی 24 ساعت آینده فرصت دارید تا پرونده را تکمیل کرده یا پرونده بسته خواهد شد.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildInviteLink(frontendRoute: string, requestId: string): string {
|
||||||
|
return `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendInviteLink(phoneNumber: string, link: string): Promise<boolean> {
|
||||||
|
return this.sendTemplate({
|
||||||
|
template: "yara724-invite-link",
|
||||||
|
receptor: phoneNumber,
|
||||||
|
token: link,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAuthOtp(
|
||||||
|
mobile: string,
|
||||||
|
otp: string,
|
||||||
|
template?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.sendTemplate({
|
||||||
|
template: template || process.env.AUTH_SMS_TEMPLATE || "",
|
||||||
|
receptor: mobile,
|
||||||
|
token: otp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendFieldExpertLink(params: {
|
||||||
|
receptor: string;
|
||||||
|
type: BlameRequestType;
|
||||||
|
expertLastName: string;
|
||||||
|
link: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return this.sendTemplate({
|
||||||
|
template: "yara-field-expert-link",
|
||||||
|
receptor: params.receptor,
|
||||||
|
token: params.type === BlameRequestType.CAR_BODY ? "بدنه" : "ثالث",
|
||||||
|
token2: params.expertLastName || "کارشناس",
|
||||||
|
token3: params.link,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTextByKey(
|
||||||
|
receptor: string,
|
||||||
|
key: string,
|
||||||
|
fallbackText: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const row = await this.smsTextDbService.findOne({ key, active: true });
|
||||||
|
const message = row?.text?.trim() || fallbackText;
|
||||||
|
try {
|
||||||
|
await this.smsGatewayService.sendMessage({ receptor, message });
|
||||||
|
this.logger.log(`[SMS] text sent key=${key} receptor=${receptor}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`[SMS] text send failed key=${key} receptor=${receptor}: ${describeSmsError(err)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendTemplate(args: TemplateArgs): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.smsGatewayService.verifyLookUp(args);
|
||||||
|
this.logger.log(
|
||||||
|
`[SMS] template sent template=${args.template} receptor=${args.receptor}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`[SMS] template send failed template=${args.template} receptor=${args.receptor}: ${describeSmsError(err)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { KavenegarModule } from "@fraybabak/kavenegar_nest";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { SmsManagerService } from "./sms-manager.service";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
KavenegarModule.forRoot({
|
|
||||||
apikey:
|
|
||||||
"75776C717969412B4B52306A5956462F4A714E6F6C65544D6A2B654B7566786E",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [SmsManagerService],
|
|
||||||
exports: [SmsManagerService],
|
|
||||||
})
|
|
||||||
export class SmsManagerModule {}
|
|
||||||
Reference in New Issue
Block a user