Ai-Mee Help Centre
Home
Features
How-To Guides
FAQ
Need Help?
Home
Features
How-To Guides
FAQ
Need Help?

Step 2 — Rewrite Scheduling Service Functions

File: api/src/services/posts.service.ts
Prerequisites: Step 1 — DB Migration applied


What This Does

Replaces the three existing scheduling service functions (scheduleManyPosts, schedulePost, unschedulePost) with versions that operate on customer_platform_post.scheduled_at instead of customer_posts.scheduled. Adds a new scheduleManyPlatformPosts function for fine-grained per-platform scheduling.


2.1 — Replace scheduleManyPosts

The function now sets scheduled_at on all customer_platform_post rows for each given postId, then sets status = 'scheduled' on the parent.

Find the existing scheduleManyPosts function (look for // scheduleManyPosts — used by the schedule-multiple-posts MCP tool) and replace the entire function with:

// ---------------------------------------------------------------------------
// scheduleManyPosts — used by the schedule-multiple-posts MCP tool
// Sets scheduled_at on all platform posts for each parent post ID.
// ---------------------------------------------------------------------------

export async function scheduleManyPosts(schedules: Array<{ postId: number; scheduledAt: string }>) {
  if (schedules.length === 0) {
    throw new ServiceError(400, 'schedules must not be empty')
  }
  if (schedules.length > 50) {
    throw new ServiceError(400, 'Cannot schedule more than 50 posts at once')
  }

  const now = new Date()
  for (const { postId, scheduledAt } of schedules) {
    const d = new Date(scheduledAt)
    if (isNaN(d.getTime())) {
      throw new ServiceError(400, `Invalid scheduled_at for post ${postId}: "${scheduledAt}"`)
    }
    if (d <= now) {
      throw new ServiceError(400, `scheduled_at for post ${postId} must be in the future`)
    }
  }

  const postIds = schedules.map((s) => s.postId)
  const { data: posts, error: fetchErr } = await supabase
    .from('customer_posts')
    .select('id, status')
    .in('id', postIds)

  if (fetchErr) throw new ServiceError(500, fetchErr.message)

  const postMap = new Map<number, { id: number; status: string }>(
    (posts ?? []).map((p: any) => [p.id, p])
  )

  const results = await Promise.all(
    schedules.map(async ({ postId, scheduledAt }) => {
      const post = postMap.get(postId)
      if (!post) return { id: postId, error: 'Post not found' }
      if (!['approved', 'scheduled'].includes(post.status)) {
        return {
          id: postId,
          error: `Cannot schedule post with status '${post.status}'. Only approved or already-scheduled posts can be scheduled.`,
        }
      }

      const scheduledIso = new Date(scheduledAt).toISOString()

      // Set scheduled_at on all platform posts for this parent
      const { error: platformErr } = await supabase
        .from('customer_platform_post')
        .update({ scheduled_at: scheduledIso, updated_at: new Date().toISOString() })
        .eq('customer_post_id', postId)

      if (platformErr) return { id: postId, error: platformErr.message }

      // Set parent status to scheduled
      const { data: updated, error: updateErr } = await supabase
        .from('customer_posts')
        .update({ status: 'scheduled', updated_at: new Date().toISOString() })
        .eq('id', postId)
        .select('id, status')
        .single()

      if (updateErr) return { id: postId, error: updateErr.message }
      return { id: (updated as any).id, status: (updated as any).status }
    })
  )

  const scheduled_count = results.filter((r) => !('error' in r)).length
  const failed_count = results.filter((r) => 'error' in r).length

  return { results, scheduled_count, failed_count }
}

2.2 — Add scheduleManyPlatformPosts (new function)

Add this immediately after the scheduleManyPosts function. This is the low-level function used by the algorithmic per-platform scheduler (Step 4).

// ---------------------------------------------------------------------------
// scheduleManyPlatformPosts — fine-grained per-platform scheduling
// Used by planAndScheduleApprovedPosts for algorithmic per-platform times.
// Each platform post gets its own scheduled_at; parent status → 'scheduled'.
// ---------------------------------------------------------------------------

export async function scheduleManyPlatformPosts(
  schedules: Array<{ platformPostId: number; scheduledAt: string; parentPostId: number }>
) {
  if (schedules.length === 0) return { results: [], scheduled_count: 0, failed_count: 0 }

  const now = new Date()
  const results: Array<Record<string, unknown>> = []
  let scheduled_count = 0
  let failed_count = 0

  // Validate all timestamps up front
  for (const { platformPostId, scheduledAt } of schedules) {
    const d = new Date(scheduledAt)
    if (isNaN(d.getTime()) || d <= now) {
      results.push({
        id: platformPostId,
        error: `Invalid or past scheduled_at: "${scheduledAt}"`,
      })
      failed_count++
    }
  }

  const validSchedules = schedules.filter((s) => !results.some((r) => r.id === s.platformPostId))

  // Update platform posts
  await Promise.all(
    validSchedules.map(async ({ platformPostId, scheduledAt }) => {
      const { data, error } = await supabase
        .from('customer_platform_post')
        .update({
          scheduled_at: new Date(scheduledAt).toISOString(),
          updated_at: new Date().toISOString(),
        })
        .eq('id', platformPostId)
        .select('id, platform, scheduled_at')
        .single()

      if (error) {
        results.push({ id: platformPostId, error: error.message })
        failed_count++
      } else {
        results.push({
          id: platformPostId,
          platform: (data as any).platform,
          scheduled_at: (data as any).scheduled_at,
        })
        scheduled_count++
      }
    })
  )

  // Set parent posts to 'scheduled' for any parent that had at least one success
  const successfulParentIds = [
    ...new Set(
      validSchedules
        .filter((s) => results.some((r) => r.id === s.platformPostId && !r.error))
        .map((s) => s.parentPostId)
    ),
  ]

  if (successfulParentIds.length > 0) {
    await supabase
      .from('customer_posts')
      .update({ status: 'scheduled', updated_at: new Date().toISOString() })
      .in('id', successfulParentIds)
      .in('status', ['approved', 'scheduled'])
  }

  return { results, scheduled_count, failed_count }
}

2.3 — Replace schedulePost

Find the existing schedulePost function (look for // schedulePost — used by the schedule-post MCP tool) and replace:

// ---------------------------------------------------------------------------
// schedulePost — used by the schedule-post MCP tool
// Sets scheduled_at on all platform posts; parent status → 'scheduled'.
// ---------------------------------------------------------------------------

export async function schedulePost(postId: number, scheduledAt: string) {
  const scheduledDate = new Date(scheduledAt)
  if (isNaN(scheduledDate.getTime())) {
    throw new ServiceError(400, 'scheduled_at must be a valid ISO 8601 date-time string')
  }
  if (scheduledDate <= new Date()) {
    throw new ServiceError(400, 'scheduled_at must be in the future')
  }

  const { data: post, error: fetchErr } = await supabase
    .from('customer_posts')
    .select('id, status')
    .eq('id', postId)
    .single()

  if (fetchErr || !post) throw new ServiceError(404, 'Post not found')

  if (!['approved', 'scheduled'].includes(post.status as string)) {
    throw new ServiceError(
      400,
      `Cannot schedule a post with status '${post.status}'. Only approved or already-scheduled posts can be scheduled.`
    )
  }

  const scheduledIso = scheduledDate.toISOString()

  // Set scheduled_at on all platform posts
  const { error: platformErr } = await supabase
    .from('customer_platform_post')
    .update({ scheduled_at: scheduledIso, updated_at: new Date().toISOString() })
    .eq('customer_post_id', postId)

  if (platformErr) throw new ServiceError(500, platformErr.message)

  // Set parent status
  const { data: updated, error: updateErr } = await supabase
    .from('customer_posts')
    .update({ status: 'scheduled', updated_at: new Date().toISOString() })
    .eq('id', postId)
    .select('id, status')
    .single()

  if (updateErr) throw new ServiceError(500, updateErr.message)

  return {
    id: (updated as any).id,
    status: (updated as any).status,
    scheduled_at: scheduledIso,
  }
}

2.4 — Replace unschedulePost

Find the existing unschedulePost function (look for // unschedulePost — used by the unschedule-post MCP tool) and replace:

// ---------------------------------------------------------------------------
// unschedulePost — used by the unschedule-post MCP tool
// Clears scheduled_at on all platform posts; parent status → 'approved'.
// ---------------------------------------------------------------------------

export async function unschedulePost(postId: number) {
  const { data: post, error: fetchErr } = await supabase
    .from('customer_posts')
    .select('id, status')
    .eq('id', postId)
    .single()

  if (fetchErr || !post) throw new ServiceError(404, 'Post not found')

  if (post.status !== 'scheduled') {
    throw new ServiceError(400, `Post is not scheduled (current status: '${post.status}')`)
  }

  // Clear scheduled_at on all platform posts
  const { error: platformErr } = await supabase
    .from('customer_platform_post')
    .update({ scheduled_at: null, updated_at: new Date().toISOString() })
    .eq('customer_post_id', postId)

  if (platformErr) throw new ServiceError(500, platformErr.message)

  // Revert parent status
  const { data: updated, error: updateErr } = await supabase
    .from('customer_posts')
    .update({ status: 'approved', updated_at: new Date().toISOString() })
    .eq('id', postId)
    .select('id, status')
    .single()

  if (updateErr) throw new ServiceError(500, updateErr.message)

  return { id: (updated as any).id, status: (updated as any).status }
}

2.5 — Verify

cd api
pnpm build
# Should compile without errors

Check that scheduleManyPlatformPosts is exported and importable by route files.


✅ Done

Proceed to Step 3 — Clients Service.