Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support per-campaign autosend limit #1473

Merged
merged 23 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion libs/spoke-codegen/src/graphql/autosending.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ fragment BasicAutosendingTarget on Campaign {
title
isStarted
autosendStatus
autosendLimit
}

fragment DetailedAutosendingTarget on Campaign {
Expand All @@ -28,6 +29,16 @@ fragment AutosendingTarget on Campaign {
...DetailedAutosendingTarget
}

query GetCampaignAutosendingLimit($campaignId: String!) {
campaign(id: $campaignId) {
id
autosendLimit
stats {
countMessagedContacts
}
}
}

query CampaignsEligibleForAutosending(
$organizationId: String!
$isStarted: Boolean!
Expand All @@ -41,7 +52,7 @@ query CampaignsEligibleForAutosending(
) {
campaigns {
...BasicAutosendingTarget
...DetailedAutosendingTarget @skip (if: $isBasic)
...DetailedAutosendingTarget @skip(if: $isBasic)
}
}
}
Expand All @@ -60,3 +71,10 @@ mutation PauseAutosending($campaignId: String!) {
autosendStatus
}
}

mutation UpdateCampaignAutosendingLimit($campaignId: String!, $limit: Int) {
updateCampaignAutosendingLimit(campaignId: $campaignId, limit: $limit) {
id
autosendLimit
}
}
295 changes: 295 additions & 0 deletions migrations/20221016075536_add_autosend_limit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
exports.up = function up(knex) {
return knex.schema
.alterTable("all_campaign", (table) => {
table.integer("autosend_limit");
table.integer("autosend_limit_max_contact_id");
table
.foreign("autosend_limit_max_contact_id")
.references("campaign_contact.id");
})
.then(() => {
return knex.schema.raw(`
create or replace view campaign as
select
id,
organization_id,
title,
description,
is_started,
due_by,
created_at,
is_archived,
use_dynamic_assignment,
logo_image_url,
intro_html,
primary_color,
texting_hours_start,
texting_hours_end,
timezone,
creator_id,
is_autoassign_enabled,
limit_assignment_to_teams,
updated_at,
replies_stale_after_minutes,
landlines_filtered,
external_system_id,
is_approved,
autosend_status,
autosend_user_id,
messaging_service_sid,
autosend_limit,
autosend_limit_max_contact_id
from all_campaign
where is_template = false;
`);
});
};

