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.