forked from Yara724/api
Compare commits
2 Commits
b5b3b722c6
...
4b0e8a3547
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0e8a3547 | ||
|
|
885678df7d |
@@ -3,4 +3,6 @@ export enum InPersonDocumentsEnum {
|
|||||||
CarCertificate = "carCertificate",
|
CarCertificate = "carCertificate",
|
||||||
DrivingLicense = "drivingLicense",
|
DrivingLicense = "drivingLicense",
|
||||||
CarGreenCard = "carGreenCard",
|
CarGreenCard = "carGreenCard",
|
||||||
|
Plate = "plate",
|
||||||
|
CarPlate = "carPlate",
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
import {
|
import {
|
||||||
ExpertProfileSnapshot,
|
ExpertProfileSnapshot,
|
||||||
ExpertProfileSnapshotSchema,
|
ExpertProfileSnapshotSchema,
|
||||||
@@ -23,6 +24,22 @@ export class ClaimActorLock {
|
|||||||
}
|
}
|
||||||
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||||
|
|
||||||
|
/** Captures queue fields before a damage-expert lock; restored if the lock TTL expires without a reply. */
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimPreLockQueueSnapshot {
|
||||||
|
@Prop({ type: String, enum: ClaimStatus, required: true })
|
||||||
|
claimStatus: ClaimStatus;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: ClaimWorkflowStep, required: true })
|
||||||
|
currentStep: ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: ClaimWorkflowStep, required: false })
|
||||||
|
nextStep?: ClaimWorkflowStep;
|
||||||
|
}
|
||||||
|
export const ClaimPreLockQueueSnapshotSchema = SchemaFactory.createForClass(
|
||||||
|
ClaimPreLockQueueSnapshot,
|
||||||
|
);
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class ClaimWorkflow {
|
export class ClaimWorkflow {
|
||||||
@Prop({ type: String, enum: ClaimWorkflowStep })
|
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||||
@@ -46,6 +63,9 @@ export class ClaimWorkflow {
|
|||||||
|
|
||||||
@Prop({ type: ClaimActorLockSchema })
|
@Prop({ type: ClaimActorLockSchema })
|
||||||
lockedBy?: ClaimActorLock;
|
lockedBy?: ClaimActorLock;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimPreLockQueueSnapshotSchema })
|
||||||
|
preLockQueueSnapshot?: ClaimPreLockQueueSnapshot;
|
||||||
}
|
}
|
||||||
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);
|
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);
|
||||||
|
|
||||||
|
|||||||
@@ -305,15 +305,7 @@ export class ExpertBlameService {
|
|||||||
for (const doc of visibleCases) {
|
for (const doc of visibleCases) {
|
||||||
const w = doc.workflow as Record<string, unknown> | undefined;
|
const w = doc.workflow as Record<string, unknown> | undefined;
|
||||||
if (!w?.locked) continue;
|
if (!w?.locked) continue;
|
||||||
const la = w.lockedAt;
|
if (!this.isBlameV2WorkflowLockCurrentlyEnforced(doc as any)) {
|
||||||
if (!la) {
|
|
||||||
staleIds.add(String(doc._id));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
Date.now() >=
|
|
||||||
new Date(la as string | Date).getTime() + this.blameV2LockTtlMs
|
|
||||||
) {
|
|
||||||
staleIds.add(String(doc._id));
|
staleIds.add(String(doc._id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2158,6 +2158,14 @@ export class ExpertClaimService {
|
|||||||
actorRole: 'damage_expert',
|
actorRole: 'damage_expert',
|
||||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||||
},
|
},
|
||||||
|
'workflow.preLockQueueSnapshot': {
|
||||||
|
claimStatus: claim.claimStatus,
|
||||||
|
currentStep:
|
||||||
|
claim.workflow?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
...(claim.workflow?.nextStep != null
|
||||||
|
? { nextStep: claim.workflow.nextStep }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
$push: {
|
$push: {
|
||||||
history: {
|
history: {
|
||||||
@@ -2264,6 +2272,7 @@ export class ExpertClaimService {
|
|||||||
"workflow.lockedAt": "",
|
"workflow.lockedAt": "",
|
||||||
"workflow.expiredAt": "",
|
"workflow.expiredAt": "",
|
||||||
"workflow.lockedBy": "",
|
"workflow.lockedBy": "",
|
||||||
|
"workflow.preLockQueueSnapshot": "",
|
||||||
},
|
},
|
||||||
$push: {
|
$push: {
|
||||||
history: {
|
history: {
|
||||||
@@ -2419,6 +2428,7 @@ export class ExpertClaimService {
|
|||||||
'workflow.lockedAt': '',
|
'workflow.lockedAt': '',
|
||||||
'workflow.expiredAt': '',
|
'workflow.expiredAt': '',
|
||||||
'workflow.lockedBy': '',
|
'workflow.lockedBy': '',
|
||||||
|
'workflow.preLockQueueSnapshot': '',
|
||||||
},
|
},
|
||||||
'workflow.currentStep': nextStep,
|
'workflow.currentStep': nextStep,
|
||||||
'workflow.nextStep': needsFactorUpload
|
'workflow.nextStep': needsFactorUpload
|
||||||
@@ -2529,6 +2539,7 @@ export class ExpertClaimService {
|
|||||||
'workflow.lockedAt': '',
|
'workflow.lockedAt': '',
|
||||||
'workflow.expiredAt': '',
|
'workflow.expiredAt': '',
|
||||||
'workflow.lockedBy': '',
|
'workflow.lockedBy': '',
|
||||||
|
'workflow.preLockQueueSnapshot': '',
|
||||||
},
|
},
|
||||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||||
...(visitSnapshot && {
|
...(visitSnapshot && {
|
||||||
@@ -2584,6 +2595,9 @@ export class ExpertClaimService {
|
|||||||
requireActorClientKey(actor);
|
requireActorClientKey(actor);
|
||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
const clientKey = actor.clientKey as string;
|
const clientKey = actor.clientKey as string;
|
||||||
|
|
||||||
|
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
||||||
|
|
||||||
const claims = await this.claimCaseDbService.find({
|
const claims = await this.claimCaseDbService.find({
|
||||||
$or: [
|
$or: [
|
||||||
// Available claims: waiting for expert, not locked
|
// Available claims: waiting for expert, not locked
|
||||||
@@ -2609,12 +2623,32 @@ export class ExpertClaimService {
|
|||||||
claimCaseTouchesClient(c, clientKey),
|
claimCaseTouchesClient(c, clientKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const c of filtered) {
|
const staleLockToReconcile = filtered.filter(
|
||||||
if (
|
(c) =>
|
||||||
c.workflow?.locked &&
|
c.workflow?.locked &&
|
||||||
!this.isClaimV2WorkflowLockCurrentlyEnforced(c)
|
!this.isClaimV2WorkflowLockCurrentlyEnforced(c),
|
||||||
) {
|
);
|
||||||
await this.expireClaimWorkflowLockV2IfStale(String(c._id));
|
const touchedIds = (
|
||||||
|
await Promise.all(
|
||||||
|
staleLockToReconcile.map(async (c) => {
|
||||||
|
const updated = await this.expireClaimWorkflowLockV2IfStale(
|
||||||
|
String(c._id),
|
||||||
|
);
|
||||||
|
return updated ? String(c._id) : null;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((id): id is string => id != null);
|
||||||
|
|
||||||
|
if (touchedIds.length > 0) {
|
||||||
|
const refreshed = await this.claimCaseDbService.find({
|
||||||
|
_id: { $in: touchedIds.map((id) => new Types.ObjectId(id)) },
|
||||||
|
});
|
||||||
|
const byId = new Map(
|
||||||
|
refreshed.map((r) => [String(r._id), r] as const),
|
||||||
|
);
|
||||||
|
for (const c of filtered) {
|
||||||
|
const r = byId.get(String(c._id));
|
||||||
|
if (r) Object.assign(c, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2648,10 +2682,14 @@ export class ExpertClaimService {
|
|||||||
const fileCtx = blame ? this.blameFileContextForExpert(blame) : {};
|
const fileCtx = blame ? this.blameFileContextForExpert(blame) : {};
|
||||||
const lockActive =
|
const lockActive =
|
||||||
!!(c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c));
|
!!(c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c));
|
||||||
|
const statusForList =
|
||||||
|
c.status === ClaimCaseStatus.EXPERT_REVIEWING
|
||||||
|
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
: c.status;
|
||||||
return {
|
return {
|
||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
status: c.status,
|
status: statusForList,
|
||||||
currentStep: c.workflow?.currentStep || "",
|
currentStep: c.workflow?.currentStep || "",
|
||||||
locked: lockActive,
|
locked: lockActive,
|
||||||
lockedBy:
|
lockedBy:
|
||||||
@@ -2792,6 +2830,18 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private claimHasDamageExpertReply(claim: {
|
||||||
|
evaluation?: {
|
||||||
|
damageExpertReply?: unknown;
|
||||||
|
damageExpertReplyFinal?: unknown;
|
||||||
|
};
|
||||||
|
}): boolean {
|
||||||
|
return !!(
|
||||||
|
claim.evaluation?.damageExpertReply ||
|
||||||
|
claim.evaluation?.damageExpertReplyFinal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True while a workflow lock is still within its window (`expiredAt` when set, else lockedAt + TTL).
|
* True while a workflow lock is still within its window (`expiredAt` when set, else lockedAt + TTL).
|
||||||
* Missing both timestamps on old documents: treat lock as enforced until cleared.
|
* Missing both timestamps on old documents: treat lock as enforced until cleared.
|
||||||
@@ -2810,27 +2860,76 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persist unlock when claim V2 workflow lock TTL has passed (matches blame V2 behaviour). */
|
/**
|
||||||
|
* EXPERT_REVIEWING + expired lock is not listed until status is reverted. Run expire for this tenant
|
||||||
|
* so the next query returns those cases as WAITING_FOR_DAMAGE_EXPERT.
|
||||||
|
*/
|
||||||
|
private async reconcileStaleExpertReviewingClaimLocksForTenant(actor: {
|
||||||
|
sub: string;
|
||||||
|
clientKey?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
const lockTtlMs = this.claimV2WorkflowLockTtlMs;
|
||||||
|
const now = new Date();
|
||||||
|
const candidates = (await this.claimCaseDbService.find(
|
||||||
|
{
|
||||||
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||||
|
"workflow.locked": true,
|
||||||
|
$or: [
|
||||||
|
{ "workflow.expiredAt": { $lte: now } },
|
||||||
|
{
|
||||||
|
$expr: {
|
||||||
|
$and: [
|
||||||
|
{ $eq: ["$workflow.locked", true] },
|
||||||
|
{
|
||||||
|
$lte: [
|
||||||
|
{ $add: ["$workflow.lockedAt", lockTtlMs] },
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ lean: true, select: "_id owner" },
|
||||||
|
)) as Array<{ _id: unknown; owner?: unknown }>;
|
||||||
|
|
||||||
|
const relevant = candidates.filter((c) =>
|
||||||
|
claimCaseTouchesClient(c, clientKey),
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
relevant.map((c) =>
|
||||||
|
this.expireClaimWorkflowLockV2IfStale(String(c._id)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist unlock when claim V2 workflow lock TTL has passed.
|
||||||
|
* If the expert never submitted a damage reply, restores queue fields from
|
||||||
|
* `workflow.preLockQueueSnapshot` (or defaults) so the case shows as WAITING_FOR_DAMAGE_EXPERT again.
|
||||||
|
*/
|
||||||
private async expireClaimWorkflowLockV2IfStale(
|
private async expireClaimWorkflowLockV2IfStale(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
if (!claim?.workflow?.locked) return;
|
if (!claim?.workflow?.locked) return false;
|
||||||
|
|
||||||
const expiredAt = claim.workflow?.expiredAt;
|
const expiredAt = claim.workflow?.expiredAt;
|
||||||
const lockedAt = claim.workflow?.lockedAt;
|
const lockedAt = claim.workflow?.lockedAt;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const ttl = this.claimV2WorkflowLockTtlMs;
|
const ttl = this.claimV2WorkflowLockTtlMs;
|
||||||
|
|
||||||
if (expiredAt && now < new Date(expiredAt as Date).getTime()) return;
|
if (expiredAt && now < new Date(expiredAt as Date).getTime()) return false;
|
||||||
if (
|
if (
|
||||||
!expiredAt &&
|
!expiredAt &&
|
||||||
lockedAt &&
|
lockedAt &&
|
||||||
now < new Date(lockedAt as Date).getTime() + ttl
|
now < new Date(lockedAt as Date).getTime() + ttl
|
||||||
) {
|
) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!expiredAt && !lockedAt) return;
|
if (!expiredAt && !lockedAt) return false;
|
||||||
|
|
||||||
const lockedById = claim.workflow.lockedBy?.actorId;
|
const lockedById = claim.workflow.lockedBy?.actorId;
|
||||||
const filter: Record<string, unknown> = {
|
const filter: Record<string, unknown> = {
|
||||||
@@ -2845,18 +2944,55 @@ export class ExpertClaimService {
|
|||||||
filter["workflow.lockedBy.actorId"] = lockedById;
|
filter["workflow.lockedBy.actorId"] = lockedById;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.claimCaseDbService.findOneAndUpdate(
|
const needsQueueRestore =
|
||||||
|
claim.status === ClaimCaseStatus.EXPERT_REVIEWING &&
|
||||||
|
!this.claimHasDamageExpertReply(claim);
|
||||||
|
|
||||||
|
const snap = claim.workflow?.preLockQueueSnapshot as
|
||||||
|
| {
|
||||||
|
claimStatus?: ClaimStatus;
|
||||||
|
currentStep?: ClaimWorkflowStep;
|
||||||
|
nextStep?: ClaimWorkflowStep;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const restoreClaimStatus = snap?.claimStatus ?? ClaimStatus.PENDING;
|
||||||
|
const restoreCurrent =
|
||||||
|
snap?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
const restoreNext =
|
||||||
|
snap?.nextStep != null
|
||||||
|
? snap.nextStep
|
||||||
|
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||||
|
|
||||||
|
const lockFieldsUnset = {
|
||||||
|
"workflow.lockedAt": "",
|
||||||
|
"workflow.expiredAt": "",
|
||||||
|
"workflow.lockedBy": "",
|
||||||
|
"workflow.preLockQueueSnapshot": "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = needsQueueRestore
|
||||||
|
? {
|
||||||
|
$set: {
|
||||||
|
"workflow.locked": false,
|
||||||
|
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
claimStatus: restoreClaimStatus,
|
||||||
|
"workflow.currentStep": restoreCurrent,
|
||||||
|
"workflow.nextStep": restoreNext,
|
||||||
|
},
|
||||||
|
$unset: lockFieldsUnset,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
$set: { "workflow.locked": false },
|
||||||
|
$unset: lockFieldsUnset,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleared = await this.claimCaseDbService.findOneAndUpdate(
|
||||||
filter as any,
|
filter as any,
|
||||||
{
|
update as any,
|
||||||
$set: { "workflow.locked": false },
|
|
||||||
$unset: {
|
|
||||||
"workflow.lockedAt": "",
|
|
||||||
"workflow.expiredAt": "",
|
|
||||||
"workflow.lockedBy": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ new: false },
|
{ new: false },
|
||||||
);
|
);
|
||||||
|
return !!cleared;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3051,10 +3187,15 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const statusForExpertApi =
|
||||||
|
claim.status === ClaimCaseStatus.EXPERT_REVIEWING
|
||||||
|
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
: claim.status;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claim._id.toString(),
|
claimRequestId: claim._id.toString(),
|
||||||
publicId: claim.publicId,
|
publicId: claim.publicId,
|
||||||
status: claim.status,
|
status: statusForExpertApi,
|
||||||
claimStatus: claim.claimStatus || 'PENDING',
|
claimStatus: claim.claimStatus || 'PENDING',
|
||||||
currentStep: claim.workflow?.currentStep || '',
|
currentStep: claim.workflow?.currentStep || '',
|
||||||
nextStep: claim.workflow?.nextStep,
|
nextStep: claim.workflow?.nextStep,
|
||||||
|
|||||||
@@ -6446,6 +6446,29 @@ export class RequestManagementService {
|
|||||||
"workflow.completedSteps": "WAITING_FOR_SIGNATURES",
|
"workflow.completedSteps": "WAITING_FOR_SIGNATURES",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (updatedRequest.type === BlameRequestType.THIRD_PARTY) {
|
||||||
|
const guiltyPartyId = updatedRequest.expert?.decision?.guiltyPartyId
|
||||||
|
? String(updatedRequest.expert.decision.guiltyPartyId)
|
||||||
|
: null;
|
||||||
|
if (guiltyPartyId) {
|
||||||
|
const damagedParty = updatedRequest.parties?.find(
|
||||||
|
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
||||||
|
);
|
||||||
|
const phone = damagedParty?.person?.phoneNumber?.trim();
|
||||||
|
if (phone) {
|
||||||
|
const publicId = String(updatedRequest.publicId);
|
||||||
|
const link = this.smsOrchestrationService.buildClaimLink(requestId);
|
||||||
|
await this.smsOrchestrationService.sendThirdPartyDamagedPartyClaimLinkNotice(
|
||||||
|
{
|
||||||
|
receptor: phone,
|
||||||
|
publicId,
|
||||||
|
link,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
finalStatus = CaseStatus.STOPPED;
|
finalStatus = CaseStatus.STOPPED;
|
||||||
message =
|
message =
|
||||||
|
|||||||
@@ -117,6 +117,20 @@ export class SmsOrchestrationService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** THIRD_PARTY only: notify the damaged (non-guilty) party that blame is completed and they can open the claim flow. */
|
||||||
|
async sendThirdPartyDamagedPartyClaimLinkNotice(params: {
|
||||||
|
receptor: string;
|
||||||
|
publicId: string;
|
||||||
|
link: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return this.sendTemplate({
|
||||||
|
template: "yara-claim-link",
|
||||||
|
receptor: params.receptor,
|
||||||
|
token: params.publicId,
|
||||||
|
token2: params.link,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async sendThirdPartyExpertStartedReviewNotice(
|
async sendThirdPartyExpertStartedReviewNotice(
|
||||||
params: {
|
params: {
|
||||||
receptor: string;
|
receptor: string;
|
||||||
|
|||||||
163
test-sms.js
163
test-sms.js
@@ -1,163 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test script for sending SMS via Kavenegar API
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node test-sms.js
|
|
||||||
*
|
|
||||||
* Make sure to update the API_KEY and RECEPTOR (your phone number) below
|
|
||||||
*/
|
|
||||||
|
|
||||||
const https = require('https');
|
|
||||||
const querystring = require('querystring');
|
|
||||||
|
|
||||||
// ===== CONFIGURATION =====
|
|
||||||
// Replace with your actual Kavenegar API key
|
|
||||||
const API_KEY = '75776C717969412B4B52306A5956462F4A714E6F6C65544D6A2B654B7566786E'; // Replace with your actual API key
|
|
||||||
|
|
||||||
// Replace with your phone number (format: 09*********)
|
|
||||||
const RECEPTOR = '09199503061'; // Replace with your actual phone number
|
|
||||||
|
|
||||||
// Optional: Sender number (if not provided, uses default account sender)
|
|
||||||
// const SENDER = '10004346'; // Optional - can be removed if you want to use default
|
|
||||||
|
|
||||||
// Test message
|
|
||||||
const MESSAGE = 'تست ارسال پیامک از سیستم Yara724';
|
|
||||||
// ===== END CONFIGURATION =====
|
|
||||||
|
|
||||||
function sendSMS(receptor, message, sender = null, options = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Build URL
|
|
||||||
const baseUrl = `https://api.kavenegar.com/v1/${API_KEY}/sms/send.json`;
|
|
||||||
|
|
||||||
// Build query parameters
|
|
||||||
const params = {
|
|
||||||
receptor: receptor,
|
|
||||||
message: encodeURIComponent(message), // Encode message for URL
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only add sender if it's provided (not null/undefined/empty)
|
|
||||||
if (sender && sender.trim() !== '') {
|
|
||||||
params.sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add optional parameters
|
|
||||||
if (options.date) params.date = options.date;
|
|
||||||
if (options.type) params.type = options.type;
|
|
||||||
if (options.localid) params.localid = options.localid;
|
|
||||||
if (options.hide) params.hide = options.hide;
|
|
||||||
if (options.tag) params.tag = options.tag;
|
|
||||||
if (options.policy) params.policy = options.policy;
|
|
||||||
|
|
||||||
const queryString = querystring.stringify(params);
|
|
||||||
const url = `${baseUrl}?${queryString}`;
|
|
||||||
|
|
||||||
console.log('\n📤 Sending SMS...');
|
|
||||||
console.log('URL:', url.replace(API_KEY, '***API_KEY***'));
|
|
||||||
console.log('Receptor:', receptor);
|
|
||||||
console.log('Message:', message);
|
|
||||||
console.log('Sender:', sender || '(Using default account sender)');
|
|
||||||
|
|
||||||
// Make HTTPS request
|
|
||||||
https.get(url, (res) => {
|
|
||||||
let data = '';
|
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
|
||||||
data += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(data);
|
|
||||||
|
|
||||||
if (response.return && response.return.status === 200) {
|
|
||||||
console.log('\n✅ SMS sent successfully!');
|
|
||||||
console.log('Response:', JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.entries && response.entries.length > 0) {
|
|
||||||
console.log('\n📊 SMS Details:');
|
|
||||||
response.entries.forEach((entry, index) => {
|
|
||||||
console.log(`\n Message ${index + 1}:`);
|
|
||||||
console.log(` Message ID: ${entry.messageid}`);
|
|
||||||
console.log(` Status: ${entry.status} (${entry.statustext})`);
|
|
||||||
console.log(` Receptor: ${entry.receptor}`);
|
|
||||||
console.log(` Sender: ${entry.sender}`);
|
|
||||||
console.log(` Cost: ${entry.cost} Rials`);
|
|
||||||
console.log(` Date: ${new Date(entry.date * 1000).toLocaleString('fa-IR')}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(response);
|
|
||||||
} else {
|
|
||||||
console.error('\n❌ Error sending SMS');
|
|
||||||
console.error('Response:', JSON.stringify(response, null, 2));
|
|
||||||
reject(new Error(`API Error: ${response.return?.message || 'Unknown error'}`));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Error parsing response:', error.message);
|
|
||||||
console.error('Raw response:', data);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on('error', (error) => {
|
|
||||||
console.error('\n❌ Network error:', error.message);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test function
|
|
||||||
*/
|
|
||||||
async function testSMS() {
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
console.log('🧪 Kavenegar SMS Test Script');
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
// Validate configuration
|
|
||||||
if (API_KEY === '613472435563797A37677331D' || API_KEY.length < 10) {
|
|
||||||
console.error('\n⚠️ WARNING: Please update API_KEY with your actual Kavenegar API key!');
|
|
||||||
console.error(' You can find it in your Kavenegar panel: https://panel.kavenegar.com/client/membership/api');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RECEPTOR === '09123456789' || !RECEPTOR.startsWith('09')) {
|
|
||||||
console.error('\n⚠️ WARNING: Please update RECEPTOR with your actual phone number!');
|
|
||||||
console.error(' Format: 09********* (11 digits starting with 09)');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Simple SMS (without sender - uses default account sender)
|
|
||||||
console.log('\n\n📝 Test 1: Simple SMS (No sender - using default)');
|
|
||||||
await sendSMS(RECEPTOR, MESSAGE);
|
|
||||||
|
|
||||||
// Wait a bit before next test
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Test 2: SMS with tag (optional - uncomment if you have tags set up)
|
|
||||||
// console.log('\n\n📝 Test 2: SMS with Tag');
|
|
||||||
// await sendSMS(RECEPTOR, 'تست پیامک با تگ', SENDER, { tag: 'test' });
|
|
||||||
|
|
||||||
// Test 3: Multiple recipients (uncomment to test)
|
|
||||||
// console.log('\n\n📝 Test 3: Multiple Recipients');
|
|
||||||
// const multipleReceptors = `${RECEPTOR},09123456789`; // Add more numbers separated by comma
|
|
||||||
// await sendSMS(multipleReceptors, 'تست پیامک به چندین گیرنده', SENDER);
|
|
||||||
|
|
||||||
console.log('\n\n' + '='.repeat(50));
|
|
||||||
console.log('✅ All tests completed!');
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n\n❌ Test failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
if (require.main === module) {
|
|
||||||
testSMS().catch((error) => {
|
|
||||||
console.error('Fatal error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { sendSMS };
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user