exports.down = function down(knex) {
return knex.schema
.raw(
`
drop view campaign cascade;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: I hate how much work this requires. abysmal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep :(

I would back a proposal for Rewired to sponsor development of an alter view drop column. There's been discussion about this since 2008.

create view campaign as
select
id,
organization_id,
title,
description,
is_started,
due_by,
created_at,
is_archived,
use_dynamic_assignment,
logo_image_url,
intro_html,
primary_color,
texting_hours_start,
texting_hours_end,
timezone,
creator_id,
is_autoassign_enabled,
limit_assignment_to_teams,
updated_at,
replies_stale_after_minutes,
landlines_filtered,
external_system_id,
is_approved,
autosend_status,
autosend_user_id,
messaging_service_sid
from all_campaign
where is_template = false;

create or replace view assignable_campaigns as (
select id, title, organization_id, limit_assignment_to_teams
from campaign
where is_started = true
and is_archived = false
and is_autoassign_enabled = true
);

create or replace view assignable_campaign_contacts as (
select
campaign_contact.id, campaign_contact.campaign_id,
campaign_contact.message_status, campaign.texting_hours_end,
campaign_contact.timezone::text as contact_timezone
from campaign_contact
join campaign on campaign_contact.campaign_id = campaign.id
where assignment_id is null
and is_opted_out = false
and archived = false
and not exists (
select 1
from campaign_contact_tag
join tag on campaign_contact_tag.tag_id = tag.id
where tag.is_assignable = false
and campaign_contact_tag.campaign_contact_id = campaign_contact.id
)
);

create or replace view assignable_needs_message as (
select acc.id, acc.campaign_id, acc.message_status
from assignable_campaign_contacts as acc
join campaign on campaign.id = acc.campaign_id
where message_status = 'needsMessage'
and (
( acc.contact_timezone is null
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) < campaign.texting_hours_end
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) >= campaign.texting_hours_start
)
or
( campaign.texting_hours_end > extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone) + interval '10 minutes')
and campaign.texting_hours_start <= extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone))
)
)
);

create or replace view assignable_campaigns_with_needs_message as (
select *
from assignable_campaigns
where
exists (
select 1
from assignable_needs_message
where campaign_id = assignable_campaigns.id
)
and not exists (
select 1
from campaign
where campaign.id = assignable_campaigns.id
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
)
);

create or replace view assignable_needs_reply as (
select acc.id, acc.campaign_id, acc.message_status
from assignable_campaign_contacts as acc
join campaign on campaign.id = acc.campaign_id
where message_status = 'needsResponse'
and (
( acc.contact_timezone is null
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) < campaign.texting_hours_end
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) >= campaign.texting_hours_start
)
or
( campaign.texting_hours_end > extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone) + interval '2 minutes')
and campaign.texting_hours_start <= extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone))
)
)
);

create or replace view assignable_campaigns_with_needs_reply as (
select *
from assignable_campaigns
where exists (
select 1
from assignable_needs_reply
where campaign_id = assignable_campaigns.id
)
);

create or replace view assignable_needs_reply_with_escalation_tags as (
select acc.id, acc.campaign_id, acc.message_status, acc.applied_escalation_tags
from assignable_campaign_contacts_with_escalation_tags as acc
join campaign on campaign.id = acc.campaign_id
where message_status = 'needsResponse'
and (
( acc.contact_timezone is null
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) < campaign.texting_hours_end
and extract(hour from CURRENT_TIMESTAMP at time zone campaign.timezone) >= campaign.texting_hours_start
)
or
( campaign.texting_hours_end > extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone) + interval '2 minutes')
and campaign.texting_hours_start <= extract(hour from (CURRENT_TIMESTAMP at time zone acc.contact_timezone))
)
)
);

create or replace view public.missing_external_sync_question_response_configuration as
select
all_values.*,
external_system.id as system_id
from (
select
istep.campaign_id,
istep.parent_interaction_id as interaction_step_id,
istep.answer_option as value,
exists (
select 1
from public.question_response as istep_qr
where
istep_qr.interaction_step_id = istep.parent_interaction_id
and istep_qr.value = istep.answer_option
) as is_required
from public.interaction_step istep
where istep.parent_interaction_id is not null
union
select
qr_istep.campaign_id,
qr.interaction_step_id,
qr.value,
true as is_required
from public.question_response as qr
join public.interaction_step qr_istep on qr_istep.id = qr.interaction_step_id
) all_values
join campaign on campaign.id = all_values.campaign_id
join external_system
on external_system.organization_id = campaign.organization_id
where
not exists (
select 1
from public.all_external_sync_question_response_configuration aqrc
where
all_values.campaign_id = aqrc.campaign_id
and external_system.id = aqrc.system_id
and all_values.interaction_step_id = aqrc.interaction_step_id
and all_values.value = aqrc.question_response_value
);

create or replace view sendable_campaigns as (
select id, title, organization_id, limit_assignment_to_teams, autosend_status, is_autoassign_enabled
from campaign
where is_started and not is_archived
);

create or replace view assignable_campaigns as (
select id, title, organization_id, limit_assignment_to_teams, autosend_status
from sendable_campaigns
where is_autoassign_enabled
);

create or replace view assignable_campaigns_with_needs_message as (
select *
from assignable_campaigns
where
exists (
select 1
from assignable_needs_message
where campaign_id = assignable_campaigns.id
)
and not exists (
select 1
from campaign
where campaign.id = assignable_campaigns.id
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
)
and autosend_status <> 'sending'
);

create or replace view assignable_campaigns_with_needs_reply as (
select *
from assignable_campaigns
where exists (
select 1
from assignable_needs_reply
where campaign_id = assignable_campaigns.id
)
);

create or replace view autosend_campaigns_to_send as (
select *
from sendable_campaigns
where
exists ( -- assignable contacts are valid for both autoassign and autosending
select 1
from assignable_needs_message
where campaign_id = sendable_campaigns.id
)
and not exists (
select 1
from campaign
where campaign.id = sendable_campaigns.id
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
)
and autosend_status = 'sending'
);
`
)
.then(() =>
knex.schema.alterTable("all_campaign", (table) => {
table.dropColumn("autosend_limit");
table.dropColumn("autosend_limit_max_contact_id");
})
);
};
14 changes: 13 additions & 1 deletion schema-dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ CREATE TABLE public.all_campaign (
autosend_user_id integer,
is_template boolean DEFAULT false NOT NULL,
messaging_service_sid text,
autosend_limit integer,
autosend_limit_max_contact_id integer,
CONSTRAINT campaign_autosend_status_check CHECK ((autosend_status = ANY (ARRAY['unstarted'::text, 'sending'::text, 'paused'::text, 'complete'::text])))
);

Expand Down Expand Up @@ -1479,7 +1481,9 @@ CREATE VIEW public.campaign AS
all_campaign.is_approved,
all_campaign.autosend_status,
all_campaign.autosend_user_id,
all_campaign.messaging_service_sid
all_campaign.messaging_service_sid,
all_campaign.autosend_limit,
all_campaign.autosend_limit_max_contact_id
FROM public.all_campaign
WHERE (all_campaign.is_template = false);

Expand Down Expand Up @@ -5060,6 +5064,14 @@ CREATE TRIGGER _500_user_team_updated_at BEFORE UPDATE ON public.user_team FOR E
CREATE TRIGGER _500_user_updated_at BEFORE UPDATE ON public."user" FOR EACH ROW EXECUTE FUNCTION public.universal_updated_at();


--
-- Name: all_campaign all_campaign_autosend_limit_max_contact_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.all_campaign
ADD CONSTRAINT all_campaign_autosend_limit_max_contact_id_foreign FOREIGN KEY (autosend_limit_max_contact_id) REFERENCES public.campaign_contact(id);


--
-- Name: all_campaign all_campaign_messaging_service_sid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
Expand Down
Loading