Skip to content

Commit

Permalink
Allow for scheduling journey entrances by hour (#615)
Browse files Browse the repository at this point in the history
  • Loading branch information
pushchris authored Jan 20, 2025
1 parent 7293979 commit e14155b
Show file tree
Hide file tree
Showing 8 changed files with 35 additions and 16 deletions.
2 changes: 1 addition & 1 deletion apps/platform/src/journey/Journey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class Journey extends Model {
name,
published: true,
})
const { steps, children } = await setJourneyStepMap(journey.id, stepMap)
const { steps, children } = await setJourneyStepMap(journey, stepMap)
return { journey, steps, children }
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/platform/src/journey/JourneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ router.get('/:journeyId/steps', async ctx => {
})

router.put('/:journeyId/steps', async ctx => {
const { steps, children } = await setJourneyStepMap(ctx.state.journey!.id, validate(journeyStepsParamsSchema, ctx.request.body))
const { steps, children } = await setJourneyStepMap(ctx.state.journey!, validate(journeyStepsParamsSchema, ctx.request.body))
ctx.body = await toJourneyStepMap(steps, children)
})

Expand Down
12 changes: 7 additions & 5 deletions apps/platform/src/journey/JourneyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Journey, { JourneyParams, UpdateJourneyParams } from './Journey'
import { JourneyStep, JourneyEntrance, JourneyUserStep, JourneyStepMap, toJourneyStepMap, JourneyStepChild } from './JourneyStep'
import { createTagSubquery, getTags, setTags } from '../tags/TagService'
import { User } from '../users/User'
import { getProject } from '../projects/ProjectService'

export const pagedJourneys = async (params: PageParams, projectId: number) => {
const result = await Journey.search(
Expand Down Expand Up @@ -107,15 +108,16 @@ export const getJourneyStepMap = async (journeyId: number) => {
return toJourneyStepMap(steps, children)
}

export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepMap) => {
export const setJourneyStepMap = async (journey: Journey, stepMap: JourneyStepMap) => {
return await App.main.db.transaction(async trx => {

const [steps, children] = await Promise.all([
getJourneySteps(journeyId, trx),
getJourneyStepChildren(journeyId, trx),
getJourneySteps(journey.id, trx),
getJourneyStepChildren(journey.id, trx),
])

const now = new Date()
const project = await getProject(journey.project_id)

// Create or update steps
for (const [external_id, { type, x = 0, y = 0, data = {}, data_key, name = '' }] of Object.entries(stepMap)) {
Expand All @@ -126,7 +128,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
let next_scheduled_at: null | Date = null
if (type === JourneyEntrance.type && data.trigger === 'schedule') {
if (step.data?.schedule !== data.schedule) {
next_scheduled_at = JourneyEntrance.fromJson({ data }).nextDate(now)
next_scheduled_at = JourneyEntrance.fromJson({ data }).nextDate(project?.timezone ?? 'UTC', now)
} else {
next_scheduled_at = step.next_scheduled_at
}
Expand All @@ -137,7 +139,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
: await JourneyStep.insertAndFetch({
...fields,
external_id,
journey_id: journeyId,
journey_id: journey.id,
type,
}, trx),
)
Expand Down
6 changes: 3 additions & 3 deletions apps/platform/src/journey/JourneyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const duplicateJourney = async (journey: Journey) => {
const params: Partial<Journey> = pick(journey, ['project_id', 'name', 'description'])
params.name = `Copy of ${params.name}`
params.published = false
const newJourneyId = await Journey.insert(params)
const newJourney = await Journey.insertAndFetch(params)

const steps = await getJourneyStepMap(journey.id)
const newSteps: JourneyStepMap = {}
Expand All @@ -165,7 +165,7 @@ export const duplicateJourney = async (journey: Journey) => {
children: step.children?.map(({ external_id, ...rest }) => ({ external_id: uuidMap[external_id], ...rest })),
}
}
await setJourneyStepMap(newJourneyId, newSteps)
await setJourneyStepMap(newJourney, newSteps)

return await getJourney(newJourneyId, journey.project_id)
return await getJourney(newJourney.id, journey.project_id)
}
8 changes: 4 additions & 4 deletions apps/platform/src/journey/JourneyStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'
import Rule from '../rules/Rule'
import { check } from '../rules/RuleEngine'
import App from '../app'
import { RRule } from 'rrule'
import { rrulestr } from 'rrule'
import { JourneyState } from './JourneyState'
import { EventPostJob, UserPatchJob } from '../jobs'
import { exitUserFromJourney, getJourneyUserStepByExternalId } from './JourneyRepository'
Expand Down Expand Up @@ -122,13 +122,13 @@ export class JourneyEntrance extends JourneyStep {
this.schedule = json?.data?.schedule
}

nextDate(after = this.next_scheduled_at): Date | null {
nextDate(timezone: string, after = this.next_scheduled_at): Date | null {

if (this.trigger !== 'schedule' || !after) return null

if (this.schedule) {
try {
const rule = RRule.fromString(this.schedule)
const rule = rrulestr(this.schedule, { tzid: timezone })

// If there is no frequency, only run once
if (!rule.options.freq) {
Expand All @@ -138,7 +138,7 @@ export class JourneyEntrance extends JourneyStep {
return rule.options.dtstart
}

return RRule.fromString(this.schedule).after(after)
return rrulestr(this.schedule, { tzid: timezone }).after(after)
} catch (err) {
App.main.error.notify(err as Error, {
entranceId: this.id,
Expand Down
6 changes: 4 additions & 2 deletions apps/platform/src/journey/ScheduledEntranceOrchestratorJob.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import App from '../app'
import { getProject } from '../projects/ProjectService'
import { Job } from '../queue'
import { JourneyEntrance, JourneyStep } from './JourneyStep'
import ScheduledEntranceJob from './ScheduledEntranceJob'
Expand All @@ -19,15 +20,16 @@ export default class ScheduledEntranceOrchestratorJob extends Job {
.whereJsonPath('journey_steps.data', '$.multiple', '=', true)
.whereNotNull('journey_steps.next_scheduled_at')
.where('journey_steps.next_scheduled_at', '<=', new Date()),
)
) as Array<JourneyEntrance & { project_id: number }>

if (!entrances.length) return

const jobs: Job[] = []
for (const entrance of entrances) {

const project = await getProject(entrance.project_id)
await JourneyStep.update(q => q.where('id', entrance.id), {
next_scheduled_at: entrance.nextDate(),
next_scheduled_at: entrance.nextDate(project?.timezone ?? 'UTC'),
})

if (entrance.list_id) {
Expand Down
10 changes: 10 additions & 0 deletions apps/ui/src/ui/RRuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ export default function RRuleEditor({ label, onChange, value }: RRuleEditorProps
setValues({ ...options, until: value ? date : null })
}}
/>
<TextInput
name="hour"
label="Hour (24hr Format)"
type="number"
min={1}
max={24}
required
value={Number(options.byhour ?? 0)}
onChange={byhour => setValues({ ...options, byhour })}
/>
<TextInput
name="interval"
label="Interval"
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/src/views/journey/steps/Entrance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ export const entranceStep: JourneyStepType<EntranceConfig> = {
const rule = RRule.fromString(schedule)
if (rule.options.freq) {
s = rule.toText()
if (rule.options.freq === RRule.DAILY) {
s += Number(rule.options.byhour) < 12
? 'am'
: 'pm'
}
} else {
s = 'once'
}
Expand Down

0 comments on commit e14155b

Please sign in to comment.