Repository: novuhq/novu
File: libs/application-generic/src/services/workflow-run.service.ts (TypeScript)

Code before:
    messages: MessageResult[]
  ): {
    deliveryLifecycleStatus: DeliveryLifecycleStatus;
    deliveryLifecycleDetail?: DeliveryLifecycleDetail;
  } {
    // Filter for channel jobs (exclude non-channel jobs like trigger, delay, digest, custom)
    const channelJobs = jobs.filter((job) => job.type && ['in_app', 'email', 'sms', 'chat', 'push'].includes(job.type));

    if (channelJobs.length === 0) {
      return {
        deliveryLifecycleStatus: DeliveryLifecycleStatus.ERRORED,
        deliveryLifecycleDetail: DeliveryLifecycleDetail.WORKFLOW_MISSING_CHANNEL_STEP,
      };
    }

    // Priority 1: INTERACTED - If any message has seen/read/snoozedUntil/archived as true
    const hasInteractedMessage = messages.some(
      (message) => message.seen || message.read || message.snoozedUntil || message.archived
    );

    if (hasInteractedMessage) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.INTERACTED };
    }

    // Priority 2: DELIVERED - If any message has been delivered (has deliveredAt) and no interaction found
    const hasInAppMessage = messages.some((message) => message.deliveredAt?.length > 0);
    if (hasInAppMessage) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.DELIVERED };
    }

    // Priority 3: SENT - If any step is COMPLETED, workflow delivery lifecycle is SENT
    const hasCompletedSteps = channelJobs.some((job) => job.status === JobStatusEnum.COMPLETED);
    if (hasCompletedSteps) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.SENT };
    }

    // Priority 4: SKIPPED - Any job with SKIPPED status OR delivery lifecycle status is "skipped"
    const skippedJobs = channelJobs.filter(
      (job) =>
        job.status === JobStatusEnum.SKIPPED ||
        (job.deliveryLifecycleState?.status && job.deliveryLifecycleState.status === 'skipped')
    );
    if (skippedJobs.length > 0) {
      // Priority order for delivery lifecycle details (highest → lowest):
      // 1. SUBSCRIBER_PREFERENCE - User preference settings
      // 2. USER_STEP_CONDITION - Step condition evaluation
      // 3. All other details (missing credentials, phone, email, etc.)
      const priorityOrder = [
        DeliveryLifecycleDetail.SUBSCRIBER_PREFERENCE,
        DeliveryLifecycleDetail.USER_STEP_CONDITION,
        DeliveryLifecycleDetail.USER_MISSING_EMAIL,

Review comment (suggestion):
```suggestion
    const hasInAppMessage = messages.some((message) => !!message.deliveredAt);
```

Code after:
    messages: MessageResult[]
  ): {
    deliveryLifecycleStatus: DeliveryLifecycleStatus;
    deliveryLifecycleDetail?: DeliveryLifecycleDetail;
  } {
    // Filter for channel jobs (exclude non-channel jobs like trigger, delay, digest, custom)
    const channelJobs = jobs.filter((job) => job.type && ['in_app', 'email', 'sms', 'chat', 'push'].includes(job.type));

    if (channelJobs.length === 0) {
      return {
        deliveryLifecycleStatus: DeliveryLifecycleStatus.ERRORED,
        deliveryLifecycleDetail: DeliveryLifecycleDetail.WORKFLOW_MISSING_CHANNEL_STEP,
      };
    }

    // Priority 1: INTERACTED - If any message has seen/read/snoozedUntil/archived as true
    const hasInteractedMessage = messages.some(
      (message) => message.seen || message.read || message.snoozedUntil || message.archived
    );

    if (hasInteractedMessage) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.INTERACTED };
    }

    // Priority 2: DELIVERED - If any message has been delivered (has deliveredAt) and no interaction found
    const hasInAppMessage = messages.some((message) => !!message.deliveredAt);
    if (hasInAppMessage) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.DELIVERED };
    }

    // Priority 3: SENT - If any step is COMPLETED, workflow delivery lifecycle is SENT
    const hasCompletedSteps = channelJobs.some((job) => job.status === JobStatusEnum.COMPLETED);
    if (hasCompletedSteps) {
      return { deliveryLifecycleStatus: DeliveryLifecycleStatus.SENT };
    }

    // Priority 4: SKIPPED - Any job with SKIPPED status OR delivery lifecycle status is "skipped"
    const skippedJobs = channelJobs.filter(
      (job) =>
        job.status === JobStatusEnum.SKIPPED ||
        (job.deliveryLifecycleState?.status && job.deliveryLifecycleState.status === 'skipped')
    );
    if (skippedJobs.length > 0) {
      // Priority order for delivery lifecycle details (highest → lowest):
      // 1. SUBSCRIBER_PREFERENCE - User preference settings
      // 2. USER_STEP_CONDITION - Step condition evaluation
      // 3. All other details (missing credentials, phone, email, etc.)
      const priorityOrder = [
        DeliveryLifecycleDetail.SUBSCRIBER_PREFERENCE,
        DeliveryLifecycleDetail.USER_STEP_CONDITION,
        DeliveryLifecycleDetail.USER_MISSING_EMAIL,